[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M70FRYFZC51YWHVNNS3862PY":3,"skill-same:01M70FRYFZC51YWHVNNS3862PY":73},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":13,"repositoryHtmlUrl":46,"repositoryHomepageUrl":6,"ownerHtmlUrl":47,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":48,"install":49,"resources":62},"01M70FRYFZC51YWHVNNS3862PY","forge-fix-runner",null,"Drain the GitHub `forge-fix` issue queue through the forge fix pipeline, one service at a time. Tallies actionable issues worst-first, scouts a service into a routing plan (clusters + quick\u002Ffull tiers), gets human approval, then dispatches fixes (quick = dispatched fix-code; full = forge-pipeline), gating every `Closes #N` on per-issue verification. Trigger when the user wants to fix triaged QA issues, \"work the forge-fix queue\", or \"run the fix runner\".","SirTheOracle\u002Fforge-toolkit","SirTheOracle","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F264051384?v=4","01M70FNRYMQR4XHVN77S3NJPAW","01M70FNRZAGW58XJ1CC6JRY1PA",0,"MIT","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-fix-runner\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"fix","forge","forge fix runner","runner","issue","at","run","human","plan","work","verification","code","when","gating","qa","issues","time","actionable","pipeline","quick","2026-11-10 10:12:30",false,"---\nname: forge-fix-runner\ndescription: >\n  Drain the GitHub `forge-fix` issue queue through the forge fix pipeline, one\n  service at a time. Tallies actionable issues worst-first, scouts a service into\n  a routing plan (clusters + quick\u002Ffull tiers), gets human approval, then dispatches\n  fixes (quick = dispatched fix-code; full = forge-pipeline), gating every\n  `Closes #N` on per-issue verification. Trigger when the user wants to fix\n  triaged QA issues, \"work the forge-fix queue\", or \"run the fix runner\".\n---\n\n# Forge Fix Runner\n\nYou turn GitHub Issues labeled `forge-fix` into merged fixes by routing them into\nthe existing forge pipeline. You are the **management layer**; forge is the engine.\nYou do NOT reinvent fixing — you select work, scout it, gate it, and keep issue\nstatus in sync.\n\n> **Project specifics** — repo name, classifier vocabulary, extra blocking labels,\n> the seed set, the readiness probes (including the two verbatim probe commands\n> execute mode needs), and any project-local caveats — live in `PROJECT.md` beside\n> this file. **Read it first.** Everything in *this* document is identical in every\n> project.\n\n**Repo:** set by `DEFAULT_REPO` in the PER-PROJECT CONFIG block of\n`scripts\u002Fqueue.py` (each project's `scripts\u002F` is a real directory, never a symlink).\n**Design of record:**\n`.dev\u002Fproposals\u002Fqa-github-issues\u002Fforge-fix-pipeline-design.md` (read it if anything\nhere is ambiguous). **Queue logic:** `scripts\u002Fqueue.py` (deterministic, tested) —\nrun it with no `--repo` flag; a project copy already points at its own repo.\n\n**Classifier:** issues bucket by a configurable label prefix — `service:*` by\ndefault, but a project may use another (FeedMint uses `area:*`). This document\nsays \"service\"; read that as whatever `CLASSIFIER_PREFIX` your project copy sets.\n\n## Non-negotiable invariants\n\n1. **Closed = actually fixed.** A `Closes #N` is emitted ONLY for an issue with a\n   passing per-issue verification row. No green check → no close keyword. Ever.\n2. **Dry-run is the default.** First pass prints every intended mutation (labels,\n   branches, PR bodies, comments, forge dispatches) and changes nothing. Write mode\n   requires the user to explicitly say \"go \u002F write \u002F execute\".\n3. **`forge-fix` is human-only.** You never add it. You only act on issues that\n   already carry it.\n4. **Status lives only in GitHub.** Labels + open\u002Fclosed + the PR's `Closes` are the\n   single source of truth. Never hand-maintain status elsewhere.\n5. **One branch per group, one PR per group.** Never a shared service branch.\n6. **One worktree per session.** A session executes in its own git worktree and\n   claims at most one queue. Two sessions in one physical root corrupt each other's\n   forge context and pane topology; `queue.py claim` refuses it mechanically with\n   `ROOT_CONFLICT`, comparing `realpath(git rev-parse --show-toplevel)` — never\n   `$PWD`, or two sessions in different subdirectories of one worktree both pass.\n7. **A session never merges, never rebases or resolves another session's branch or\n   conflict, and never force-releases the infra lock.** Integration is the operator's,\n   through `main`, on their cadence.\n\n> **What fan-out buys, stated once and un-skimmably.** With the infra mutex correctly\n> extended, `fix-code`, `fix-qa`, `fix-qa-retry` and step-5 verification **serialize\n> globally across worktrees** — they contend for one shared test database. Expect\n> wall-clock ≈ *(sum of every bucket's infra time)* + *(the heaviest queue's non-infra\n> time)*: roughly **1.5–1.9×** on a full-tier-heavy queue and close to **1×** on an\n> all-quick queue. **Those numbers are an estimate pending timing data, never a\n> contract.** \"The wall-clock of the heaviest queue\" would need a per-worktree test\n> database and per-worktree ports, which are deliberately out of scope. **The\n> delivered value is distribution automation** — nobody hand-deals `g1 … g7` every\n> morning — not throughput. `infra_required` is therefore `True`-only in practice on\n> GoParent today.\n\n## The actionable predicate (what you may pick up)\n\nUse `scripts\u002Fqueue.py` — do not eyeball labels. An issue is actionable iff:\nopen + the trigger label + exactly one classifier label + none of the project's\n`BLOCKING_LABELS` (by default `needs-retest` \u002F `in-progress` \u002F `fix-pr-open`).\n(So an issue carrying `forge-fix` + `needs-retest` is correctly skipped — it is\nalready awaiting retest.) All four inputs come from the PER-PROJECT CONFIG block.\n\n```\npython3 scripts\u002Fqueue.py tally                    # worst-first services, proposes next\npython3 scripts\u002Fqueue.py select --service \u003CNAME>  # the actionable queue for one service\n```\n\n## Procedure\n\n### 0. Preflight (abort on any failure)\n\nWritten for **concurrency**: under fan-out a sibling pipeline is *normal*, and the\ndangerous failure is a tab that \"helpfully\" supersedes a live one. The destructive\nbranches below were written for one-pipeline-at-a-time and must not fire.\n\n- `~\u002Fbin\u002Fforge-bridge identity` — require `identity_state=MATCH` **from a real\n  in-pane probe**. A headless tab in a root with exactly one live session *adopts that\n  session's identity* and would then read and write its context. If you are not in a\n  pane of your own session: **stop**.\n- `~\u002Fbin\u002Fforge-bridge context` — this is **per-session**; everything it shows is your\n  own tab's. **Never offer to supersede. Never act on a `legacy shared context`\n  hint.** A sibling's active slug is not yours to see or to clear.\n- `~\u002Fbin\u002Fforge-bridge infra-lock status` — `HELD live by \u003Cother slug>` is the\n  **expected** state under fan-out. Report it and continue. **Never\n  `release --force`** (invariant 7).\n- `git status` clean, and **not** a detached HEAD. Do **not** require the branch to be\n  `main`: `forge worktree ensure` cuts `forge\u002F\u003Csession>`, so a correctly provisioned\n  fan-out worktree is never on `main` and that check would refuse every assisting tab.\n  What must hold is a clean, non-detached worktree and a fetched remote base that\n  resolves: `git fetch origin && git rev-parse --verify origin\u002F\u003Cbase_branch>`.\n- `gh auth status` shows issue + PR scope.\n- The slug you plan to use does not collide with an existing `.dev\u002Fproposals\u002F\u003Cslug>\u002F`.\n\n### 1. Pick the service\nRun `queue.py tally`. Propose the worst-first service as a one-liner the user can\nredirect (\"Run \u003CSERVICE> next? N criticals\"). Counts are live — never hard-code them.\n\n### 2. Scout (read-only — change no code)\nThe scout is a fixed contract (prompt `~\u002F.config\u002Fforge\u002Fprompts\u002Ffix-scout.txt` + the group\nschema in step 3), so it is **agent-agnostic** — run it either way:\n- **Inline:** spawn a read-only Explore subagent over the service's actionable queue.\n- **Dispatched (e.g. to Codex):** pick a scout slug `\u003Csvc>-scout-\u003Cdate>`; write the\n  actionable issues (numbers, coded IDs, full bodies from `gh issue view`) to\n  `.dev\u002Fproposals\u002F\u003Cscout-slug>\u002Fscout-input.md`; then\n  `~\u002Fbin\u002Fforge-bridge dispatch --slug \u003Cscout-slug> --stage fix-scout --worker codex-a`.\n  The worker writes `.dev\u002Fproposals\u002F\u003Cscout-slug>\u002Frouting-plan.md`.\n\nEither way the scout reads each issue body + the implicated code and produces **groups**,\neach a cluster (several issues, ONE shared root cause) or a singleton. A cluster means\n*one fix resolves all covered issues* — shared root cause, NOT shared service or shared\nsymptom wording. Derive groups from evidence; never assume a service is one cluster.\n(Live caution from GoParent: one service's queue mixed \"section empty\" and \"code\nshowing\" symptoms that turned out to have entirely different causes.)\n\n**Multi-cause issues (many-to-many):** an issue can have several causes, and a cause can\nspan issues. A group's `Closes` may list an issue only if the group fixes ALL its causes;\ndo not carve a quick sub-fix out of a multi-cause issue that also has a full-tier cause\n(fold it into the full group). Group by subsystem; use `drop_conditions` to move uncertain\nissues between groups. (See the design doc's \"Multi-cause issues\" section.)\n\nIf you suspect a **cross-service** root cause, flag it and STOP for the user — do not\nforce it into a service-local group.\n\n### 3. Write the plan DIRECTORY + get approval  (INITIATOR ONLY)\n\n**Location — the PRIMARY root, never your worktree.** Your worktree is itself\ndisposable and gets pruned; the plan must outlive it and resolve identically from\nevery sibling session:\n\n    PRIMARY_ROOT=\"$(dirname \"$(git rev-parse --path-format=absolute --git-common-dir)\")\"\n    PLAN_DIR=\"$PRIMARY_ROOT\u002F.dev\u002Fproposals\u002Fqa-github-issues\u002Ffix-plans\u002F\u003CSERVICE>-\u003CYYYY-MM-DD>\"\n\nThat is the same anchor the infra lock resolves, and it is byte-identical from a\nlinked worktree and from the primary.\n\n    \u003CPLAN_DIR>\u002F\n        plan.md            # deal table + one YAML record per bucket   (human audit; SEALED)\n        deal.json          # CANONICAL mechanical source               (SEALED)\n        packets\u002F\u003Cgid>.md   # one self-contained task packet per bucket (SEALED)\n        manifest.json      # sha256 of every sealed file, written at approval\n        claims\u002F            # MUTABLE — created on demand by `queue.py claim`\n        journal\u002F\u003Cqueue>.md # MUTABLE — append-only, exactly ONE writer per file\n\n**Immutability rule (invariant 4, applied literally).** After approval, `plan.md`,\n`deal.json` and `packets\u002F` are **read-only to every session** and their hashes are\nsealed in `manifest.json`. `deal.json` is **canonical** for anything a claim or an\nexecute-mode decision depends on; `plan.md` is the human artifact. `claim` and\nexecute mode both refuse on a seal mismatch. No session writes per-bucket status\nanywhere. Progress is reconstructed from GitHub: `in-progress` ⇒ claimed,\nunstarted-or-running; `fix-pr-open` + an open PR carrying the bucket's `Closes` set ⇒\ndone; issue closed ⇒ merged. This removes the three-concurrent-writers lost-update\nshape entirely.\n\n**Grouping rules — now load-bearing, not advice.** Two concurrently executing\nbranches must not touch one file, and a bucket can never see another bucket's\nunmerged work:\n\n- **Groups that would edit the same files MUST be merged into one bucket.**\n- **A group that consumes a symbol another group creates MUST be merged into that\n  group.**\n- **`depends_on` must be empty.** A non-empty value is refused at `deal`,\n  `deal --verify`, `claim` and `packet-check`. There is no execution path for it:\n  every bucket branches from a freshly fetched `origin\u002F\u003Cbase_branch>` and no session\n  waits for a merge, so a later bucket branches from a ref that does not contain the\n  earlier one's work. Queue locality supplies **ordering**, not **ancestry** — keeping\n  a chain in one queue would only make the failure sequential instead of parallel.\n  Re-introducing chains needs an explicit merge gate or stacked ancestry; both are out\n  of scope. Do not smuggle one in as queue order.\n\n**Packet contract** — `packets\u002F\u003Cgid>.md` **is** the `problem-statement.md`. Step 4 no\nlonger writes one; it copies this in. YAML front matter first (mechanically validated\nby `packet-check`), verbatim issue bodies after:\n\n    ---\n    group_id · service · tier · slug · branch_name · queue_id · plan_dir\n    base_branch · base_sha\n    covered_issue_numbers · covered_coded_ids · current_labels\n    root_cause_hypothesis (NON-AUTHORITATIVE) · confidence · anchors\n    required_tests · infra_required · effort_budget · close_keywords · drop_conditions\n    verification_targets:   # ONE ROW PER COVERED ISSUE\n      - issue · coded_id · symptom · check · evidence\n    ---\n    VERBATIM `gh issue view` body for every covered issue.\n\n`packet-check` gates the invariant: **every `covered_issue_number` has a verification\nrow, and no row names an issue the bucket does not cover** — an issue with no row\ncould never earn its `Closes`.\n\n**Packet → constraint ledger.** Every fix-pipeline slug needs\n`.dev\u002Fproposals\u002F\u003Cslug>\u002Fconstraints.yml` before its first dispatch; `forge-bridge\ndispatch` refuses a carrier stage without one. In this lane it is **generated, not\nauthored**, because the packet already holds verbatim operator text:\n\n- one row per covered issue, `source: operator`, `source_ref: \"packet:#\u003Cn>\"`;\n- `verbatim` sliced from that issue's body — the sentence stating the obligation, not\n  a summary of it;\n- `check` seeded from that issue's `verification_targets` row;\n- `principle` is the general rule the issue implies; `binds` is the scope it governs,\n  which is expected to be WIDER than the one surface the issue names;\n- `scope: user-visible` only when the issue names a surface a person can reach.\n\n**Slicing a quote out of a free-form issue body is a heuristic, not a parse. Emit\n`source: inferred` on any row you cannot cleanly attribute.** A confident-looking\n`verbatim` that is not what the reporter wrote manufactures exactly the false authority\nthe mechanism exists to prevent — and an `inferred` row is honest, reviewable, and still\ncarried into every stage prompt.\n\nOne record per bucket in `deal.json` (mirrored into `plan.md` prose):\n\n```yaml\ngroup_id:                # \u003Cservice>-\u003CYYYY-MM-DD>-g1\nservice:                 # \u003Cservice>   (the classifier value)\ntier:                    # quick | full\ncovered_issue_numbers:   # [68, 69]\ncovered_coded_ids:       # [\u003CSVC>-012, \u003CSVC>-013]\ncurrent_labels:          # per-issue label snapshot (for idempotency\u002Fabort)\nroot_cause_hypothesis:   # short prose (NON-AUTHORITATIVE for full pipeline)\nconfidence:              # high|medium|low + one-line why\nslug:                    # \u003Cservice>-\u003Cshort-cause-description>\nbranch_name:             # fix\u002F\u003Cservice>-\u003Cshort-cause-description>\nclose_keywords:          # \"Closes #68 #69\"  (emitted to PR ONLY after verification)\nrequired_tests:          # commands the fix must pass\ndrop_conditions:         # when to un-cover an issue from this group\napproval_status:         # pending | approved | dropped\nbase_branch:             # main   \u003C- BARE. `origin\u002F` is added at USE, never stored.\nbase_sha:                # 40-hex sha of origin\u002F\u003Cbase_branch> at grouping time\nanchors:                 # symbol anchors (function\u002Fclass names), never line numbers\ndepends_on:              # [] — MUST be empty (see the grouping rules above)\ninfra_required:          # derived (S6); absent => treat as true\neffort_budget:           # {worker_stages: N, production_files: M} — the PRE-CODE\n                         # budget (see \"Effort budget\" below). Derived from the\n                         # evidence, never from the tier; absent => refuse to deal.\nweight:                  # derived: TIER_BASE[tier] + len(covered_issue_numbers)\nqueue:                   # S1|S2|S3 — operator-editable BEFORE approval only\npacket:                  # packets\u002F\u003Cgroup_id>.md\n```\n\n> **`base_branch` is stored bare.** Store `main`, not `origin\u002Fmain`, and build\n> `origin\u002F$base_branch` at the point of use. Storing the qualified ref produced\n> `origin\u002Forigin\u002Fmain` at every branch cut and every moved-ground check.\n\n**Then deal and check:**\n\n```bash\npython3 scripts\u002Fqueue.py deal --plan \"$PLAN_DIR\u002Fdeal.json\" --sessions 3 --write\npython3 scripts\u002Fqueue.py deal --verify --plan \"$PLAN_DIR\u002Fdeal.json\"\npython3 scripts\u002Fqueue.py packet-check --plan-dir \"$PLAN_DIR\"\n```\n\nPresent the deal table with **per-queue weight totals alongside bucket counts**, so a\nsingle-bucket S3 reads as a heavy bucket and not as a bug. (There are no \"units\":\nchains are refused, not collapsed.) The derived `infra_required` value is shown per\nbucket. The user may flip tiers, split\u002Fmerge groups, drop groups, or move a bucket\nbetween queues — then re-run `deal --verify`. Before approving a 3-way deal, check\nheadroom with `~\u002Fbin\u002Fforge-bridge usage --refresh`; deal to 2 if it is thin.\n\n**The approval transaction — ordered, and every step is idempotent.** Owner-aware\nstatus and `ROOT_CONFLICT` both assume every queue has a claim, so the initiator's own\ntransition cannot be left implicit. Do these in exactly this order; a crash at any\nboundary is recoverable by re-running:\n\n1. Set `approval_status: approved` on the blessed buckets; re-run `deal --verify` and\n   `packet-check`. **Execute ONLY approved buckets.**\n2. `python3 scripts\u002Fqueue.py seal --plan-dir \"$PLAN_DIR\"` — the artifacts freeze.\n3. Apply `in-progress` to every covered issue of every approved bucket\n   (check-before-write, so re-running adds nothing twice). On a **partial failure do\n   not roll back**: record which issues were labelled in `journal\u002FS1.md`, report the\n   failures, and **stop** — do not claim, do not print the tab block.\n   `reconcile --dry-run` is the recovery path; the labels are not lost work.\n4. `python3 scripts\u002Fqueue.py claim --plan-dir \"$PLAN_DIR\" --queue S1 --root \"$(git rev-parse --show-toplevel)\"`\n   — the initiator claims **before** any assisting tab can.\n5. **Only now** print the tab block (step 3b).\n\n`in-progress` at approval time is what makes a second tab's `tally` refuse. It also\ncreates an orphaned-label class if a session dies, which is why `reconcile` ships in\nthe same change and is the documented remedy.\n\n### 3b. Open the assisting tabs — BEFORE you start bucket 1\n\n`forge worktree ensure` refuses to cut a worktree when the source root has **tracked\nmodifications** and a session is live there. `e24e1be` stopped the worktree\n*container* dirtying `git status`, but the refusal keys off\n`git status --porcelain -uno` — tracked changes — which is exactly what you create the\nmoment bucket 1 starts. So tabs 2 and 3 must be opened first. Print exactly this:\n\n```\nOpen BOTH tabs now, before I start bucket 1:\n  \u002Fforge-fix-runner execute \u003CABS-PLAN-DIR> S2\n  \u002Fforge-fix-runner execute \u003CABS-PLAN-DIR> S3\n```\n\nIf a tab is opened late and hits `source has tracked modifications`, the remedy is\n`forge-start --allow-dirty-base \u003Csession>`, and for fan-out that is **correct, not\nmerely tolerated**: the worktree is cut from the resolved base, and an executor\nbranches every bucket from a fresh `origin\u002F\u003Cbase_branch>` anyway, so it must not\ninherit your uncommitted work. The tool's warning exists for the ordinary case, which\nthis is not.\n\n### 4. Execute mode — how an assisting tab enters\n\nEntered **only** as `\u002Fforge-fix-runner execute \u003Cabs-plan-dir> \u003Cqueue-id>`. Both\narguments are required; if either is missing, **stop and ask** — never fall through to\nstep 1.\n\n> **You do not triage. Steps 1, 2 and 3 do not exist for you.**\n\n1. Preflight (step 0). `queue.py verify-seal --plan-dir \u003Cabs>`. Load `deal.json` (the\n   canonical source — not `plan.md`); refuse if the queue id is absent, if the seal\n   does not verify, or if any bucket in the queue is not `approval_status: approved`.\n2. **Claim first, then lock, then check readiness.** This order is not negotiable:\n\n   ```bash\n   python3 scripts\u002Fqueue.py claim --plan-dir \u003Cabs> --queue \u003Cid> --root \"$(git rev-parse --show-toplevel)\"\n   ~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cfirst-slug> --stage fix-verify\n   python3 scripts\u002Fqueue.py readiness --plan-dir \u003Cabs> --queue \u003Cid> --root \"$(git rev-parse --show-toplevel)\" \\\n       --db-probe   \"\u003Csee PROJECT.md>\" \\\n       --import-probe \"\u003Csee PROJECT.md>\"\n   ~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cfirst-slug> --stage fix-verify\n   # readiness FAILED -> release the lock, then\n   #   queue.py release --plan-dir \u003Cabs> --queue \u003Cid> --owner-token \u003Ct>\n   #   print the remediation and STOP. Never execute a queue you cannot verify.\n   ```\n\n   The readiness check is not a formality. On GoParent it exercises `pytest`, whose\n   `conftest.py` runs a session-scoped autouse fixture that terminates every other\n   connection to the shared test database. Running it **before** claiming and\n   **outside** the lock would perform the exact cross-session collision this design\n   exists to prevent — and two tabs in one root can both reach it before\n   `ROOT_CONFLICT` refuses either. It covers **every toolchain the queue uses**, not\n   one command from the first bucket.\n\n   **Both probe commands are project-specific and live in `PROJECT.md`. Use them\n   verbatim.** They exist because of two measured hazards:\n   - **`--db-probe`** — an unseeded worktree does *not* fail to start; it resolves the\n     hardcoded `DATABASE_URL` default in the app config and would silently target a\n     different database.\n   - **`--import-probe`** — a symlinked `.venv` carries an editable-install `.pth`\n     pointing at the **primary** checkout's source. The probe in `PROJECT.md`\n     reproduces what pytest does (`sys.path.insert(0,'src')`); a **bare**\n     `import app` resolves to the primary even on a correctly seeded worktree and\n     would fail the healthy case.\n3. Work the buckets in listed order (4.5).\n4. Drained → `queue.py release`, confirm no open pending, confirm you hold no infra\n   lock, print the summary from `journal\u002F\u003Cqueue>.md`, **stop**. No requesting more\n   work, no idling.\n\n**Recovery — a reopened tab given the same queue id.** `claim` steals the stale claim\nof a dead session automatically. Then, before re-cutting anything:\n\n- Re-derive per-bucket status from `gh issue list` + `gh pr list`; skip buckets that\n  are done (`fix-pr-open` + an open PR carrying their `Closes` set).\n- For the first not-done bucket, **check for an existing `fix\u002F\u003Cslug>` branch first** —\n  a bucket a dead session had started looks identical to an unstarted one, and\n  `git checkout -b` fails outright if the branch already exists. In order:\n  - the recorded root still exists and the branch is checked out there → **reattach**\n    to that worktree and continue;\n  - else `forge recover --dry-run` reports bridge residue → run `forge recover` first;\n  - else `git worktree prune`, then re-cut.\n  **Never silently discard committed or uncommitted work.** If the branch carries\n  commits you did not make, or uncommitted work you cannot attribute, stop and surface\n  it.\n\n### 4.5 Per-bucket protocol (identical for the initiator)\n\n1. `git fetch origin`. If `origin\u002F\u003Cbase_branch>` has moved from `base_sha`, run the\n   **anchor re-check**: every recorded anchor must still resolve (`git grep -n` on the\n   symbol, never the line). Any anchor missing ⇒ **INVALIDATED**: comment on the\n   covered issues, remove `in-progress`, leave them **open and actionable**, append the\n   reason to `journal\u002F\u003Cqueue>.md`, and **continue to the next bucket**. Report\n   invalidations at the end; do not surface mid-run.\n\n   > This is **conservative invalidation**, deliberately — not \"re-scope and carry\n   > on\". Its cost is one re-scout; the cost of the alternative is a wrong `Closes`,\n   > so the check is tuned to over-invalidate. Be honest about its limit: **a symbol\n   > can survive with changed semantics and pass this check.** That is a known\n   > false-negative, not a claim of safety.\n   >\n   > **Full-tier late-overlap checkpoint.** A full-tier investigation can discover\n   > file overlap with a bucket a sibling has *already started*. When it does, stop,\n   > journal it, and surface it — that is a conflict with work being done.\n\n2. `gh issue view` each covered issue; drop from `close_keywords` any that are now\n   closed or already carry `fix-pr-open`. All gone ⇒ skip the bucket (journal it,\n   report it).\n3. `git checkout -b fix\u002F\u003Cslug> \"origin\u002F$base_branch\"` — **always from freshly fetched\n   `origin\u002F\u003Cbase_branch>`, never from the previous bucket's branch.** This is how \"no\n   stacked PRs\" becomes mechanical rather than aspirational. Note the single\n   `origin\u002F`: `base_branch` is stored bare precisely so this is not\n   `origin\u002Forigin\u002Fmain`.\n4. Copy `packets\u002F\u003Cgid>.md` to `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (it already\n   *is* the problem statement); write `fix-plan.md` for the quick tier.\n5. Execute by tier (below). Both tiers run their infra stages under the lock; the\n   bridge refuses them otherwise (`INFRA_LOCK_REQUIRED`, exit 5).\n6. **Step-5 verification under the infra lock (Shape B)** — see step 5.\n7. Open the PR, then move to the next bucket **immediately**. Never wait for the\n   operator's merge.\n\n> **Surface to the operator ONLY on a conflict with work being done** — a\n> rebase\u002Fmerge conflict against `origin\u002F\u003Cbase_branch>`, a bucket whose files an open\n> `fix\u002F*` PR already changed, an infra-lock `TIMEOUT` escalation, or the full-tier\n> overlap checkpoint above. Use `forge ask --session-scope`. Otherwise work silently\n> to the end of your queue.\n\nFor each bucket, by tier:\n\n**quick** — bounded change; the coder stage is **DISPATCHED to a worker pane**:\n\n> **Quick tier is not orchestrator-local, and it never was.** It dispatches\n> `fix-code` to a worker pane exactly as the full tier does; what makes it\n> \"quick\" is the number of worker stages (one), never where they run. Nothing in\n> this tier executes in the runner's own pane. The retired wording \"inside forge\n> via a single pane\" read as orchestrator-local execution, disqualified the quick\n> tier on that basis, and directly caused the documented misroute on #34. Do not\n> reintroduce it.\n\n1. Write `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (verbatim covered-issue bodies +\n   the scout hypothesis labeled *non-authoritative* + `required_tests` + the per-issue\n   verification targets).\n2. Write a **minimal** `.dev\u002Fproposals\u002F\u003Cslug>\u002Ffix-plan.md` (the scout's bounded plan —\n   this is the frozen plan the mechanical coder consumes; \"quick\" still has a plan).\n3. Ask `forge codex-lane --root \"$PROJECT_ROOT\" --stage fix-code --worker codex-a`\n   for the protected lane decision. In the default `contain` rollout, dispatch\n   the coder stage to `claude-opus` (the reviewed-host lane). Use `codex-a` only\n   when the result explicitly says `lane=codex`; never bypass `LANE_REQUIRED`.\n\n   **`fix-code` is an infra stage (Hard Rule 23) — wrap the dispatch in Shape A.**\n   Its capability class is `commit`, so the bridge REFUSES the dispatch outright\n   (`INFRA_LOCK_REQUIRED`, exit 5) unless this session already holds the lock for\n   this exact `(slug, stage)`. This is not optional:\n\n   ```bash\n   ~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cslug> --stage fix-code\n   ~\u002Fbin\u002Fforge-bridge dispatch --slug \u003Cslug> --stage fix-code --worker \u003CW>\n   #   dispatch FAILS -> infra-lock release --slug \u003Cslug> --stage fix-code ; surface ; STOP\n   # wait loop: DONE|ERROR -> release. HOLD through PROMPTING\u002FSTALLED\u002FTIMEOUT\u002FDEAD\u002FBLOCKED.\n   ~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cslug> --stage fix-code\n   ```\n\n   Under fan-out a `WAIT reason=held-by-live-session` here is **expected** — a\n   sibling session is on the shared stack. Wait it out. A `TIMEOUT` is a conflict\n   with work being done, so surface it (see **Surfacing**). Never\n   `release --force` another session's lock.\n\n   Full tier inherits the same protection through the orchestrator, which locks\n   `fix-code` \u002F `fix-qa` \u002F `fix-qa-retry` per Hard Rule 23.\n\n   The selected worker writes `fix-coder-report.md` + test evidence on branch\n   `fix\u002F\u003Cslug>` and requests its exact-path commit through the broker.\n4. **Escalation backstop:** if the worker reports it needs more than the planned files,\n   can't form the minimal plan, or the cause is uncertain, it stops without fixing —\n   remove `in-progress`, re-route the group to **full**, note why in the artifact and\n   an issue comment.\n\n**full** — murky\u002Frisky\u002Fflow-breaking, the whole pipeline:\n1. Write `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (as above; scout hypothesis is\n   routing metadata only — A\u002FB investigators must validate independently, C reconciles).\n2. **Re-tier checkpoint — MANDATORY, not advisory.** After `fix-investigate` returns\n   and BEFORE `fix-plan` is dispatched, read `diagnosis.md`, re-evaluate the tier\n   against it, and append exactly one line to `journal\u002F\u003Cqueue>.md`:\n   `TIER-CONFIRMED full: \u003Creason>` or `TIER-REDUCED full -> quick: \u003Creason>`.\n   The tier chosen at scout time is **provisional by definition** — it precedes the\n   evidence, so it is the one routing decision that must be re-made once evidence\n   exists. Full tier may continue ONLY when the diagnosis shows something the quick\n   tier cannot carry: a genuinely open design decision, a multi-file blast radius, or\n   an unresolved contradiction. **\"The diagnosis was thorough\" is not a reason to stay\n   full.** On `TIER-REDUCED`, stop the full pipeline and finish the bucket on the quick\n   tier, writing `fix-plan.md` from the diagnosis you already have.\n3. Run the forge pipeline for the slug (`forge-pipeline \u003Cslug>` in a forge session via\n   the orchestrator). It produces diagnosis → plan → code → qa on branch `fix\u002F\u003Cslug>`.\n\n> **Effort budget — it bounds the PRE-CODE stages, not the review loop.** Every packet\n> carries `effort_budget`: an expected worker-stage count and an expected\n> production-file count, both derived from the evidence. It governs\n> `fix-investigate`, `fix-plan` and `fix-plan-revise`, because that is where the time\n> actually goes — 132 of #39's 265 worker-minutes were spent in those three stages\n> under no budget of any kind. **Bounding only the `fix-plan-review` ↔\n> `fix-plan-revise` loop does NOT satisfy this**: #39 carried that bound, the bound\n> held, and the run still cost 4.4 hours.\n>\n> Before dispatching any pre-code stage, compare the run's worker-stage count so far\n> against `effort_budget.worker_stages`. A stage that would take the run past its\n> budget is **not dispatched**: STOP and surface to the operator with the numbers —\n> budgeted vs. actual worker stages, budgeted vs. actual production files.\n>\n> **Scope-growth signal.** When `fix-plan.md` names more production files than the\n> diagnosis implicates, the plan has **outgrown its diagnosis**. Surface that with the\n> two counts; never absorb it. On #34 a single sentence demanding executable proof\n> where a text assertion was called for grew the plan from four files to seventeen,\n> and nothing flagged it.\n\n### 5. Per-issue verification (gates the PR) — UNDER THE INFRA LOCK\n\nThis step runs `required_tests` **inline in your own session**, with no dispatch\nand no `wait` to key off — so it takes **Shape B**\n(`forge-orchestrator\u002FSKILL.md`, Hard Rule 23). It is not bookkeeping: this is\nwhere the backend suites actually execute, and GoParent's\n`backend\u002Ftests\u002Fconftest.py` runs a **session-scoped autouse** fixture that\n`pg_terminate_backend()`s every other connection to the shared\nthe shared test database. Two sessions verifying at once corrupt each\nother's failure counts **silently**, and a corrupted failure count is how a\n`Closes #N` reaches an unfixed bug.\n\n```bash\n~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cslug> --stage fix-verify\n# run required_tests here; capture evidence paths; build the verification table\n~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cslug> --stage fix-verify\n```\n\n**Release on the success path AND on the failure path.** A step 5 that errors\nbefore the release leaks the lock and blocks every sibling session until a\ndead-session steal or an operator force-release. Do **not** wrap the release in a\nblanket `trap EXIT` — that would release while a test process may still be live.\n\n`fix-verify` is a **lock label, not a dispatchable stage**: `infra-lock acquire`\nvalidates neither `--slug` nor `--stage`, so it works with no bridge change. It is\ndeliberately absent from `stage_capabilities` so the infra-lock guard leaves it\nunguarded — absence yields rc 2, which that guard reads as \"skip\", so absence is *not*\nwhat stops a dispatch (with a template present the render would succeed). What refuses\nit is `cmd_dispatch`'s fix-stage legality gate, which reads the same rc 2 as \"not a\nroutable fix stage\" for `fix-*` names only.\n\nBuild the verification report — one row per covered issue:\n`issue # | coded ID | symptom | check (command\u002Fmanual) | result | evidence`.\nA cluster that resolves 6 of 7 covered issues closes only the 6; the 7th is dropped\n(see Cluster abort).\n\n**Cheap confirmation before a corrective round.** Before dispatching a corrective\nworker round for a *suspected regression*, re-run the failing test **at least 3x on\nthe fix branch AND at least 3x on the unmodified base**, and compare failure\n**IDENTITY** — which test, which assertion — never failure **counts**. A single run of\neach tree cannot distinguish a regression from a flake. Record all six runs (command,\ntree, failure IDs) in `journal\u002F\u003Cqueue>.md`. Only a failure present on the fix branch\nand absent from the base, by identity, across those runs justifies a corrective\ndispatch.\n\nMeasured, not hypothetical: on #39 one regression call inferred from a single run of\neach tree triggered a 48-minute corrective `fix-code` round that produced no code, and\na rising pass count (401→410 while failures went 4→5) hid the discrepancy from a\ntotals comparison. The 3x re-run would have cost about six minutes.\n\n### 6. Open the PR (you\u002Frunner own this — not the worker)\nOne PR per group, from `fix\u002F\u003Cslug>` to the default branch. The body ends with\n`Closes #…` listing **only** verified issues. On PR open: for each covered issue,\nremove `in-progress`, add `fix-pr-open`, and comment the PR link (tag the comment\n`\u003C!-- forge-runner:\u003Cgroup_id> -->` so reruns update, not duplicate).\n\n### 7. Report and stop\nSummarize groups dispatched, PRs opened, issues dropped\u002Fescalated. **Merging is a\nhuman action** — on merge GitHub auto-closes the issues; the project's daily QA\ntask (`\u003Cproject>-qa-review` \u002F `\u003Cproject>-qa-issue-sync`) then reconciles status,\ntypically adding `needs-retest` and pinging the tester. Do not merge.\n\n## Cluster abort (when a covered issue turns out different)\nIf a covered issue has a different cause (found during fix or failing verification),\nand a branch\u002FPR already exists:\n1. remove that number from the PR's `Closes …` text (edit the PR body);\n2. remove its `in-progress`\u002F`fix-pr-open` label;\n3. comment on the issue explaining the drop from `\u003Cgroup_id>`;\n4. **in execute mode the plan directory is sealed**, so instead of editing the\n   artifact, append the drop and its reason to `journal\u002F\u003Cqueue>.md` — one writer per\n   file, never read by another session, purely for the end-of-run report. (During\n   triage, before approval, editing the artifact is still correct.) The same applies to\n   recording an invalidation reason. Steps 1–3 and 5 are GitHub mutations and are\n   unchanged;\n5. leave the issue open and actionable for a future group.\nNever let a silent `Closes` close an unfixed bug.\n\n## Idempotency\n- Refuse to dispatch a group whose covered issues already carry `in-progress` or\n  `fix-pr-open` (the predicate already enforces most of this).\n- **`in-progress` is applied at APPROVAL time, not at dispatch.** That is what makes a\n  second tab's `tally` refuse instead of producing a rival grouping. It also means a\n  crashed session leaves permanently non-actionable issues — so `queue.py reconcile`\n  is the paired remedy and **ships in the same change**. Reconcile is **owner-based**:\n  a live claim, a live tmux incarnation, an open referencing PR, or a surviving\n  `fix\u002F*` branch each protect the label. **Elapsed time never does** — a full-tier\n  bucket can legitimately run for a day. A foreign-host claim is reported, never\n  auto-removed: its liveness is unverifiable from here.\n  Write the plain label `in-progress` in **every** project; FeedMint's\n  `status:in-progress` is an inbound mirror owned by the QA sync and is read-only to\n  the runner.\n- Run state lives in the sealed artifact (`approval_status`, drops) plus GitHub.\n  Reruns read both; nothing else is authoritative.\n- All runner comments are tagged `\u003C!-- forge-runner:\u003Cgroup_id> -->`; check-before-write.\n\n## Scope guards\n- Local fixing only — no CI (`claude-code-action`).\n- Two tiers only (no `lite`). No stacked PRs. No cross-service grouping (flag + stop).\n- **No parallel TRIAGE.** Exactly one session tallies, scouts, groups and deals. A\n  second grouping of one queue is the failure this guard exists to prevent.\n- **Parallel EXECUTION is permitted**: at most 3 sessions (initiator + 2), one claimed\n  queue each, one bucket at a time per session, **each session in its own git\n  worktree**. Never a second plan for one service concurrently; never two sessions on\n  one queue id; never a bucket split across sessions.\n- Never modify files outside `.dev\u002Fproposals\u002F\u003Cslug>\u002F` and the fix's own source\n  changes — with **one narrow, stated exception**: a session may create\n  `\u003Cplan-dir>\u002Fclaims\u002F\u003Cqueue>.json` and append to `\u003Cplan-dir>\u002Fjournal\u002F\u003Cqueue>.md` under\n  the primary root. It may write **nothing else** there, never another queue's\n  journal, and never any sealed file. An unstated exception is exactly the thing that\n  drifts.\n- You add status labels (`in-progress`\u002F`fix-pr-open`) and runner comments; you never\n  add `forge-fix` and never write to the Google Sheets.\n\n## Maintainer note — deployment topology\n\n*(Operators can skip this section; it is for whoever edits the toolkit.)*\n\n`skills\u002Fforge-fix-runner\u002FSKILL.md` in `forge-toolkit` is the **only** copy of this\ndocument. Each project's `\u003Cproject>\u002F.claude\u002Fskills\u002Fforge-fix-runner\u002FSKILL.md` is a\n**symlink** to it. `scripts\u002F` stays a **physical directory** in every project — the\nPER-PROJECT CONFIG block is the whole point, and a symlinked `scripts\u002F` would erase\nit. That is exactly the verified topology of `~\u002F.claude\u002Fskills\u002Fforge-fix-runner\u002F`\n(SKILL.md symlink, real `scripts\u002F` dir).\n\n**Convert one project first.** Symlink resolution is proven for `~\u002F.claude\u002Fskills\u002F`;\na project-level `.claude\u002Fskills\u002F` is a *different loader path*. Convert\none project, invoke the skill there, confirm it loads. Only then convert the other\ntwo. **Fallback if it does not resolve:** keep physical copies and rely on\n`test_matches_template_outside_config_block()` to catch drift either way.\n\n**The worktree inversion, stated so nobody debugs it twice:** a git worktree gets a\n*physical snapshot* of `.claude\u002F` at creation time, and `shutil.copytree(…,\nsymlinks=True)` **preserves the symlink** — so after conversion a worktree's copy is a\nsymlink pointing at the **primary** checkout. A toolkit edit you are testing inside a\nforge-toolkit worktree is therefore *not* what loads. Edit and test from the primary\ncheckout, or accept that the worktree copy is inert.\n\nProject-specific prose lives in `PROJECT.md` next to each project's copy:\n\n| Project | Repo | `CLASSIFIER_PREFIX` | Extra blocking labels |\n|---|---|---|---|\n| `\u003Cproject-a>` | `\u003Corg>\u002F\u003Crepo-a>` | `service:` | — |\n| `\u003Cproject-b>` | `\u003Corg>\u002F\u003Crepo-b>` | `area:` | `status:in-progress` |\n| `\u003Cproject-c>` | `\u003Corg>\u002F\u003Crepo-c>` | `service:` | — |\n\nProcedure\u002Finvariant changes belong to **all** copies — when you change one, propagate\nto the rest and re-run each `test_queue.py`. Note that git worktrees get a physical\nsnapshot of `.claude\u002Fskills\u002F` at creation time; those snapshots are disposable, and\nrefreshing the parent checkout is what fixes future ones.\n","# Forge Fix Runner\n\nYou turn GitHub Issues labeled `forge-fix` into merged fixes by routing them into\nthe existing forge pipeline. You are the **management layer**; forge is the engine.\nYou do NOT reinvent fixing — you select work, scout it, gate it, and keep issue\nstatus in sync.\n\n> **Project specifics** — repo name, classifier vocabulary, extra blocking labels,\n> the seed set, the readiness probes (including the two verbatim probe commands\n> execute mode needs), and any project-local caveats — live in `PROJECT.md` beside\n> this file. **Read it first.** Everything in *this* document is identical in every\n> project.\n\n**Repo:** set by `DEFAULT_REPO` in the PER-PROJECT CONFIG block of\n`scripts\u002Fqueue.py` (each project's `scripts\u002F` is a real directory, never a symlink).\n**Design of record:**\n`.dev\u002Fproposals\u002Fqa-github-issues\u002Fforge-fix-pipeline-design.md` (read it if anything\nhere is ambiguous). **Queue logic:** `scripts\u002Fqueue.py` (deterministic, tested) —\nrun it with no `--repo` flag; a project copy already points at its own repo.\n\n**Classifier:** issues bucket by a configurable label prefix — `service:*` by\ndefault, but a project may use another (FeedMint uses `area:*`). This document\nsays \"service\"; read that as whatever `CLASSIFIER_PREFIX` your project copy sets.\n\n## Non-negotiable invariants\n\n1. **Closed = actually fixed.** A `Closes #N` is emitted ONLY for an issue with a\n   passing per-issue verification row. No green check → no close keyword. Ever.\n2. **Dry-run is the default.** First pass prints every intended mutation (labels,\n   branches, PR bodies, comments, forge dispatches) and changes nothing. Write mode\n   requires the user to explicitly say \"go \u002F write \u002F execute\".\n3. **`forge-fix` is human-only.** You never add it. You only act on issues that\n   already carry it.\n4. **Status lives only in GitHub.** Labels + open\u002Fclosed + the PR's `Closes` are the\n   single source of truth. Never hand-maintain status elsewhere.\n5. **One branch per group, one PR per group.** Never a shared service branch.\n6. **One worktree per session.** A session executes in its own git worktree and\n   claims at most one queue. Two sessions in one physical root corrupt each other's\n   forge context and pane topology; `queue.py claim` refuses it mechanically with\n   `ROOT_CONFLICT`, comparing `realpath(git rev-parse --show-toplevel)` — never\n   `$PWD`, or two sessions in different subdirectories of one worktree both pass.\n7. **A session never merges, never rebases or resolves another session's branch or\n   conflict, and never force-releases the infra lock.** Integration is the operator's,\n   through `main`, on their cadence.\n\n> **What fan-out buys, stated once and un-skimmably.** With the infra mutex correctly\n> extended, `fix-code`, `fix-qa`, `fix-qa-retry` and step-5 verification **serialize\n> globally across worktrees** — they contend for one shared test database. Expect\n> wall-clock ≈ *(sum of every bucket's infra time)* + *(the heaviest queue's non-infra\n> time)*: roughly **1.5–1.9×** on a full-tier-heavy queue and close to **1×** on an\n> all-quick queue. **Those numbers are an estimate pending timing data, never a\n> contract.** \"The wall-clock of the heaviest queue\" would need a per-worktree test\n> database and per-worktree ports, which are deliberately out of scope. **The\n> delivered value is distribution automation** — nobody hand-deals `g1 … g7` every\n> morning — not throughput. `infra_required` is therefore `True`-only in practice on\n> GoParent today.\n\n## The actionable predicate (what you may pick up)\n\nUse `scripts\u002Fqueue.py` — do not eyeball labels. An issue is actionable iff:\nopen + the trigger label + exactly one classifier label + none of the project's\n`BLOCKING_LABELS` (by default `needs-retest` \u002F `in-progress` \u002F `fix-pr-open`).\n(So an issue carrying `forge-fix` + `needs-retest` is correctly skipped — it is\nalready awaiting retest.) All four inputs come from the PER-PROJECT CONFIG block.\n\n```\npython3 scripts\u002Fqueue.py tally                    # worst-first services, proposes next\npython3 scripts\u002Fqueue.py select --service \u003CNAME>  # the actionable queue for one service\n```\n\n## Procedure\n\n### 0. Preflight (abort on any failure)\n\nWritten for **concurrency**: under fan-out a sibling pipeline is *normal*, and the\ndangerous failure is a tab that \"helpfully\" supersedes a live one. The destructive\nbranches below were written for one-pipeline-at-a-time and must not fire.\n\n- `~\u002Fbin\u002Fforge-bridge identity` — require `identity_state=MATCH` **from a real\n  in-pane probe**. A headless tab in a root with exactly one live session *adopts that\n  session's identity* and would then read and write its context. If you are not in a\n  pane of your own session: **stop**.\n- `~\u002Fbin\u002Fforge-bridge context` — this is **per-session**; everything it shows is your\n  own tab's. **Never offer to supersede. Never act on a `legacy shared context`\n  hint.** A sibling's active slug is not yours to see or to clear.\n- `~\u002Fbin\u002Fforge-bridge infra-lock status` — `HELD live by \u003Cother slug>` is the\n  **expected** state under fan-out. Report it and continue. **Never\n  `release --force`** (invariant 7).\n- `git status` clean, and **not** a detached HEAD. Do **not** require the branch to be\n  `main`: `forge worktree ensure` cuts `forge\u002F\u003Csession>`, so a correctly provisioned\n  fan-out worktree is never on `main` and that check would refuse every assisting tab.\n  What must hold is a clean, non-detached worktree and a fetched remote base that\n  resolves: `git fetch origin && git rev-parse --verify origin\u002F\u003Cbase_branch>`.\n- `gh auth status` shows issue + PR scope.\n- The slug you plan to use does not collide with an existing `.dev\u002Fproposals\u002F\u003Cslug>\u002F`.\n\n### 1. Pick the service\nRun `queue.py tally`. Propose the worst-first service as a one-liner the user can\nredirect (\"Run \u003CSERVICE> next? N criticals\"). Counts are live — never hard-code them.\n\n### 2. Scout (read-only — change no code)\nThe scout is a fixed contract (prompt `~\u002F.config\u002Fforge\u002Fprompts\u002Ffix-scout.txt` + the group\nschema in step 3), so it is **agent-agnostic** — run it either way:\n- **Inline:** spawn a read-only Explore subagent over the service's actionable queue.\n- **Dispatched (e.g. to Codex):** pick a scout slug `\u003Csvc>-scout-\u003Cdate>`; write the\n  actionable issues (numbers, coded IDs, full bodies from `gh issue view`) to\n  `.dev\u002Fproposals\u002F\u003Cscout-slug>\u002Fscout-input.md`; then\n  `~\u002Fbin\u002Fforge-bridge dispatch --slug \u003Cscout-slug> --stage fix-scout --worker codex-a`.\n  The worker writes `.dev\u002Fproposals\u002F\u003Cscout-slug>\u002Frouting-plan.md`.\n\nEither way the scout reads each issue body + the implicated code and produces **groups**,\neach a cluster (several issues, ONE shared root cause) or a singleton. A cluster means\n*one fix resolves all covered issues* — shared root cause, NOT shared service or shared\nsymptom wording. Derive groups from evidence; never assume a service is one cluster.\n(Live caution from GoParent: one service's queue mixed \"section empty\" and \"code\nshowing\" symptoms that turned out to have entirely different causes.)\n\n**Multi-cause issues (many-to-many):** an issue can have several causes, and a cause can\nspan issues. A group's `Closes` may list an issue only if the group fixes ALL its causes;\ndo not carve a quick sub-fix out of a multi-cause issue that also has a full-tier cause\n(fold it into the full group). Group by subsystem; use `drop_conditions` to move uncertain\nissues between groups. (See the design doc's \"Multi-cause issues\" section.)\n\nIf you suspect a **cross-service** root cause, flag it and STOP for the user — do not\nforce it into a service-local group.\n\n### 3. Write the plan DIRECTORY + get approval  (INITIATOR ONLY)\n\n**Location — the PRIMARY root, never your worktree.** Your worktree is itself\ndisposable and gets pruned; the plan must outlive it and resolve identically from\nevery sibling session:\n\n    PRIMARY_ROOT=\"$(dirname \"$(git rev-parse --path-format=absolute --git-common-dir)\")\"\n    PLAN_DIR=\"$PRIMARY_ROOT\u002F.dev\u002Fproposals\u002Fqa-github-issues\u002Ffix-plans\u002F\u003CSERVICE>-\u003CYYYY-MM-DD>\"\n\nThat is the same anchor the infra lock resolves, and it is byte-identical from a\nlinked worktree and from the primary.\n\n    \u003CPLAN_DIR>\u002F\n        plan.md            # deal table + one YAML record per bucket   (human audit; SEALED)\n        deal.json          # CANONICAL mechanical source               (SEALED)\n        packets\u002F\u003Cgid>.md   # one self-contained task packet per bucket (SEALED)\n        manifest.json      # sha256 of every sealed file, written at approval\n        claims\u002F            # MUTABLE — created on demand by `queue.py claim`\n        journal\u002F\u003Cqueue>.md # MUTABLE — append-only, exactly ONE writer per file\n\n**Immutability rule (invariant 4, applied literally).** After approval, `plan.md`,\n`deal.json` and `packets\u002F` are **read-only to every session** and their hashes are\nsealed in `manifest.json`. `deal.json` is **canonical** for anything a claim or an\nexecute-mode decision depends on; `plan.md` is the human artifact. `claim` and\nexecute mode both refuse on a seal mismatch. No session writes per-bucket status\nanywhere. Progress is reconstructed from GitHub: `in-progress` ⇒ claimed,\nunstarted-or-running; `fix-pr-open` + an open PR carrying the bucket's `Closes` set ⇒\ndone; issue closed ⇒ merged. This removes the three-concurrent-writers lost-update\nshape entirely.\n\n**Grouping rules — now load-bearing, not advice.** Two concurrently executing\nbranches must not touch one file, and a bucket can never see another bucket's\nunmerged work:\n\n- **Groups that would edit the same files MUST be merged into one bucket.**\n- **A group that consumes a symbol another group creates MUST be merged into that\n  group.**\n- **`depends_on` must be empty.** A non-empty value is refused at `deal`,\n  `deal --verify`, `claim` and `packet-check`. There is no execution path for it:\n  every bucket branches from a freshly fetched `origin\u002F\u003Cbase_branch>` and no session\n  waits for a merge, so a later bucket branches from a ref that does not contain the\n  earlier one's work. Queue locality supplies **ordering**, not **ancestry** — keeping\n  a chain in one queue would only make the failure sequential instead of parallel.\n  Re-introducing chains needs an explicit merge gate or stacked ancestry; both are out\n  of scope. Do not smuggle one in as queue order.\n\n**Packet contract** — `packets\u002F\u003Cgid>.md` **is** the `problem-statement.md`. Step 4 no\nlonger writes one; it copies this in. YAML front matter first (mechanically validated\nby `packet-check`), verbatim issue bodies after:\n\n    ---\n    group_id · service · tier · slug · branch_name · queue_id · plan_dir\n    base_branch · base_sha\n    covered_issue_numbers · covered_coded_ids · current_labels\n    root_cause_hypothesis (NON-AUTHORITATIVE) · confidence · anchors\n    required_tests · infra_required · effort_budget · close_keywords · drop_conditions\n    verification_targets:   # ONE ROW PER COVERED ISSUE\n      - issue · coded_id · symptom · check · evidence\n    ---\n    VERBATIM `gh issue view` body for every covered issue.\n\n`packet-check` gates the invariant: **every `covered_issue_number` has a verification\nrow, and no row names an issue the bucket does not cover** — an issue with no row\ncould never earn its `Closes`.\n\n**Packet → constraint ledger.** Every fix-pipeline slug needs\n`.dev\u002Fproposals\u002F\u003Cslug>\u002Fconstraints.yml` before its first dispatch; `forge-bridge\ndispatch` refuses a carrier stage without one. In this lane it is **generated, not\nauthored**, because the packet already holds verbatim operator text:\n\n- one row per covered issue, `source: operator`, `source_ref: \"packet:#\u003Cn>\"`;\n- `verbatim` sliced from that issue's body — the sentence stating the obligation, not\n  a summary of it;\n- `check` seeded from that issue's `verification_targets` row;\n- `principle` is the general rule the issue implies; `binds` is the scope it governs,\n  which is expected to be WIDER than the one surface the issue names;\n- `scope: user-visible` only when the issue names a surface a person can reach.\n\n**Slicing a quote out of a free-form issue body is a heuristic, not a parse. Emit\n`source: inferred` on any row you cannot cleanly attribute.** A confident-looking\n`verbatim` that is not what the reporter wrote manufactures exactly the false authority\nthe mechanism exists to prevent — and an `inferred` row is honest, reviewable, and still\ncarried into every stage prompt.\n\nOne record per bucket in `deal.json` (mirrored into `plan.md` prose):\n\n```yaml\ngroup_id:                # \u003Cservice>-\u003CYYYY-MM-DD>-g1\nservice:                 # \u003Cservice>   (the classifier value)\ntier:                    # quick | full\ncovered_issue_numbers:   # [68, 69]\ncovered_coded_ids:       # [\u003CSVC>-012, \u003CSVC>-013]\ncurrent_labels:          # per-issue label snapshot (for idempotency\u002Fabort)\nroot_cause_hypothesis:   # short prose (NON-AUTHORITATIVE for full pipeline)\nconfidence:              # high|medium|low + one-line why\nslug:                    # \u003Cservice>-\u003Cshort-cause-description>\nbranch_name:             # fix\u002F\u003Cservice>-\u003Cshort-cause-description>\nclose_keywords:          # \"Closes #68 #69\"  (emitted to PR ONLY after verification)\nrequired_tests:          # commands the fix must pass\ndrop_conditions:         # when to un-cover an issue from this group\napproval_status:         # pending | approved | dropped\nbase_branch:             # main   \u003C- BARE. `origin\u002F` is added at USE, never stored.\nbase_sha:                # 40-hex sha of origin\u002F\u003Cbase_branch> at grouping time\nanchors:                 # symbol anchors (function\u002Fclass names), never line numbers\ndepends_on:              # [] — MUST be empty (see the grouping rules above)\ninfra_required:          # derived (S6); absent => treat as true\neffort_budget:           # {worker_stages: N, production_files: M} — the PRE-CODE\n                         # budget (see \"Effort budget\" below). Derived from the\n                         # evidence, never from the tier; absent => refuse to deal.\nweight:                  # derived: TIER_BASE[tier] + len(covered_issue_numbers)\nqueue:                   # S1|S2|S3 — operator-editable BEFORE approval only\npacket:                  # packets\u002F\u003Cgroup_id>.md\n```\n\n> **`base_branch` is stored bare.** Store `main`, not `origin\u002Fmain`, and build\n> `origin\u002F$base_branch` at the point of use. Storing the qualified ref produced\n> `origin\u002Forigin\u002Fmain` at every branch cut and every moved-ground check.\n\n**Then deal and check:**\n\n```bash\npython3 scripts\u002Fqueue.py deal --plan \"$PLAN_DIR\u002Fdeal.json\" --sessions 3 --write\npython3 scripts\u002Fqueue.py deal --verify --plan \"$PLAN_DIR\u002Fdeal.json\"\npython3 scripts\u002Fqueue.py packet-check --plan-dir \"$PLAN_DIR\"\n```\n\nPresent the deal table with **per-queue weight totals alongside bucket counts**, so a\nsingle-bucket S3 reads as a heavy bucket and not as a bug. (There are no \"units\":\nchains are refused, not collapsed.) The derived `infra_required` value is shown per\nbucket. The user may flip tiers, split\u002Fmerge groups, drop groups, or move a bucket\nbetween queues — then re-run `deal --verify`. Before approving a 3-way deal, check\nheadroom with `~\u002Fbin\u002Fforge-bridge usage --refresh`; deal to 2 if it is thin.\n\n**The approval transaction — ordered, and every step is idempotent.** Owner-aware\nstatus and `ROOT_CONFLICT` both assume every queue has a claim, so the initiator's own\ntransition cannot be left implicit. Do these in exactly this order; a crash at any\nboundary is recoverable by re-running:\n\n1. Set `approval_status: approved` on the blessed buckets; re-run `deal --verify` and\n   `packet-check`. **Execute ONLY approved buckets.**\n2. `python3 scripts\u002Fqueue.py seal --plan-dir \"$PLAN_DIR\"` — the artifacts freeze.\n3. Apply `in-progress` to every covered issue of every approved bucket\n   (check-before-write, so re-running adds nothing twice). On a **partial failure do\n   not roll back**: record which issues were labelled in `journal\u002FS1.md`, report the\n   failures, and **stop** — do not claim, do not print the tab block.\n   `reconcile --dry-run` is the recovery path; the labels are not lost work.\n4. `python3 scripts\u002Fqueue.py claim --plan-dir \"$PLAN_DIR\" --queue S1 --root \"$(git rev-parse --show-toplevel)\"`\n   — the initiator claims **before** any assisting tab can.\n5. **Only now** print the tab block (step 3b).\n\n`in-progress` at approval time is what makes a second tab's `tally` refuse. It also\ncreates an orphaned-label class if a session dies, which is why `reconcile` ships in\nthe same change and is the documented remedy.\n\n### 3b. Open the assisting tabs — BEFORE you start bucket 1\n\n`forge worktree ensure` refuses to cut a worktree when the source root has **tracked\nmodifications** and a session is live there. `e24e1be` stopped the worktree\n*container* dirtying `git status`, but the refusal keys off\n`git status --porcelain -uno` — tracked changes — which is exactly what you create the\nmoment bucket 1 starts. So tabs 2 and 3 must be opened first. Print exactly this:\n\n```\nOpen BOTH tabs now, before I start bucket 1:\n  \u002Fforge-fix-runner execute \u003CABS-PLAN-DIR> S2\n  \u002Fforge-fix-runner execute \u003CABS-PLAN-DIR> S3\n```\n\nIf a tab is opened late and hits `source has tracked modifications`, the remedy is\n`forge-start --allow-dirty-base \u003Csession>`, and for fan-out that is **correct, not\nmerely tolerated**: the worktree is cut from the resolved base, and an executor\nbranches every bucket from a fresh `origin\u002F\u003Cbase_branch>` anyway, so it must not\ninherit your uncommitted work. The tool's warning exists for the ordinary case, which\nthis is not.\n\n### 4. Execute mode — how an assisting tab enters\n\nEntered **only** as `\u002Fforge-fix-runner execute \u003Cabs-plan-dir> \u003Cqueue-id>`. Both\narguments are required; if either is missing, **stop and ask** — never fall through to\nstep 1.\n\n> **You do not triage. Steps 1, 2 and 3 do not exist for you.**\n\n1. Preflight (step 0). `queue.py verify-seal --plan-dir \u003Cabs>`. Load `deal.json` (the\n   canonical source — not `plan.md`); refuse if the queue id is absent, if the seal\n   does not verify, or if any bucket in the queue is not `approval_status: approved`.\n2. **Claim first, then lock, then check readiness.** This order is not negotiable:\n\n   ```bash\n   python3 scripts\u002Fqueue.py claim --plan-dir \u003Cabs> --queue \u003Cid> --root \"$(git rev-parse --show-toplevel)\"\n   ~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cfirst-slug> --stage fix-verify\n   python3 scripts\u002Fqueue.py readiness --plan-dir \u003Cabs> --queue \u003Cid> --root \"$(git rev-parse --show-toplevel)\" \\\n       --db-probe   \"\u003Csee PROJECT.md>\" \\\n       --import-probe \"\u003Csee PROJECT.md>\"\n   ~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cfirst-slug> --stage fix-verify\n   # readiness FAILED -> release the lock, then\n   #   queue.py release --plan-dir \u003Cabs> --queue \u003Cid> --owner-token \u003Ct>\n   #   print the remediation and STOP. Never execute a queue you cannot verify.\n   ```\n\n   The readiness check is not a formality. On GoParent it exercises `pytest`, whose\n   `conftest.py` runs a session-scoped autouse fixture that terminates every other\n   connection to the shared test database. Running it **before** claiming and\n   **outside** the lock would perform the exact cross-session collision this design\n   exists to prevent — and two tabs in one root can both reach it before\n   `ROOT_CONFLICT` refuses either. It covers **every toolchain the queue uses**, not\n   one command from the first bucket.\n\n   **Both probe commands are project-specific and live in `PROJECT.md`. Use them\n   verbatim.** They exist because of two measured hazards:\n   - **`--db-probe`** — an unseeded worktree does *not* fail to start; it resolves the\n     hardcoded `DATABASE_URL` default in the app config and would silently target a\n     different database.\n   - **`--import-probe`** — a symlinked `.venv` carries an editable-install `.pth`\n     pointing at the **primary** checkout's source. The probe in `PROJECT.md`\n     reproduces what pytest does (`sys.path.insert(0,'src')`); a **bare**\n     `import app` resolves to the primary even on a correctly seeded worktree and\n     would fail the healthy case.\n3. Work the buckets in listed order (4.5).\n4. Drained → `queue.py release`, confirm no open pending, confirm you hold no infra\n   lock, print the summary from `journal\u002F\u003Cqueue>.md`, **stop**. No requesting more\n   work, no idling.\n\n**Recovery — a reopened tab given the same queue id.** `claim` steals the stale claim\nof a dead session automatically. Then, before re-cutting anything:\n\n- Re-derive per-bucket status from `gh issue list` + `gh pr list`; skip buckets that\n  are done (`fix-pr-open` + an open PR carrying their `Closes` set).\n- For the first not-done bucket, **check for an existing `fix\u002F\u003Cslug>` branch first** —\n  a bucket a dead session had started looks identical to an unstarted one, and\n  `git checkout -b` fails outright if the branch already exists. In order:\n  - the recorded root still exists and the branch is checked out there → **reattach**\n    to that worktree and continue;\n  - else `forge recover --dry-run` reports bridge residue → run `forge recover` first;\n  - else `git worktree prune`, then re-cut.\n  **Never silently discard committed or uncommitted work.** If the branch carries\n  commits you did not make, or uncommitted work you cannot attribute, stop and surface\n  it.\n\n### 4.5 Per-bucket protocol (identical for the initiator)\n\n1. `git fetch origin`. If `origin\u002F\u003Cbase_branch>` has moved from `base_sha`, run the\n   **anchor re-check**: every recorded anchor must still resolve (`git grep -n` on the\n   symbol, never the line). Any anchor missing ⇒ **INVALIDATED**: comment on the\n   covered issues, remove `in-progress`, leave them **open and actionable**, append the\n   reason to `journal\u002F\u003Cqueue>.md`, and **continue to the next bucket**. Report\n   invalidations at the end; do not surface mid-run.\n\n   > This is **conservative invalidation**, deliberately — not \"re-scope and carry\n   > on\". Its cost is one re-scout; the cost of the alternative is a wrong `Closes`,\n   > so the check is tuned to over-invalidate. Be honest about its limit: **a symbol\n   > can survive with changed semantics and pass this check.** That is a known\n   > false-negative, not a claim of safety.\n   >\n   > **Full-tier late-overlap checkpoint.** A full-tier investigation can discover\n   > file overlap with a bucket a sibling has *already started*. When it does, stop,\n   > journal it, and surface it — that is a conflict with work being done.\n\n2. `gh issue view` each covered issue; drop from `close_keywords` any that are now\n   closed or already carry `fix-pr-open`. All gone ⇒ skip the bucket (journal it,\n   report it).\n3. `git checkout -b fix\u002F\u003Cslug> \"origin\u002F$base_branch\"` — **always from freshly fetched\n   `origin\u002F\u003Cbase_branch>`, never from the previous bucket's branch.** This is how \"no\n   stacked PRs\" becomes mechanical rather than aspirational. Note the single\n   `origin\u002F`: `base_branch` is stored bare precisely so this is not\n   `origin\u002Forigin\u002Fmain`.\n4. Copy `packets\u002F\u003Cgid>.md` to `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (it already\n   *is* the problem statement); write `fix-plan.md` for the quick tier.\n5. Execute by tier (below). Both tiers run their infra stages under the lock; the\n   bridge refuses them otherwise (`INFRA_LOCK_REQUIRED`, exit 5).\n6. **Step-5 verification under the infra lock (Shape B)** — see step 5.\n7. Open the PR, then move to the next bucket **immediately**. Never wait for the\n   operator's merge.\n\n> **Surface to the operator ONLY on a conflict with work being done** — a\n> rebase\u002Fmerge conflict against `origin\u002F\u003Cbase_branch>`, a bucket whose files an open\n> `fix\u002F*` PR already changed, an infra-lock `TIMEOUT` escalation, or the full-tier\n> overlap checkpoint above. Use `forge ask --session-scope`. Otherwise work silently\n> to the end of your queue.\n\nFor each bucket, by tier:\n\n**quick** — bounded change; the coder stage is **DISPATCHED to a worker pane**:\n\n> **Quick tier is not orchestrator-local, and it never was.** It dispatches\n> `fix-code` to a worker pane exactly as the full tier does; what makes it\n> \"quick\" is the number of worker stages (one), never where they run. Nothing in\n> this tier executes in the runner's own pane. The retired wording \"inside forge\n> via a single pane\" read as orchestrator-local execution, disqualified the quick\n> tier on that basis, and directly caused the documented misroute on #34. Do not\n> reintroduce it.\n\n1. Write `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (verbatim covered-issue bodies +\n   the scout hypothesis labeled *non-authoritative* + `required_tests` + the per-issue\n   verification targets).\n2. Write a **minimal** `.dev\u002Fproposals\u002F\u003Cslug>\u002Ffix-plan.md` (the scout's bounded plan —\n   this is the frozen plan the mechanical coder consumes; \"quick\" still has a plan).\n3. Ask `forge codex-lane --root \"$PROJECT_ROOT\" --stage fix-code --worker codex-a`\n   for the protected lane decision. In the default `contain` rollout, dispatch\n   the coder stage to `claude-opus` (the reviewed-host lane). Use `codex-a` only\n   when the result explicitly says `lane=codex`; never bypass `LANE_REQUIRED`.\n\n   **`fix-code` is an infra stage (Hard Rule 23) — wrap the dispatch in Shape A.**\n   Its capability class is `commit`, so the bridge REFUSES the dispatch outright\n   (`INFRA_LOCK_REQUIRED`, exit 5) unless this session already holds the lock for\n   this exact `(slug, stage)`. This is not optional:\n\n   ```bash\n   ~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cslug> --stage fix-code\n   ~\u002Fbin\u002Fforge-bridge dispatch --slug \u003Cslug> --stage fix-code --worker \u003CW>\n   #   dispatch FAILS -> infra-lock release --slug \u003Cslug> --stage fix-code ; surface ; STOP\n   # wait loop: DONE|ERROR -> release. HOLD through PROMPTING\u002FSTALLED\u002FTIMEOUT\u002FDEAD\u002FBLOCKED.\n   ~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cslug> --stage fix-code\n   ```\n\n   Under fan-out a `WAIT reason=held-by-live-session` here is **expected** — a\n   sibling session is on the shared stack. Wait it out. A `TIMEOUT` is a conflict\n   with work being done, so surface it (see **Surfacing**). Never\n   `release --force` another session's lock.\n\n   Full tier inherits the same protection through the orchestrator, which locks\n   `fix-code` \u002F `fix-qa` \u002F `fix-qa-retry` per Hard Rule 23.\n\n   The selected worker writes `fix-coder-report.md` + test evidence on branch\n   `fix\u002F\u003Cslug>` and requests its exact-path commit through the broker.\n4. **Escalation backstop:** if the worker reports it needs more than the planned files,\n   can't form the minimal plan, or the cause is uncertain, it stops without fixing —\n   remove `in-progress`, re-route the group to **full**, note why in the artifact and\n   an issue comment.\n\n**full** — murky\u002Frisky\u002Fflow-breaking, the whole pipeline:\n1. Write `.dev\u002Fproposals\u002F\u003Cslug>\u002Fproblem-statement.md` (as above; scout hypothesis is\n   routing metadata only — A\u002FB investigators must validate independently, C reconciles).\n2. **Re-tier checkpoint — MANDATORY, not advisory.** After `fix-investigate` returns\n   and BEFORE `fix-plan` is dispatched, read `diagnosis.md`, re-evaluate the tier\n   against it, and append exactly one line to `journal\u002F\u003Cqueue>.md`:\n   `TIER-CONFIRMED full: \u003Creason>` or `TIER-REDUCED full -> quick: \u003Creason>`.\n   The tier chosen at scout time is **provisional by definition** — it precedes the\n   evidence, so it is the one routing decision that must be re-made once evidence\n   exists. Full tier may continue ONLY when the diagnosis shows something the quick\n   tier cannot carry: a genuinely open design decision, a multi-file blast radius, or\n   an unresolved contradiction. **\"The diagnosis was thorough\" is not a reason to stay\n   full.** On `TIER-REDUCED`, stop the full pipeline and finish the bucket on the quick\n   tier, writing `fix-plan.md` from the diagnosis you already have.\n3. Run the forge pipeline for the slug (`forge-pipeline \u003Cslug>` in a forge session via\n   the orchestrator). It produces diagnosis → plan → code → qa on branch `fix\u002F\u003Cslug>`.\n\n> **Effort budget — it bounds the PRE-CODE stages, not the review loop.** Every packet\n> carries `effort_budget`: an expected worker-stage count and an expected\n> production-file count, both derived from the evidence. It governs\n> `fix-investigate`, `fix-plan` and `fix-plan-revise`, because that is where the time\n> actually goes — 132 of #39's 265 worker-minutes were spent in those three stages\n> under no budget of any kind. **Bounding only the `fix-plan-review` ↔\n> `fix-plan-revise` loop does NOT satisfy this**: #39 carried that bound, the bound\n> held, and the run still cost 4.4 hours.\n>\n> Before dispatching any pre-code stage, compare the run's worker-stage count so far\n> against `effort_budget.worker_stages`. A stage that would take the run past its\n> budget is **not dispatched**: STOP and surface to the operator with the numbers —\n> budgeted vs. actual worker stages, budgeted vs. actual production files.\n>\n> **Scope-growth signal.** When `fix-plan.md` names more production files than the\n> diagnosis implicates, the plan has **outgrown its diagnosis**. Surface that with the\n> two counts; never absorb it. On #34 a single sentence demanding executable proof\n> where a text assertion was called for grew the plan from four files to seventeen,\n> and nothing flagged it.\n\n### 5. Per-issue verification (gates the PR) — UNDER THE INFRA LOCK\n\nThis step runs `required_tests` **inline in your own session**, with no dispatch\nand no `wait` to key off — so it takes **Shape B**\n(`forge-orchestrator\u002FSKILL.md`, Hard Rule 23). It is not bookkeeping: this is\nwhere the backend suites actually execute, and GoParent's\n`backend\u002Ftests\u002Fconftest.py` runs a **session-scoped autouse** fixture that\n`pg_terminate_backend()`s every other connection to the shared\nthe shared test database. Two sessions verifying at once corrupt each\nother's failure counts **silently**, and a corrupted failure count is how a\n`Closes #N` reaches an unfixed bug.\n\n```bash\n~\u002Fbin\u002Fforge-bridge infra-lock acquire --slug \u003Cslug> --stage fix-verify\n# run required_tests here; capture evidence paths; build the verification table\n~\u002Fbin\u002Fforge-bridge infra-lock release --slug \u003Cslug> --stage fix-verify\n```\n\n**Release on the success path AND on the failure path.** A step 5 that errors\nbefore the release leaks the lock and blocks every sibling session until a\ndead-session steal or an operator force-release. Do **not** wrap the release in a\nblanket `trap EXIT` — that would release while a test process may still be live.\n\n`fix-verify` is a **lock label, not a dispatchable stage**: `infra-lock acquire`\nvalidates neither `--slug` nor `--stage`, so it works with no bridge change. It is\ndeliberately absent from `stage_capabilities` so the infra-lock guard leaves it\nunguarded — absence yields rc 2, which that guard reads as \"skip\", so absence is *not*\nwhat stops a dispatch (with a template present the render would succeed). What refuses\nit is `cmd_dispatch`'s fix-stage legality gate, which reads the same rc 2 as \"not a\nroutable fix stage\" for `fix-*` names only.\n\nBuild the verification report — one row per covered issue:\n`issue # | coded ID | symptom | check (command\u002Fmanual) | result | evidence`.\nA cluster that resolves 6 of 7 covered issues closes only the 6; the 7th is dropped\n(see Cluster abort).\n\n**Cheap confirmation before a corrective round.** Before dispatching a corrective\nworker round for a *suspected regression*, re-run the failing test **at least 3x on\nthe fix branch AND at least 3x on the unmodified base**, and compare failure\n**IDENTITY** — which test, which assertion — never failure **counts**. A single run of\neach tree cannot distinguish a regression from a flake. Record all six runs (command,\ntree, failure IDs) in `journal\u002F\u003Cqueue>.md`. Only a failure present on the fix branch\nand absent from the base, by identity, across those runs justifies a corrective\ndispatch.\n\nMeasured, not hypothetical: on #39 one regression call inferred from a single run of\neach tree triggered a 48-minute corrective `fix-code` round that produced no code, and\na rising pass count (401→410 while failures went 4→5) hid the discrepancy from a\ntotals comparison. The 3x re-run would have cost about six minutes.\n\n### 6. Open the PR (you\u002Frunner own this — not the worker)\nOne PR per group, from `fix\u002F\u003Cslug>` to the default branch. The body ends with\n`Closes #…` listing **only** verified issues. On PR open: for each covered issue,\nremove `in-progress`, add `fix-pr-open`, and comment the PR link (tag the comment\n`\u003C!-- forge-runner:\u003Cgroup_id> -->` so reruns update, not duplicate).\n\n### 7. Report and stop\nSummarize groups dispatched, PRs opened, issues dropped\u002Fescalated. **Merging is a\nhuman action** — on merge GitHub auto-closes the issues; the project's daily QA\ntask (`\u003Cproject>-qa-review` \u002F `\u003Cproject>-qa-issue-sync`) then reconciles status,\ntypically adding `needs-retest` and pinging the tester. Do not merge.\n\n## Cluster abort (when a covered issue turns out different)\nIf a covered issue has a different cause (found during fix or failing verification),\nand a branch\u002FPR already exists:\n1. remove that number from the PR's `Closes …` text (edit the PR body);\n2. remove its `in-progress`\u002F`fix-pr-open` label;\n3. comment on the issue explaining the drop from `\u003Cgroup_id>`;\n4. **in execute mode the plan directory is sealed**, so instead of editing the\n   artifact, append the drop and its reason to `journal\u002F\u003Cqueue>.md` — one writer per\n   file, never read by another session, purely for the end-of-run report. (During\n   triage, before approval, editing the artifact is still correct.) The same applies to\n   recording an invalidation reason. Steps 1–3 and 5 are GitHub mutations and are\n   unchanged;\n5. leave the issue open and actionable for a future group.\nNever let a silent `Closes` close an unfixed bug.\n\n## Idempotency\n- Refuse to dispatch a group whose covered issues already carry `in-progress` or\n  `fix-pr-open` (the predicate already enforces most of this).\n- **`in-progress` is applied at APPROVAL time, not at dispatch.** That is what makes a\n  second tab's `tally` refuse instead of producing a rival grouping. It also means a\n  crashed session leaves permanently non-actionable issues — so `queue.py reconcile`\n  is the paired remedy and **ships in the same change**. Reconcile is **owner-based**:\n  a live claim, a live tmux incarnation, an open referencing PR, or a surviving\n  `fix\u002F*` branch each protect the label. **Elapsed time never does** — a full-tier\n  bucket can legitimately run for a day. A foreign-host claim is reported, never\n  auto-removed: its liveness is unverifiable from here.\n  Write the plain label `in-progress` in **every** project; FeedMint's\n  `status:in-progress` is an inbound mirror owned by the QA sync and is read-only to\n  the runner.\n- Run state lives in the sealed artifact (`approval_status`, drops) plus GitHub.\n  Reruns read both; nothing else is authoritative.\n- All runner comments are tagged `\u003C!-- forge-runner:\u003Cgroup_id> -->`; check-before-write.\n\n## Scope guards\n- Local fixing only — no CI (`claude-code-action`).\n- Two tiers only (no `lite`). No stacked PRs. No cross-service grouping (flag + stop).\n- **No parallel TRIAGE.** Exactly one session tallies, scouts, groups and deals. A\n  second grouping of one queue is the failure this guard exists to prevent.\n- **Parallel EXECUTION is permitted**: at most 3 sessions (initiator + 2), one claimed\n  queue each, one bucket at a time per session, **each session in its own git\n  worktree**. Never a second plan for one service concurrently; never two sessions on\n  one queue id; never a bucket split across sessions.\n- Never modify files outside `.dev\u002Fproposals\u002F\u003Cslug>\u002F` and the fix's own source\n  changes — with **one narrow, stated exception**: a session may create\n  `\u003Cplan-dir>\u002Fclaims\u002F\u003Cqueue>.json` and append to `\u003Cplan-dir>\u002Fjournal\u002F\u003Cqueue>.md` under\n  the primary root. It may write **nothing else** there, never another queue's\n  journal, and never any sealed file. An unstated exception is exactly the thing that\n  drifts.\n- You add status labels (`in-progress`\u002F`fix-pr-open`) and runner comments; you never\n  add `forge-fix` and never write to the Google Sheets.\n\n## Maintainer note — deployment topology\n\n*(Operators can skip this section; it is for whoever edits the toolkit.)*\n\n`skills\u002Fforge-fix-runner\u002FSKILL.md` in `forge-toolkit` is the **only** copy of this\ndocument. Each project's `\u003Cproject>\u002F.claude\u002Fskills\u002Fforge-fix-runner\u002FSKILL.md` is a\n**symlink** to it. `scripts\u002F` stays a **physical directory** in every project — the\nPER-PROJECT CONFIG block is the whole point, and a symlinked `scripts\u002F` would erase\nit. That is exactly the verified topology of `~\u002F.claude\u002Fskills\u002Fforge-fix-runner\u002F`\n(SKILL.md symlink, real `scripts\u002F` dir).\n\n**Convert one project first.** Symlink resolution is proven for `~\u002F.claude\u002Fskills\u002F`;\na project-level `.claude\u002Fskills\u002F` is a *different loader path*. Convert\none project, invoke the skill there, confirm it loads. Only then convert the other\ntwo. **Fallback if it does not resolve:** keep physical copies and rely on\n`test_matches_template_outside_config_block()` to catch drift either way.\n\n**The worktree inversion, stated so nobody debugs it twice:** a git worktree gets a\n*physical snapshot* of `.claude\u002F` at creation time, and `shutil.copytree(…,\nsymlinks=True)` **preserves the symlink** — so after conversion a worktree's copy is a\nsymlink pointing at the **primary** checkout. A toolkit edit you are testing inside a\nforge-toolkit worktree is therefore *not* what loads. Edit and test from the primary\ncheckout, or accept that the worktree copy is inert.\n\nProject-specific prose lives in `PROJECT.md` next to each project's copy:\n\n| Project | Repo | `CLASSIFIER_PREFIX` | Extra blocking labels |\n|---|---|---|---|\n| `\u003Cproject-a>` | `\u003Corg>\u002F\u003Crepo-a>` | `service:` | — |\n| `\u003Cproject-b>` | `\u003Corg>\u002F\u003Crepo-b>` | `area:` | `status:in-progress` |\n| `\u003Cproject-c>` | `\u003Corg>\u002F\u003Crepo-c>` | `service:` | — |\n\nProcedure\u002Finvariant changes belong to **all** copies — when you change one, propagate\nto the rest and re-run each `test_queue.py`. Note that git worktrees get a physical\nsnapshot of `.claude\u002Fskills\u002F` at creation time; those snapshots are disposable, and\nrefreshing the parent checkout is what fixes future ones.\n",{"name":5,"description":7},"main","50bb36677eced958128cea262c304bf9ceee4d4e","skills\u002Fforge-fix-runner","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit","https:\u002F\u002Fgithub.com\u002FSirTheOracle","2026-10-20 00:50:37",{"source":8,"skillName":5,"commands":50,"prompt":60,"zipAvailable":61},[51,54,57],{"tool":52,"command":53},"npx","npx skills add SirTheOracle\u002Fforge-toolkit --skill forge-fix-runner",{"tool":55,"command":56},"bunx","bunx skills add SirTheOracle\u002Fforge-toolkit --skill forge-fix-runner",{"tool":58,"command":59},"pnpm","pnpm dlx skills add SirTheOracle\u002Fforge-toolkit --skill forge-fix-runner","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-fix-runner\u002FSKILL.md",true,[63,69],{"path":64,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":67,"sourceHtmlUrl":68},"scripts\u002Fqueue.py","SCRIPT","脚本",63177,"https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-fix-runner\u002Fscripts\u002Fqueue.py",{"path":70,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":71,"sourceHtmlUrl":72},"scripts\u002Ftest_queue.py",45345,"https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-fix-runner\u002Fscripts\u002Ftest_queue.py",[74,98,118,139,162,176],{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":38},"01M70FS350DG1MT6NBPYRPBFBQ","forge-orchestrator","tmux-based orchestrator for multi-agent building. Coordinates work across Claude Code and two Codex workers via forge-bridge. Translates user requests into dispatched tasks with structured audit logging. Replaces forge-dispatch, forge-state.yml, and stage-routing-map.yml.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-orchestrator\u002FSKILL.md",[],[19,81,82,83,84,29,85,86,27,87,88,89,90,91,92,93,94,95,96,97],"orchestrator","forge orchestrator","bridge","map","tasks","across","audit","multi-agent","logging","tmux","for","audit logging","workers","stage","with","multi","state",{"skillNo":99,"name":100,"nameZh":6,"description":101,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":104,"publishedAt":38},"01M70FRVVW3JAGQQK3FFR2VYRC","forge-coder","Procedural coding skill. Reads implementation.md, validates diffs against the codebase, applies changes in execution order, runs tests after each commit group, and produces a coder-report.md artifact.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fforge-coder\u002FSKILL.md",[],[19,105,106,107,108,109,110,111,112,113,114,115,116,117],"forge coder","coder","diffs","tests","execution","commit","changes","artifact","procedural","report","codebase","implementation","coding",{"skillNo":119,"name":120,"nameZh":6,"description":121,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":122,"categories":123,"keywords":124,"publishedAt":38},"01M70FRRYF6E22ECC8AGYYJEQ7","fix-reproducer","Optional first stage of the fix-pipeline. Verifies a reported bug is reproducible and captures deterministic repro steps, environment state, and evidence. Single procedural executor — does not spawn sub-agents and does not diagnose. Branches by environment: dev path runs the bug locally; prod path gathers observational evidence from the deployed environment. Always writes repro.md, even on NOT REPRODUCED. Trigger when the user invokes a fix-pipeline with the reproduce step enabled.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Ffix-reproducer\u002FSKILL.md",[],[125,126,18,127,128,129,130,36,131,132,133,134,135,136,137,30,94,95,97,138],"reproducer","fix reproducer","deterministic","evidence","executor","diagnose","Reproducible","environment","trigger","branches","dev","single","repro","first",{"skillNo":140,"name":141,"nameZh":6,"description":142,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":143,"categories":144,"keywords":145,"publishedAt":38},"01M70FRN8THYBT2DJVX99XQ3CM","fix-plan-reviewer","Single-agent adversarial reviewer for fix plans. Reads diagnosis.md and fix-plan.md, verifies the plan actually addresses the diagnosed cause, attacks the plan from multiple angles (cause coverage, defense adequacy, scope correctness, regression risk, rollback integrity, test plan integrity, risk honesty), and produces fix-review.md with severity-rated issues. Does NOT propose fixes for the issues — that is the revise mode of adversarial-fix-plan. Does NOT re-investigate the diagnosis. Trigger as the review stage of a fix-pipeline, after adversarial-fix-plan has produced fix-plan.md.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Ffix-plan-reviewer\u002FSKILL.md",[],[146,18,147,26,148,149,150,151,36,152,153,33,154,155,156,157,158,159,160,161],"fix plan reviewer","reviewer","plan reviewer","coverage","scope","regression","re","mode","review","diagnosis","defense","integrity","addresses","revise","attacks","as",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":166,"categories":167,"keywords":168,"publishedAt":38},"01M70FRHGG6HN0VW483NDKX161","fix-coder","Mechanical single-executor that applies a vetted fix plan to the codebase. Reads fix-plan.md (the Changes, Defenses, and Test Plan sections), translates file-level changes into concrete diffs, applies them, runs the test plan, validates that the bug is no longer reproducible (dev) or that the test exercising the failing path passes (prod), and produces a single fix commit. Stays strictly in scope: does NOT modify the plan, does NOT re-investigate, does NOT touch files outside the plan. Always writes fix-coder-report.md, even on failure. Trigger as the implementation stage of a fix-pipeline, after fix-plan-reviewer has approved fix-plan.md.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Ffix-coder\u002FSKILL.md",[],[18,169,106,129,150,36,152,170,171,107,172,110,147,26,173,174,161,175,131,133],"fix coder","failing","mechanical","files","bug","path","test plan",{"skillNo":177,"name":178,"nameZh":6,"description":179,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":180,"categories":181,"keywords":182,"publishedAt":38},"01M70FREVCBF0YX1FX8WNAQTGP","docs-refresh","Generates and maintains living documentation from the current codebase state. Reads a project-local manifest (.claude\u002Fdocs-refresh.yml) that declares doc targets, sections, source inventories, and refresh boundaries. Uses marker blocks in docs to safely regenerate content without trampling manual prose.","https:\u002F\u002Fgithub.com\u002FSirTheOracle\u002Fforge-toolkit\u002Fblob\u002Fmain\u002Fskills\u002Fdocs-refresh\u002FSKILL.md",[],[183,184,185,186,187,188,189,190,97,191,115,192,193,194,195,196,197],"refresh","docs refresh","docs","project","manual","local","manifest","documentation","targets","blocks","doc","prose","source","content","marker"]