Gates
What a gate is in this harness, the four enforcement levels, the gates that can block a feature, and the waiver mechanism. Per-step enforcement values are listed in steps; the integrity check suite is a different thing and lives in validation.
What a gate is
A gate is a check that can block progression. Every step passes through two of them before the flow moves on:
- The prerequisite gate. Every step names the steps it depends on. The gate passes when all of them are satisfied — where satisfied means
done,skipped, orstale. A skipped step therefore never wedges its dependents. - The completion gate. After a step runs, the engine recomputes whether it is actually done by reading evidence off disk, and writes the verdict to
.pipeline/gates/<step>.json. The loop owns that verdict. An agent cannot declare its own step complete; see evidence model.
A step that never ran still leaves a verdict. When the engine resolves a verdict-bearing step by skipping it — complexity tier, work track, bootstrap mode, an upstream skip, disable: true, a false when:, or an advisory step that failed and was auto-skipped in auto mode — it writes {"satisfied": true, "reason": "skipped: <cause>"} to .pipeline/gates/<step>.json. Satisfaction is unchanged (the selector has always treated a skipped gate as satisfied); what changes is that the skip is recorded. Before this, a skipped gate left no verdict file at all and the selector fell back to the step’s own status flag. Read the skipped: prefix as “this gate was deliberately not run”, never as “this gate’s evidence passed”.
Those two are orthogonal. Prerequisites answer may this run yet; completion answers did it actually happen. A step can pass the first and fail the second forever, which is exactly what a halt looks like.
The four enforcement levels
Enforcement is a property of the step, not of the gate. It decides what happens when the step fails.
| Level | On failure in auto mode | In interactive mode | Can config disable it? |
|---|---|---|---|
advisory | marked skipped, run continues | recovery menu, skip offered | yes |
gating | run halts | recovery menu, tagged [gating], skip withheld | no, unless the step definition opts in |
structural | run halts | recovery menu, skip offered | never — the opt-in flag is ignored |
mechanical | — | — | not accepted by the config validator |
Read the levels as a statement about who can decline the step:
- Advisory steps are useful, not load-bearing. Memory recall, exploration, and architecture diagrams. If one fails unattended the run keeps going rather than stranding an otherwise valid feature.
- Gating steps are the correctness contract. A failure means the thing being gated is not true yet, so the run stops rather than proceeding on a false premise. The interactive recovery menu drops the
skipoption for these — you can retry, fix interactively, go back, or quit, but you cannot wave one through. - Structural steps are mechanics the flow cannot proceed without: the worktree, the build itself, the rebase. They fail the run in auto mode like a gating step, and no project config can turn one off.
- Mechanical is declared in the type union and documented as the hook-based enforcement tier, but no step definition uses it and the config validator rejects it. It is currently a reserved word.
Exactly one built-in gating step opts into being disabled by project config, and that opt-in is per step and must be committed in .ai-conductor/config.yml. The point of the restriction is that a partial or copy-pasted config can never silently drop a guardrail.
The gate catalog
Gates come in five families. The families exist in different places in the codebase because they run at different times against different evidence.
| Family | Runs | Blocks | Count |
|---|---|---|---|
| prerequisite | before every step | that step | 1 (universal) |
| per-step completion | after a step runs, and whenever the loop re-scores it | that step, and the loop | 12 |
| land-time | when a spec PR is landed | the spec, before anything is built | 10 |
| self-host | before the finish step, only when the harness is building itself | the PR | 6 |
| hook | at the moment of a tool call | the individual edit, command, or dispatch | see settings and hooks |
Commit scope-containment boundary
The generated commit-msg hook checks staged paths on a task-attributed commit against the active task’s declared files. A path outside that declaration is diagnosed with one copy-pasteable Scope: <path> — <rationale> trailer per path. Those trailers make an intentional widening visible to the engine-side containment floor and to build_review; they do not declare a task complete or waive semantic scope review.
Containment ships report-only by default because false-positive refusals can stall a live build. The checker prints every verified violation but returns 0, so the commit proceeds and the containment floor retains the evidence for later review. Set build_review.scopeContainmentEnforced: true to enable refusal. Its three-valued contract keeps the hook safe if state is unavailable: 0 allows (including a reported default violation), 2 is a positive refusal when enforcement is enabled, and every other result is an abstention that the hook logs and allows. The hook converts only 2 to Git’s blocking exit 1; malformed or missing task state never blocks a build.
At build_review, the containment floor writes .pipeline/containment-floor.json. Every violation is also printed in the step output and warning log with its task id, commit SHA, and offending paths. Every accepted widening is supplied directly to the isolated grader with its path, rationale, task id, and commit SHA, because the grader judges the branch diff rather than commit messages. The rubric projection carries that diff by reference — per-file paths, change kinds, and hunk line ranges anchored to the merge base — and the grader session, running inside the feature worktree, reads the referenced file contents and per-path diffs itself instead of receiving the raw diff text inline.
The advisory remains in the warning log and step output on every lap. On a failing lap it follows the review’s own reason, so the failure and retry lines name the actual review failure; on a passing lap it precedes the review output.
Declared pattern replication check
A plan may declare, in its header, that it replicates an existing source file under a rename map (**Pattern-source:** / **Rename-map:**, parsed by plan-pattern-source.ts). When the declaration resolves, build_review runs a deterministic content-comparison check — the engine’s first — before the grader runs: it reads the declared copy target, applies the rename map to the source, and requires an exact match. Unlike the per-task floors above, which are fail-soft and never change success, a copy mismatch fails the step outright, and its diagnostic (missing target, unexpected target, a rename-map collision, or a content mismatch naming the first differing line and column) is returned in place of a grader verdict — no RED evidence is derived from it, and it never runs at acceptance_specs. A malformed declaration (one header line without the other, an unresolvable source path, or an invalid rename-map pair) fails build_review before either the equivalence check or the grader runs, so a half-declaration can never be read as no declaration at all.
Per-step completion gates
These are the twelve gates that decide whether a step’s work is real. Ten replace the default “did the step’s artifact glob match anything” check with a custom predicate; two more run only in the verdict layer, so they can be strict without disturbing the linear walk.
| Step | What it protects against |
|---|---|
stories | stories that exist but do not specify behavior — every story needs a happy path and a negative path, each with scenarios, and none may be draft |
plan | a plan that does not cover the feature’s stories, scoped to this feature’s plan and stories |
build | tasks reported complete without work — task rows are re-seeded and re-derived from the plan each evaluation, so a forged row fails; a task carrying Done when: checks additionally must show each check true before it closes, and a check the approved plan cannot make true is reported as a plan gap rather than repaired off-plan |
acceptance_specs | acceptance specs that never ran — proof is required that this feature’s specs executed and failed, so a collection error or a skipped spec cannot pass for RED |
build_review | an incomplete build — built-in opt-in rubrics (testQuality and security, both off by default) plus opt-in installed custom review policies, judged from frozen review input rather than self-reports and through the shared decision/repair path; an empty rubric set is a PASS with nothing dispatched. See build_review.custom_rubrics. |
test_suite | a stale green — the fingerprint is re-inspected every time, so the evidence file’s existence can never satisfy it |
manual_test | a whitewashed retest — after a recorded FAIL, HEAD must have moved before an all-PASS attempt is accepted |
prd_audit | a partial or malformed audit report passing as complete — exactly one graded verdict row (PASS, FIXABLE, PLAN_GAP, or OVER_SCOPE) is required for every acceptance criterion across the feature’s stories; a FIXABLE naming no plan task blocks. A cited Plan task is resolved against the ids the active plan actually declares — an id the plan does not carry is rejected by name, and a report whose active plan cannot be resolved at all is rejected fail-closed rather than having its citations taken on trust. A finding without an owning criterion is a unique NC.<n> OVER_SCOPE row in ## Findings without an owning criterion; its visible-scope authority binds immutable original evidence and can apply to later wording only through a fresh durable relation. Invalid or duplicate rows are rejected individually while valid siblings remain routable, but any rejected row blocks with its diagnostic. Malformed/unsupported history, missing attribution, write or lease failure, invalid provider output, stale binding, overflow, uncertain relation, and projection failure halt with the named records and recovery action; none erase valid sibling authority. Only the ## Verdict Table section’s story rows count as verdicts, so a prior-cycle history table cannot block an all-PASS audit. An unresolvable or unreadable criterion set also blocks fail-closed |
architecture_review_as_built | an unrecognized verdict passing by default — only an explicit approval verdict satisfies it |
finish | a publication outcome that was never coherently recorded — .pipeline/finish-choice is the final record, not the source of interactive intent; a pr outcome additionally requires the recorded PR identity and verified publication evidence |
finish (release readiness) | a configured release-disposition result that is missing, stale, malformed, or unreadable — FINISH reports the exact typed condition before dispatching prose authoring or judgment, or making a publication mutation |
finish (prose authorship) | a retained PR whose body is still the engine-seeded placeholder, or whose exact authored revision received a persisted revision_required judgment — the coordinator dispatches its author_pr_prose pass with the branch diff and feature artifacts; a judged-deficient revision also carries the concrete objection into that pass. It accepts the pass only when fresh observation selects the next valid publication transition. The judgment pass is therefore never handed unauthored prose, and a prose defect neither commits the shipped record nor deadlocks the author→judge revision lap |
finish (presentation) | a PR shipping with halt boilerplate or an engine-generated floor body (the body-floor marker plus floor content — a marker an authoring pass left behind on real prose does not count) — either classification keeps a bounded prose pass required and prevents the final outcome record. Every completion-gate refusal in this class is classified missing: 'presentation', which routes the loop back into finish for a body rewrite rather than into /remediate or build; that re-dispatch is bounded to one attempt per pr_url (recorded in .pipeline/pr-body-regen-attempt.json), after which the engine’s deterministic body floor runs as a last resort so the feature still converges. A reused halt PR’s presentation is repaired earlier still — whenever the retained SHIP PR identity is resolved (SHIP-phase adoption, the pre-finish snapshot, or the finish-time restore), so SHIP steps that run before finish do not read a needs-remediation placeholder; a lighter clear additionally runs once at the start of every dispatch regardless of phase, so a resumed BUILD step is not left holding the placeholder either; the draft→ready flip stays finish-only |
Within build_review, testQuality’s revert-and-rerun preflight runs only when the rubric is opted in (build_review.rubrics.testQuality.enabled: true); with the rubric off, no preflight runs. The counterfactual is classified solely by the scoped command’s exit code; the engine does not parse runner-specific output. Exit code zero stays green and every nonzero exit is counterfactual RED. Only launch, timeout, and signal are scoped-run infrastructure outcomes. The preflight is evidence the judge may cite, never a finding by itself — a test that stays green under revert is not automatically a failure. A preflight infrastructure failure carries its bounded diagnostic excerpt on the existing .pipeline/events.jsonl event spine and in the build_review aggregate, so a materialization or scoped-run failure remains diagnosable after the mechanical allowance is exhausted.
Each predicate’s exact file, format, and failure text is in artifacts.
Tree-attesting admission
A step may declare itself tree-attesting only when its completion predicate re-verifies the current tree rather than trusting persisted step state. At the dispatch boundary, a persisted done status for such a step is fast-forwarded only when that predicate still passes; a stale or indeterminate result falls through to normal dispatch. The check reads evidence and does not reconcile or rewrite state. skipped remains a scheduling decision and is never re-evaluated by this rule.
The same rule applies at resume entry (--resume, including a daemon restart): a done step with a satisfied on-disk gate verdict is re-checked against its tree-attesting predicate before resume clamps its starting index to a later step. A predicate that no longer passes, or that throws, pulls the resume entry back to that step instead of trusting the stale verdict — this is what lets a daemon restart after a rebase land on test_suite rather than resuming past it into build_review.
The current tree-attesting set is {build, test_suite}. build re-derives task completion from the current history, and test_suite re-inspects its content fingerprint. This admission rule prevents a rebase-invalidated suite proof from allowing build_review to run ahead of test_suite while preserving the ordinary fast path for a current proof.
BUILD-verification round authority
test_suite is the sole BUILD verifier. After BUILD is repaired, it re-inspects the current suite evidence; a satisfied gate verdict on disk never skips that verification by itself. Its current result is the sole authority that marks the verifier satisfied for that round.
Land-time gates
These run when the composer loop lands a spec branch, outside the step loop. They protect the base branch from specs that would waste a build.
| Gate | Refuses |
|---|---|
| required artifacts | a spec missing its PRD, stories, or plan, or with an empty one |
| draft/stub reject | any artifact still marked draft, or a known stub string |
| stories approval | stories without the explicit acceptance marker — not being draft is not enough |
| ADR status | any ADR under .docs/decisions/ whose first line-anchored Status: declaration is not APPROVED or SUPERSEDED, or that declares no status at all — fenced code-block examples of rejected statuses are excluded from matching |
| tier agreement | a declared complexity tier that disagrees with the artifacts present |
| coherence | a traceability record that does not connect outcomes, requirements, accepted ADRs, stories, and tasks, or stories that do not tie out to the PRD |
| mermaid render | a diagram that does not render — previously prose guidance, now enforced |
| diagram presence | a non-Small architecture artifact with no fenced Mermaid block |
| protected-target plan | a task that directs BUILD to amend another feature’s sealed DECIDE artifact |
| plan completion checks | a task with no Done when: block, a blank check, fewer than two checks, or more than five checks; fenced-code examples are ignored |
| plan task count | a plan with 41 or more parsed tasks unless it has exactly one **Scope-exception:** declaration with a non-empty rationale; 21–40 tasks are a plan-authoring warning, not a land refusal |
| architecture obligation coverage | a decision in a changed land-accepted ADR (APPROVED or SUPERSEDED) with no unique disposition, an invented decision, an invalid disposition, a nonexistent task, or task evidence absent from the cited task’s Done when: block |
Before land, plan authoring runs ai-conductor plan-protected-targets <plan-path>. It is a blocking, read-only check that reports every offending task/path pair. Land repeats the same judgment against the plan being landed, so a plan cannot bypass the rule by skipping the authoring command. Both gates apply at every tier and judge only the current plan, not historical plans already merged.
The coherence gate is itself layered. Tier S always engages its criterion layer, carried directly in the plan. At other tiers, a change set with no coherence artifact path is treated as a legacy change rather than a violation. Once engaged, the story, criterion, orphan-task, and coverage-table layers are always required; the functional-requirement layer only on the product track; the outcome layer only when outcomes exist; and the ADR layer whenever the current spec change set contains a .docs/decisions/adr-* path, including a deletion. The ADR row pool itself contains only non-deleted ADRs, so a deletion-only change engages the layer but passes with no ADR row. It aggregates every waivable gap rather than stopping at the first, and reports them as one error. Already-landed specs whose coherence artifacts predate criterion rows remain valid for daemon discovery and BUILD. See composer loop.
When that ADR pool contains citable decisions, the plan must also carry an ## Architecture Obligation Coverage table with exactly one row per <adr-stem>#D<n>. A row dispositions the decision to real plan tasks, existing implementation, or no implementation change. The engine validates complete and unique decision coverage, the closed disposition vocabulary, real task ids, and an exact evidence fragment from a cited task’s Done when: block. Missing or malformed bookkeeping is non-waivable. The subsequent coherence-check judgement decides whether the mapped task or existing/no-change evidence actually satisfies the decision; the engine does not derive that semantic answer from matching text.
The criterion layer requires exactly one row for every happy- and negative-path criterion extracted from the stories artifact. Each row must mark the criterion covered, cite an existing plan task, quote an exact span from at least one cited task after whitespace normalization, and carry the authored diff-locality disposition diff-local. outside-diff records that the criterion depends on state beyond the feature diff and blocks land unless a fresh coherence waiver covers the reported gap. The engine reads that disposition; it does not infer locality from the criterion’s prose. Omitted, invented, duplicate, non-covered, ungrounded, and missing-disposition rows are also coverage gaps. A malformed criterion row or a stories artifact with no parseable criteria is defective evidence and fails before waiver evaluation. See artifacts for the row format.
Each task’s leading **Story:** line cites its story with story-N, Story N, bare N, or epic-N; the cited id may include dots or hyphens. A task that cites no declared story is reported as an orphan. The failure names the unbindable id and these accepted spellings; a missing or empty reference line is reported as absent rather than assigned an invented id.
The functional-requirement layer checks both directions, because coverage alone is only half of a tie-out. Forward, a PRD requirement no story cites — or whose only citing stories no task covers — is a gap. Reverse, a story that cites an FR-N the PRD never declares, or that cites no requirement at all, is a gap against that story’s id. The reverse direction runs on the product track only: a technical-track spec has no PRD, so it has no requirement layer to tie out against. What the gate does not judge is whether a story semantically delivers the requirement it cites — a story whose scenarios contradict its own FR is a fail verdict the /coherence-check skill records, not a set comparison. Story-versus-story contradictions belong to conflict-check earlier in DECIDE; this gate compares each story against the PRD only.
Earlier in DECIDE, conflict_check also compares each relevant story with the selected approved ADR corpus. The default change_set corpus is bounded to the current spec’s ADRs; repo_wide narrows all approved ADRs to overlapping subjects and records the ADRs it examined and excluded. That judgment resolves ADR-versus-story conflicts before planning. This is separate from the coherence gate: any current-change-set ADR path engages its ADR layer, while only non-deleted ADR files enter the traceability-row pool; no conceptual applicability judgment expands that row set.
Self-host gates
When the harness builds itself, an extra bundle activates as one unit behind a single decision. It covers version approval, the release artifact gate, a live-boundary fingerprint, a build-auth preflight, a sandbox build environment, and a skill relink preflight. These protect the running checkout from the build that is modifying it. Details and the release-gate specifics are in releases and self-hosting.
Fail-closed semantics
Every completion gate fails closed. Missing evidence, stale evidence, malformed evidence, and a non-passing verdict all leave the gate unsatisfied — none of them is treated as “probably fine”.
Three specific forms this takes:
- Presence is never proof. Several gates re-derive their answer even when a passing artifact is sitting right there, because the artifact could describe a previous state of the code.
- Freshness is part of the check. A verdict must be newer than a floor — the current judging attempt when there is one, otherwise the run’s session start. The SHIP-tail verdict gates additionally require an engine-stamped dispatch
runIdwhen one is available; a mismatch is no verdict and retries rather than routing the earlier report’s findings. Unstamped legacy artifacts retain the mtime fallback. A small filesystem-clock tolerance applies to the attempt floor only. - Undeterminable is a failure, not a pass. When a gate cannot compute its input at all — an unresolvable plan among several, a change set git cannot produce, a scope it cannot bound — it blocks. It does not guess.
The one deliberate fail-open: when a run carries no session-start timestamp at all, freshness checks pass on file presence, so upgrading the harness mid-feature does not strand an in-flight build.
Kickback and remediation routing
A blocking gate in the tail loop does not simply stop. It routes.
Kickback re-opens an upstream gate by writing an unsatisfied verdict that carries provenance: which step re-opened it, and the evidence. Four steps opt in as kickback targets — prd, architecture_review, stories, plan — so the furthest back the loop can throw work is the spec. Kickbacks per gate are capped; past the cap the run halts instead of cycling.
An autonomous run may enter a DECIDE step only with explicit operator direction. The same fail-closed policy is consulted at all four navigation seams: the forward walk, the verdict-aware resume clamp, the verdict-driven scanKickbackVerdicts rewind, and the planner-driven planRemediation rewind. An unknown target or phase, or an unsatisfied or unverified DECIDE completion contract, writes a needs-human HALT and launches no provider. This supersedes the two-seam DECIDE-kickback policy from #551.
The policy fast-forwards without dispatch only when the DECIDE step is tier-skipped, has no completion contract, or has a verified satisfied contract. Otherwise an operator must create a matching decide-grant; the grant authorizes one named step and is consumed immediately before that step dispatches. The grant is stored in the daemon-owned .daemon/grants/<slug>.json, outside every feature worktree, so a build agent cannot author its own authorization; a decide-grant.json inside .pipeline/ authorizes nothing. plan is excluded entirely — it is refused before any grant is consulted, and the CLI rejects --step plan, because a daemon that re-plans rewrites an approved DECIDE artifact with no human at the gate. A planRemediation rewind that names a DECIDE step is the one exception: remediation explicitly asking to revise that step is evidence the accepted artifact needs another look, so a satisfied contract does not fast-forward it either — the same grant is still required. Interactive runs retain their existing DECIDE authoring path.
Remediation is what a blocking SHIP audit — prd_audit’s FIXABLE grades, or the as-built review’s BLOCKED verdict — does when the fix is not obvious. It classifies each gap and routes it to the earliest step that can close it — build, acceptance specs, architecture review, or plan — all of which sit before the gate that found it. A fifth disposition, publication, covers a gap whose only defect is the published PR prose (a placeholder or wrong-template body, a stale title, a missing Closes reference): it routes to finish, the step that owns PR prose, and — unlike the four step-valued targets — appends nothing to .docs/plans/<slug>.md, because a PR body fix is not plan work and amending the plan from here trips the protected-artifact self-amendment guard. Two gap categories cannot be routed and halt for a human instead: architectural clarity and product scope. Neither is something an unattended run should decide.
A finish-gate refusal that is itself a publication defect never reaches the planner at all. The gate already names exactly what is wrong with the PR, so the loop re-dispatches finish directly for a body rewrite — bounded to one re-dispatch, after which the gate’s own last-resort body floor converges the feature. Routing that through the planner is what once turned a 30-second gh pr edit into an 18-task rebuild.
A verified FINISH publication transition is progress, not a failed attempt: it immediately re-enters FINISH without spending the step retry budget or advancing its model-escalation rung. This separate allowance is bounded to 14 verified transitions per FINISH step entry. If publication still has not converged when the allowance is exhausted, the conductor writes a needs-human HALT naming the last transition rather than looping indefinitely. The stalled-feature runbook defines diagnosis and recovery for that halt.
If the remediation plan is missing, stale, malformed, or has gaps it does not cover, the engine falls back to deterministic routing rather than trusting a partial plan. Unknown dispositions are dropped, not honored.
An ordinary build disposition must carry concrete tasks — a taskless build gap is dropped and the run halts instead of dispatching an empty route to the builder. The one exception is a build-stall question: there the answer legitimately lives in the gap’s rationale with tasks: [], so a taskless build is accepted only when the gap’s source is a build-stall.
For a prd_audit FIXABLE finding, the remediation disposition identifies the finding by its report criterion: a feature without a PRD uses S<story>.<ordinal> (for example, S5.1), while a PRD-backed finding may use its FR-N identity. The engine matches criterion keys case-insensitively when admitting the planner’s task, so a case-only spelling difference cannot strand an otherwise authorized repair. An ID that matches neither an admitted criterion nor another authorized gate finding halts with the rejected IDs and the available admission keys rather than appending unbounded work.
A sixth disposition, existing-task, covers a current prd_audit FIXABLE or as-built REMEDIABLE finding whose remedy an existing active-plan task’s Done when already admits. The planner binds the gap to the real plan task id(s); the engine re-stages those rows to pending in .pipeline/task-status.json and kicks back to build without appending anything to the plan. It records a lap-only pending charge under the owning gate’s key (gates.prd_audit or gates.architecture_review_as_built), keyed by the repair obligation id, and charges that lap when build dispatches (see remediation caps settle at the build transition). It never draws from the shared plan-growth allowance, so a lap-cap halt names lap cap reached (n/n) rather than the growth figures. A bound id absent from the active plan halts needs-human naming that id. The no-op escalation stays armed for every gate on the lap: each participating gate banks the pre-re-stage resolved count, so a BUILD that only re-completes the re-staged rows on a byte-identical tree is classified no-work and halts instead of admitting another lap. When the same validation-group round also carries a manual_test FAIL, the consolidated kickback owns the work order: the finding rides that single merged rewind, and the existing-task lap, pending-finding, and re-stage mechanics do not run.
Remediation tasks must not order a regression. A task that removes, replaces, rewrites, or relaxes existing code, tests, or assertions has to name the completed plan task or story criterion whose delivered behavior and coverage survive the change, and — unless the evidence shows that coverage is redundant — carry the replacement in the same task as the removal. Removing a workaround does not license dropping the assertion beside it: the next audit re-raises the lost coverage and the lap is spent restoring it. This applies to every remediation trigger, not only the finish verification’s test failures.
The same bar covers regression by omission. When a task changes an enumeration, registry, vocabulary, id list, grammar, or any value a second location duplicates or must agree with, it has to name that counterpart and bring it along in the same task, or derive both from one source so they cannot drift again — the stronger fix wherever the evidence supports it. A pair that silently diverges stays invisible until something reads both and fails, which is one audit lap later than the task that split them.
Remediation tasks close the class, not the cited instance. A gap’s evidence records where the auditor looked, not how far the defect reaches, so a task is swept for every site of the same shape before it is emitted — sibling sites carrying the same wrong predicate, missing guard, or stale literal, and whatever a removal orphans, such as the deleted arm’s last caller or its fixtures. This is the main defence against audit cycling: a task that repairs one site of a class buys a single cycle and generates its own successor at the next site, which is how a feature whose blocking-gap count is genuinely falling can still spend four cycles on one requirement.
The sweep is bounded by plan admission and does not widen the diff on its own authority. A sibling site is included only when an existing plan task admits it — the same coverage test remediation applies before selecting plan — and one that no task admits is recorded in the disposition’s rationale as found-and-excluded rather than fixed. That boundary is deliberate: closing a class by adding work the plan does not cover is exactly what prd_audit grades OVER_SCOPE, and an unauthorized addition can leave remediation with nothing dispatchable at all, which costs more than the audit cycle the sweep was meant to save.
A remediation gap that requires amending another feature’s sealed DECIDE artifact is not eligible for build or acceptance_specs. It returns to the owning DECIDE step; in daemon mode the existing DECIDE kickback policy reaches the operator gate rather than attempting a BUILD-side bypass.
A prior lap’s FAIL is not a fresh verdict
build_review completion reads .pipeline/build-review.json and compares its lapId against lap-<HEAD>. A non-PASS aggregate whose lapId names an earlier HEAD is scored absent — “no fresh verdict” — rather than reused as the current lap’s outcome: a rubric that FAILed a prior lap never kicks back findings the current lap has not itself judged, and the run instead re-dispatches build_review to produce a verdict for the code actually at HEAD. A git rev-parse HEAD failure skips the check and preserves the older behavior rather than blocking on an unresolvable HEAD. PASS aggregates are unaffected — they keep the existing code-stamp preservation path that lets a same-surface PASS survive re-dispatch. The stale condition is recorded on the event spine as build_review_stale_aggregate (telemetry only; never consulted for routing) and consumes no kickback budget.
The same rule guards the daemon’s post-retry kickback route. When build_review exhausts its retries, the daemon re-reads the raw verdict to build the kickback evidence; a stale-lap FAIL aggregate found there is discarded outright (the artifact is deleted, exactly like the stale-mirage disposition) and the run re-lands on build_review instead of kicking its prior-lap findings back to build. Without this, a stored lapId that never matches a moving HEAD replayed already-fixed findings as kickbacks indefinitely. The discard also emits build_review_stale_aggregate. A second stale-lap FAIL in the same run means the grader itself is stamping a prior lap, so the run halts needs-human instead of re-landing again.
A below-cap mechanical (infrastructure) fault publishes no aggregate at all, so it cannot be stale by this check; the last such fault is instead recorded on the kickback ledger’s build_review gate entry (lastMechanicalFault) and surfaces in ai-conductor build-review findings and in the exhausted-mechanical-allowance HALT when the current lap has no readable diagnostic of its own — see the runbook.
Where a build_review FAIL goes
build_review no longer judges plan conformance, outcome delivery, or mechanism soundness (FR-1): the scope, completeness, and rootCause rubrics are retired. The container ships built-in testQuality and security (both off by default) and opt-in installed custom review policies; their findings remain subject to the same aggregate decision and repair contract. See build_review.custom_rubrics. A testQuality finding — a test that could pass against a stub of the behavior it claims to cover — is a local diff defect the builder can fix in place, so a build_review FAIL routes straight to build; there is no remediation-planner branch for a build_review FAIL. The questions the retired rubrics used to ask now live at SHIP:
| Retired rubric | Question it asked | Now owned by |
|---|---|---|
scope | Does the diff contain work the plan does not describe? | prd_audit’s OVER_SCOPE grade (scope-as-intent, see below) |
completeness | Does the diff cover everything the plan describes? | prd_audit’s FIXABLE/PLAN_GAP grades against story acceptance criteria, and the per-task Done when: evidence check at task close |
rootCause | Does the mechanism actually close the defect? | The as-built architecture review’s PLAN_GAP/BLOCKED verdict |
prd_audit’s grades and routing
prd_audit runs on every feature, regardless of complexity tier or work track (FR-8) — no tier or track skip remains on the step, and the skill itself does not infer one from tier, track, or the absence of a PRD. When a feature’s stories carry no acceptance criteria to judge, there is nothing to grade and the audit trivially passes. It judges the shipped implementation against the stories’ acceptance criteria as the authority, using PRD functional requirements as context for intent when a PRD exists (FR-7). Each finding carries exactly one grade:
| Grade | Meaning | What happens |
|---|---|---|
PASS | The shipped behavior satisfies the criterion. | Nothing — no finding is recorded. |
FIXABLE | The criterion is unmet and an existing plan task owns the repair; the finding names that task and the criterion (FR-11) or is rejected as malformed. | Appends at most one remediation lap’s worth of tasks, capped at both a fixed count (default 5) and a fraction of the authored task count (default 25%), whichever is lower — both operator-configurable (FR-12). Exceeding the cap, or needing a second lap, halts for the operator listing every finding (FR-13); the tasks are appended and left pending first, and the halt fires at the build transition (see remediation caps settle at the build transition). |
PLAN_GAP | The criterion is unmet and no plan task owns the repair. | Halts for the operator when the unmet criterion is a happy-path scenario; for a negative-path or edge scenario it is recorded in the verdict and the shipped record and the feature may ship, unless operator configuration requires a halt (FR-14). |
OVER_SCOPE | Shipped behavior goes beyond the planned implementation, judged against intent — the PRD’s Goals/Non-Goals and In/Out Scope when a PRD exists, otherwise the stories plus the plan’s stated outcome (FR-9). | A widening within intent is self-accepted and recorded. A widening outside intent with no user-visible effect is recorded in the verdict and the shipped record and the feature ships. Every outside-visible finding is presented in one decision block. An explicit accept applies only through its immutable original source/case reference and a fresh relationship. A refusal remains blocking and re-halts as “refused — rework required”; its decision block offers an explicit revision linked to that refusal, never an implicit acceptance. Legacy attributed evidence remains available for reconciliation, while malformed or unsupported legacy history stays visible and halts with recovery rather than being treated as absent. |
No SHIP-phase gate — prd_audit, the as-built review, or manual_test — can send work back to build that the approved plan does not authorize; every off-plan need is a halt or a recorded, non-blocking finding (FR-17). At this feature’s ship, FR-17 is delivered for prd_audit and the as-built review only; the manual_test route (conductor.ts:4726-4800) was out of scope because manual_test appears zero times in this feature’s plan, stories, and coherence mapping — tracked as #1826, which also carries the prd_audit impl-only fallback (conductor.ts:8872-8917).
The as-built architecture review’s checks and verdict
The as-built review runs on every feature (FR-15). Its checks are conditional on artifact presence and complexity tier — the reachability sweep and the plan-gap check run at every tier; adrCompliance runs whenever approved ADRs exist; diagramDrift runs where diagrams exist — and each is operator-configurable per tier via architecture_review_as_built.checks.<name>.tiers. Its verdict is one of APPROVED, PLAN_GAP, or BLOCKED (FR-16): PLAN_GAP means the code faithfully implements the approved design and the design itself is the limit. The outcome it is judged against is the sealed story criteria under .docs/stories/ — superseded .docs/intake/ capture is never the authority — so the gap is recorded in the verdict and the shipped record and ships when those criteria are satisfied, and halts only when a sealed story criterion is unmet.
Under the default conditional review mode, a non-auto run prompts the operator to review the step’s unapproved artifacts whenever the typed verdict in .pipeline/architecture-review-as-built.json is not APPROVED, or is missing or unreadable (fail-closed). A clean APPROVED verdict auto-approves. This step ignores the review-required-<step> marker.
A BLOCKED report must contain exactly one ## Blocking Findings table with Finding, Class, Governing clause, and Summary columns. Class is either REMEDIABLE or DESIGN. Every REMEDIABLE row names an approved ADR decision (<ADR filename stem> decision <number>, where the word decision is optional) or a task in the feature’s active plan; a missing or malformed table, class, clause, or row is invalid and halts for a human. Inline markdown emphasis around the clause — a backticked or bolded stem — is stripped before the clause is resolved, so `adr-x` + Decision 4 resolves exactly as adr-x decision 4 does. A clause naming more than one reference (Task 9 and Task 10) remains unresolvable: cite one clause per row and split the finding.
When every valid finding is REMEDIABLE, daemon runs with as-built remediation enabled can dispatch the bounded remediation route, append the authorized repair work, and re-stage BUILD. The route is bounded by architecture_review_as_built.max_remediation_laps and the shared plan-growth allowance; exhausting either produces a kickback-cap halt at the build transition, after the round’s tasks are appended and pending (see remediation caps settle at the build transition). A DESIGN finding (including a mixed report) halts for a human decision and names each DESIGN finding with its governing clause. An all- REMEDIABLE report that cannot route halts needs-human, names whether remediation was disabled, the run was not a daemon, or the planner produced no usable plan, and lists every blocking finding. An invalid report halts with its parse fault. After a later successful as-built verdict, each remediated finding is retained in the verdict artifact and the shipped record.
Per-task Done when: evidence
Per-task delivery is evidenced at build when a task closes: each Done when: check parsed from the plan must be shown true before the task counts as complete (FR-6). A check that cannot be made true under the approved plan is reported as a plan gap, not repaired off-plan. A plan authored before this change — with no Done when: blocks — closes tasks on the prior evidence rule instead (FR-21).
Bounded plan growth
The total number of tasks a plan can accumulate after approval is bounded by the authored count plus the capped remediation additions from prd_audit and enabled as-built review remediation (FR-18). Each gate has its own remediation-lap cap, but both draw from the same plan-growth allowance. ai-conductor daemon status prints a PLAN GROWTH [<slug>]: line per in-progress feature — authored count, added count broken down by gate, and tasks remaining under the cap (FR-19; see daemon status).
Remediation caps settle at the build transition
A prd_audit or as-built remediation round never halts on its lap or plan-growth cap before the repair reaches the plan. /remediate runs, its admitted tasks are appended, committed, and seeded pending, and the round records one pendingRepair on the kickback ledger: a receipt id, the per-gate lap and growth charges, and the appended task ids. Laps and growth are unchanged at that point, and the appended tasks are excluded from the authored count so they neither refund allowance nor inflate the 25% denominator.
When build next dispatches, the conductor settles the pending repair all or nothing under one ledger lease. Within every allowance, each named gate is charged its lap and growth, plan_growth is emitted, and the record is removed, so a retried or restarted build charges nothing again. When any named allowance is exhausted, nothing is charged, build stays not done, and the feature halts kickback-cap naming the gate, the allowance, and every finding with a fresh halt generation. A consumed kickback-budget raise therefore resumes straight into build on the already-appended tasks instead of re-auditing unchanged code; a raise that is too small or names the other gate halts again at the same boundary.
A malformed pendingRepair reads the prd_audit, as-built, and growth allowances as exhausted, so build does not dispatch. When the route-into-no-op guard halts needs-human after an append, the pending repair is discarded without charging. build_review, test_suite, and manual_test kickbacks settle no pending repair, and a coverage_binding existing-task reopen still charges its lap at re-stage.
The build rework hint for a testQuality FAIL carries best-effort plan contract: and prior attempts: pointer lines derived from the raw rubric aggregate — a plan contract: pointer names the active plan’s owning task for a finding anchored to a plan task or an owned file, and a prior attempts: pointer lists earlier .pipeline/build-review/<lap>/*.json findings that share the same canonical anchor. Pointer derivation is advisory: a missing active plan, an unreadable prior-lap artifact, or an anchor with no unique matching task yields no pointer for that finding rather than blocking the dispatch. Since a build_review FAIL now routes straight to build rather than through /remediate, this pointer derivation is scoped to that rework hint; /remediate dispatches are triggered by prd_audit, the as-built review, finish verification, and build stalls instead.
Every exit from a build_review FAIL block consults the disposition store using the effective verdict, not only the raw aggregate that first reported the FAIL. An operator ai-conductor build-review accept can land while the remediation planner is composing rework from that raw aggregate (a window of minutes); when every graded finding is accepted at routing time, the composed rework is dropped and build_review re-lands instead. Its re-run settles from cache, applies the dispositions, and re-dispatches only infrastructure-failed rubrics. Without this guard a kickback has ordered removal of exactly the surface the operator had just accepted.
The cache identity also includes the judging engine (adr-2026-08-21): the engine’s 12-hex content stamp and a sha256: digest of the rubric’s installed SKILL.md. A cached judgement made under a different engine build or edited rubric skill text is discarded (miss reasons engine-version-mismatch / skill-digest-mismatch) and re-judged; each discard is a build_review_cache_discarded event in events.jsonl, the daemon log, and the audit trail.
Each rubric has a closed engine-owned finding vocabulary, repeated in its provider-facing skill contract: testQuality uses test-insensitive; security uses its ten security concern kinds. The parser normalizes harmless casing and underscore variation before validation. A value outside the rubric’s vocabulary is rejected and receives the bounded repair/rerun path below; it cannot become a new finding identity or burn a kickback.
A rubric session that answers but misses the judged-result JSON contract does not burn its dispatch. The engine embeds the exact per-rubric result schema (including the nested anchor object’s field names) in every rubric prompt, and on a shape failure issues exactly one bounded repair invocation — the rejection diagnosis, the schema, and a capped excerpt of the session’s own previous output, asking for the JSON re-emitted verbatim in shape only. Only when the repair turn also fails does the rubric settle as an invalid-provider-result infrastructure failure, and that failure then carries a bounded (≤2 KB) raw-output excerpt in its diagnostic detail instead of a bare label.
The graded diff excludes paths the engine authors rather than the builder — .docs/shipped/ and .pipeline/. No plan task can describe harness machinery output, so grading it guarantees a scope finding the builder cannot legitimately act on.
The graded diff also excludes one file the builder normally does own: the feature’s own plan, when its divergence from the graded base is exactly the engine-appended ### Task rem-* blocks recorded in .pipeline/engine-state.json — the same test the protected-artifact seal applies before it tolerates the append. Any other amendment (an edited earlier line, an unrecorded task id, added prose) fails that test and is graded in full. The plan body prd_audit and the as-built review judge against is unaffected: it still carries every appended remediation task, and a rem-* task’s outcome is bounded by its own text and never enlarges any other task’s outcome — remediation-lap products are inputs to converge on, never a surface that expands what a later lap must litigate.
The graded diff also excludes paths touched only by feature commits Git identifies as patch-equivalent to commits already on the resolved review base. This is path-scoped and fail-closed: a path stays graded if a novel feature commit also touched it, or if the patch-equivalence probe or path attribution cannot establish the exclusion. The build_review_base event payload carries the filtered commit set and excluded paths, while the daemon log renders its filtered-commit count for the operator.
When a deterministic BUILD verification gate — test_suite or any other gate in that group — fails, the engine accumulates the sanitized failure in .pipeline/build-review-rebase-repairs.json. The ledger is outside rewritten Git history, so repeated rebases retain earlier entries without treating commit trailers as authority. A failure is attributed to a base advance by joining it against rebase_changed events on .pipeline/events.jsonl — the durable, append-only event spine — requiring both that the failure was observed after the advance and that its diagnostic overlaps a path the advance changed. A bare time-window match is not enough: overlap is required so a genuinely unplanned deletion is never laundered as a repair. This join replaces an earlier, transient signal (a kickback field on the gate’s own verdict file) that a later run of the same gate silently overwrote.
The ledger and the build_review_repair_context telemetry event it fed were judgement context for the retired scope and tautology rubrics; the retained testQuality rubric does not consume either. Both mechanisms are retained as dead weight rather than removed, since the ledger itself is populated independently of build_review (test-suite-remediation.ts reads it during rebase repair).
Operator-authorized protected-artifact reseals
An ai-conductor reseal an operator runs mid-feature writes a new baseline at the current commit and appends a rebaselines entry to .pipeline/protected-artifact-seal.json recording the trigger (operator-reseal) and rationale, and a protected_artifact_reseal audit record with an operator origin — so the override is auditable rather than silent. The reseal is consulted by the protected-artifact seal check itself (a resealed path is no longer refused as a feature-authored DECIDE change once the seal rebaselines onto a later merge base) and, together with every feature commit’s Scope: trailer rationales, supplied to prd_audit as immutable OVER_SCOPE intent evidence to judge, not an instruction to follow — an operator claim it still has to weigh against the PRD’s or stories’ stated intent. The reseal rationale is not rendered into the retained testQuality rubric’s prompt, since testQuality judges tests, not plan or scope conformance.
Kickback counting is untouched by the consolidation — a build_review FAIL routed to build counts against the per-gate cap like any other.
Post-join build-review adjudication
For a current aggregate FAIL, the daemon normally runs one remediate judgement after the rubric join. The judgement receives the complete unresolved finding set and the feature’s prior case history; it does not replace the raw verdict in .pipeline/build-review.json or the operator’s exact-finding dispositions. build_review.adjudication.enabled: false retains the legacy raw-FAIL route.
The engine validates the judgement as a complete source-to-case mapping, assigns durable case and effect identities, and records the feature-local state before applying an effect. An action publishes a durable BUILD work order and returns to build; a justified deferral files or reuses its marked intake issue; a rejection has no external effect. A refute row can instead settle an already attempted action case once: it must bind that case, carry high-confidence path/excerpt evidence and at least one refuted assertion, and changes the case to a resolved refutation without charging another BUILD route. It emits remediation_case_refuted. A refutation may retain only a complete deferral for a narrow remainder; the refuted source settles only after that residual is applied, while a reserved or failed residual remains blocking. A second refutation of the same case halts needs-human. In a mixed lap, an action route takes precedence while an uncovered infrastructure failure remains blocking after the BUILD attempt. Invalid, incomplete, stale, repeated, or unfinished case state halts rather than silently routing or passing.
The case store and work order survive a daemon restart. BUILD stamps the work order before it starts, so an already attempted case cannot receive another free route; a later clean review settles cases whose sources are absent. Inspect the durable state and lifecycle events in artifacts when diagnosing a halt.
Not every gate reruns on retry. For the three judged SHIP gates, a genuine fresh non-passing decision routes immediately, while an identical repeat on provably unchanged inputs only routes on the second attempt — retrying a judgement that already looked at the same bytes is not progress.
A step’s own refusal ends the run. A step that decides its work cannot honestly be done — say acceptance_specs finding that the accepted DECIDE artifacts contradict already-merged code — can write .pipeline/HALT with a needs-human .pipeline/HALT.class and refuse. When an attempt settles with such a marker, the run stops and surfaces that HALT’s own body as the halt reason, instead of spending the rest of the retry budget re-dispatching the same unresolvable condition and reporting a generic gate miss. Only a marker that appeared or changed during that attempt counts: .pipeline/HALT persists across steps and runs, so a leftover marker from earlier work never suppresses a legitimate retry, and anything ambiguous is treated as leftover.
Exhausted but working. A build step whose retry budget runs out is not automatically a wedge. Three signals do three distinct jobs, and none substitutes for another: the attributed-task count is advisory routing and telemetry, commit movement is the liveness authority, and build_review is the sole completion authority. So when the budget exhausts but at least one attempt moved HEAD — real work landed, just without a Task: trailer attributing it — the run routes through the same advance seam a completed build uses, straight into build_review, instead of the generic “retries exhausted” halt, but only when the worktree is clean. Dirty paths keep the build halted so they can be committed or discarded; they never ride the commit-movement route. Which plan task ids were left unresolved is recorded in conduct-state.json so the decision stays visible. This is not an always-pass: build_review re-grades the diff against the plan on its own evidence and can still FAIL, kicking the build back under the same per-gate kickback cap as any other build_review kickback, so repeated route→FAIL cycles — including no-op commits offered as movement — are bounded exactly like everything else. A build with zero commit movement across every attempt never routes; it keeps the ordinary remediation-then-halt path.
When routing runs out, the run writes a halt marker and stops. See stalled or stuck feature.
Waivers
Two gates accept a committed waiver: the self-host release gate and the land-time coherence gate. Both use the same file idiom — a Waives: line and a non-empty Rationale: — and both parse strictly: a missing line, an empty gap list, an empty rationale, or an unrecognized name makes the waiver malformed, and a malformed waiver is never silently accepted.
| Release waiver | Coherence waiver | |
|---|---|---|
| Directory | .docs/release-waivers/ | .docs/coherence-waivers/ |
| Vocabulary | fixed — four canonical breaking-surface names | dynamic — only gap ids the validator actually reported for this change set |
| Waives | a breaking-surface classification that is internal-only in fact | a coverage gap the author can justify |
Three rules apply to both, and they are what makes a waiver a record rather than an escape hatch:
- Freshness. The waiver must be added or modified in this change set. A waiver merged by a prior feature never satisfies a later one. Without this, one waiver would permanently disarm a gate.
- Total coverage. A waiver must cover every classified surface or reported gap. Partial coverage blocks, and the failure names the gap that is still uncovered.
- Some things are unwaivable. A fabricated identifier cited in a legacy traceability row, a malformed criterion row, or a stories artifact with no parseable criteria is an evidentiary defect, not a coverage gap, and no waiver clears it. An undeterminable change set cannot be waived either — the gate does not know what it would be waiving. And a change that genuinely alters CLI, hook, or schema behavior needs a real migration block, not a waiver.
What a gate is not
Three things in this repo are easy to conflate and are not the same:
- Gates block a feature’s progression. This page.
- The integrity suite validates the harness repo’s own structure before a commit — validation. The BUILD
test_suitegate runs it with the conductor test suite before SHIP. - The release gate is a self-host SHIP gate for release metadata and migration validation — releases.
Per-step enforcement values and skip rules: steps. Gate-related config keys: configuration.
Directory hints in build review
A plan Files entry ending in / describes a directory, not a source file. Build review does not read it as a Git blob or seed dependency discovery from it. Changed test files beneath that directory still enter review through the pinned Git diff; explicit test-file entries still seed discovery. Missing required source blobs and invalid file paths continue to block review.
Test-quality candidate references
A reviewer may cite a resolved candidate using the source-region reference supplied by the engine. When that reference identifies exactly one validated candidate, the engine translates it to the existing test-title and occurrence identity before validating the finding. This prevents a source hash versus title hash mismatch from discarding an otherwise actionable review. Shared source references require a canonical test reference; excluded, indeterminate, or foreign regions remain rejected. Finding identities, prior dispositions, and the substantive test-quality verdict do not change.