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:
- Inventory —
list_commits/branch list/list_merge_requestsfor the project. Read only. - Classify — each branch:
DURABLE(main/release),ACTIVE(unmerged unique work),MERGED,SUPERSEDED,ABANDONED(per the evidence checklist ininvestigation-methodology.md— do not classify on branch existence alone). Each MR: live/duplicate/wrong-target/conflicted-with-content/conflicted-empty. - 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.jsonname) —bluefly/<repo-slug> - Drupal machine name (module/theme
.info.ymlnamekey, 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-dockerdeploy 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/*andfix/*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.