Execution Skills

September 11, 2026 · View on GitHub

Domain: pipeline

Overview

Execution behavior (apply, review, hydrate) is accessed via /fab-continue, which dispatches to the appropriate behavior based on the active stage. The pipeline has 6 stages: intake → apply → review → hydrate → ship → review-pr. Intake is the sole planning/gate stage; apply and review handle execution; ship and review-pr handle integration (PR creation and PR review feedback). There is no tasks stage (qszh) and no spec stage (j6cs) — at apply entry the skill generates a unified plan.md carrying ## Requirements + ## Tasks + ## Acceptance in one pass (written once per apply invocation; skipped on resume when plan.md already exists). The artifact flow is intake.md → plan.md → code; there is no spec.md artifact. /fab-continue also dispatches ship (via /git-pr behavior) and review-pr (via /git-pr-review behavior). /fab-archive exists as a standalone housekeeping skill (not a pipeline stage) for moving completed changes to the archive — it requires hydrate: done but does not require ship/review-pr completion. All execution behaviors in /fab-continue inherit the optional [change-name] argument, which is passed to the preflight script for transient change resolution without modifying .fab-status.yaml.

Status mutations: All .status.yaml progress transitions, plan/acceptance updates, and confidence writes use fab status CLI event commands (start, advance, finish, reset, fail, skip, set-acceptance, set-confidence) via the Bash tool, rather than direct file editing. Invoking the legacy set-checklist errors immediately with a pointer to set-acceptance (qszh). Any fab status <event> <change> tasks invocation errors immediately with "tasks" stage was removed — run ... apply instead. plan.md is now generated at apply entry., and any fab status <event> <change> spec invocation errors with "spec" stage was removed — spec.md is now generated at apply entry. Use "apply". (mirroring the tasks branch in validateStage) (j6cs). The skip event transitions {pending,active} → skipped with forward cascade (all downstream pending stages become skipped). Skipped stages are treated as resolved for progression (like done). Resetting a skipped stage follows normal reset mechanics (skipped → active, downstream cascade to pending). This centralizes validation and ensures atomic writes with last_updated refresh. The driver parameter is optional but skills always pass it. Stage metrics (started_at, completed_at, driver, iterations) are updated automatically as side-effects.

Pull-based bookkeeping (y022): Plan bookkeeping commands (e.g., fab status set-acceptance) are supplemented by fab status refresh, self-healed at the transition seams (fab status advance/finish, fab preflight) — never a harness hook, since a hook fires only in the Claude harness and this is correctness-critical state (see hooks-may-enhance-never-own.md). Refresh is a reliability layer — it catches bookkeeping the agent forgets. For plan.md, refresh does heading-bounded section parsing: counts - [ ] + - [x] items between ## Tasks and the next ## heading for plan.task_count; same between ## Acceptance and the next ## heading for plan.acceptance_count; counts - [x] in ## Acceptance for plan.acceptance_completed. Missing sections leave the corresponding fields untouched (defensive — avoid overwriting valid values with zero on a malformed in-progress write). plan.generated flips to true on first refresh after the first write and remains true thereafter. Refresh reads both intake.md and plan.md; spec.md is not read (a leftover spec.md on disk MUST NOT match, so editing it cannot silently overwrite the authoritative intake confidence) (j6cs) (y022). Writes to legacy tasks.md / checklist.md are bookkeeping no-ops (those files exist only on in-flight migrations and recognizing them would re-populate a checklist: block that does not exist). Skills keep their existing bookkeeping instructions unchanged for agent-agnostic portability (non-Claude-Code agents rely on skill instructions only). All bookkeeping commands are idempotent, so both fab status refresh and the skill running the same command produces no conflict.

