Skip to content

02 — Repository Governance Workflow

Status: Draft Authority: Bluefly Engineering Governance Stability: Operational — evolves as tooling improves Governs under: 01-repository-governance-standard.md Version: 0.1.0


Purpose

This document defines the current operational workflow for repository governance. It sequences the capabilities defined in the standard and specifies concrete commands, decision points, and operator gates.

When the OSSA Repository Governor is available, this workflow is replaced by ossa repo * commands. Until then, this is the manual procedure.


Prerequisites

Before any repository work begins:

# Verify 1Password auth by resolving one known reference.
# Do not run `op whoami` from a non-interactive agent shell — it needs a CLI
# session token and will hang; a successful `op read` is the proof that matters.
op read op://BlueflyAgents/Gitlab/BLUEFLY_AGENT_TOKEN/token > /dev/null && echo AUTH_OK

# Never dump a resolved process environment (`op run -- env`) — that prints secret
# values. Never create or depend on an `.op-env` file; see
# `standards/core/STD-SEC-001-authentication-and-secrets.md`.

# Verify git version
git --version    # must be >= 2.39

# Verify blu CLI (note broken subcommands — see Known Issues)
blu --version

Stop condition: If op run fails authentication, halt everything. Do not proceed.


Scope

This workflow applies to all repositories under:

  • <WORKSPACE_ROOT>/worktrees/
  • <WORKSPACE_ROOT>/worktrees/__DRUPAL/DDev-Addons/
  • <WORKSPACE_ROOT>/worktrees/__DRUPAL/Module/
  • <WORKSPACE_ROOT>/worktrees/__DRUPAL/Recipes/
  • <WORKSPACE_ROOT>/worktrees/__DRUPAL/Themes/

Symlinks are skipped. Directories without .git/ are noted and skipped.


Capability 1 — Inventory

Mode: Read only. No mutations.

For each repository:

git -C <dir> branch --show-current
git -C <dir> remote get-url origin
git -C <dir> status --short
git -C <dir> status -sb
git -C <dir> log -1 --oneline
git -C <dir> log origin/$(git -C <dir> branch --show-current)..HEAD --oneline 2>/dev/null
git -C <dir> stash list

Also check: - existence of .git/index.lock - existence and executability of .git/hooks/*

Output: machine-readable JSON record + operator table.

Gate: Review the inventory. Confirm tooling is working before proceeding.


Capability 2 — Classification

Mode: Read only. No mutations.

Apply the priority tier rules from the standard to each inventory record.

Check Tier
No remote tracking branch OR local-only branch OR last push > 30 days 0
git log origin/branch..HEAD returns commits 1
git status --short returns modified or untracked files 2
All changed files match generated/runtime/cache patterns 3
Clean worktree, no ahead/behind 4

Output: Priority queue file, sorted T0 → T4.

Gate: Operator reviews the queue. Explicitly approves which tiers to proceed with. No capability beyond this point runs without that approval.


Capability 3 — Protection

Mode: Read only. Evaluates worktree, never stages.

For each repository approved for preservation:

Step 1 — Generate candidate diff (without staging)

git -C <dir> diff HEAD        # modified tracked files
git -C <dir> ls-files --others --exclude-standard   # untracked files

Step 2 — Path scan

Reject if any path matches the block list (see standard §Protection Capability).

Additional blocks for this environment: - .entire/ — runtime state - .ddev/.dbimage — DDEV DB snapshots - .qmd/*.sqlite* — QMD index files - CREW_EXECUTION_* receipts in wrong paths - _quarantine/ contents

Step 3 — Content scan

Reject if candidate diff content matches:

glpat-[a-zA-Z0-9_-]{20}          GitLab PAT
ghp_[a-zA-Z0-9]{36}              GitHub token
gho_[a-zA-Z0-9]{36}              GitHub OAuth
sk-[a-zA-Z0-9]{48}               OpenAI key
sk-ant-[a-zA-Z0-9-]+             Anthropic key
AKIA[0-9A-Z]{16}                 AWS access key
ops_[a-zA-Z0-9]+                 1Password service token
OP_SERVICE_ACCOUNT_TOKEN=[^\s]   Raw OP token
-----BEGIN .* PRIVATE KEY-----   SSH/TLS private key

Step 4 — Hook integrity check

for hook in .git/hooks/*; do
  bash -n "$hook" 2>&1    # syntax check only
done

Flag hooks that reference missing commands (e.g., blu secure).

Output

Per repository: CLEAN, BLOCKED, or REQUIRES_OPERATOR On BLOCKED: record reason and file/pattern. Do not proceed for that repository.

Gate: All CLEAN verdicts are confirmed before staging begins. BLOCKED and REQUIRES_OPERATOR repos are set aside for operator resolution.


Capability 4 — Planning

Mode: Read only. Produces a plan, does not execute it.

For each repository with a CLEAN protection verdict:

Determine the preservation branch:

if current_branch in [release/*, protected]:
  preservation_branch: chore/preserve/{YYYYMMDD}
  reason: "Protected branch — Cedar policy requires feature/* path"
else:
  preservation_branch: current_branch

Check Cedar (current manual equivalent):

# Does the branch protection policy allow direct commit?
# release/v0.1.x → NO (hook confirms)
# chore/*, feature/*, bugfix/*, hotfix/* → YES
# main, master, develop → check repo-specific policy

Output: Per-repository plan file:

repository: <name>
path: <absolute>
tier: <0-4>
protection_verdict: CLEAN
current_branch: <branch>
preservation_branch: <branch>
files_to_stage: <count>
commit_message: "chore: preserve current workspace state before consolidation"
cedar_check: ALLOW | DENY | REQUIRES_OPERATOR

Gate: Operator reviews and approves the plan. Only approved repositories proceed to execution.


Capability 5 — Execution

Mode: Writes. One repository at a time.

For each approved repository, in tier order (T0 first):

# 1. Confirm no changes since plan was generated
git -C <dir> status --short

# 2. If preservation branch needed, create it
git -C <dir> checkout -b chore/preserve/$(date +%Y%m%d)

# 3. Stage
git -C <dir> add -A

# 4. Re-run content scan on actual staged diff
git -C <dir> diff --cached | <content_scanner>
# If BLOCKED: git restore --staged . (requires operator authorization)
# Record the restore as a governance event

# 5. Commit
git -C <dir> commit -m "chore: preserve current workspace state before consolidation"

# 6. Record pre-push SHA
LOCAL_SHA=$(git -C <dir> rev-parse HEAD)

Stop on any hook failure. Do not use --no-verify. Investigate the failure.

Output: Commit SHA per repository, pre-push receipt.


Capability 6 — Push

Mode: Writes. One repository at a time.

op run --account blueflyiollc -- git -C <dir> push -u origin HEAD

Stop on push failure. Record the exact error. Do not continue to next repository until failure is understood.

Known pre-receive behaviour — creating a new branch that carries commits

OBSERVED 2026-09-14 on blueflyio/blu/blucity-docs. Pushing a new branch whose first push also carries commits was rejected:

remote: GitLab:
 ! [remote rejected]   feature/<name> -> feature/<name> (pre-receive hook declined)

The GitLab message is empty. There is no rule name, no reason, and nothing actionable — so the failure looks like a broken credential, a bad branch name, or a content rule, and each agent that hits it re-derives the answer from scratch.

It is none of those. The same commit, the same branch name and the same author push successfully when the branch is created first and the commits pushed second:

# 1. Create the branch at the current release tip — no commits carried.
git push origin origin/release/v0.1.x:refs/heads/feature/<name>

# 2. Push the work onto the now-existing branch.
git push origin feature/<name>

Isolated by changing one variable at a time: branch naming was ruled out (feature/* branches already exist on the remote, including one pushed the same day), commit-message length was ruled out (subjects up to 107 characters exist on release/v0.1.x), and author identity was unchanged between the rejected and accepted pushes.

What this does NOT establish. The rule's identity and intent are NOT_ESTABLISHED — no project push-rule configuration was read, and a hook that declines with an empty message cannot be diagnosed from the client side. Treat the two-step sequence as a workaround with a recorded observation, not as an explanation, and do not infer that the rule is wrong until someone reads it.

The reportable defect is the empty message, not the rule. A gate whose refusal carries no readable reason fails open in practice: the caller cannot tell a policy denial from a transport error, and the most likely response is a retry or a workaround rather than compliance. Fixing the message is worth more than relaxing the rule.


Capability 7 — Verification

For each pushed repository:

LOCAL=$(git -C <dir> rev-parse HEAD)
REMOTE=$(git -C <dir> ls-remote origin HEAD | awk '{print $1}')

if [ "$LOCAL" = "$REMOTE" ]; then
  echo "VERIFIED: $name"
else
  echo "MISMATCH: $name — local=$LOCAL remote=$REMOTE"
fi

Output: Verification receipt per repository.


Receipt Format

Every execution emits this receipt:

kind: RepositoryPreservationReceipt
version: "0.1"
repository:
  name: <string>
  path: <absolute>
  remote: <url>
branch:
  original: <string>
  preservation: <string>
  created_new: <bool>
classification:
  tier: <0-4>
  tier_name: <DATA_LOSS_RISK|UNPUSHED|WORKING_TREE|GENERATED|HEALTHY>
protection:
  verdict: CLEAN | BLOCKED | REQUIRES_OPERATOR
  patterns_checked: <list>
  blocked_patterns: <list>
cedar:
  decision: ALLOW | DENY | REQUIRES_OPERATOR
  policy_ref: <string>
commit:
  sha: <string>
  message: <string>
push:
  sha: <string>
  verified: <bool>
  local_matches_remote: <bool>
operator: <string>
timestamp: <iso8601>
session_ref: <bead_id or session_id>

Known Issues (Session 2026-06-30)

blu secure subcommand missing

Eight repos blocked by:

error: unknown command 'secure' (Did you mean setup?)
BLOCKED: blu secure scan failed

The blu CLI's secure subcommand does not exist at current installed version. Required action: Update blu CLI or patch hooks to use current subcommand name. Affected: foundation-bridge, api-schema-registry, security-policies, agent-tailscale, technical-docs, agentic_canvas_blocks, ai_agents_communication, others.

release/v0.1.x protection hook (working correctly)

Thirty repos blocked from direct commits to release/v0.1.x. Hook is functioning as designed. Use chore/preserve/YYYYMMDD branches. Required action: Operator authorizes preservation branch creation for these 30 repos.

Deletion guard hook (working correctly)

Eight repos blocked because staged deletes were detected. Hook is functioning as designed. Required action: Operator confirms each deletion is intentional before proceeding.

ContextControl.ai — live tokens in diff

Staged diff contained: glpat-, ghp_, sk-, OP_SERVICE_ACCOUNT_TOKEN= Repository has staged changes (from session 2026-06-30) sitting in the index. Required action: Human inspection of staged diff before any commit.

git -C <WORKSPACE_ROOT>/worktrees/ContextControl.ai \
  diff --cached | grep -n 'glpat-\|ghp_\|sk-\|OP_SERVICE_ACCOUNT'

ddev-claude-drupal — sk- in diff

Staged diff contained: sk- Required action: Verify whether this is a live key or a documentation example.

git -C <WORKSPACE_ROOT>/worktrees/__DRUPAL/DDev-Addons/ddev-claude-drupal \
  diff --cached | grep -n 'sk-'

context-cli — committed locally, push failed

Commit exists locally. Pre-push hook blocked because branch has unpushed history. Required action: Rebase and re-push.

duadp — index.lock

git add -A failed: .git/index.lock exists. Required action: Confirm no git process running, remove lock, retry from Capability 3.


Operator Decision Queue (Current Session)

Before resuming, the following decisions are required:

[ ] 1. Inspect ContextControl.ai staged diff for live tokens
[ ] 2. Inspect ddev-claude-drupal staged diff for sk- content
[ ] 3. Authorize chore/preserve/20260701 branch creation (30 release/v0.1.x repos)
[ ] 4. Fix blu secure hook (blu CLI update or hook patch)
[ ] 5. Confirm staged deletions are intentional (8 deletion-guard repos)
[ ] 6. Authorize context-cli rebase and re-push
[ ] 7. Clear duadp index.lock and retry from Capability 3

Part B — GitLab-side repository hygiene convergence

Status: Active (operator priority, 2026-09-03) Scope: GitLab-side project state (branches, MRs, CI ownership, naming, generated content) — distinct from Capabilities 1-7 above, which govern local workstation preservation before consolidation. A repository can be HEALTHY under Capabilities 1-7 (clean worktree, nothing to preserve) and still carry heavy entropy under Part B (stale branches, duplicate MRs, wrong CI ownership). Relationship to other standards: git-completion-contract.md (ES-GITCC) is the hard completion law for ordinary repo work (commit → push → MR to release/v0.1.x → CI pass → merge → verify → worktree cleanup). gitlab_components/standards/REPO-TASK-OPERATING-CONTRACT.md is the mechanical checklist; it must not be read as stopping at mergeable. Part B is a larger, periodic unit of work — a full per-project convergence pass — and produces its own receipt (below), not the per-task closeout.

Goal states

Every project converges toward one of:

State Meaning
CLEAN_PROJECT Durable branches only, zero stale/superseded MRs, CI consumes shared components, naming conforms, no generated content tracked, no personal-path contamination
BORING_PROJECT CLEAN_PROJECT plus no unusual branch/MR activity in the trailing convergence window — nothing left to converge
STANDARD_PROJECT BORING_PROJECT plus the repo-shape-by-type table below is fully satisfied for its declared type

Categories in scope

Inconsistent naming, duplicate implementations, stale branches, abandoned MRs, generated junk, local-path contamination, wrong CI ownership, wrong MR targets, duplicate files/trees, nonstandard package names, repo-specific CI copies duplicating gitlab_components, stale worktrees, unresolved branch divergence.

Execution rules

Situation Action
main / release/* Durable authority — never deleted, never a direct-commit target for feature work
Feature/fix branch, real unmerged work Preserve
Merged branch Delete
Content-superseded branch (same intent already landed via a different branch) Delete
Duplicate MR (same fix opened twice) Close the superseded one with a note pointing at the surviving MR
Wrong-target MR (e.g. targets main instead of release/*) Retarget if the tooling allows it; otherwise open a replacement MR at the correct target and close the original as superseded
Conflicted MR with useful content Clean worktree, fetch target, merge/rebase locally, resolve semantically, test, push, get green. Local merging is allowed and expected — do not fetishize GitLab UI conflict resolution
Conflicted MR with no unique content Close and delete the branch
Generated artifact with a proven source Regenerate from source, or untrack per the package's own contract — never hand-edit generated output
Repo-local CI duplicating gitlab_components Remove the duplicate, consume the shared component
Random naming variant Converge to the canonical form (see Naming standard, below)
Dead scaffolding / AI-generated prose / duplicate docs Remove only after proving no unique intent is lost and ownership is clear

Governing principle: preserve unique intent, remove duplicate state. This is not blind history rewriting and not mass deletion — every deletion in the closeout receipt must trace to a specific merged-into, superseded-by, or zero-unique-content finding.

Do not report an inventory dump as the deliverable. Inventory is the first step of the sequence below, not the output.

Sequence

Same INVENTORY → CLASSIFY → CONVERGE shape as Capabilities 1-4 above, applied to GitLab state instead of local worktree state:

  1. Inventory — list_commits/branch list/list_merge_requests for the project. Read only.
  2. Classify — each branch: DURABLE (main/release), ACTIVE (unmerged unique work), MERGED, SUPERSEDED, ABANDONED (per the evidence checklist in investigation-methodology.md — do not classify on branch existence alone). Each MR: live/duplicate/wrong-target/conflicted-with-content/conflicted-empty.
  3. Converge — apply the execution rules table above. Do the actual pushes, closes, and deletes; do not stop at a plan.

Durable branch invariant

DURABLE_BRANCHES_PER_PROJECT <= 5

Counts only branches that persist independent of any single session: main, release/*, and any long-lived integration branch the project has explicitly declared. Feature/fix/chore branches with active unmerged work do not count against this limit — they are transient by construction and clear via the execution rules above (merge → delete, or close-superseded → delete).

Gas City exception: polecat/session-branches created by the gc session lifecycle (e.g. admin-mcp/*, session-scoped working branches) are not hand-deleted and do not count toward this invariant either way. They are retired by the session lifecycle that created them, not by this convergence process. Do not delete a gc-owned session branch as part of a Part B sweep — that is a different owner's cleanup path.

MR budget

No fixed numeric ceiling is set here — an arbitrary number would be a guess, not a rule. The operative constraint is qualitative: open MR count should track live in-flight review work only. Any MR that is merged, superseded, wrong-targeted-and-replaced, or content-empty-after-conflict is not "in-flight" and must be closed per the execution rules table, not left open as history. A project whose open-MR count is not explained by currently-active review work has failed Part B convergence regardless of what number that count is.

Naming standard scope

Convergence includes bringing every one of the following into its canonical form when a project has drifted:

  • GitLab path — blueflyio/<group>/<repo>, lowercase, hyphenated, matching the actual owning group (see bc-yh4 / repo-shape table below for group placement)
  • Composer package name (composer.json name) — bluefly/<repo-slug>
  • Drupal machine name (module/theme .info.yml name key, hook prefixes) — snake_case, matching the module directory name exactly
  • PHP namespace — Drupal\<machine_name>\... for Drupal-owned code, Bluefly\<PascalCaseRepo>\... for platform services
  • Service name (in CI, Compose, and agent-docker deploy config) — must match across all three; a service renamed in one place and not the others is a naming defect
  • CI component name (when consumed from gitlab_components) — matches the component's published name, not a locally-invented alias
  • Branch name — per ~/.claude/rules/workflow.md: bugfix/*, feature/<n>-*, chore/*, release/v<x>.<y>.<z>; feat/* and fix/* are rejected by several repos' pre-receive hooks and must not be used
  • MR title — states the change, not the ceremony (fix: <what> / feat: <what> / chore: <what>, not "Draft: WIP" or a bare issue number)
  • Generated directories — named per the generator's own convention (e.g. src/generated/), never hand-renamed, never hand-populated

Repo shape by type

Type Expected shape
Platform service (TypeScript, common_npm/*, agent-platform/services/*) src/, openapi/<domain>/openapi.yaml as spec source, src/generated/* untouched generated output, .gitlab-ci.yml consuming shared components only, package.json name bluefly/<repo-slug>
OSSA manifest project (platform-agents/*) Manifests only — no service code, no common_npm reimplementation
CLI / automation (agent-buildkit, blu-cli) src/cli/*, no standalone scripts/ dir, .beads/ tracks only config.yaml/metadata.json/.gitignore per bc-yh4
Drupal custom module/theme Independent git clone when structured that way (web/modules/custom/*), own .info.yml, own MR — never bundled into the parent site repo's MR
Rig / City repo (blucity, agent-docker) .beads/ per bc-yh4 contract; .beads/formulas/* and .claude/skills/* are gc/City-projected symlinks and are not tracked (see blucity!175) — a rig repo that still tracks these has drifted
Docs repo (blucity-docs) Engineering-Standard/standards/* as the only standards tree; no competing top-level .md at repo root beyond README.md/AGENTS.md

Required closeout receipt (per project, per convergence pass)

Distinct from the single-task closeout in gitlab_components/standards/REPO-TASK-OPERATING-CONTRACT.md. Emit this for every Part B sweep:

kind: RepositoryConvergenceReceipt
version: "0.1"
project: <gitlab path>
before:
  branches: <count>
  open_mrs: <count>
after:
  branches: <count>
  open_mrs: <count>
merged: <list of MR iids merged this pass>
closed_superseded: <list of {mr_iid, superseded_by}>
branches_deleted: <list of branch names>
unique_work_preserved: <list of {source, landed_in} for anything at risk of loss>
ci_converged_to_shared_components: <list of {job_or_include, component}>
naming_defects_fixed: <list of {path_or_field, before, after}>
generated_content_decisions: <list of {path, decision: regenerated|untracked|left_as_is, reason}>
pipeline: <status of the resulting default/release branch pipeline after convergence>
clean_worktree: <bool — true only if a local clone was used and git status is clean after the pass>
goal_state_reached: CLEAN_PROJECT | BORING_PROJECT | STANDARD_PROJECT | NOT_REACHED
operator: <string>
timestamp: <iso8601>
session_ref: <bead_id or session_id>

Future enforcement (not built tonight — file as a follow-up bead)

gitlab_components is the eventual owner of these as CI gates, once the manual convergence pass above has run enough projects to prove the rule set: branch topology (durable-branch invariant), MR target validation, personal-path contamination scan, duplicate-YAML-key lint, repo-local CI duplication detection, generated-content policy enforcement, commit identity validation, package naming lint, required/forbidden file presence. Building these gates before the manual pass has validated the rules against real repos would encode guesses as enforcement — the manual pass is the specification work; the gates come after.