Pipeline invocation: /fab-fff runs the full 6-stage pipeline (intake through review-pr); /fab-ff runs intake through hydrate (4 stages). All three pipeline skills (/fab-continue, /fab-ff, /fab-fff) dispatch review to a sub-agent in a separate execution context, producing structured findings with three-tier priority (must-fix / should-fix / nice-to-have). /fab-continue preserves manual rework on failure; both /fab-ff and /fab-fff use autonomous rework with bounded retry ({max_cycles}-cycle cap — the integer from the Max cycles: {N} line under ## Rework Budget in fab/project/code-review.md, default 3 when the file/section/line is absent (c5tr); escalation after 2 consecutive fix-code failures — a threshold that stays hard-coded; stop on exhaustion). Both /fab-ff and /fab-fff have a single identical confidence gate (the intake gate, flat 3.0) (j6cs) and accept --force to bypass it. Only /fab-fff extends past hydrate to invoke /git-pr behavior for the ship stage and /git-pr-review behavior for the review-pr stage. Both accept an optional [change-name] argument, plus mutually-exclusive --light / --full flags that force the lane at the plan-time fork (§ Shared Pipeline Bracket below). Everything the two orchestrators share lives once in the _pipeline.md internal partial (szxd) — see § Shared Pipeline Bracket below; fab-ff.md/fab-fff.md are thin wrappers (parameter table + driver-specific content).

Status-transition ownership (orchestrator/sequencer owns) (fgxx): Intake is the sole context boundary — it is the only stage /fab-continue runs in the main session. /fab-continue (a one-stage sequencer) dispatches every post-intake stage (apply / review / hydrate) as a sub-agent, with no caller-aware conditional (fgxx). The orchestrators (/fab-ff//fab-fff, via _pipeline.md) run the SAME dispatch in their full lane; their light lane — forked once at apply entry on plan task count (§ Shared Pipeline Bracket below) — is the one post-intake foreground execution path: apply task execution and hydrate (and, for /fab-fff, ship and review-pr) run inline in the orchestrator's own context. Review is dispatched in both lanes, and /fab-continue's dispatches stay unconditional either way.

The owning sequencer is a pure sequencer: dispatch block → read returned status/findings → decide proceed / loop / stop. It runs fab agent <stage> -o yaml immediately before each post-intake dispatch, surfaces the resolved provider/model/model_alias/effort plus dispatch: presence (carried into the dispatch prompt and/or echoed in step output — compliance visibility, so a skipped/mis-resolved tier — or a CLI dispatch — is visible rather than silent) (tykw), then branches on the resolved dispatch: key (aetz) (tykw): absent ⇒ native Agent-tool dispatch — the tier applied through two seams (m3d4): the model half via the Agent tool's model param (empty ⇒ omit/inherit; the param is a hard short-alias enum opus/sonnet/haiku/fable, not the full claude-* id in the YAML model key — so the model half is resolved with fab agent <stage> -o yaml, which emits the alias directly on the model_alias key — a deterministic Go-side translation) (yky7) and the effort half via an imperative instruction in the subagent prompt (Operate at `<effort>` reasoning effort for this task.; omitted when empty — the Agent tool has no effort param); present ⇒ the CLI adapter (fab dispatch) per _preamble.md § CLI-Adapter Dispatch — the block prompt on stdin to the mode's entry verb, a blocking fab dispatch wait <change> <stage> --timeout 300 (run as a background command wherever the harness re-invokes the agent on exit, foreground blocking as the cross-harness fallback — push, not poll: turns are spent only when the wait returns), and the five-state machine (running/done/failed/failed (no-result)/orphaned; a review verdict: fail inside a done result is a review outcome, not a dispatch failure; failed (no-result) is never treated as done) — where the model/effort ride the dispatch.command value so the Agent-tool seams do not apply, and there is NO fallback to the provider's interactive_command (tykw). After done the wiring reads the result and reaps, stage-aware: every stage but apply is reaped immediately on done-read, while the apply pane survives until review passes (or the run stops past apply for good) because it is the pane arm's resume target. The call itself stays unconditional and dumb (the whole guard is in Go) and is pane hygiene rather than state cleanup, so .fab-dispatch/ keeps its no-automatic-GC posture (zfl7); the one exception is the deferred apply reap, which is gated on the arm — it fires at a moment every arm reaches, including the native and headless arms that wrote no pane record, and a missing record is one of the two cases reap treats as a real error rather than a no-op, so an ungated call would halt the pipeline right after every passing review. (3d (aetz) wiring against the fixed contract harness-adapters.md, which catalogs three adapters — native Agent-tool / headless CLI / interactive pane; runtime is runtime/dispatch.md.) Pane mode is an option inside the dispatch:-present arm, not a third branch (zxe0): a CLI-dispatched worker may run interactively in a tmux pane — split into the dispatching agent's own window when the dispatcher is a tmux pane on the target server, else a new window named fab-{id}-{stage} — the outer adapter branch (native vs. CLI) still keys on dispatch: key presence alone — the labelled dispatch.rung selects only the entry within the CLI arm — the resolver output is unchanged, the same block prompt content still goes in on stdin, the same blocking wait observes it (a pane dispatch's state comes from the same result-presence-plus-pane-liveness rule status derives), and the wiring handles a three-state subset (running/done/orphanedfailed/failed (no-result) are unreachable with no exit-code channel, and the state strings are byte-identical). What differs is the entry: fab dispatch start launches only the headless arm, and the pane arm is fab dispatch open → the readiness gatefab dispatch deliver (§ The readiness gate). The wiring branches on the labelled dispatch.rungrung: headless goes straight to start, rung: pane straight to open, and rung: pane is what tells the site it is on the pane arm (the deferred apply reap's gate). start's pane-refusal stays as defense-in-depth, not discovery: a pane landing is refused before stdin is read and before any state write, so a mislabelled or stale-environment invocation re-runs as open with nothing consumed. The pane shape (split vs. new window) resolves from the environment with no flag involved, an auto-selected pane descends the dispatch.mode ladder to headless with a stderr notice rather than failing (the mode: headless (descended: pane unavailable: …) selection suffix), and the dispatched …/opened …/delivered … lines name the shape and the selection source. An orphaned pane dispatch is surfaced with fab pane capture [-L <server>] <pane> rather than fab dispatch logs; and steering the worker's pane changes no contract — the worker still owes its result file and terminal refresh, and the orchestrator still owns every transition (ship/review-pr workers excepted — they self-manage their own stage's transitions; _preamble.md § Dispatch-Prompt Obligations). The observation also carries bounded recovery (mnri) — composed OVER those states, adding none: orphaned spends one automatic fab dispatch restart (relaunched from the persisted prompt, mode re-derived from the current environment, so a pane dispatch orphaned by a dead tmux server lands headless), after which the orchestrator re-arms wait; failed gets no automatic restart but the orchestrator MAY spend the same single budget on a clearly-transient log-tail signature; failed (no-result) always escalates and never restarts; and a read-only peek on every timeout-return of fab dispatch wait (--timeout 300, the same ~5-minute cadence) classifies a still-running worker three ways (progressing / parked ⇒ kill+restart in budget / awaiting human ⇒ notify without killing). The budget lives in the orchestrator's context, never on disk (last-attempt-only preserved), escalation is per-mode evidence + a gated rk notify + the stage's existing failure path, and the pipeline never sends keys to a WORKER — its verbs against one are peek/kill/restart/notify/stop/reap, with the pre-delivery pane the single carve-out (§ The readiness gate). Sites reference the canonical procedure in _preamble.md rather than restating either state machine, the pane option, or the recovery policy.

The readiness gate (pane arm): between fab dispatch open and fab dispatch deliver the orchestrator loops fab dispatch ready <change> <stage> and branches on the reported word. ready ⇒ deliver. booting ⇒ wait briefly and re-probe; boot re-probes cost no judgment round, but 5 consecutive booting reports promote the pane to parked so a TUI that never finishes starting cannot spin the loop. parked ⇒ a judgment round: the orchestrator reads the snippet the report carries and MAY answer the wall itself with raw tmux [-L <server>] send-keys, because a pane that has not been delivered to holds no stage context to corrupt — it is not yet a worker, which is the entire basis of the carve-out. The budget is at most 2 rounds per gate, and login/credential walls escalate immediately and are never answered, whatever the budget's state. Exhaustion escalates rather than descending: it surfaces the capture evidence, sends rk notify behind the fail-silent command -v rk gate, stops on the existing failure path, and leaves the pane alive for a human — descent is a pre-launch capability decision, and re-making it here would silently change which adapter ran the stage. From successful delivery onward the ordinary rule applies again: a wall that appears mid-stage escalates — with one qualification: a delivery that never reached the worker (a false-verified delivery) leaves the pane holding no stage context, so the judgment rounds stay legal against it. Detecting that case is the stall guard's job: on a no-progress wait timeout-return (no result file, screen unchanged) the orchestrator judges the captured screen before re-arming — _preamble.md § The pane readiness gate owns the guard (capture-based, read-only, never a probe re-run); it is pointed at, not restated, here. The gate amortizes, since first-run walls are mostly workspace-scoped (trust is per-worktree-path), so the first pass in a checkout clears them for every later pane worker there. _preamble.md § The pane readiness gate owns the rule; runtime/dispatch.md owns the probe's mechanics. This same two-seam + visibility contract governs the whole fab-ff/fab-fff orchestrator family — the shared _pipeline.md bracket's per-stage dispatch note + Steps 1–3 + Auto-Rework re-apply/re-review, and fab-fff.md's full-lane Steps 4–5 (ship/review-pr — preceded by the optional Step 3.5 Linear-link step, which runs inline and resolves no role: see issue-linking.md) — with review resolving once for the single review agent (same model + same effort instruction), exactly like every other stage (pag2). The canonical contract is _preamble.md § Subagent Dispatch → Per-Stage Model Resolution (mirrored in _shared/context-loading.md § Per-Stage Model Resolution); the lone residual is a first-class per-sub-agent effort param on the Agent tool, a harness ask outside fab's control. It owns the fab status transitions (finish/fail/reset) itself; it never reaches into block internals. Every /fab-continue-behavior subagent prompt carries the block-contract carve-out (refined by 3d (aetz)): do NOT run fab status transition commands (start/advance/finish/reset/fail/skip); return results onlybut DO end with a terminal fab status refresh (the 3a pull-based recompute — not a transition, so the sequencer/orchestrator still owns every transition; part of the dispatch-prompt obligations that bind all three adapters — native, headless CLI, and interactive pane — per _preamble.md § Dispatch-Prompt Obligations, where only the delivery mechanism varies (the dispatched prompt / the command's stdin / a prompt file plus a pointer handed over at spawn) while the prompt content is composed identically). This is the universal block contract carried in the dispatch prompt, owned by _preamble.md § Dispatch-Prompt Obligations and referenced by _pipeline.md § Stage Dispatch Procedure, not a per-caller skip rule baked into the block. The block returns results/findings only; it never branches on caller and takes no §Verdict-style decision itself. (For hydrate the orchestrator runs the finish, e.g. fab status finish <change> hydrate fab-ff for the auto path or ... fab-continue for the manual sequencer.) The failure-policy fork is invocation-level, in the orchestrator, not the block: Path A (manual /fab-continue) runs the interactive § Verdict rework menu on the dispatched review block's returned findings; Paths B/C/D (/fab-ff//fab-fff) run _pipeline.md's autonomous Auto-Rework Loop. The block always returns findings; who acts on a fail verdict is the orchestrator's concern — no "skip §Verdict when subagent" flag re-encodes the removed conditional.

Ship and review-pr are the transition exception, but NOT a role-resolution or dispatch-branch exception: /fab-continue's ship and review-pr rows resolve fab agent ship -o yaml / fab agent review-pr -o yaml before delegating — surfacing the required YAML keys and branching on dispatch: key presence exactly like every other dispatched stage (native arm through the two model/effort seams; dispatch:-present arm through the CLI adapter per _preamble.md § CLI-Adapter Dispatch — rung branch, blocking fab dispatch wait, done-read reap under the immediate-reap row; these workers are never named or continued), mirroring /fab-fff's full-lane Steps 4–5 exactly (the caller-invariance invariant — a stage resolves the same role regardless of caller; in the light lane those steps run inline with no YAML stage resolution; /fab-fff alone carries the optional pre-ship Step 3.5 Linear-link step — see issue-linking.md — which /fab-continue does not mirror). Only the transition ownership differs: /git-pr and /git-pr-review self-manage their own stage transitions on every arm, so their dispatched prompts carry the result-file (ship: status/pr_url/summary, plus reason on failure; review-pr: status + four-class outcome success|failure|no-reviews|timeout + summary), context, and terminal-refresh obligations but NOT the block-contract transition prohibition — the self-managing-stages carve-out, owned by _preamble.md § Dispatch-Prompt Obligations — so /fab-continue's ship and review-pr dispatch rows — which (w7dp) pass the resolved change explicitly (/git-pr {name} / /git-pr-review {name}, the transient-override + branch-guard contract) and cite active only (ready is unreachable: ship's AllowedStates are {pending, active, done, skipped}, review-pr's add failed) — run finish only if the stage is still active after the behavior returns. The review-pr Fail branch carries the same only-if-still-active guard (uliv) — git-pr-review's Step 6 normally runs fail itself, so fab-continue runs fail <change> review-pr only when the stage is still active after the behavior returns, never as a second (CLI-rejected) fail. And the review-pr timeout outcome deliberately leaves the stage active — report and stop, no re-finish. This matches _review.md's "verdict transitions remain in each orchestrator's own file" and eliminates textually-mandated double-finish CLI errors.

Review-failed handling (split by invoker) (szxd): progress.review == failed is a deliberate resting state, reached two ways — an exhausted /fab-ff//fab-fff rework loop (the final failure fires fail only, no reset — see § Shared Pipeline Bracket) or an interruption between fail review and reset apply. The two consumer paths differ by invoker: /fab-continue does NOT re-run review — its Step 1 has a review/failed dispatch row (szxd) that runs fab status reset <change> apply fab-continue (the same post-fail reset the Verdict fail path runs — review cascades to pending, apply re-activates) and presents the Verdict-fail rework menu directly, stopping for the user's choice. (The row keys on the progress map via a Step 1 guard — preflight surfaces the parked failure through display_stage/display_state via DisplayStage's failed tier (dkn3), but the derived routing stage lands on the next pending stage, so the progress map is the reliable dispatch key (w7dp).) Orchestrator re-runs of /fab-ff//fab-fff keep the autonomous recovery in _pipeline.md Resumability: fab status start <change> review (the review-specific failed→active transition), then resume from the review step. The dispatch tables prescribe only valid transitions: intake-ready rows read "finish intake (auto-activates apply)" — finish atomically activates the next pending stage, and a separate start apply call would hard-error (start only accepts pending/failed); the review-fail row uses reset <change> apply fab-continue (matching the Verdict section). In the bracket's Step 1, the intake-finish condition is "if progress.intake is not done, finish intake" — /fab-new leaves intake at ready (the normal hand-off state) and finish accepts both active and ready.

Review-pr-failed dispatch + idempotent reset (w7dp): progress.review-pr == failed (a failed PR-review run — gh missing, no PR found, or a processing error) has its own dispatch row, so it never falls through to the "all done" row and reads as complete. fab-continue's Step 1 has a review-pr/failed row, keyed off the progress map via the same guard mechanism as the review row: re-execute /git-pr-review {name} (the explicit change argument) — git-pr-review's own Step 0 fab status start <change> review-pr performs the recovery (the CLI's review-pr start transition accepts failed → active), and its Step 6 terminal routing then applies with the usual only-if-still-active guards. The row deliberately does NOT route through reset: reset's From-set is {done, ready, skipped} — it excludes failed, so the CLI would error. The Reset Flow is idempotent (Constitution III): a target stage already active skips the fab status reset call and proceeds straight to execution (re-running an interrupted reset is a state-wise no-op); a failed target routes to the matching failed dispatch row instead of resetting (start owns failed→active — review/review-pr only); a pending target errors with Stage '{stage}' has not run yet — nothing to reset. The Step 4 event-command list documents reset as done/ready/skipped → active, matching status.go (k4ge), and the apply-entry intake.md-missing error points at /fab-continue intake (the intake reset, whose flow actually regenerates the intake) instead of plain /fab-continue (which would re-enter apply and loop into the same error). fab-continue's Error Handling recovery commands stay argless by design — the change reference of the current invocation is implied (active, or fab-continue's own [change-name] override).

Pipeline orchestrator: /fab-proceed is a context-aware orchestrator that detects the current pipeline state and runs the minimum prefix steps needed before delegating to /fab-fff. It accepts no arguments or flags — everything is inferred from context. It does not run preflight or load preamble; it delegates those concerns entirely to /fab-fff. State detection follows a 5-step pipeline: (1) active change check via fab resolve --folder --or-none — branching on the (none) token (dow0): (none) means no active change, any folder name means one is active, and a non-zero exit is a real error (surface per the failure rule), never the no-active-change signal; (2) branch check via git branch --show-current compared to the resolved change folder name (runs only when Step 1 found an active change); (3) conversation classification as substantive or empty/thin (substantive = contains at least one of technical requirements, design decisions, specific values, or problem statements — anything else is empty/thin; this is a single classifier, no separate "thin but non-empty" tier); (4) unactivated intake scan of fab/changes/ (excluding archive/), retaining the full candidate list sorted by date-descending; (5) dispatch decision that combines Steps 1–4. Steps 3 and 4 are order-independent and both run whenever no active change was found in Step 1 — conversation context is NOT gated on intake absence. The dispatch table maps 7 state combinations to step sequences:

Active change?Branch matches?ConversationUnactivated intake?Relevant?Steps
YesYes/fab-fff only
YesNo/git-branch/fab-fff
NoSubstantiveNone_intake/fab-switch/git-branch/fab-fff
NoSubstantive≥1Clearly relevant/fab-switch/git-branch/fab-fff
NoSubstantive≥1Not clearly relevant_intake/fab-switch/git-branch/fab-fff (emit bypass notes)
NoEmpty/thin≥1/fab-switch/git-branch/fab-fff (pick by date-recency)
NoEmpty/thinNoneError: "Nothing to proceed with — start a discussion or run /fab-new (or /fab-draft) first."

The create-new rows run a micro-change backstop first (slpa): before dispatching _intake, the conversation's described change is evaluated against the micro criteria owned by fab-new.md § Step -1 (a pointer — the criteria are not restated in fab-proceed.md, per the owner-or-pointer rule). When all three criteria hold and the conversation carries no explicit go-ahead (e.g., "use fab anyway"), the skill STOPs with a gate-failure-style direct-fix message — the zero-prompt posture is preserved (no interactive prompt), no state is created, and the skill does not perform the fix itself; a re-invocation after an explicit go-ahead dispatches normally, and no argument or flag exists for it. Resume and active-change rows never evaluate the backstop. The create-new rows then dispatch the shared _intake Create-Intake Procedure ({questioning-mode} = promptless-defer) — not the full /fab-new skill — and chain /fab-switch/git-branch (3xaj). _intake stops at intake ready and does NOT activate or branch (activate + branch are /fab-new's Steps 10–11 tail, kept at the call site per the EXTRACTION BOUNDARY), so /fab-switch (activate) + /git-branch (branch) are required to reach the active-and-branched end state. The /fab-switch-prefixed relevant-intake rows and the branch-mismatch row keep /git-branch for the same reason they always did (switching activates a change but creates no branch); the git-branch Dispatch section's "runs when" claim matches the table.

Relevance assessment: When conversation is substantive AND ≥1 unactivated intake exists, the skill scores each candidate's topical relevance. Relevance is judged by reading each candidate's title, ## Origin, ## Why, and ## What Changes sections — not the slug alone (slugs are terse and routinely misrepresent content). Clearly relevant requires shared topic + overlapping terminology + consistent scope; partial or vague overlap does NOT qualify. If multiple candidates are equally clearly relevant, date-descending prefix (sort -t- -k1,1r | head -1) is used as the tiebreak — this is the ONLY use of date-recency in the substantive branch. In the empty/thin + ≥1 intake branch, date-recency is the primary pick (no relevance check runs), preserving the "resume yesterday's draft" flow; this is safe because an empty/thin conversation carries no competing signal.

Asymmetric-bias rule: When a candidate's relevance is genuinely ambiguous (neither clearly relevant nor clearly unrelated), it MUST be classified as not clearly relevant, falling through to /fab-new. Failure modes are asymmetric: false-positive (activating an unrelated draft) corrupts the draft and conflates features in pipeline output — recovery requires manual rollback; false-negative (creating a new intake when the draft was relevant) leaves the draft intact — the user sees the bypass note and can run /fab-switch {name} to recover. Biasing toward the recoverable failure is the design intent. Relevance judgment is inline LLM judgment — no new fab subcommand, classifier, or embedding index.

Bypass notes: When the skill dispatches the create-new path (_intake) (3xaj) despite ≥1 unactivated intake being present (substantive + not clearly relevant row), each scanned draft is surfaced as a line in the output using the exact wording Note: unactivated draft {name} exists — not relevant to current conversation, left untouched. Multiple notes appear in date-descending order and are emitted BEFORE any step reports (e.g., Created intake:, Branch:), so the reader sees context before action. No notes are emitted on the activation paths (clearly-relevant row, empty/thin + ≥1 intake row) or when no drafts were scanned.

Each prefix step (the _intake Create-Intake Procedure, /fab-switch, /git-branch) is dispatched as a subagent via the Agent tool per _preamble.md § Subagent Dispatch, and each resolves a role by name before dispatching: /fab-switch and /git-branch resolve fab agent fast -o yaml; the _intake create-intake dispatch resolves fab agent default -o yaml. So no prefix-step dispatch runs at a merely-inherited session model — this is why fast is multi-referent (ship stage + these prefix steps). The final /fab-fff delegation is invoked via the Skill tool in the current context (not as a subagent), since it is the terminal operation and its output should be visible to the user. /fab-proceed does not pass --force or any flags to /fab-fff. The skill is idempotent — re-running detects which steps are already complete and skips them. The skill remains zero-prompt: ambiguous relevance is resolved by the asymmetric-bias rule, never by asking the user. The create-an-intake sub-operation dispatches the shared _intake Create-Intake Procedure with {questioning-mode} = promptless-defer (the subagent reads .agents/skills/_intake/SKILL.md) (3xaj) — /fab-proceed's state-detection decides whether to create an intake; _intake performs it; activate/branch is the separate /fab-switch/git-branch chain on the create-new rows (see the dispatch table above). The dispatch is promptless and promptless-defer is the defer-and-surface contract (w7dp), encoded in the called helper: the procedure asks NO questions — would-be-asked SRAD Unresolved decisions are recorded in the intake's ## Assumptions table as Unresolved rows with Rationale Deferred — promptless dispatch, returned in the subagent result, and surfaced by /fab-proceed as informational lines before the /fab-fff delegation; the intake gate is the structural backstop (a single Unresolved row zeroes fab score, failing the gate) — see planning-skills.md § Promptless Dispatch and § The _intake Shared Create-Intake Procedure, and _srad.md's Critical-Rule carve-out. When conversation context synthesis feeds the _intake dispatch, the skill extracts decisions, rejected alternatives, constraints, and specific values from the live conversation only — bypassed drafts are NOT mined for synthesis content. Skill file: $(fab kit-path)/skills/fab-proceed.md.

Skill inventory — change creation: Two skills handle change creation: /fab-new (create + activate, description: "Start a new change — creates the intake, activates it, and creates the git branch." plus the shared micro-change anti-trigger sentence carried byte-identically by all four routing skills' descriptions (slpa) — see planning-skills.md § Micro-Change Backstop; skill file: $(fab kit-path)/skills/fab-new.md) and /fab-draft (create without activating, description: "Create a change intake without activating it.", skill file: $(fab kit-path)/skills/fab-draft.md). /fab-new calls fab change switch "{name}" after advancing intake to ready, so the change is immediately active and .fab-status.yaml is created as a side effect. /fab-draft performs the identical Steps 0–9 (create folder, initialize .status.yaml, generate intake, advance to ready) but does NOT call fab change switch and does NOT create the .fab-status.yaml symlink — both /fab-new and /fab-draft are thin call-sites over the shared _intake Create-Intake Procedure ({questioning-mode} = interactive): /fab-new adds its Steps 10–11 activate/branch tail, /fab-draft stops at ready (3xaj). See planning-skills.md § The _intake Shared Create-Intake Procedure. The Next: line for /fab-new reads /fab-continue, /fab-fff, /fab-ff, or /fab-clarify (no activation preamble); the Next: line for /fab-draft includes the activation preamble: /fab-switch {name} to make it active, then /fab-continue, /fab-fff, /fab-ff, or /fab-clarify. Both skills appear under the "Start & Navigate" group in fab help. Both carry a Key Properties Idempotent? declaration: re-running with an already-used backlog/Linear ID routes to resume instead of erroring, while a natural-language re-run intentionally creates a new change — see planning-skills.md § Re-Run Semantics (Idempotency).

Status confidence display: /fab-status shows confidence read from the persisted .status.yaml confidence block (intake is the sole scoring source (j6cs)). It displays Confidence: {score} of 5.0 (...) — there is no (indicative) qualifier (j6cs). When no confidence data exists, it falls back to Confidence: not yet scored. fab score --check-gate --stage intake emits count fields (certain, confident, tentative, unresolved) and compares against the flat 3.0 intake gate (there is no spec branch).

PR review handling: /git-pr-review is a standalone autonomous skill that processes GitHub PR review comments from any reviewer — human or bot. It drives the review-pr pipeline stage, integrating with statusman for stage tracking (start/finish/fail) and phase sub-state tracking via stage_metrics.review-pr.phase (values: received, triaging, fixing, pushed, replying). The detection flow checks for existing reviews: check GET /reviews for existing non-PENDING reviews with inline comments via GET /pulls/{number}/comments — if found, proceed directly to fetch all comments across all reviewers; if no reviews with comments exist, Phase 2 (Copilot-only) runs. Phase 2 is Copilot-only: attempt gh pr edit {number} --add-reviewer copilot-pull-request-reviewer; on success, poll gh pr view --json reviews every 30 seconds for up to 10 minutes (20 attempts); when Copilot review appears, process it via Step 3+; if 10 minutes elapse with no review, print Copilot review requested but not yet available. Re-run /git-pr-review to process when ready.

Phase 2 poll discipline + two-login query semantics (260615-qg64): two distinct logins govern Phase 2 and conflating them is the #1 cause of a poll that never sees a review that in fact landed. The value passed to gh pr edit --add-reviewer is copilot-pull-request-reviewer, and that same string is the landed-review author login: once a Copilot review lands, the review object in the reviews array carries author.login == "copilot-pull-request-reviewer" (commonly surfaced as copilot-pull-request-reviewer[bot]). The entry that surfaces under the PR's requested_reviewers (the request side) carries the different login Copilot. So the landed-review detection predicate MUST match author.login == "copilot-pull-request-reviewer" (the review-author login on the reviews array) — gh pr view {number} --json reviews -q '.reviews | map(select(.author.login == "copilot-pull-request-reviewer")) | length'NOT Copilot (a predicate keyed on the request-side Copilot login never matches a landed review object and the poll times out spuriously even though the review arrived). This is deliberately-set behavior (the Phase 2 .author.login filter (n30u) — "Copilot" in requested_reviewers vs "copilot-pull-request-reviewer[bot]" in reviews) (u1m1). Confirming the request itself succeeded is a separate question and MUST use REST requested_reviewers (gh api repos/{owner}/{repo}/pulls/{number}/requested_reviewers) — GitHub's GraphQL reviewRequests field omits bot/app reviewers like Copilot, so a GraphQL request check would falsely report no request. The poll cadence is 30s × 20 (10-minute window; Copilot lands ~4.5–6.5 min, comfortably inside). The poll runs synchronously to completion: when /git-pr-review runs as a dispatched subagent (e.g. from /fab-fff Step 5), the subagent MUST NOT yield, return, or hand back control while the poll is pending — it stays in the loop until a review appears or all 20 attempts are exhausted, then proceeds to Step 3 or the timeout exit. This is a permanent directive: the subagent stalled or died mid-poll 4× in prior efforts, leaving review-pr stuck active; the correct behavior is patience-to-completion, not an early return. /fab-fff Step 5's dispatch prompt mirrors this don't-yield directive into the dispatch seam (the poll stays owned by /git-pr-review, not relocated to the orchestrator — see § Shared Pipeline Bracket). On poll timeout (all 20 attempts exhausted with no landed review), print the re-run message (which names the explicit <change> when one was passed in Step 0) (w7dp) and route to Step 6 with outcome timeout — the review-pr stage is deliberately left active (no finish, no fail: finishing would mark the stage done with the requested review unprocessed, and start cannot reactivate a done stage; the re-run picks up the still-active stage); on non-zero exit from the gh pr edit command, print No automated reviewer available. Run /git-pr-review when reviews are added. and route to Step 6 with outcome no-reviews (clean finish). Copilot is the only reviewer Phase 2 requests; the pre-ship holistic read belongs to the single review agent in _review.md, which does it itself during the review stage (iqju) (pag2). The Copilot toggle lives in fab/project/code-review.md § Review Tools (tykw): /git-pr-review reads the copilot bullet there; absent section / absent bullet = enabled. A --tool flag forces a specific reviewer (valid values: copilot only) and force-overrides the toggle. If code-review.md § Review Tools lists copilot: false, Phase 2 skips immediately with "No automated reviewer available." (There is no review_tools.copilot config key — see _shared/configuration.md § code-review.md.) Comments are triaged with a disposition intent: fix (will change code — reply confirms with description and short SHA), defer (valid concern, out of scope — reply includes reason), skip (nitpick, stale, or not applicable — reply includes reason), or informational (no reply). Replies use past-tense to confirm outcomes: Fixed —, Deferred —, Skipped —. The skill states this taxonomy once (szxd): Step 4's classify-and-assign list defines the dispositions, the Disposition Reference table is the single reply-format source (Step 5.5 composes from it, preserving the 7-char-SHA + description detail for fix), and the Rules section carries only the four general lines (fully autonomous; fail-fast except declared best-effort steps; targeted fixes only; the umbrella idempotency line). After commit and push (Step 5), a new Step 5.5 posts reply comments via REST API (POST /pulls/{n}/comments with in_reply_to) for each comment with a disposition. Reply posting is best-effort — failures are logged but do not abort the skill. The step also runs when no code changes were made (all deferred/skipped) to close the communication loop. Deduplication on re-run checks existing replies for Fixed —/Deferred —/Skipped — prefixes to avoid posting duplicate replies. The phase sub-state replying is set before posting replies. Commit messages are reviewer-aware: single reviewer → fix: address review feedback from @{username}, multiple reviewers → fix: address PR review feedback. Comment fetching uses GET /pulls/{number}/comments with --paginate for high-comment PRs, fetching id and node_id fields alongside comment data. GitHub's REST API does not expose thread resolution state on individual comments — all non-reply comments are processed regardless of resolution. The skill is idempotent — re-running after fixes finds no new modifications and exits cleanly; re-running after replies skips already-replied comments. Split commit/push failure semantics + unpushed-commit re-run gate (g8st): Step 5 handles its two failure modes separately — a commit failure runs git reset to clear staged changes, prints the error, and STOPs (no partial state — true for a failed commit); a push failure keeps the commit (git reset cannot undo it, and discarding it would lose the fixes), prints the push error plus recovery guidance (git pull --rebase && git push, then re-run /git-pr-review — naming the explicit <change> when one was passed in Step 0, since an argless re-run would resolve the active change instead) (w7dp), and STOPs without posting replies — no Fixed reply may cite an unpushed SHA. The complementary re-run gate: before Step 5 may declare "No changes needed" on a clean tree, it checks for unpushed commits (git log --oneline @{u}..HEAD, a missing upstream counts as unpushed); if any exist, it pushes them first (push failure there re-applies the push-failure handling) and proceeds to Step 5.5 with replies citing the now-pushed SHA — the stranded-fix scenario ends with the commit on the remote. Step 6 is the exit point for every terminal path after Step 0, with two exceptions — Steps 1, 2, and 4 route their terminal conditions to it with a named outcome from the four-class set {success, failure, no-reviews, timeout}. The exceptions STOP directly without reaching Step 6 (uliv): Step 1.5 (invalid --tool value — stops before any review processing) and Step 5 (commit failure — stops after git reset, no partial state; push failure — stops keeping the commit, with recovery guidance and no replies posted; the re-run's unpushed-commit gate completes the cycle). After Step 6 finishes the stage, Step 6.5 commits the review-pr status bookkeeping (the .status.yaml + .history.jsonl writes produced by fab status finish), mirroring git-pr's Step 4c: gated to the success / no-reviews path (not the fail or timeout paths — neither runs finish) AND on a change resolved in Step 0 (active or explicit) — the Step 6/6.5/phase fab status gates key on that resolution, not specifically the active change (w7dp) — guarded by git diff --cached --quiet for idempotency, committed as Update review-pr status, and pushed best-effort — a transient push failure is reported but does not STOP the skill or fail the stage (a deliberate softening of git-pr's fail-fast push, fitting the terminal stage; the local commit is retained for a later reconciling push).

Never hand-merge a generated index.md / log.md (xu0k): the PR-feedback path is where branch↔main divergence and conflict resolution actually surface, so /git-pr-review carries the FKF §5 never-hand-merge pointer: on any merge conflict in a generated docs/memory/**/index.md or log.md (encountered during a git pull --rebase for a push failure, or resolving PR feedback), MUST NOT hand-merge — resolve the conflicts in the topic files (and .status.yaml/log.seed.md seed inputs) only, re-run fab docs-index docs/memory, and take its output wholesale. fab docs-index docs/memory --check at review-pr backstops staleness. This is the same prose-only pointer /git-pr carries at its 3a-bis sub-step; the normative home is fkf.md §5 (+ shipped extract). See memory-docs/templates.md § Blocking Content Class + Never-Hand-Merge Generated Indexes.

Explicit change argument + branch-matches-change guard (w7dp): both ship-pipeline skills accept an optional explicit <change> argument. When provided, Step 0 resolves it via fab change resolve <change> — a transient override (.fab-status.yaml untouched) used for ALL status transitions and artifact paths. An explicit argument that fails to resolve is a hard STOP (Cannot resolve change '<change>'. — a named-but-unresolvable target is a caller error, never a silent fall-back to the active change); argless resolution failure keeps the silent {has_fab} = false degradation. Argument classification is value-based: in /git-pr, an argument matching one of the 7 PR type words is the <type>, anything else is the change reference — callers SHOULD pass the change folder name rather than a bare 4-char id (an id spelling feat/docs/test would classify as a type; a folder name {YYMMDD}-{XXXX}-{slug} never matches a type token), which is why /fab-fff Steps 4–5 dispatch /git-pr {name} and /git-pr-review {name} (the folder name from preflight, defined in the wrapper's parameter notes — the optional Step 3.5 /fab-issue invocation likewise keys on {name}) instead of {id}. In /git-pr-review, --tool and its value are consumed first, so a tool value can never be misread as a change. Both skills also gained a branch-matches-change guard in Step 0, running BEFORE any status mutation (git-pr's before Step 0a's fab status start; git-pr-review's before its Step 0 start): the current branch MUST match the resolved change — exact folder-name equality OR the folder name as a substring of the branch (the same two-form match the former git-pr Step 1b nudge used) — and on mismatch the skill STOPs with the mismatch report and recovery guidance (/git-branch, /fab-switch, or pass the intended change explicitly); no autonomous checkout. An empty branch (detached HEAD) routes to each skill's detached-HEAD STOP instead of a confusing empty-name mismatch — git-pr's detached-HEAD STOP also fires in Step 0 before Step 0a's mutation (verify-before-mutate parity with git-pr-review). This guard closes the wrong-change ship class — a pipeline run driving a <change-name> override while ship/review-pr mutate the ACTIVE change's status and push whatever branch is checked out (see the Design Decision below) (w7dp). docs/specs/architecture.md § Git Integration states the matching state-bookkeeping vs ship-path split — see change-lifecycle.md § Git Integration.

PR shipping: /git-pr drives the ship pipeline stage, integrating with statusman for stage tracking (start/finish). All PRs are created as drafts (gh pr create --draft) — this is unconditional with no configuration toggle. Developers mark PRs ready for review manually after inspecting the agent-generated implementation. The ship stage has no failed state — git-pr fails fast and the user retries. Statusman calls are best-effort (silently ignored on failure). After PR creation, Step 4 executes three sub-steps in order: 4a (record PR URL via fab status add-pr), 4b (finish ship stage via fab status finish — best-effort), 4c (commit and push both .status.yaml and fab/changes/{change-name}/.history.jsonl to git). All status mutations (4a, 4b) occur before the commit boundary (4c), ensuring no uncommitted fab state files remain after PR creation completes. /git-pr resolves change context exactly once, in a unified Step 0 (accepts the optional explicit <change> argument) (szxd) (w7dp) producing four variables — {has_fab} (did fab change resolve succeed), {name}, {has_intake}, {change_type} — which Steps 0a/0b/1/2/3c/4a–4c consume (there is no Step 1b); later steps MUST NOT re-run fab change resolve. The Step 0b and Step 3c step names are load-bearing — _cli-fab.md and prmeta.go cite them by name.

Post-commit docs-index refresh — sub-step 3a-bis (o203) (ugde): /git-pr Step 3 carries a #### 3a-bis. Refresh Memory Indexes sub-step positioned between 3a (Commit) and 3b (Push) — the only pipeline position where git log reports the change's own content commit. Its job is log.md, a freeze-on-write projection of committed git history: it must capture this change's own entry while the change's commit is still reachable (pre-squash). The hydrate-stage regen (Step 5) runs entirely pre-commit, so it cannot see the change's own commit; 3a-bis closes that gap immediately after 3a commits, before 3b pushes. The index does not participate: it carries no dates (a pure function of content) (ugde), so its regen half is a reliable no-op and log.md is the sole reason 3a-bis exists. It runs fab docs-index docs/memory (byte-stable; writes only docs/memory/ index + log files) and, when docs/memory/ changed (git diff --quiet -- docs/memory exits non-zero), stages git add docs/memory and makes a SEPARATE docs: refresh memory indexes follow-up commit — never --amend (keeps 3a's authored content commit intact and reviewable; squash collapses the pair on merge anyway). It is gated on BOTH {has_fab} (the Step 0 variable) AND 3a-having-just-committed-this-invocation (the has_uncommitted path ran) — skipped entirely otherwise, so it is a silent no-op on the "already shipped" / no-change re-run paths and when /git-pr runs standalone outside a fab project ({has_fab} false → general-purpose standalone use unaffected). The git diff --quiet -- docs/memory guard suppresses an empty follow-up commit when nothing drifted (Constitution III idempotency); a regen-or-commit failure → report + STOP, leaving the 3a content commit intact (a failed refresh degrades to a benign stale log.md recoverable by re-running fab docs-index docs/memory — never a torn state). There is no push inside 3a-bis — 3b ("if has_unpushed or just committed") pushes both the content commit and the index-refresh commit together. The step lives in ship, not hydrate, precisely because hydrate is entirely pre-commit (no in-hydrate regen can see the change's own commit); this is the in-skill {has_fab}-gated route, chosen over stage_hooks.ship.post. 3a-bis is skill prose only — a caller of the unchanged fab docs-index docs/memory.

Never hand-merge a generated index.md / log.md (xu0k). The 3a-bis index-refresh sub-step carries the FKF §5 never-hand-merge rule: generated docs/memory/**/index.md + log.md are single-writer, byte-stable artifacts, so on any merge conflict in one, /git-pr MUST NOT hand-merge — resolve the conflicts in the topic files (and .status.yaml/log.seed.md seed inputs) only, re-run fab docs-index docs/memory, and take its output wholesale as the resolution. Hand-merging a generated index is exactly the failure that propagates a corrupted row across branches. The pointer is prose-only (no Go/CLI change); the normative home is fkf.md §5 (+ its shipped extract src/kit/reference/fkf.md), which also documents the non-normative, documentation-only .gitattributes merge-driver recipe (not auto-installed). See memory-docs/templates.md § Blocking Content Class + Never-Hand-Merge Generated Indexes. Steps 3a-bis and 4c hold the full guards; the Key Properties row points to them.

## Meta body-retrofit onto an existing OPEN PR — sub-step 3d (t54n): /git-pr injects the ## Meta block only on PR create (Step 3c), so an OPEN PR authored off-pipeline (or created before fab adopted the branch) carries a body with no ## Meta. Step 3d closes that gap on the existing-OPEN-PR path — the path that otherwise only records the URL. It is the ship-stage Meta retrofit /fab-adopt relies on (Step 5 of that orchestrator), but it is general: any OPEN-PR ship benefits. It fires only when {has_fab} AND the PR was already OPEN at Step 1 (pr_state == OPEN — i.e. Step 3c did NOT just create it; a freshly created PR already carries Meta from 3c, so retrofitting would be redundant). Idempotency guard: if the PR body already contains a ## Meta heading, it makes no edit and continues (a second run is a no-op — Constitution III), keeping the "already shipped" re-run a clean no-op. Otherwise it renders the block by reusing Step 3c's mechanismMETA=$(fab pr-meta "{name}" --type {type} --issues "{issues}") — and applies it by prepending Meta above the existing body via printf '%s\n\n%s\n' "$META" "$existing_body" | gh pr edit --body-file - (stdin, avoiding shell-escaping the body). On empty/failed fab pr-meta (no fab context) it omits the block, exactly as the create path's {has_fab} = false fallback does.

Ship-pipeline git-state hardening (g8st): /git-pr verifies git state before mutating, instead of assuming an attached, main-defaulted, clean world:

  • Detached-HEAD STOP: Step 1 captures the current branch; an empty git branch --show-current means a detached HEAD (git symbolic-ref -q HEAD exits 1). Step 2's detached-HEAD guard — checked before the default-branch guard — STOPs with Cannot ship from a detached HEAD — check out a branch first (run /git-branch). before any commit or push. The detached-HEAD STOP also fires in Step 0, before Step 0a's fab status start, on the {has_fab} path (verify-before-mutate; Step 2's guard still covers the no-fab path) (w7dp).
  • Resolved default-branch guard: Step 1 resolves {default_branch} via git symbolic-ref --short refs/remotes/origin/HEAD (stripping origin/), falling back to gh repo view --json defaultBranchRef -q .defaultBranchRef.name, falling back to treating literal main/master as the default when both fail (the literal check is the documented last fallback, not dead text). The Step 2 guard uses the resolved name — PR creation from the default branch is blocked even when it isn't named main. (Convention shared with the operator — see change-lifecycle.md § Git Integration.)
  • Expected-area staging guard (no more git add -A): Step 3a's guard evaluates FIRST, before anything is staged, so its STOP path leaves no staged index (verify-before-mutate). Untracked files (git status --porcelain ?? lines) outside the expected write areas — each source_paths entry from fab/project/config.yaml, plus docs/ and fab/ (absent config.yaml degrades to docs/ + fab/ only) — cause a STOP listing the offending files. Otherwise the step stages tracked changes with git add -u and explicitly adds the in-area untracked files. An autonomous run cannot sweep unrelated untracked files (scratch files, logs) into a pushed commit.
  • PR-state branching: Step 1 reads the state, number, and url fields from gh pr view --json number,state,url. Step 3 branches on pr_state explicitly: OPEN → existing behavior (short-circuit "already shipped" when nothing else to do); CLOSED → treated as no PR for creation purposes — Step 3c creates a fresh PR (gh pr create works after a closed PR; shipping intent is explicit, /git-pr was just invoked); MERGED → STOP at Step 3 entry, before any commit/push, with PR #{number} for this branch is already merged — {url} and new-change/branch guidance. Only an OPEN PR takes the "already shipped" no-op short-circuit; the Key Properties idempotency row conditions the re-run no-op on it.

PR type system: /git-pr supports 7 PR types (feat, fix, refactor, docs, test, ci, chore) derived from Conventional Commits. Types are resolved via a four-step chain: explicit argument → read from .status.yaml → infer from fab change intake → infer from diff. The type controls the PR title prefix ({type}: {issues} {title} when the change has linked issues — space-joined from fab status get-issues, see issue-linking.md{type}: {title} otherwise). The PR body has two layers: an agent-generated ## Summary + optional ## Changes (prose synthesized from intake.md), and a mechanically-rendered ## Meta block. Title derivation uses intake heading when available, commit subject otherwise, regardless of type.

Mechanical ## Meta block via fab pr-meta (rj31): The entire ## Meta block — the 5-column top table (Change ID | Type | Confidence | Plan | Review), the Impact table + caption, the optional **Issues**: line, and the **Pipeline:** line (six fixed-order stages with per done stage and hyperlinked intake/apply labels) — is rendered deterministically by the fab pr-meta <change> --type <type> [--issues "<space-joined IDs>"] subcommand, whose stdout /git-pr Step 3c pastes verbatim (omitting the block on non-zero exit / empty stdout, matching the legacy {has_fab} = false path). Element order (pnao): heading → top table → Impact table + caption → optional Issues → Pipeline (LAST). Each table/paragraph is blank-line separated so GitHub renders them distinctly. The top table's first header is Change ID; a present id is backtick-wrapped (e.g. `zq9x`), the empty-fallback stays bare. **Pipeline:** carries the colon inside the bold span. Impact is one normalized, self-labeling table (pnao): a single Impact | +/− | Net table — the compact +/− is the column header only, while the data rows carry spaced +A / −B figures; the first column header is Impact, so the table needs no separate **Impact**: lead-in line. Numeric columns are right-aligned. It carries the fixed taxonomy raw / true / impl / tests / excluded where raw = true + excluded and true = impl + tests. true is ALWAYS the post-exclude diff (the bold row, always present) — the label never flips meaning. The table adapts by DROPPING rows, never reshaping: the raw row is shown whenever true_impact_exclude is configured (Excluding != nil, even when raw equals true — a configured-but-no-op exclude still earns its own row; a deliberate user decision, see the Unified-PR-Template Design Decision below), and is omitted only when no excludes are configured (Excluding == nil), where true is definitionally identical to raw so no redundant duplicate row appears; nested └ impl / └ tests rows appear only when a tests pair is present. Below the table a <sub> (small-text, NOT italic) provenance caption co-locates the excludes note and the fab-kit binary version stamp — <sub>excludes fab/, docs/ · generated by fab-kit vX.Y.Z</sub> (excludes clause omitted when none configured; vdev rendered honestly on dev builds). Emphasis stays bold-only and the caption uses <sub> because GitHub's Markdown sanitizer strips style/class (so row backgrounds / colored numbers are impossible) but keeps <sub> on its HTML allowlist. Type resolution (Step 0b), issue gathering (Step 1), and the agent-generated Summary/Changes stay skill-side; the progress token remains ✓ body — meta + summary + changes. fab pr-meta is self-contained — it reads .status.yaml, parses plan.md task checkboxes, reads config (true_impact_exclude, test_paths, project.linear_workspace), computes impact via internal/impact, and resolves git/gh context (branch, owner/repo, merge-base) itself; the skill passes only <change>, --type, and optional --issues. The Meta block's determinism lives in Go (tested via golden-output tests), so there is no per-run drift. gh failure degrades to plain-text Pipeline labels; a missing merge-base drops only the Impact block — never a hard /git-pr failure. /git-pr does not call fab impact directly (the subcommand remains public for other consumers). Blob URLs use https://github.com/{owner}/{repo}/blob/{branch}/... to resolve against the feature branch instead of main. See schemas.md for the true_impact render-time impl residual and the fab pr-meta CLI reference for the full signature, output contract, and exit codes.

PR change metadata: Change identity and linked issues are part of the mechanically-rendered ## Meta block (see "Mechanical ## Meta block via fab pr-meta" above). The Meta table's Change ID cell carries the 4-char change ID from .status.yaml (backtick-wrapped when present; bare when absent); the optional **Issues**: line (rendered only when the skill passes a non-empty --issues, gathered via fab status get-issues — the same issues array the _intake pull path and /fab-issue write, see issue-linking.md) renders each ID as a Linear hyperlink https://linear.app/{linear_workspace}/issue/{ISSUE_ID} when project.linear_workspace is configured, bare comma-joined IDs otherwise, positioned below the Impact table + caption and above Pipeline (pnao). Missing fields show . When the change cannot be resolved, fab pr-meta exits non-zero and the entire Meta block is omitted.

Requirements

Stage-Conditional Helper Loading (/fab-continue)

fab-continue.md's frontmatter is helpers: [_srad]; _generation and _review load at their point of use via explicit in-body read instructions (the stage-conditional loading mode in _preamble.md § Skill Helper Declaration (zc9m); see _shared/context-loading.md):

  • _generation is read only when generating an artifact: at apply entry when plan.md needs generating (Plan Generation Procedure), AND on the rare intake-active regeneration path (the dispatch row instructs reading it before regenerating a missing intake.md — intake generation also lives in _generation.md).
  • _review is read only when entering Review Behavior.
  • Hydrate, ship, review-pr invocations and apply-resumes (with plan.md present) load neither — saving 8.7–19.2KB per invocation on those paths.
  • _srad stays unconditional: apply records graded SRAD assumptions on the dominant path, and intake-stage backward-compat work also grades.

fab-ff/fab-fff deliberately keep [_generation, _review, _srad, _pipeline] as unconditional frontmatter helpers — their auto-rework loop has the orchestrator itself editing plan.md ## Requirements/## Tasks/## Acceptance, which genuinely needs _generation at orchestrator level. (_pipeline is the wrappers' entire body — unconditional by construction (szxd).)

Shared Pipeline Bracket (_pipeline.md)

The shared /fab-ff//fab-fff orchestration lives once in src/kit/skills/_pipeline.md (szxd) — an internal partial following the _generation/_review conventions (frontmatter user-invocable: false / disable-model-invocation: true, orchestration-note preamble) and the sixth allowed helpers: value (see _shared/context-loading.md). It is parameterized by {driver} (fab-ff | fab-fff — passed to the fab status event commands the bracket shows it on and used in re-run guidance; the fail/recovery commands are deliberately driver-less, and the bracket's own parameter description and both wrappers' parameter rows carry this scoped claim) (w7dp) and {terminal} (hydrate | review-pr), declared in each wrapper's two-row parameter table. The bracket holds: pre-flight (intake prerequisite + the single flat-3.0 intake gate, skipped on --force), context loading, the subagent-dispatch note (incl. the block-contract carve-out refined by 3d (aetz): do NOT run fab status transition commands — start/advance/finish/reset/fail/skip; return results only — but DO end with a terminal fab status refresh <change>, per _preamble.md § Dispatch-Prompt Obligations), Resumability (skip done stages; review-failedfab status start <change> review, then resume from the review step), Steps 1–3 (apply → review → hydrate), the Auto-Rework Loop, the exhaustion stop, and the Shared Error Handling rows with {driver}-parameterized messages. The loop's cycle cap is the {max_cycles} knob (c5tr), defined once at the top of _pipeline.md § Auto-Rework Loop: the integer from the Max cycles: {N} line under ## Rework Budget in fab/project/code-review.md (already in the always-load layer), defaulting to 3 when the file, the section, or the line is absent. Only the cap is configurable; the escalation threshold (2 consecutive fix-code attempts) is fixed. Every cycle-count mention in the loop, the stop, the stop-summary template, the shared error rows, and the wrappers' Purpose lines references the knob instead of hard-coding 3. The wrappers keep only driver-specific content: fff's Steps 4–5 (ship/review-pr incl. the timeout outcome — their subagent prompts deliberately do NOT carry the no-fab status instruction, since git-pr/git-pr-review self-manage transitions) and each driver's Output block and error rows (ff adds none).

Inline plan co-gen + the one-time lane fork (two-lane bracket): at apply entry the bracket co-generates plan.md inline in the orchestrator's own context (per _generation.md's Plan Generation Procedure) in BOTH lanes; in the full lane the dispatched apply worker then receives the finished plan through the plan-exists seam (its Plan Generation sub-step skips because plan.md already exists — task execution only). Large-scope guard: when the intake's affected scope is obviously large, the orchestrator MAY skip inline co-gen and dispatch apply-with-co-gen exactly as before — graceful degradation to the shipped path, with the criterion left to orchestrator judgment. Immediately after co-gen the orchestrator forks once on the number of task entries in plan.md ## Tasks (all phases): ≤ 5 → LIGHT lane, > 5 → FULL lane, threshold hardcoded at 5 (a light_max_tasks config knob is a recorded follow-up). Per-invocation --light / --full flags (mutually exclusive — passing both is a usage error) skip the count check. The lane is decided once and never revisited mid-run — there is no promotion valve; scope growth discovered during rework rides the rework backstop. The lane is also never persisted: on any resume that skips co-gen (including progress.apply: done, where Step 1 is skipped entirely) the orchestrator re-derives the lane deterministically from the existing plan's task count, with re-passed flags taking precedence. Both state machines gain zero states and zero transitions — the same finish/fail/reset choreography fires in the same order in both lanes and the review cycle-count invariant holds verbatim; v1 is skill-prose only (no Go, .status.yaml schema, or config-registry change).

Light-lane execution loci: in the LIGHT lane, apply task execution (Step 1) and hydrate (Step 3) run inline in the orchestrator's own context, following the same /fab-continue Behavior sections a dispatched worker follows — plus, for {terminal} = review-pr only, the optional Step 3.5 Linear link (inline in both lanes — see issue-linking.md) and ship + review-pr (the driver's Steps 4–5), which is today's standalone /git-pr / /git-pr-review path: those skills keep managing their own stage transitions exactly as standalone. Inline stages skip YAML stage resolution and run on the session model (an undispatched stage MAY report the configured profile but MUST NOT switch the session model — _preamble.md § Per-Stage Model Resolution); inline review-pr also removes the subagent yield-seam hazard the fff Step 5 synchronous-poll directive exists to fight. Review (Step 2 and every re-review) stays a fresh dispatched worker in BOTH lanes — reviewer independence is never traded away (see § Design Decisions). Light-lane rework stays inline under the same {max_cycles} budget and the same per-cycle fail+reset choreography (item 3 runs the rework inline instead of re-dispatching); exhaustion parks review: failed exactly as in the full lane, and a parked light run re-enters however the user chooses, including --full. Worker continuation (_preamble.md § Worker Continuation) is a FULL-lane-only concern — in the light lane the orchestrator IS the apply author and remembers what the reviewer rejected.

Per-cycle rework choreography (stated once, f071): every cycle of the Auto-Rework Loop runs the same five-item sequence — (1) the status pair fab status fail <change> review then fab status reset <change> apply {driver}, repeated on every failed review verdict that starts a new cycle, not just the first, so all conforming runs leave the same .status.yaml history shape (stage_metrics.review.iterations feeds PR meta); (2) triage + exactly one rework action (fix code / revise plan / revise requirements per the decision heuristics — disjoint (w7dp): code-fails-a-correct-requirement → fix code; the-requirement-itself-is-wrong-or-drifted → revise requirements; each failure description appears exactly once across the three bullets); (3) re-dispatch apply resume-first — reach the existing apply worker by its arm's own mechanism (the native handle apply-{id}, or the still-live apply pane via fab dispatch deliver … --prompt-file) with the item-2 instructions, otherwise dispatch a fresh /fab-continue Apply Behavior subagent (same prompt contract) — in the light lane the rework instead runs inline in the orchestrator's own context (no dispatch, no continuation — the orchestrator is the apply author) — then fab status finish <change> apply {driver}; (4) dispatch a fresh /fab-continue Review Behavior subagent — never reuse a prior review subagent's context; (5) verdict — pass finishes review and proceeds; fail starts the next cycle or exhausts.

Apply-worker continuation (native and pane arms — full lane only) (tv3g, 3oz7): the auto-rework-capable orchestrators keep their apply worker alive across rework cycles instead of paying a cold start per cycle — a continued worker still holds the always-load layer, intake.md, plan.md, the affected memory, and the source files it just wrote, and it remembers what the reviewer already rejected. This apparatus is full-lane-only: in the light lane the orchestrator IS the apply author, so there is no worker to continue. The two resumable arms differ only in how the worker is reached — a named in-context handle on the native arm, a still-live tmux pane on the pane arm — and share every rule that matters: apply-only scope, a mandatory fresh-dispatch fallback, profile fixity, and reviewer independence. _preamble.md § Worker Continuation owns the mechanics; _pipeline.md carries only the wiring (Step 1 naming, item-3 resume-first, the release rule) and points at that owner.

  • Naming (native): the native-branch apply dispatch from an auto-rework-capable orchestrator (/fab-ff, /fab-fff, and /fab-adopt as a partial consumer of the loop) passes name: "apply-{id}", {id} being the 4-char change ID (e.g. apply-tv3g). The pane arm names nothing extra — the pane itself is the handle, which is why the wiring defers its reap.
  • Continuation (native): a later cycle sends the item-2 triaged findings and chosen rework action to apply-{id} (SendMessage) instead of spawning a fresh agent, and instructs the worker to RE-READ from disk every artifact the orchestrator edited at item 2 — always plan.md — because its in-context copy predates those edits. The message re-states the block contract (results only, no fab status transition command, terminal fab status refresh <change>) and deliberately does NOT re-carry the standard subagent context files, which the worker already holds (see _shared/context-loading.md § Standard Subagent Context).
  • Continuation (pane): a pane worker never exits on completion — it writes its result and sits at its prompt — so the worker the native arm keeps in memory is here simply still on screen, and continuation is the same verified delivery step pointed at a different prompt. The orchestrator writes the continuation prompt to .fab-dispatch/{id}/apply-continuation.md under the native arm's content rules verbatim, then runs fab dispatch deliver <change> apply --prompt-file …. A verified delivery clears the previous cycle's result file so the dispatch reads running again and the next wait observes the new cycle rather than returning on the old result; a delivery that never verifies restores it, which is what keeps the fallback executable without a kill.
  • Fallback: reachability is established by attempting the send (native) or the delivery (pane) — there is no separate probe, and a failure is the unreachable signal. Every unreachable case dispatches fresh per the ordinary Stage Dispatch Procedure with the full dispatch-prompt obligations, so a broken resume path degrades to ordinary fresh-dispatch behavior rather than to a pipeline failure (Constitution III). Native: the orchestrator session was resumed or restarted so handles did not survive; the harness has no named-agent/SendMessage capability; the send errors; the worker was never named. Pane: the pane was reaped or killed, the record is not pane-mode, the choreography exhausted its retry, or deliver refused a still-mid-stage worker — the fresh path there is a full open → gate → deliver. A fresh fallback dispatch re-establishes the handle wherever its arm's naming capability exists.
  • Profile fixity: a resumed worker keeps the model and effort of its first dispatch; fab agent apply -o yaml is NOT re-run on the resume path, on either arm. Resolution runs — and is surfaced — only on fresh dispatches, initial or fallback.
  • Release: once review passes (item 5's finish review) or the loop stops at exhaustion, the orchestrator stops continuing the apply worker. On the native arm release is passive — no teardown call exists or is needed, the handle is simply never used again. On the pane arm this moment is exactly where the deferred fab dispatch reap <change> apply fires, gated on the apply launch having landed on pane. Hydrate and every later stage always dispatch fresh in the full lane (in the light lane they run inline — see the lane paragraphs above).
  • Scope guard: continuation exists only for the apply worker inside the auto-rework loop. Review workers are never named, never resumed, and never delivered into — item 4's fresh-worker rule is reviewer-independence design and is untouched (pag2). Within the CLI-adapter branch, headless is non-resumable by decision.

Cycle-count invariant + baseline convention (260615-qg64): _pipeline.md's Auto-Rework Loop states the iteration-counting invariant explicitly — a choreography property, not a state-machine one (the state machine is correct as-is). stage_metrics.review.iterations is exactly the number fab pr-meta renders as "{N} cycle(s)" (prmeta.go reviewCell), and it is advanced by exactly one event: a review transition to state == "active" (in applyMetricsSideEffect, internal/status/status.go, Iterations++ fires only under the active case). The reset apply in item 1 cascades review → pending, which the Go layer treats as iterations-preserving — the same function's pending/skipped case clears only the timing fields and never increments or zeroes the counter. Therefore the only thing that advances the counter is the finish apply auto-activation of review at item 3, so the choreography MUST drive exactly one review → active re-entry per rework cycle (via item 3) and MUST NOT re-enter review by any other path nor rely on reset to bump/zero the counter — re-entering review by a non-active path (or skipping the finish apply after a trivial fix) is the under-count bug that collapses a multi-cycle run to "1 cycle". Baseline convention (oracle: TestStageMetrics_IterationsAccumulateAcrossReworkCycles): iterations counts the initial review entry plus each rework re-entry — i.e. the total number of review → active transitions. So an initial review attempt + N rework cycles leaves iterations == N+1, rendered as "{N+1} cycle(s)" (e.g. an initial fail + 2 rework cycles → iterations 3 → "✓ 3 cycles", never "✓ 1 cycle"). See schemas.md § Cascade preserves iterations for the state-machine half.

Exhaustion terminal state: after the {max_cycles}-th cycle's re-review fails (default 3), the orchestrator runs fab status fail <change> review only — no trailing reset — leaving review: failed (apply remains done) as the defined terminal state, then stops with the per-cycle summary pointing at /fab-continue. That is exactly the resting state /fab-continue's review-failed dispatch row consumes (reset apply + rework menu — see the overview's review-failed paragraph), making the stop message truthful. The stop guidance is override-aware and executable as written (w7dp): the summary points at /fab-continue <change> (naming the change — the run may have been driving a non-active override, and an argless invocation would resolve the ACTIVE change), and the alternative intake-deepening route is /fab-continue <change> intake then /fab-clarify <change>, plus a delete-plan.md note where plan regeneration is the intent (the documented force-regeneration mechanism — plan.md is preserved on reset). A bare /fab-clarify intake is not executable — intake parses as a change-name substring and clarify is stage-guard-blocked post-intake — so no recovery site cites that form (w7dp); fab-continue's own interactive sites use the argless forms (the change reference of the current invocation is implied, active or override). The bracket's gate-fail guidance also names the change (Run /fab-clarify <change> to resolve, then re-run /{driver} <change>.), and the task-fail error row reads re-run /{driver} <change>.

Apply Behavior (via /fab-continue)

/fab-continue dispatches apply behavior to a sub-agent when the active stage is apply (always dispatched post-intake (fgxx); the sequencer owns the surrounding fab status transitions). Apply runs as two sub-steps inside that single dispatch: (1) Plan Generation sub-step — writes plan.md; skipped on resume when plan.md already exists; (2) Task Execution sub-step — parses plan.md ## Tasks and runs tasks in dependency order, running tests after each completed task. Apply MUST ignore the ## Acceptance section — that is review's surface.

Plan Generation Sub-Step (apply entry)

On first apply invocation (when plan.md does not yet exist), the agent reads .agents/skills/_generation/SKILL.md at this point of use (stage-conditional — not a frontmatter helper of fab-continue (zc9m)) and runs the Plan Generation Procedure from _generation.md: reads intake.md, generates ## Requirements FIRST (RFC-2119 statements with stable R# IDs + GIVEN/WHEN/THEN scenarios, from the intake-derived design), then walks those requirements once and emits paired Task + Acceptance entries into the same plan.md with stable headings ## Requirements, ## Tasks (consumed by apply), and ## Acceptance (consumed by review). Trace annotations are REQUIRED (j6cs): each task carries <!-- R# -->; each acceptance item names its R#. No [NEEDS CLARIFICATION] markers — under-specified points become graded SRAD assumptions in ## Assumptions. Acceptance items use A-NNN IDs in newly generated plans. The procedure includes a one-release legacy spec.md ingestion path (fold a leftover spec.md into ## Requirements if present and plan.md lacks them). fab status refresh recomputes .status.yaml plan block from plan.md (counts, generated: true), self-healed at the next advance/finish/preflight. On subsequent /fab-continue calls (resume), plan.md already exists and this sub-step is skipped — task execution resumes from the first unchecked item.

The apply precondition is intake.md MUST exist (used to generate requirements + plan). There is no spec.md or tasks.md precondition (j6cs) (qszh).

Pattern Extraction

Before executing the first unchecked task, the agent reads existing source files in the areas the change will touch and extracts: naming conventions, error handling style, typical structure, and reusable utilities. These patterns are held as context for all subsequent task execution. If config.yaml defines a code_quality section, its principles are loaded as additional constraints and test_strategy governs test timing (default: test-alongside). Pattern extraction is skipped when resuming mid-apply.

Task Execution Sub-Step

  1. Parse plan.md ## Tasks (everything between ## Tasks and the next ## heading or EOF) for unchecked items (- [ ])
  2. Execute tasks in dependency order
  3. Respect parallel markers [P]
  4. For each unchecked task:
    1. Read source files relevant to this task
    2. Implement per spec, constitution, and extracted patterns
    3. Prefer reusing existing utilities over creating new ones
    4. Keep functions focused — consider extracting if implementation exceeds the codebase's typical function size
    5. Write tests per code_quality.test_strategy (default: test-alongside)
    6. Run tests, fix failures
    7. Mark [x] immediately (apply does not touch the ## Acceptance section)
  5. Update .status.yaml progress after each task; fab status refresh also reflects task count changes from plan.md, self-healed at the next transition seam

Resumability

Apply behavior is inherently resumable. If the agent is interrupted mid-run, re-invoking /fab-continue picks up from the first unchecked item. The markdown checklist is the progress state — no separate tracking needed. The plan-generation sub-step is idempotent on plan.md presence — if a partially-checked plan.md exists, generation is skipped and execution resumes immediately. To force plan regeneration, the user MUST delete plan.md before re-running.

Context

Loads: config, constitution, specs/index.md, plan.md (when it exists), intake.md (requirement-generation input), relevant source code (files referenced in tasks), neighboring files for pattern extraction.

Review Behavior (via /fab-continue)

/fab-continue dispatches to review behavior after apply completes. Review behavior is defined in _review.md (a shared internal skill file following the same pattern as _generation.md). fab-continue.md delegates to _review.md rather than inlining review dispatch logic — the same delegation pattern used for planning stages — and reads .agents/skills/_review/SKILL.md at this point of use (stage-conditional (zc9m); not a frontmatter helper). For /fab-ff//fab-fff, the shared bracket (_pipeline.md Step 2) (szxd) notes _review.md as the authoritative source for review dispatch instructions (both wrappers keep it as an unconditional frontmatter helper — see § Stage-Conditional Helper Loading above). _review.md's trailing note points the rework loop at fab-continue.md's Verdict section (manual) and _pipeline.md § Auto-Rework Loop (ff/fff) (szxd).

Review Mode (full | diff-only) (t54n) (pag2): _review.md is the single authority for review dispatch, so the orchestrator MAY pass a mode parameter — inherited uniformly by /fab-continue, /fab-ff, /fab-fff, and /fab-adopt. mode selects whether the merged prompt carries the plan-conformance steps (there is only one review agent) (pag2). full (the default when the param is omitted) carries both checklists: the plan-conformance steps AND the holistic-diff focus areas. diff-only (used by /fab-adopt; see § /fab-adopt) carries the holistic-diff focus areas alone — the plan-conformance steps are omitted (nothing for them to validate on a reverse-engineered thin plan). mode gates two things: the Preconditions (plan.md MUST exist with ## Tasks/## Acceptance populated, all tasks [x]) are checked only in full — in diff-only they are skipped entirely (the agent reads the diff directly, not plan.md); and the plan-conformance steps inside the merged procedure are included only in full. The pass/fail rule is identical for both modes — "any must-fix → fail", and zero findings passes, so an empty diff-only result (the worker judged the diff and found nothing worth reporting) passes best-effort (adoption must not hard-block on a clean holistic read). No speculative plan-conformance-only mode is added — no caller exists (parsimony).

Single-agent dispatch (pag2): the review stage dispatches exactly ONE review sub-agent. The dispatched review block IS the single review agent: the worker reads _review.md at entry and runs the whole merged review inline itself — there is no nested Agent-tool dispatch inside the review block, no parallel dispatch, and no separate findings-merge step. This makes native Agent-tool dispatch and CLI fab dispatch structurally identical for review (one worker runs the whole review inline, so there is nothing to degrade). The review procedure carries no reviewer-selection machinery of its own: pre-ship reviewer choice is a configuration decision (agent.profiles.review.provider, per-session via FAB_AGENT_PROFILES), and post-ship cross-vendor eyes come from Copilot at review-pr. (Why exactly one agent — see the Single Review Agent Design Decision below; why selection lives in config — see Reviewer Diversity Lives Outside the Review Block.)

The single agent's prompt carries both checklists:

  • Plan-conformance steps (full mode only): validates the implementation against plan.md's ## Requirements/## Tasks/## Acceptance. Performs the checks below (acceptance-item inspection, scoped test runs, requirements spot-check, memory-drift check, code-quality check, parsimony pass, deletion-candidate prompt).
  • Holistic-diff focus areas (both modes): a holistic diff review with full repository access — the git diff of all changed files (git diff <base>...HEAD vs the default-branch merge-base), the changed-file list, and full read access. The worker performs this read itself — no external reviewer CLI is probed or spawned. Focus areas: interface contract violations, inconsistencies with documented patterns in memory files, missing cross-references, behavioral regressions requiring full-repo context, and structural issues (duplication, abstraction violations).

Framing line (carried verbatim in _review.md, which the worker reads): "conformance to plan.md is necessary but not sufficient; also judge the diff on its own merits against the repo." There is no read-prohibition and no phase-ordering on plan.md — the worker MAY read anything in any order (plan.md rides in the diff regardless, so an ordering instruction would be flaky). Keep the merged prompt lean (checklist-fatigue mitigation): the tasks-all-[x] check is Precondition-covered (in full mode) and not re-verified as a checklist step.

Context: Standard subagent context files (per _preamble.md § Standard Subagent Context) plus, in full mode, change-specific files: plan.md (its ## Requirements is the behavioral reference), relevant source files, target memory file(s) from docs/memory/, and the change's change_type — carried in the dispatch prompt by the sequencer (which reads it from .status.yaml, since fab preflight does not emit this field; the parsimony/deletion-candidate skip conditions key on it). (spec.md does not exist and is not loaded.)

Precondition (full mode only): plan.md MUST exist with ## Tasks and ## Acceptance sections populated, and all ## Tasks items [x]. If the ## Acceptance section is missing, review STOPs with plan.md missing Acceptance section.

Validation Checks (plan-conformance, full mode only)

The single review agent performs these plan-conformance checks in full mode (the tasks-all-[x] check is a Precondition, not repeated as a step):

  1. All acceptance items in plan.md ## Acceptance verified and checked off — inspects relevant code/tests per A-NNN item, marks [x] in place. fab status refresh propagates the count to plan.acceptance_completed, self-healed at the transition seams; review MAY also call fab status set-acceptance for explicit updates
  2. Run tests affected by the change (scoped to modules touched, not the full suite)
  3. Features match the requirements (spot-check key scenarios from plan.md's ## Requirements)
  4. No memory drift detected (implementation doesn't contradict memory files)
  5. Code quality check — for each file modified during apply: naming conventions consistent with surrounding code, functions focused and appropriately sized, error handling consistent with codebase style, existing utilities reused. If code-quality.md defines ## Principles, check each applicable principle. If ## Anti-Patterns defined, check for violations. Report code quality issues with specific file:line references; classify as should-fix by default, and as must-fix only when they correspond to spec mismatches, functional defects, or violations of hard project constraints
  6. Parsimony pass — evaluate the apply-stage diff against the question "Could the plan's ## Requirements be satisfied with less code?" Findings cite specific file paths and line ranges (no abstract "could be smaller"). Each finding is classified into one of four categories with mapped severity: reuse-existing-utility → Should-fix, zero-call-sites → Must-fix, duplicated-logic → Must-fix, verbosity → Nice-to-have. Threshold: hard-coded 100 net added lines (advisory — below threshold the pass still runs and may emit findings; above threshold the agent applies stricter scrutiny). The pass is silently skipped when change_type is in the hard-coded skip list [docs, chore, ci], OR when fab/project/code-review.md ## Parsimony Pass Enabled: false (default-on when section absent). Threshold and skip list are NOT project-configurable; the only project knob is the on/off toggle in code-review.md
  7. Deletion-candidate prompt — answer "What existing code (files, functions, branches, config) did this change make redundant or unused?" Output is a structured list of candidates (each naming a specific symbol/file path/block with one-line justification), or the literal None — this change adds new functionality without making existing code redundant when truthful. Output appended as a top-level ## Deletion Candidates section to plan.md (below ## Notes, or end of file if absent); replaced (not duplicated) on rework. The section is omitted entirely (not even "None") when change_type is in the shared skip list [docs, chore, ci]. Distinct from ## Acceptance > ### Removal Verification (planned vs discovered). The review agent MUST NOT auto-delete — findings are surfaced for the human reviewer

Findings (single unified list)

The single review agent returns one unified prioritized findings list (must-fix / should-fix / nice-to-have) — there is a single source, so there is no cross-source deduplication or merge step (pag2). The agent consolidates everything it found (plan-conformance + holistic-diff + parsimony) into one list. Pass/fail determination is deterministic and orchestrator-owned: if any must-fix finding exists → review fails; no must-fix findings (including zero findings) → review passes. should-fix and nice-to-have findings are reported but never block — there is no agent discretion in the verdict (required for unattended post-intake operation).

Structured Review Output

The single review agent returns structured findings with a three-tier priority scheme:

  • Must-fix: Spec mismatches, failing tests, checklist violations — always addressed during rework
  • Should-fix: Code quality issues, pattern inconsistencies — addressed when clear and low-effort
  • Nice-to-have: Style suggestions, minor improvements — may be skipped

Each finding includes: severity tier, description, and file:line reference where applicable.

On Pass

All checks succeed → stage advances to review done via fab status finish <change> review. Review outcomes are auto-logged: Finish calls log.Review(…, "passed", "") for the review/review-pr stages. Skills do not call fab log review manually.

On Failure

Review failure is auto-logged: fab status fail <change> review [driver] [rework]Fail calls log.Review(…, "failed", rework) for the review/review-pr stages. Skills do not call fab log review manually. Rework behavior differs by invoking skill:

/fab-continue (manual rework): Presents the review agent's prioritized findings to the user, then offers three rework options:

  • Fix code — the agent identifies affected tasks, unchecks them in plan.md ## Tasks with <!-- rework: reason --> annotations, re-runs apply, then dispatches a fresh review agent for re-review
  • Revise plan — the user edits plan.md ## Tasks (add/modify tasks) and/or ## Acceptance items, then the agent re-runs apply for unchecked tasks and dispatches a fresh review agent for re-review
  • Revise requirements — the user edits plan.md's ## Requirements section directly (and any dependent ## Tasks/## Acceptance), then re-runs apply for unchecked tasks and dispatches a fresh review agent for re-review (there is no spec stage to reset to) (j6cs). To force a full plan regeneration from scratch, delete plan.md before re-running.

/fab-ff / /fab-fff (auto-loop + stop): Identical behavior for both drivers — literally the same text (szxd): _pipeline.md § Auto-Rework Loop, with {driver} as the event-command driver. The agent triages the sub-agent's prioritized findings and autonomously selects the rework path (up to {max_cycles} cycles — the code-review.md § Rework Budget knob, default 3) (c5tr). Each cycle runs the five-item choreography in § Shared Pipeline Bracket above (fail+reset status pair, one rework action, resumed-or-re-dispatched apply, fresh review sub-agent, verdict). On exhaustion after {max_cycles} cycles, fires fail only (no reset — terminal state review: failed) and stops with a per-cycle summary pointing at /fab-continue for the manual rework menu.

Escalation rule (applies to /fab-ff and /fab-fff auto-loops): After 2 consecutive "fix code" attempts, the agent MUST escalate to "revise plan" or "revise requirements". Non-fix-code actions reset the consecutive counter.

Comment triage: The applying agent triages review comments by priority — not all comments need to be implemented. Must-fix items are always addressed. Should-fix items are addressed when clear and low-effort. Nice-to-have items may be acknowledged but deferred.

The general rule: artifacts at and after the re-entry point are regenerated or updated; artifacts before it are preserved.

Context

Loads: config, constitution, specs/index.md, plan.md (incl. its ## Requirements), target memory file(s) from docs/memory/, relevant source code (files touched by the change).

Hydrate Behavior (via /fab-continue)

/fab-continue dispatches to hydrate behavior after review passes. It completes the pipeline: validates review passed and hydrates learnings into memory files. The change folder remains in fab/changes/ after hydrate — archiving is a separate step via /fab-archive.

Behavior

  1. Final validation — review MUST have passed (all tasks [x] in plan.md ## Tasks, all acceptance items [x] in plan.md ## Acceptance including N/A items)
  2. Concurrent change check — scan fab/changes/ for other active changes whose plans reference the same memory files. If found, warn: "Change {name} also modifies {file}. After this hydrate, that change's requirements were written against a now-stale base. Re-review with /fab-continue after switching to it."
  3. Read ## Deletion Candidates — when plan.md contains a ## Deletion Candidates section (written by review's deletion-candidate check, Validation Check 7), hydrate MAY reference the candidates in memory updates (e.g., a Design Decision noting follow-up cleanup) but MUST NOT generate or modify the section. An absent section is treated as "no findings" without error. The section is omitted entirely for [docs, chore, ci] change types
  4. Hydrate into docs/memory/:
    • From plan.md's ## Requirements → integrate new/changed requirements and scenarios into the Requirements section. Remove requirements the plan explicitly deprecates (### Deprecated Requirements). Extract durable design decisions into Design Decisions section
    • Merge as current truth (present-truth body style, FKF §3.3): the unit of memory is the topic/section, not the change. Hydrate locates the section that already documents a topic and rewrites it to state current truth — it does not append (or update-in-place) a change-keyed delta entry. Superseded statements are removed, not narrated: the body never carries "renamed X→Y in {id}", "this inverts/supersedes {id}'s claim", or "was old.value". Body provenance is citation-only — a trailing (change-id) citation or the *Introduced by*: {change-name} field on a Design Decision; the dated what is captured once via fab status set-summary → the per-folder log.md (the C-lite step), so the body carries no transition narration. Headings carry no change-ids: a heading names its topic (## Dispatch States), never a change (### Dispatch States (xu0k)) — hydrate never introduces a change-id-suffixed heading (change-ids stay citation-only in body text). This rewrite is what makes hydrate idempotent — a re-run (e.g. a hydrate interrupted after memory writes but before fab status finish) rewrites the same section to the same current truth without duplicating Requirements or Design Decision entries. Same contract as docs-hydrate-memory's present-truth merge and review's replaced-in-place ## Deletion Candidates; fab-continue's Key Properties idempotency row covers hydrate via this rule. Normative home: fkf.md §3.3 (+ shipped extract src/kit/reference/fkf.md)
    • Rationale lands in Design Decisions, not narration (FKF §3.3): any why, rejected alternative, or constraint explanation goes into a ## Design Decisions entry in the four-field shape (Decision / Why / Rejected / Introduced by), never as inline narration in Overview/Requirements prose. The changelog-bullet shape is banned inside ## Design Decisions — an entry like - **{change-id} — retired X** is change history (log.md's job, §6), not a design decision; a DD entry heading is a decision title, never a change-id
    • Compare against existing memory file to determine what's new vs changed vs removed — no explicit delta markers needed
    • Minimize edits to unchanged sections to prevent drift
    • Author FKF frontmatter on every new or modified memory file — both type: memory (the constant FKF type, fkf.md §3.1; stamp it if a pre-existing file lacks it) and a curated, accurate description: (§3.2 — the generated index reads each row's Description from it). The description: is a single-line scalar with a 500-character/rune soft cap (measured on the quote-stripped value) (xu0k) and MUST be free of change-ids (no — xu0k suffix, no (d9rs) citation — provenance belongs in the body) — a routing signal for the always-load layer, so detail belongs in the file body (## Overview/## Requirements/## Design Decisions), never in the description. This cap is read at hydrate time via the fab-continue Hydrate Step 4 bullet and the on-demand $(fab kit-path)/reference/fkf.md §3.2 citation, so every hydrate run (including consuming-repo runs) applies it. After any body edit, re-check the description: still routes — one line, ≤500 chars, change-id-free (a body edit can leave the description stale — describing the wrong scope, or drifting far past cap; re-reading it after the body grows is what keeps the routing signal accurate). fab docs-index docs/memory warns over the 500 soft cap — advisory to 1000 runes, blocking --check past it (mxgu); a registry-gated change-id in the description is likewise a blocking finding (the enforced §3.2 ban). New domains get a domain folder; the domain index.md is generated (not hand-written)
    • No ## Changelog write (8fr5): memory files carry no per-file ## Changelog section (FKF §3.3). Instead, record the one-line what-changed once via fab status set-summary {change} "<one-line what-changed>" — the C-lite summary: source field (FKF §6.3) that fab docs-index docs/memory joins with git history to generate the per-folder log.md (8fr5) (oovf). Hydrate never strips an existing ## Changelog body — the body-preserving discipline.
    • Bundle-relative cross-links (FKF §7): any memory↔memory link hydrate writes uses the bundle-relative /... form (resolved from docs/memory/); links out of the bundle (sources, specs, URLs) stay repo-relative/absolute-URL
  5. Post-hydrate self-check — scoped to files touched this run, run before the index regen (step 6). Re-read every file touched this run and strip any transition phrasing just introduced — no "renamed / now / previously / no longer / was old.value" narration, no change-keyed delta paragraph left below an older paragraph on the same topic, no change-ids in headings — and confirm each touched file's description: still routes (one line, ≤500 chars, change-id-free). This is a self-review of this hydrate's own writes, not a corpus sweep (draining pre-existing debt across the tree is /docs-distill-memory's job). A merge that already rewrote each section to current truth leaves nothing to strip — the step is the safety net that catches narration reflexively introduced during the write
  6. Regenerate indexes — run fab docs-index docs/memory (tciy). It deterministically rewrites the root docs/memory/index.md (domains-only), every domain index.md (file rows — | File | Description |; content-only, no dates (ugde)), and — (sx7a) — every {domain}/{sub-domain}/index.md, adding a ## Sub-Domains table to any domain that has them (emitted only when sub-domains exist, so flat domains stay byte-identical), all from folder contents + frontmatter, byte-stable and idempotent. The hydrate skill never hand-edits index rows. Refuse-before-regen guard (defense-in-depth) (glwc): before that regen, consult fab docs-index docs/memory --check; on exit 2 (destructive loss — a hand-managed description would regenerate to , a tombstone row would drop, or a custom grouping would flatten), refuse to regenerate and surface the → run /docs-reorg-memory to remediate ... pointer (the orchestrator that relocates tombstone rows and dispatches /docs-hydrate-memory backfill mode for descriptions; backfill alone does not relocate tombstones). This guard is a no-op for born-compatible fab-kit trees — a tree hydrated by the pipeline is provably always exit 0/1, never 2, so it never fires here (do NOT mistake it for dead code or remove it). It is defense-in-depth for the pathological case of a pre-fab-kit tree reaching the pipeline's hydrate stage; the loss logic lives entirely in Go (tiered --check exit codes 0/1/2 — see kit-architecture.md § fab docs-index), so this site adds only a one-line exit-code consult, the same primitive /docs-hydrate-memory and /docs-reorg-memory consult (see memory-docs/hydrate.md § Refuse-Before-Regen Guard). The blocking content class (xu0k) (mxgu) is a separate signal this guard does NOT key on: four description:/frontmatter signatures — a corrupted (unclosed / quote-strip-failing) frontmatter block, a registry-gated change-id in description:, or a gross over-cap description: over 1000 runes — make fab docs-index docs/memory --check fail (exit ≥ 1) independent of drift, but none is tier-2 destructive loss, so the guard (exit == 2 only) does not fire on any of them and the remediation is fix-the-file (repair the frontmatter, or trim/de-cite the description), not /docs-reorg-memory (a reorg repairs neither source corruption nor a bad routing signal). Heed (but do not block on) any non-fatal advisory warnings it prints across the recursive tree — the shape-bound width/depth warnings, the 501–1000-rune description: trim nag, and the FKF present-truth debt meters (narration density, file size, _unsorted/ non-empty, broken bundle-relative links) (mxgu), none of which block — they signal over-wide / over-deep folders docs-reorg-memory can rebalance or distillation debt docs-distill-memory can trim (its apply path actually moves files, rewrites the broken relative links in both directions, and re-runs fab docs-index docs/memory under a no-dangling-link guard (sx7a); the rebalance itself is a deliberate, separately-reviewed /docs-reorg-memory run, not part of hydrate)
  7. Return completion status — the sequencer runs fab status finish <change> hydrate after the block returns (the block runs no fab status command); this advances .status.yaml to hydrate: done
  8. Pattern capture (optional) — if the change introduced non-obvious implementation patterns that future changes should follow (e.g., a new error handling approach, a reusable abstraction), note them in the relevant memory file's ## Design Decisions section as a four-field entry (Decision / Why / Rejected / Introduced by) stating present-tense design intent — provenance is the *Introduced by*: {change-name} field (or a trailing (change-id) citation), never free-text change-name narration in the prose and never a - **{change-id} — …** changelog bullet (FKF §3.3). Skip for implementations that follow existing patterns

Recovery

Hydration modifies memory files in-place. If the merge goes wrong, the only recovery is git checkout on the affected memory files. Commit (or at least review the diff) before pushing after hydrate.

Context

Loads: config, constitution, specs/index.md, plan.md (incl. its ## Requirements), intake.md, target memory file(s) from docs/memory/, docs/memory/index.md and relevant domain indexes.

/fab-adopt [<slug>] (Adoption Orchestrator)

/fab-adopt brings a completed-but-off-pipeline change into the Fab pipeline (t54n). The trigger is mid-flight adoption (scenario B): a feature branch authored without fab with an OPEN PR or no PR yet — typically after a reviewer notes "you skipped fab-kit and might have missed a few checks." It is a thin orchestrator in the /fab-proceed / /fab-ff lineage: it reuses existing skills/procedures as sub-agents and introduces only what is genuinely new (the diff→intake and thin diff→plan procedures in _generation.md, see planning-skills.md § Shared Generation Partial; and _review.md's diff-only mode — the single review dispatch minus the plan-conformance steps, § Review Behavior above). It declares helpers: [_srad, _generation, _review, _pipeline] (_srad because the reconstructed intake is graded) and takes an optional <slug> (derived from the PR title or branch name when omitted). The framing — the real pipeline entered late, with apply skipped — is recorded in change-lifecycle.md § Adoption.

Step 0 — Guards & diff base (no mutation before all pass): reusing /git-pr's guard idioms verbatim, STOP (no fab change new, no status mutation) on: detached HEAD or the default branch (/git-pr's messages); PR state == MERGED (scenario A — retroactive backfill of already-merged work, out of scope — OPEN and no-PR both proceed); a branch already mapped to a fab change (fab resolve --folder "$(git branch --show-current)" --or-none prints a folder name — anything but (none) (dow0) → already in the pipeline, point at /fab-continue); or an empty diff against the resolved merge-base (base=$(git merge-base HEAD origin/{default}); nothing to adopt). The diff (git diff {base}...HEAD) and changed-file list are captured here for the generation pass and the diff-only review.

Steps 1+2 — ONE main-session generation pass: the same agent (NOT a dispatched apply) reads the diff + PR body once and: (1) fab change new --slug {slug} against the current branch + activate it (the branch already exists, so /fab-new Step 11 row 1/2 "already active"/"checked out" applies — no recreate/rename); (2) reconstruct intake.md via the Intake-from-Diff Procedure (SRAD + fab score); (3) a human-confirmation checkpoint presents the reconstructed intent + assumptions for confirm/correct — the late deliberation the bypass skipped — then on confirm fab status advance {name} intakefinish {name} intake (auto-activates apply); (4) write the MINIMAL plan.md via the Plan-from-Diff Procedure from the same understanding (no re-read). Both -from-Diff procedures live in _generation.md.

Step 2 (state) — apply → skipped, review → active via existing transitions only (no Go change): fab status skip {name} apply (cascades all downstream → skipped) then fab status reset {name} review fab-adopt (skipped → active, cascades its downstream → pending), then fab status set-summary {name} "adopted off-pipeline change; apply skipped". The skip-then-reset ordering is load-bearing — see schemas.md § Adoption's apply=skipped, review=active is a transition COMPOSITION for the mechanism.

Steps 3–6 — review → hydrate → ship → review-pr (the real, late pipeline): the orchestrator owns every verdict/finish transition (the dispatched blocks run no fab status — the universal block contract). Step 3 — Review, dispatched mode: diff-only (resolve fab agent review -o yaml, apply model+effort seams): the single review agent reads git diff {base}...HEAD natively with the plan-conformance steps omitted, and preconditions are skipped. Pass (incl. zero findings → best-effort pass) → finish review; fail → auto-rework per _pipeline.md § Auto-Rework Loop budget when run autonomously, but the interactive default hands findings back to the user rather than auto-editing a hand-authored branch (resolved Open Question — respecting hand-authored code). Step 4 — Hydrate, dispatched verbatim (reuses _pipeline.md Step 3 unchanged) → finish hydrate: this is the permanent-loss recovery — docs/memory/ finally reflects what shipped. Step 5 — Ship: dispatch /git-pr {name} (folder name, not bare id); the OPEN-PR path's Step 3d body-retrofit injects ## Meta when the PR body lacks one (§ ## Meta body-retrofit above), or /git-pr creates the PR fresh when pr_state == none (no separate --no-pr flag — the create path already handles it; resolved Open Question). Step 6finish ship auto-activates review-pr; output Next: /git-pr-review. The skill prints an honest-state summary: intake/review/hydrate/ship/review-pr all genuinely ran (just late, after the code was written) — only apply is skipped. /fab-adopt makes no commit itself (Step 5's /git-pr commits the reconstructed fab/ artifacts and retrofits the PR body); it is partially idempotent (Step 0 guards STOP cleanly pre-mutation; the collision guard routes a post-creation re-run to /fab-continue; the dispatched stages and the Meta retrofit are themselves resumable/idempotent). Discoverable under the Planning /fab-help group via a one-line "fab-adopt": "Planning" entry in skillToGroupMap (fab_help.go) — without it the help auto-scan would bucket it under "Other".

/fab-archive (Standalone Skill)

/fab-archive is a standalone housekeeping command — not a pipeline stage. It supports two modes: archive (default) moves completed changes to the archive; restore moves archived changes back to active. fab-archive.md is a single document (szxd): mode detection and both argument lists are stated once at the top, with a ## Restore Mode section holding only restore-unique content — including the mode-specific pre-flight waiver (no standard preflight, hydrate guard waived — the opposite of archive mode).

Dirty-tree disclosure (g8st): the skill's "safe to re-run" claim is explicitly qualified — it covers fab state, not git state. Both modes move tracked files and edit fab/backlog.md / fab/changes/archive/index.md with no commit step; every archive or restore leaves uncommitted moves and backlog/index edits in the working tree for the caller to commit (e.g., via /git-pr). The skill deliberately does NOT commit autonomously — commit ownership stays with /git-pr. The shared Key Properties table carries a Leaves uncommitted changes? row with separate Archive and Restore values stating this.

Archive Mode

Precondition

Requires hydrate: done in .status.yaml. If hydrate is not done, it stops with: "Hydrate has not completed. Run /fab-continue to hydrate memory first."

Behavior
  1. Move change folderfab/changes/{name}/fab/changes/archive/{name}/. Create archive/ if needed. No rename. 1b. Delete dispatch state (6sgj) — remove the change's .fab-dispatch/{id}/ dir. In internal/archive.Archive() this runs immediately after the folder move (os.RemoveAll(dispatch.DirFor(repoRoot, id)), repoRoot = filepath.Dir(fabRoot), id via resolve.ExtractID(folder)): best-effort — an absent dir is a no-op and a removal error never undoes the completed move. Dispatch artifacts are transient comms, not history, so this is one of the two deterministic fab dispatch cleanup paths (no automatic GC) and Restore() does NOT recreate them (see runtime/dispatch.md).
  2. Update archive index — prepend entry to fab/changes/archive/index.md (create with backfill if missing). Format: - **{folder-name}** — {1-2 sentence description}. Most-recent-first. The index write is atomic (internal/atomicfile temp+fsync+rename) and honestly reported (hv7t): updateIndex checks its read/write errors, a failure yields index: failed in the YAML result, and — the move having already happened — the CLI prints the YAML report then exits non-zero (the ArchiveWithBacklog partial-success pattern: non-nil result alongside the error, with the backlog mark still attempted). fab batch archive counts such a change archived with a stderr warning rather than failed.
  3. Mark backlog items done — exact-ID check (always), then keyword scan with interactive confirmation
  4. Clear pointer — remove .fab-status.yaml symlink only if the archived change is the active one
Fail-Safe Order of Operations

Steps execute in this order for safety. Folder move first (recoverable if interrupted — re-run detects folder already in archive and completes remaining steps). Dispatch-state deletion (1b) immediately after the move — best-effort, so its failure never blocks the archive. Index after folder is in place. Backlog marking after index. Pointer last.

Idempotent Re-Archive (Soft Skip)

Re-running archive on a genuinely archived change — source folder gone from fab/changes/, only the archive entry remains — is a soft skip: Archive() falls back to an archive scan (resolveArchive) when active-change resolution fails and returns ErrAlreadyArchived, which fab change archive renders as already archived: {change} with exit 0. An argument matching no change anywhere propagates the original resolution error (non-zero), and ambiguous archive matches do not soft-skip. Batch archive routes genuinely-archived named targets through the same path via the exported archive.IsArchived (counted skipped, exit 0).

Not-found vs. ambiguous is a typed distinction (jznd). The soft-skip decision branches on internal/resolve's typed sentinels rather than re-resolving against the archive to guess. archive.go Archive gates the archive-scan/already-archived fallback on errors.Is(err, resolve.ErrNotFound); cmd/fab/batch_archive.go runBatchArchive checks errors.Is(err, resolve.ErrAmbiguous) first and surfaces it (warn + continue) so an ambiguous name — a real user error — is never silently soft-skipped as "already archived". Only ErrNotFound (with archive.IsArchived confirming the archive entry) takes the idempotent soft-skip. See schemas.md § internal/resolve Typed Errors for the sentinel/classifiedError mechanism (the surfaced message strings are unchanged).

Re-Archive Recovers a Failed Backlog Mark

ArchiveWithBacklog (src/go/fab/internal/archive/archive.go) still attempts backlog.MarkDone when Archive returns ErrAlreadyArchived — re-deriving the folder from whichever location the change lives in now (resolve.ToFolder for the destination-exists case; the archive scan resolveArchive for the usual soft-skip path above, where the source folder is gone) and treating the mark as best-effort (its error is not propagated). MarkDone is idempotent (returns already when the item was marked before), and ErrAlreadyArchived propagates unchanged with a nil result — the soft-skip exit semantics (k4ge) hold, and the recovery fires on both the genuinely-archived re-run and the half-completed case. Unit-tested via TestArchiveWithBacklog_ReRunRecoversBacklogMark; the fab change archive row in _cli-fab.md documents the soft-skip re-attempt.

Restore Mode (/fab-archive restore <change-name> [--switch])

Restores an archived change back to fab/changes/. Inverse of the archive operation. Preserves all artifacts and .status.yaml without modification — no status reset, no artifact regeneration.

Precondition

<change-name> is required. Resolved via case-insensitive substring matching against folder names in fab/changes/archive/. Supports exact/single/ambiguous/no-match flows (same pattern as /fab-switch).

Behavior
  1. Move change folderfab/changes/archive/{name}/fab/changes/{name}/. No rename. All artifacts preserved. Restore() does NOT recreate the .fab-dispatch/{id}/ dispatch-state dir that archive deleted (6sgj) — dispatch artifacts are transient comms, not history.
  2. Remove archive index entry — remove the entry for {name} from fab/changes/archive/index.md. Preserve empty index file. The rewrite derives from a whole-file read (internal/lines) and lands atomically (hv7t) — entries after the removed one cannot be silently dropped by a truncated scan — and removeFromIndex returns (string, error): a failed read/rewrite surfaces as index: failed in the YAML while the restore move still completes, with the CLI printing the report then exiting non-zero (mirroring the archive handler). A missing index file remains the benign index: not_found with nil error.
  3. Update pointer (conditional) — if --switch flag provided, create .fab-status.yaml symlink pointing to fab/changes/{name}/.status.yaml. Otherwise no-op. A failed activation is surfaced as pointer: failed in the YAML report — rendered by the skill as Pointer: ✗ activation failed — run /fab-switch {name} manually (k4ge). pointer: skipped strictly means --switch was not passed; the move and index results are still reported either way.

Steps execute 1→3 for safety. If interrupted, re-run detects folder already in fab/changes/ and completes remaining steps (index cleanup, optional pointer update).

Key Properties

  • Does NOT modify .status.yaml progress (may update last_updated)
  • Accepts optional [change-name] argument for targeting a specific change (archive mode)
  • Conditional pointer clearing in archive mode — only removes .fab-status.yaml when the archived change is the active one
  • Leaves uncommitted changes in both modes — moved files + backlog/index edits, for the caller to commit (no autonomous commit step) (g8st)
  • Re-archive is a soft skip that still re-attempts the backlog mark, recovering an earlier failed mark (g8st)
  • Restore mode requires explicit <change-name> — no "restore most recent" convenience
  • Restore mode optionally activates via --switch flag

Design Decisions

Acceptance Tests Implementation Fidelity and Code Quality

Decision: The plan.md ## Acceptance section validates "does the code match the spec?" (implementation fidelity) and "is the code well-written?" (code quality). Code Quality is always included with at least two baseline items (pattern consistency, no unnecessary duplication); additional items derive from config.yaml code_quality section when present. Why: Requirement quality is addressed at intake (via /fab-clarify) and at apply-entry requirement generation, but code quality is only observable at review time. The baseline items are universally applicable; project-specific standards come from config. Rejected: Code quality as opt-in only — would miss quality checks on projects without code_quality config. SpecKit-style requirement-quality checklist — duplicates planning-stage work. Introduced by: doc/fab-spec/TEMPLATES.md; Updated by: 260215-r8k3-DEV-1024-code-quality-layer; 260423-qszh-merge-tasks-checklist (formerly "Checklist Tests…"; review consumes plan.md ## Acceptance instead of the standalone checklist.md)

Sub-Agent Over Inline Review

Decision: Review validation is dispatched to a sub-agent in a separate execution context, replacing inline review by the applying agent. Why: Same-context review is fundamentally limited by shared cognitive biases. The sub-agent provides a fresh perspective — it has no shared context with the applying agent beyond the explicitly provided artifacts. Rejected: Multiple inline review passes (still shares context), external review tool integration (too prescriptive, not portable). Introduced by: 260216-gqpp-DEV-1040-code-review-loop

Standard Subagent Context Template

Decision: All subagent prompts include a standard set of fab/project/** files (config.yaml, constitution.md, context.md, code-quality.md, code-review.md) defined in _preamble.md § Standard Subagent Context. Review behavior references this template instead of listing files ad-hoc. Why: Without the template, each skill that dispatched subagents maintained its own context list, creating silent quality gaps when files were omitted and drift risk as new project files were added. The template in _preamble.md centralizes the list so all subagents — including nested sub-subagents — inherit project principles automatically. Total context cost is ~150 lines, negligible. Rejected: Selective per-subagent file lists (maintenance burden, drift risk), loading only for review subagents (apply and other subagents also benefit from project principles). Introduced by: 260318-dzze-standard-subagent-context

Priority-Based Comment Triage

Decision: The applying agent triages review comments by severity (must-fix / should-fix / nice-to-have) rather than implementing all of them. Why: Prevents infinite rework loops over diminishing-return suggestions. Must-fix items ensure correctness; nice-to-have items allow pragmatic completion. Rejected: Fix all comments (leads to infinite loops on style disagreements), ignore all non-critical (misses genuine should-fix quality issues). Introduced by: 260216-gqpp-DEV-1040-code-review-loop

fab-ff Gains Auto-Loop with Interactive Fallback

Decision: /fab-ff auto-loops between apply and review (up to 3 cycles) before falling back to interactive rework. Why: Sub-agent review enables tighter automated feedback cycles. The interactive fallback preserves fab-ff's semi-interactive character — the user is never locked out of control. Rejected: Keep fab-ff fully interactive (wastes the fresh-context benefit on simple fixes), make fab-ff fully autonomous like fab-fff (loses the semi-interactive identity). Introduced by: 260216-gqpp-DEV-1040-code-review-loop

Review Failure Offers Multiple Re-Entry Points

Decision: On review failure, the agent presents three options (fix code, revise plan, revise requirements) and the user chooses where to loop back. All three now operate on plan.md — there is no separate spec stage to reset to. Why: Not all review failures are implementation bugs. Some require revisiting the requirements or task list. Giving the user explicit choice prevents the agent from guessing wrong about where the problem originated. Rejected: Always looping back to apply — misses cases where the requirements were wrong. Introduced by: doc/fab-spec/SKILLS.md; Updated by: 260423-qszh-merge-tasks-checklist ("revise tasks" is now "revise plan"; rework edits plan.md ## Tasks and/or ## Acceptance); 260601-j6cs-merge-spec-into-apply ("revise spec → reset to spec stage" → "revise requirements" edits plan.md ## Requirements)

Apply Absorbs Plan Generation; plan.md is Apply's Single Artifact

Decision: The apply skill (defined in fab-continue.md § Apply Behavior) gains a Plan Generation entry sub-step that writes plan.md before any task execution. That file (j6cs) carries THREE parser-contract sections — ## Requirements (the requirement discipline absorbed from the removed spec stage), ## Tasks (parsed by apply), and ## Acceptance (parsed by review) — all co-generated in one pass. The sub-step is skipped on resume when plan.md already exists. No new dedicated skill (/fab-plan) is introduced; no separate progress.plan or progress.spec stage exists. The three ## headings are the stable parser contract — phase/category subheadings underneath are presentational. Why: The sub-steps share context (intake, memory, code-quality config), execute back-to-back without user intervention, and have no decision boundary between them. Folding requirement generation in too (j6cs) means a single pass over the intake-derived design emits requirements, tasks, and acceptance — drift between them is mechanically impossible. Keeping the stage name apply (not renaming to execute/implement) avoids migration churn across state table, .status.yaml, all skills, and user muscle memory for marginal semantic gain. Rejected: New /fab-plan skill — adds command surface for what is mechanically a single autonomous step. Dedicated progress.plan/progress.spec stage with separate tracking — recreates the no-decision gate these changes exist to remove. Keeping spec.md as a separate apply-entry artifact — leaves an unread file. Section markers like <!-- TASKS-START --> — uglier, identical guarantees. Single mixed list with item-type markers — destroys imperative-vs-declarative framing reviewers rely on. Introduced by: 260423-qszh-merge-tasks-checklist; Updated by: 260601-j6cs-merge-spec-into-apply (added ## Requirements as a third co-generated section)

Hydrate Semantically, Not by Delta Markers

Decision: The agent compares plan.md's ## Requirements against existing memory files to determine what's new, changed, or removed. No ADDED/MODIFIED/REMOVED markers in the requirements. Why: The requirements read as a straightforward requirements document. Delta markers would clutter them and couple them to the hydration mechanism. Rejected: Explicit delta markers — clutters requirements, requires discipline to maintain, fragile to editing. Introduced by: doc/fab-spec/TEMPLATES.md; Updated by: 260601-j6cs-merge-spec-into-apply (source artifact spec.mdplan.md ## Requirements)

Concurrent Change Warning on Hydrate

Decision: Before hydrating, scan for other active changes that reference the same memory files and warn the user. Why: Hydration updates the memory files, which may invalidate assumptions in other active changes. The warning prompts re-review rather than allowing silent drift. Rejected: Blocking hydrate if concurrent changes exist — too restrictive, especially for independent changes that happen to touch the same domain. Introduced by: doc/fab-spec/SKILLS.md

Hydrate is a Pipeline Stage, Archive is Not

Decision: Memory hydration (hydrate) is a tracked pipeline stage; folder housekeeping (/fab-archive) is a standalone skill. Why: Memory hydration is the logical completion of the agent's work — it closes the feedback loop from implementation back to memory files. Folder housekeeping is a user-triggered cleanup action with no bearing on artifact quality. Rejected: Both as pipeline stages — would add another stage for marginal benefit. Neither as pipeline stages — would lose the memory hydration automation. Introduced by: 260213-jc0u-split-archive-hydrate

Restore as Subcommand, Not Separate Skill

Decision: Archive restore is a subcommand of /fab-archive (/fab-archive restore <name>), not a separate /fab-restore skill. Why: Archive and restore are paired inverse operations. Grouping them under the same skill maintains conceptual cohesion and avoids skill proliferation. Users naturally look for restore under the archive command. Rejected: Separate /fab-restore skill — adds a new top-level command for a narrow, complementary operation. Introduced by: 260214-v7k3-archive-restore-mode

fab-archive Clears Pointer Conditionally

Decision: /fab-archive only removes .fab-status.yaml when the archived change is the active one. Why: If archiving a non-active change (via change-name argument), clearing the pointer would disrupt the user's active work context. Rejected: Always clear — would lose active change context when archiving a different change. Never clear — would leave stale pointer after archiving the active change. Introduced by: 260213-jc0u-split-archive-hydrate

Unified PR Template with Conditional Field Population

Decision: /git-pr uses a single unified PR body template for all PR types. Fab-linked fields are conditionally populated based on artifact availability — whether the change resolves and artifacts exist — not on PR type. These fields live (rj31) in a mechanically-rendered ## Meta block (5-column top table Change ID | Type | Confidence | Plan | Review, a self-labeling single-table Impact | +/− | Net block + <sub> caption, an optional **Issues**: line, and the **Pipeline:** line last — normalized (pnao), with the 260625 layout revision making Pipeline the final element) produced by fab pr-meta, with for unavailable cells; the whole block is omitted when the change can't be resolved. The agent-generated ## Summary / ## Changes prose stays type-agnostic too. Why: A test or docs change that went through the full fab pipeline deserves the same quality signals as a feat change. Gating template richness on type hid real planning work and reduced reviewer confidence. The unified template always shows the same structure, reducing cognitive overhead. Mechanizing the Meta block (rj31) further guarantees the structure is byte-for-byte identical across runs. Rejected: Two-tier templates gated on type (hides work for non-feat types), keep type-gating but extend Tier 1 to all types (still requires two code paths), omit columns when empty (inconsistent table shape across PRs). For the fab pr-meta mechanization (rj31): pushing the whole PR body into Go (would force Summary/Changes into mechanical text extraction, degrading prose quality), and a flags-only formatter (leaves the skill re-deriving git/gh inputs each run — partial drift remains). Introduced by: 260305-b0xs-unified-pr-template; Replaces: 260225-54vl-smart-git-pr-category-taxonomy (Two-Tier PR Templates with Type Resolution); Updated by: 260604-rj31-mechanical-pr-meta (the ## Meta block — table, Pipeline, Issues, Impact — is now rendered by fab pr-meta, replacing the inlined Step 3c formatting prose; the former "Stats table" five columns are now ID | Type | Confidence | Plan | Review); Updated by: 260625-pnao-normalize-pr-meta-impact-block (the Meta Impact block is now a single self-labeling Impact | +/− | Net table with the raw / true / impl / tests / excluded taxonomy and a <sub> provenance caption stamping the fab-kit binary version, replacing the prior three render shapes; true is always the post-exclude diff, fixing the "total flips meaning" bug. The layout revision in the same change re-titled the top-table header IDChange ID (id backtick-wrapped), dropped the **Impact**: lead-in, and reordered the block to heading → top table → Impact + caption → optional Issues → **Pipeline:** last. The raw row is shown whenever excludes are configured, even when raw equals true — an explicit user decision ("always show raw when excludes are configured"), not a rendering convenience)

Execution Stage Reset Preserves Artifacts and Checkboxes

Decision: /fab-continue apply re-runs apply behavior starting from the first unchecked task. It does NOT uncheck all tasks. fab status reset apply also preserves plan.md on disk (qszh) — reset modifies .status.yaml state only; artifact files persist. The apply entry's plan-generation sub-step is idempotent on plan.md presence and is skipped when the file exists. To force plan regeneration the user MUST delete plan.md before re-running. Why: Task checkboxes and acceptance items reflect actual implementation progress. Silently unchecking them would discard valid work. Review rework (Option 1: "Fix code") handles targeted unchecking with <!-- rework: reason --> annotations. Treating plan.md like every other change artifact (intake.md) under reset preserves the existing artifact-file convention and Constitution III (idempotency). Rejected: Resetting all checkboxes on apply reset — too destructive, discards completed work. Auto-deleting plan.md on apply reset — violates the artifact-file convention and surprises users who reset to re-run only the task execution sub-step. Introduced by: 260212-a4bd-unify-fab-continue; Updated by: 260423-qszh-merge-tasks-checklist (extended to plan.md artifact preservation; formerly "Execution Stage Reset Preserves Task Checkboxes")

Review Active Triggers Forward Progression

Decision: When the active stage is review, /fab-continue runs the review behavior (advancing toward hydrate), not re-review. Re-review is available via /fab-continue review reset. Why: The normal flow always advances. review: active means "review needs to run"; review: done means "review passed, hydrate is next." This avoids ambiguity about whether the command should redo or advance. Rejected: Having review active trigger re-review — conflicts with the forward-progression model. Introduced by: 260212-a4bd-unify-fab-continue

Generic Review Skill Replaces Copilot-Specific Fix

Decision: /git-pr-review (formerly /git-review — distinct from the internal review stage) is a standalone skill that handles all PR review types (human or bot), replacing /git-pr-fix and /git-pr Step 6 (inline auto-fix). It checks for existing reviews with comments — if found, processes them; if none, runs Phase 2 to request an automated review. It drives the review-pr pipeline stage with statusman integration. Why: The old architecture had fragmented review handling across multiple skills. The new skill consolidates all review handling into a single flow: check for existing reviews, process them or request one. Rejected: Separate skills per reviewer type (duplication, same API shape). Introduced by: 260303-i58g-extract-pr-review-skill

Copilot-Only Phase 2 in /git-pr-review

Decision: Phase 2 of /git-pr-review requests Copilot and nothing else. Phase 2 attempts gh pr edit {number} --add-reviewer copilot-pull-request-reviewer; on success, polls for the review up to 10 minutes (30s interval); on request failure (no automated reviewer available), stops with a clean finish (outcome no-reviews); on poll timeout, leaves the review-pr stage active for a later re-run (outcome timeout, 260611-9u91 — the original design (iqju) also clean-finished the timeout, which marked the stage done with the requested review still pending and unrecoverable via start). The Copilot toggle is read from fab/project/code-review.md § Review Tools (copilot: true/false; absent = enabled) — (tykw), which retired the review_tools config block into that prose section (see _shared/configuration.md § code-review.md). Why: Local CLI reviewers are architecturally misplaced post-ship — they lack full repo access and run after the PR was created, making rework noisy (amends, force-pushes). The pre-ship review stage already reads the whole diff with full repository access (the single review agent in _review.md). Copilot, as a GitHub-infrastructure reviewer, belongs post-PR. Poll-and-wait (vs stop-immediately) allows a single /fab-fff pipeline run to process the Copilot review without user re-intervention. Rejected: Keeping Codex/Claude in Phase 2 — redundant with the pre-ship review stage, architecturally late; stop-immediately after Copilot request — requires user re-invocation and breaks /fab-fff pipeline continuity. A cascading Copilot→Codex→Claude Phase 2 with a per-tool review_tools config block (260403-oh82) — local CLI reviewers duplicate what the provider system and the pre-ship review stage already cover, and the config block is retired into code-review.md § Review Tools (tykw). Introduced by: 260405-iqju-consolidate-review-types

_review.md Shared Review Skill (Extracted from fab-continue.md)

Decision: Review behavior lives in the shared internal _review.md skill file (extracted from fab-continue.md), following the same pattern as _generation.md. The review agent's merged procedure is defined in _review.md; orchestrators (fab-continue, fab-ff, fab-fff) reference it by name. Why: _generation.md proved that centralizing shared logic in an internal partial makes it easy to update and maintain — one file, one place. Centralizing the review procedure in _review.md prevents divergence risk when behavior needs updating (at introduction it held two inward/outward sub-agents, since collapsed to the single agent) (pag2). Both fab-continue and the orchestrators reference _review.md, ensuring review behavior is authoritative in one location. Rejected: Keeping review logic inlined in fab-continue.md — creates divergence risk when behavior needs updating, since orchestrators would need their own copies or cross-references. Introduced by: 260405-iqju-consolidate-review-types

Single Review Agent (pag2)

Decision: The review stage dispatches exactly one review sub-agent, which runs the whole merged review (plan-conformance + holistic-diff + parsimony) inline itself — no nested sub-agent dispatch inside the review block, no parallel inward/outward pair, no findings-merge step. Why: A two-reviewer split promises independence it cannot deliver — plan.md/intake.md are committed on the change branch, so they ride inside the git diff <base>...HEAD handed to any "independent" diff reviewer anyway. Nested dispatch inside the review block is also operationally hazardous: a resumed orchestrator loses its background children (the resumed-orchestrator incident class (fyn5) (gvxd)), stranding reviews mid-flight. One inline agent removes the nesting hazard at its root and makes native Agent-tool dispatch and CLI fab dispatch structurally identical for review. Reviewer diversity comes from outside the review block — see Reviewer Diversity Lives Outside the Review Block below. Rejected: The two-parallel-reviewer (inward + outward) shape with a findings merge — compromised independence, nesting-degradation machinery, and the resumed-orchestrator hazard. Introduced by: 260704-pag2-single-review-agent

Reviewer Diversity Lives Outside the Review Block

Decision: The review stage runs exactly one agent and invokes no external reviewer CLI — the worker performs the holistic-diff read itself with full repository access. Pre-ship reviewer choice is a configuration decision (agent.profiles.review.provider, per-session via FAB_AGENT_PROFILES); post-ship cross-vendor eyes come from Copilot at review-pr. code-review.md § Review Tools carries the copilot toggle alone. Why: Reviewer selection is already a first-class configuration decision, so the review procedure carries none of its own — one grammar (fab agent -o yaml, the providers: table, per-role model/effort fills, the dispatch.mode ladder) governs every agent the pipeline runs, and the worker simply reviews. Rejected: An external-tool cascade inside the review agent (a command -v codex probe with a claude fallback, gated by its own codex/claude bullets in code-review.md § Review Tools) — it bypasses the provider grammar entirely, composes badly (an opus-at-high-effort worker babysitting a ~10-minute codex subprocess poll; review: { provider: codex } would spawn codex-inside-codex), and empirically (2026-08-09) surfaced no finding the single worker had not already reported, for ~15 minutes of wall-clock. A provider-resolved headless "second opinion" knob — deferred rather than built, since no evidence yet shows a second opinion is missed once Copilot covers the post-ship cross-vendor read. Deleting code-review.md § Review Tools outright — /git-pr-review Phase 2 reads its copilot bullet. Introduced by: 260808-rvcs-retire-codex-claude-review-cascade

fab-continue Loads Heavyweight Helpers Per Stage (zc9m)

Decision: fab-continue.md declares only helpers: [_srad] in frontmatter; _generation (9.5KB) and _review (10.6KB) are loaded via explicit in-body read instructions at their points of use — _generation at apply entry when plan.md needs generating and on the intake-active regeneration path, _review at Review Behavior entry. _preamble.md § Skill Helper Declaration was extended to sanction this stage-conditional loading mode so the frontmatter contract stays honest (frontmatter = unconditional pre-body loads; in-body reads = conditional ones, intentionally absent from the list). fab-ff/fab-fff keep all their helpers unconditional (three at the time; four (szxd) added _pipeline). Why: /fab-continue is a six-stage dispatcher, but each invocation runs exactly one stage — hydrate, ship, review-pr, and apply-resumes need neither heavyweight helper, yet paid 8.7–19.2KB for both on every invocation. Point-of-use reads scope the cost to the stages that consume the content (apply plan-generation/intake-regen → _generation; review → _review). _srad stays unconditional because apply grades SRAD assumptions on its dominant path. The fab-ff/fab-fff exclusion is evidence-based: finding f074 (apply the same split to the orchestrators) was refuted — their auto-rework loop has the orchestrator itself editing plan.md ## Requirements/## Tasks/## Acceptance, which needs _generation at orchestrator level. Rejected: Keeping [_generation, _review] unconditional in fab-continue (the pre-zc9m state — pays for both helpers on stages that use neither). Making _srad stage-conditional too (apply always grades assumptions; the helper is used on the dominant path). Applying the split to fab-ff/fab-fff (f074 refuted). Introduced by: 260611-zc9m-preamble-context-diet

ff/fff Bracket Single-Sourced in _pipeline.md; Exhaustion Terminal State = review failed (szxd)

Decision: The shared /fab-ff//fab-fff pipeline bracket is extracted to the _pipeline.md internal partial, loaded via frontmatter helpers: (the bracket is the wrappers' entire body, so the load is unconditional — the honest-declaration contract (zc9m)), parameterized by {driver} and {terminal}. The per-cycle rework choreography is stated exactly once there (fail+reset pair on every failed verdict, apply re-dispatch, fresh review subagent). At exhaustion the orchestrator fires fail without a trailing reset, making review: failed the defined terminal state — the only state from which /fab-continue's new review-failed dispatch row (and _preamble's "review (fail)" state) can present the rework menu, so the stop message is truthful. That row replaces fab-continue's former failed→start review resume guard: both the interrupted-fail→reset case and the exhaustion case land on the same deliberate resting state, and the menu is strictly more useful than an unconditional re-review (orchestrators keep the start review recovery in _pipeline.md Resumability — an autonomous re-run wants re-review). The 5 shared error rows live in _pipeline.md with {driver}-parameterized message text; each wrapper keeps only driver-specific rows (fff: ship/review-pr). Why: fab-ff/fab-fff shared ~88% of their content verbatim (107 of 136 lines), and drift was real — "Two gates" vs the constitution's single-intake-gate framing, fab-ff-only post-bail /fab-clarify guidance. The under-specified choreography let two conforming implementations leave different .status.yaml histories (stage_metrics.review.iterations feeds PR meta, so the divergence was observable — f071). The ff/fff exhaustion message pointed users at /fab-continue for "manual rework options" its dispatch table had no row for (f019). Rejected: In-body point-of-use load of _pipeline (that pattern is for conditional loads; the bracket is unconditional). Fail+reset on the final failure too (leaves apply active; /fab-continue would silently re-run apply→review instead of showing the menu). Keeping both the resume guard and the menu row in fab-continue (contradictory dispatch for the same state). Duplicating the shared error rows per wrapper (re-creates the drift surface). Introduced by: 260611-szxd-skills-twins-self-duplication-refactor

Apply-Worker Continuation Spans the Native and Pane Arms, With a Mandatory Fresh Fallback

Decision: The auto-rework loop resumes its apply worker across cycles on the native Agent-tool arm and the pane arm, and every unreachable case degrades silently to a fresh dispatch carrying the full dispatch-prompt obligations. Reachability is decided by attempting the send (native) or the delivery (pane) rather than by a probe. Headless stays non-resumable, and review workers are excluded — they stay fresh every cycle. Why: A fresh worker per cycle pays a cold start in which it re-loads the always-load layer, intake.md, plan.md, the affected memory, and the source files its predecessor just wrote, while the actual delta is a handful of findings; it also loses the loop's negative knowledge — what the reviewer already rejected, the recurring cause of a rework that creatively rewords a fix the reviewer specified verbatim. Neither resumable arm needs machinery built for it: the native handle lives entirely in the orchestrator's own context, and a pane worker is still on screen after it finishes, so continuation is the delivery step it already has, pointed at a continuation prompt. Making the fallback load-bearing keeps Constitution III intact: correctness never depends on a session or a pane surviving, so the worst case of a broken resume path is the status quo. Reviewer independence is a separate design property, so apply reuse must not touch it. Rejected: Headless-CLI session resume (needs session-ID persistence and provider resume_command grammar — deferred indefinitely). A dedicated fab dispatch resume verb (the pane arm's resume is deliver with a different prompt file; a second verb would be a second delivery engine to harden). A persisted "context brief" artifact (recovers far less; may still be layered on). Treating an unreachable worker as a pipeline failure (converts a harmless optimization miss into a stop). Reusing the review worker too (destroys the reviewer-independence the fresh-worker rule exists for). Introduced by: 260808-tv3g-native-apply-worker-resume; Updated by: 260809-3oz7-pane-readiness-gate-sendkeys-delivery

A Continuation Message Is Not a Dispatch

Decision: A continuation carries only the result obligation and the terminal fab status refresh <change> epilogue, plus an instruction to re-read from disk every artifact the orchestrator edited at item 2. It does not re-carry the standard subagent context files, and it does not re-run fab agent apply -o yaml — the resumed worker keeps its first-dispatch profile. The rules are the same whichever arm carries the message. Why: The context files are already in the worker's context, and skipping them is the entire point of continuing it; the block contract is restated because a resumed worker that skipped its refresh is the one identified semantic risk. The re-read instruction closes the complementary hazard: the orchestrator edits plan.md between cycles, so the worker's in-context copy is stale by the time the continuation arrives. Re-resolving the profile on a resume would surface a model the cycle demonstrably cannot honor, which inverts the purpose of the surfacing rule (make a mis-resolution visible). Rejected: Re-carrying the full standard subagent context on every continuation (defeats the optimization). Re-resolving the profile each cycle for uniformity (prints a resolution the cycle does not honor). Trusting the worker's in-context plan.md (silently reworks against pre-edit tasks). Introduced by: 260808-tv3g-native-apply-worker-resume

The stall guard is capture-based judgment, not a probe re-run

Decision: On a no-progress wait timeout-return (no result file, screen unchanged against the previous round) the orchestrator judges the fab pane capture output before re-arming — a first-run wall or a bare shell prompt means the delivery never happened and the gate's judgment rounds re-enter. No readiness verb is re-run; the guard is owned by _preamble.md § The pane readiness gate and pointed at from the wait step and the timeout-return peek table. Why: fab dispatch ready refuses exactly Delivered && !ResultPresent — the stall state itself — and fab pane ready would type a sentinel into a possibly-live worker, violating "the pipeline NEVER sends keys to a WORKER". With the gate's takeover precondition in place, a bare shell prompt on screen is a conclusive signal rather than a guess. Rejected: Carving out the dispatch ready refusal (weakens a guard that exists for a reason); naming fab pane ready (reintroduces mid-stage typing). Introduced by: 260829-57mp-pane-readiness-agent-takeover

Parsimony + Deletion-Candidate Prompts Co-located in _review.md

Decision: The parsimony pass (Validation Check 6) and the deletion-candidate prompt (Validation Check 7) live in the review agent's plan-conformance checks in _review.md — co-located rather than split across stages or skills. Parsimony emits findings into the unified review report (severity tiers); deletion-candidate emits a ## Deletion Candidates section appended to plan.md. Both share a single hard-coded skip list [docs, chore, ci] and run alongside the other plan-conformance checks. Why: Both passes are diff-critique cognitive work — same context (the apply-stage diff), same agent invocation, same applicability profile. Splitting them across stages (e.g., parsimony in review, deletion-candidate in hydrate) would force the same diff to be re-loaded in a different cognitive mode and a different agent context. Co-location amortizes diff-loading cost, keeps the policy surface small (one skip list, one toggle), and matches the user's mental model — both ask "what could be smaller / what's now redundant?" The toggle in code-review.md is parsimony-only because deletion-candidate has no false-positive cost (the literal "None" output is cheap and occasionally trains the agent loop on the question itself). Rejected: Deletion-candidate at hydrate (different cognitive mode, requires re-loading the diff). Separate skill/helper file for parsimony or deletion-candidate (proliferates helpers; both passes share _review.md's review dispatch infrastructure). Independent skip lists per pass (drift risk; users would need to coordinate two settings). Introduced by: 260507-ogf2-restrain-ai-code-bloat

Autonomous Git Paths Verify State Before Mutating (g8st)

Decision: The no-questions git paths (/git-pr, /git-pr-review, /git-branch, /fab-new Step 11, /fab-archive, operator git ops) share one design stance: verify git state before mutating, and report failures honestly — never assume a clean, attached, main-defaulted, local-only world. Concretely: /git-pr STOPs on detached HEAD before any commit/push, evaluates the expected-area untracked guard before staging anything (STOP listing files outside source_paths/docs//fab/; git add -u + in-area untracked otherwise — never git add -A), branches on PR state (OPEN short-circuit / CLOSED fresh PR / MERGED STOP), and guards on the resolved default branch (git symbolic-ref --short refs/remotes/origin/HEADgh repo view --json defaultBranchRef → literal main/master); /git-pr-review splits commit-failure (reset+STOP — genuinely no partial state) from push-failure (keep the commit + recovery guidance + no replies citing unpushed SHAs) and gates re-runs on an unpushed-commit check; /git-branch STOPs with the candidate list on ambiguous multi-match (stderr-keyed, no junk branch) and checks out remote-only branches with --track instead of recreating divergent locals; the branch-creation twins append a non-blocking dirty-tree carried-over note (warn, never stash-prompt); /fab-archive discloses the dirty tree it leaves instead of contradicting it with an unqualified "safe to re-run", and its Go re-archive path re-attempts the backlog mark. Why: These paths run unattended (ff/fff/operator adoption) — when an assumption broke they did not fail, they corrupted state silently: a detached HEAD shipped a refspec-less push; a rejected push posted "Fixed — {sha}" replies citing a commit that never reached the remote, permanently stranding the fix; git add -A swept unrelated untracked files into pushed commits; the operator's queue cherry-picked against a hardcoded, never-fetched origin/main. Verify-before-mutate keeps the autonomy posture (warn or STOP with actionable guidance, never prompt) while making every failure mode honest and recoverable on re-run. Rejected: Hard stop-on-any-untracked (blocks every change that legitimately creates files); keep-git add -A-and-disclose (the sweep finding stays live); reopen-on-CLOSED or stop-on-both-CLOSED-and-MERGED PR policies (shipping intent is explicit — the user/orchestrator just invoked /git-pr); stash-prompt at branch creation (a blocking prompt violates the no-questions/orchestrated autonomy posture); an autonomous commit step in /fab-archive (changes blast radius and overlaps /git-pr's commit ownership). Introduced by: 260612-g8st-git-state-hardening

Ship-Time Skills Take an Explicit Change and Enforce Branch↔Change Correspondence (w7dp)

Decision: /git-pr and /git-pr-review accept an optional explicit <change> argument — transient resolution via fab change resolve <change> (.fab-status.yaml untouched), hard STOP when an explicit argument fails to resolve, value-based classification keeping git-pr's 7 type tokens unambiguous (fab-fff dispatches the collision-proof folder name {name}, never the 4-char {id}). Both skills guard in Step 0, before any status mutation, commit, or push: the current branch must match the resolved change — exact folder-name equality OR the folder name as a branch substring (the former Step 1b nudge's two-form match) — else STOP with recovery guidance (/git-branch, /fab-switch, or pass the intended change); no autonomous checkout. There is no Step 1b nudge. Why: fab-fff's Steps 4–5 passed change: {id} in their prompts, but both skills self-resolved only the ACTIVE change — under the advertised <change-name> override the pipeline worked one change while ship/review-pr mutated another's status and pushed whatever branch was checked out (wrong-change PR). A named-but-unresolvable dispatch target is exactly that failure class, so explicit-arg failure is a hard STOP rather than a silent {has_fab}=false degradation. Guard placement before fab status start keeps the STOP path mutation-free (verify-before-mutate (g8st)); the two-form match keeps one matching contract in the pipeline and tolerates prefixed branch names. Rejected: A fab-fff Step-4 precondition STOP instead of skill-side guards (Theme 2's alternative — leaves direct /git-pr invocations unguarded). Strict branch equality (breaks legitimate prefixed-branch setups for no safety gain). Autonomous checkout on mismatch (a no-questions skill switching branches under the user is worse than the STOP). Keeping the nudge alongside the guard (a dead advisory behind a hard STOP is noise). Introduced by: 260612-w7dp-orchestrator-dispatch-review-pr-recovery

Recovery Guidance Is Override-Aware (w7dp)

Decision: Any re-run/recovery guidance printed by a skill that can be driven with an explicit change argument MUST name that change in the suggested commands (/fab-continue <change>, /fab-continue <change> intake, /fab-clarify <change>, re-run /{driver} <change>, re-run /git-pr-review <change> when an explicit change was passed); argless forms are reserved for sites where the current invocation's change reference is implied (active, or that skill's own override). Why: The orchestrators and ship-pipeline skills advertise <change-name> overrides — a stop/failure message printed mid-override that suggests an argless command silently re-targets the ACTIVE change, the same wrong-change class the explicit-argument work closed. Naming the change makes every printed recovery route executable as written against the change the run was actually driving. Rejected: Argless-everywhere with a "re-add the override" footnote (relies on the user reconstructing override state at the exact moment they are recovering from a failure). Introduced by: 260612-w7dp-orchestrator-dispatch-review-pr-recovery (pattern capture)

Ship/PR Skills Pre-Approve Only Their Mutation Tools

Decision: /git-pr and /git-pr-review list only their mutation commands in frontmatter allowed-toolsBash(git:*) and Bash(gh:*) (git-pr-review also Bash(command:*)). Why: Under the Claude Code skills contract, allowed-tools pre-approves the listed tools without restricting other tools, so the Read/Edit operations the skill bodies require remain callable under normal permission settings; a minimal list is the whole contract. Rejected: Enumerating every tool the body uses — longer frontmatter with no semantic gain. Introduced by: 260811-xy7a-condense-spec-skill-mirrors (migrated from the condensed SPEC mirrors)

Plan-Time Task-Count Fork for the Light Lane

Decision: The /fab-ff//fab-fff bracket decides the execution lane once, immediately after inline plan co-generation, on the number of task entries in plan.md ## Tasks: ≤ 5 → light lane (inline execution), > 5 → full lane (dispatched). Per-invocation --light / --full flags skip the count check. Why: Loom-archive evidence (972 completed changes, 927 with recoverable task counts) shows rework rate is a clean gradient over plan size — 8% at 1–3 tasks, 7% at 4–5, 11% at 6–8, 16% at 9–12, 29% at 13+ — so task count is the right signal; at the ≤5 cut ~21% of the archive is light-eligible at plan time and finishes with zero rework. The fork rides an existing seam (the apply contract's plan-exists skip) and changes zero state machinery. Rejected: Change-type entry heuristic (infer light from docs/chore/test/ci at intake) — the same archive data kills it: 68% of type-eligible changes outgrow the size threshold while 154 small feat/fix changes escape it (type recall ~26%, precision ~32%). New stage states for the lane (would fork every .status.yaml consumer). Introduced by: 260811-3ol6-light-lane-inline-small-changes

No Promotion Valve Between Lanes

Decision: There is no light→full mid-run promotion. Light-lane rework stays inline under the same {max_cycles} budget and per-cycle fail+reset choreography; exhaustion parks review: failed exactly as in the full lane; scope growth discovered mid-rework rides the same rework backstop; a parked light run re-enters however the user chooses, including --full. Why: Every diff — light or full — passes the same independent review, so a misclassified small change wastes bounded inline rework cycles but can never ship unreviewed; bounded inline rework plus exhaustion parking is the backstop, with zero mid-run mode machinery. Rejected: Promotion valve (light→full on the 2nd rework cycle) — adds mid-run mode machinery for a case the backstop already covers. Introduced by: 260811-3ol6-light-lane-inline-small-changes

Review Never Runs Inline in Either Lane

Decision: Review (initial and every re-review) stays a fresh dispatched worker in both the light and full lanes. Why: Reviewer independence is the pipeline's highest-value dispatch — author self-review shares the author's blind spots, and sweep misses are this repo's top rework cause (fab/project/code-quality.md § Sibling & Mirror Sweeps); a fresh reviewer over a tiny diff is cheap anyway. Rejected: Inline review in the light lane — destroys reviewer independence for exactly the small changes most prone to skipping scrutiny. Introduced by: 260811-3ol6-light-lane-inline-small-changes

Ship/Review-PR Dispatch Sites Branch on dispatch: Presence Like Every Other Stage

Decision: fab-fff Steps 4–5 and /fab-continue's ship/review-pr rows branch on the resolved dispatch: key exactly like apply/review/hydrate — absent ⇒ the native Agent-tool arm through the two seams (unchanged); present ⇒ the CLI adapter per _preamble.md § CLI-Adapter Dispatch (rung branch, blocking fab dispatch wait, done-read reap under the immediate-reap row). The dispatched /git-pr / /git-pr-review workers self-manage their own stage's transitions on every arm — the one delta from other stage workers — so their prompts carry the result-file/context/terminal-refresh obligations but not the transition prohibition (the carve-out is owned by _preamble.md § Dispatch-Prompt Obligations). The light lane is untouched (ship/review-pr stay inline there), and _pipeline.md § Stage Dispatch Procedure itself stays scoped to apply/review/hydrate — fab-fff owns its Steps 4–5 text. Why: The resolver's surfaced answer now means the same thing at every dispatch site; under a non-native agent.workers provider the native-only wiring silently inverted the fast role, running ship on the inherited session model. Zero behavior change when the ladder resolves native (dispatch: absent — the native arm is the prior path verbatim). Rejected: A config-side per-machine override (agent.profiles.fast.provider: claude) — treats the symptom at one site on one machine. Orchestrator-owned transitions for ship/review-pr — would fork /git-pr's behavior by caller and require a larger refactor of its Step 0/4/6 internals; the only-if-still-active guards already reconcile orchestrator and worker. Introduced by: 260903-0y4c-ship-reviewpr-cli-dispatch