Batch engine internals

Contributor notes on the Claude Code workflows that drive a batch of cards, what installs, what runs it, the four judgment stages, resume and recovery, and what happens under other tools.

pair-cli install writes two directories that carry the batch engine:

Installed toWhat it is
.claude/workflows/the orchestration workflows
.claude/agents/the agent definitions those workflows dispatch to

They install unconditionally, on every project, whatever tool you use.

Three workflows ship:

WorkflowWhat it drives
pair-implement-batcha batch of cards, each to a review-approved PR through four judgment stages (preparation, independent contract validation + seal, implementation, independent final verification) resuming a cycle from its first incomplete step
pair-refine-batcha batch of backlog cards, classify, refine, or read-only triage; under the default always gate the refine prompt is unchanged and writes Ready as before; under a declared never/when gate Ready is written only by complete after a task breakdown, so a refine-only run leaves cards held. By its dispatched prompts it writes to the PM tool only and touches no file in the repo: that is process, not permission: see the note below the agent table
pair-loopthe unattended delivery loop, selects eligible cards via /pair-next, runs a dependency + mutex analysis, composes pair-implement-batch for the actual batch, hands the autonomy policy (until / prepare / merge) to the batch, which merges a review-approved card only when the merge gate allows it, and evaluates the stop predicate. Realization for the /pair-loop skill in Claude Code (ADR-017 §4); elsewhere the skill takes a degraded one-card + continue-token path and this workflow never runs

pair-refine-batch takes an optional prepare argument (always | never | when[; has: <labels>][; lacks: <labels>]) for refine items. The effective gate is resolved with the precedence argument > ## Autonomy in automation.md > default always. Under always the human path is unchanged. Under never the card is refined unattended ($approval: auto), assumptions are recorded, and Ready is written only after a task breakdown. when does the same unless a listed condition fires at a stage boundary, then the card stays Draft with needs-review and one escalation comment. The result gains a held bucket: cards refined or escalated but not Ready.

pair-refine-batch deliberately carries neither of the two knobs pair-implement-batch has. It has no parallelism cap because each card costs one API round trip and holds no worktree, no branch and no CPU: the cap exists on the implement engine because a card there occupies a working tree.

And it has no args.pipeline because it drives pair's own refinement lifecycle by name (/pair-capability-classify, /pair-process-refine-story, …) rather than a configurable one; generalizing it is a separate change, and the file says so at the top.

Workflows are Claude-Code-specific

The engine is a workflow script driven by Claude Code's orchestration primitives: it spawns subagents by type and runs their steps in parallel. No other tool executes it today.

Under any other tool the files are inert. They are never read, never executed, and cost about 150 KB on disk, mostly the implement engine itself, which carries its rationale inline. The workflows' own dry-run test suites are development artifacts and are excluded from the install, so they are not part of that figure. Nothing errors.

What the agent layer can do

Under Claude Code they are not inert, and it is worth knowing exactly what arrives.

.claude/agents/ declares five subagents, each with real tool access:

AgentTools it declaresWhat that lets it do
pair-implementerRead, Edit, Write, Bash, Grep, Glob, Skillwrite files, run commands, create worktrees, commit, push, open or update pull requests, always above a sealed acceptance contract (/pair-workflow-implement-phase, /pair-workflow-green-fix)
pair-reviewerRead, Grep, Glob, Bash, Skillthe independent final verifier (/pair-workflow-review-phase): read the code, run commands (git, gh), run the custody script, publish the marker-keyed review comments; by its own instructions it does not edit, commit or merge
pair-fix-test-authorRead, Edit, Write, Bash, Grep, Glob, Skillthe preparation owner (/pair-workflow-red-spec): inventory, grouping, tests and fixtures only; it cannot commit, push, post or edit production artifacts by instruction
pair-red-contract-verifierRead, Grep, Glob, Bash, Skillthe independent validator and sealer (/pair-workflow-red-verify): reproduce the contract, reject with every gap or run red-snapshot.mjs seal; read-only apart from the seal commit the script makes
pair-contract-generatorRead, Write, Bashread a KB template and write the derived *.contract.json cache beside it

One more agent type arrives with no definition of ours: general-purpose. pair-refine-batch spawns it at both of its stages. It is a host built-in of Claude Code, not a file pair-cli install writes, so it is not in the table above, and that is precisely why it is worth naming here: it carries your tool configuration's unrestricted set, Bash included, which is wider than any of the shipped definitions above. What keeps that engine to PM-tool writes is the read-only clause in every prompt it dispatches, not a tool grant.

Read the third column as process, not as permission. Bash is unrestricted wherever it is declared, so writing files, committing, pushing and gh pr merge are all technically inside the authority you install. What keeps pair-reviewer and pair-red-contract-verifier read-only, pair-fix-test-author test-only, and keeps merge out of every run that did not declare one is instruction (the engine's dispatched prompts and each agent's own definition) not the tool grant.

So after an install or update, your project contains definitions that (when you invoke a batch) can create worktrees, write files, commit, push branches and open pull requests under your credentials. The Tools it declares column is checked against each agent's frontmatter by the docs staleness gate, so it cannot understate what arrives; the column beside it is prose the gate does not check.

They do nothing on their own. Nothing runs until you start a batch.

No prompt this engine dispatches to a judgment agent instructs a merge, and by default nothing merges. With nothing declared every run stops at PR-review-approved and reports ready-for-merge, leaving the merge decision to you. Only when you declare until: merged (an argument, ## Autonomy in tech/automation.md, or a legacy ## Auto-Advance <tier>) does the batch run the merge stage (cycle-merge.mjs, through an agent, pinned to the head the verifier reviewed, after re-reading the tier, pair-review and pair-explicit-approval) and only when your merge gate (merge: never | when …) allows it; merge: always (the default) parks the card awaiting-human, and a gate condition that fires parks it escalated. Both are asserted by dry-run tests that inspect every dispatched prompt and every returned status.

Every shipped name carries a pair- prefix, file names, agent names, and the workflows' registry names (meta.name) alike, so neither an existing .claude/agents/reviewer.md nor your own implement-batch workflow is overwritten or shadowed by one of ours.

They install anyway rather than being gated on the tool detected at install time, because that detection is a snapshot: a user who adopts Claude Code later would need a full re-install rather than an update to acquire files that were always harmless to have.

The degraded path

Without a workflow runtime you drive the same lifecycle by hand, one card at a time. The per-step skills are the same ones the engine composes, and they work standalone under any tool:

  1. /pair-process-implement: implement the card, test-first, on its own branch
  2. /pair-capability-publish-pr: open or update the PR, run the tier-resolved gate
  3. /pair-process-review: review it independently, from the acceptance criteria and the diff
  4. /pair-process-implement again: apply the findings, then re-review

What you lose is the orchestration, not the process: parallelism across cards, the isolated worktree per card, the executable acceptance contract sealed before the implementation, the durable cycle state that survives a context reset, and the convergence rule that decides when a card is done.

Merge is the human gate unless your merge gate says otherwise

By default no configuration merges: the engine stops at a review-approved PR and reports it, and the decision to merge is yours, on every path, convergence, escalation, or failure. A batch is the delivery cycle on N cards, so it takes the cycle's own until / prepare / merge arguments; under until: merged it merges a card only when the merge gate allows it, and the rule is autonomy-policy.mjs's, never the sandbox's. Where there is no Workflow tool, run /pair-workflow-cycle one card at a time with the same arguments.

Configuring it

The engine ships with pair's own values as defaults, so it runs in a pair-shaped project with no configuration. A project whose skills, worktree root or base branch differ passes args.pipeline to override them per key; anything left unmentioned keeps its default.

An unknown key, an empty override, a retired key or a maxParallelism below 1 is rejected loudly rather than ignored: a discarded setting would run the batch on values you did not choose while reporting success.

Four judgment stages, one transition authority

A story's cycle is four judgment dispatches (ADR-024), each a skill invoked by name with typed arguments; the workflow file carries no phase policy of its own and dispatches nothing mechanical:

StageSkillWhoWhat it owns
1 preparation/pair-workflow-red-specpair-fix-test-authorthe authoritative inventory (AC or finding → producer → inputs, representations, consumers → equivalence classes and interactions → executable evidence), the grouping of a remediation round, the executable acceptance contract (red witnesses, positive controls that may pass, stable row ids) as tests and fixtures only, before any production edit; initial, remediation, repair and revision modes
2 validation + seal/pair-workflow-red-verifypair-red-contract-verifierreproduces every row and artifact at the base, re-derives the inventory from the producer, returns every gap in one typed rejection (one repair, then failed-contract), and on approval runs red-snapshot.mjs seal in the same execution
3 implementation/pair-workflow-implement-phase · /pair-workflow-green-fixpair-implementerGREEN above the seal, inside fixScope, never touching a sealed test byte; the initial stage publishes the story's one PR through the configured publish-pr skill, the remediation stage updates it; an approved test that still fails returns here once on the same seal
4 final verification/pair-workflow-review-phasepair-reviewerred-snapshot.mjs verify-chain first (a breach is terminal), then the fixers' evidence re-run, the witnesses and controls, every prior finding with an explicit transition, the delta and its consumers, cross-group interactions, the adopted risk-tier passes (general via /pair-process-review; security and boundary on risk:yellow; architecture and an adversarial second pass on risk:red); one verdict, the head reviewed, readiness only when the remote head equals it; as the last non-partial reviewer it concludes the required pair-review commit status and the ONE pr-state:* label (pr-state.mjs conclude, the KB mapping), merge stays a human's

A fresh card has no up-front contract (ADR-024, amendment of 2026-09-23). Its first stage is implementation (stage 3): tests and code are written together, test-first, above the base, and an informal self-review is fixed but never recorded. Its first final verification (stage 4) names, per acceptance criterion, the test that proves it. A weak or missing test is an ordinary finding, and every finding carries a reproducer or a concrete failure scenario.

Stages 1 and 2 run for review findings, where the failing test is still written by someone other than the fixer. A run directory that already holds a0 contract handoffs finishes on the old path. Cold path: 2 dispatches (implement, verify); one fix round adds 4.

Once per run and cache-by-hash, the review template's vocabulary contract is resolved by the first review dispatch ($contractSpec → the reviewer invokes /pair-workflow-contract-phase, whose ensure-contract.mjs check reads a fresh cache without generating anything, and returns templateContract): no generator-only dispatch (AC-06, S7).

That first review runs on the loose skeleton and is validated against the vocabulary it brought back; every later review is enum-locked. It is the template contract; the acceptance contract a story is judged against is the one stage 1 prepares and stage 2 seals.

Transport constraint (recorded as tech debt, ADL 2026-09-13): every card and PR write the engine performs goes through the GitHub CLI (gh issue view|edit|create, gh api …/comments|statuses|labels), so the workflow supports GitHub as code host and PM tool, driven from Claude Code; an adopter elsewhere gets a typed gh … failed refusal, never a silent skip. The exit path is one cardTransport seam selected by adoption.

Every stage begins and ends with cycle-state.mjs (a script shipped inside each skill) which reads the phase handoffs under .pair/working/runs/<runId>/<story>/ in the main checkout (never a worktree), publishes the stage's own handoff atomically (sequenced, predecessor-checked, a stale writer is refused, a held lock is never broken) and derives the first incomplete step.

A stage dispatched when another step is due returns { status: "redirect", next } without spending judgment; the coordinator follows next, validates every typed result and enforces the budgets: it never derives a transition of its own. Handoffs stay the storage authority: there is no register, no projector.

Resume, recovery, findings

  • Same inputs, unchanged head ⇒ the cycle continues from its first incomplete step; a completed cycle is a cheap identity check (remote head equals the verified head) and no new judgment. A different invocation id for the same PR finds the existing run directory (other-run) instead of opening a second cycle; several candidate run directories are incompatible until the caller names one.
  • Moved head or changed relevant inputs ⇒ the prior findings plus the delta and its affected boundaries are re-validated (re-review), never a fresh full pass; an unrelated card edit invalidates nothing.
  • An approved witness fails on production ⇒ back to green-fix on the same seal, once; a second failure is failed-fix. A genuine contract gap ⇒ red-spec revises only the affected group (r<n>-g<k>-rev<m>), stable row ids kept, the changed rows re-validated and sealed as a successor snapshot; verify-chain treats the earlier seal as history.

A revision or repair inherits the predecessor's fixScope and may only add paths: the sealer refuses a narrowed one (fixScope-narrowed, engine 3.0.8). A custody breach ⇒ failed-custody, the trusted snapshot preserved, nothing reset.

  • Findings carry an id assigned once by the verifier that first saw them (r<round>[-<reviewer>]-<n>), a kind (defect, regression, approved-test-failing, contract-gap, question), a transition on every later review (open, resolved, superseded, human) and blocking, computed from the severity floor and re-checked by the coordinator; a severity change needs severityEvidence; a dropped, duplicated or invented id fails the verification closed.

An external finding (the card, the PR body) is a location, never acceptance: it stays blocking until a human disposition or a read-back-verified correction, and the story escalates rather than converging around it.

  • Publication is idempotent: the first review, the synthesis and an escalation are one comment each PER RUN, keyed by a hidden marker that carries the run id (<!-- pair:first-review #<story> PR#<n> run:<runId> -->, from 4.0.1) and edited in place through pr-comment.mjs upsert, which reads the PR back before writing: a restart under another invocation never posts twice, and a later cycle on the same PR posts its own comments instead of overwriting the previous cycle's.

The scope-decision packet stays PR-scoped: its sc- ids and the maintainer's answer outlive cycles.

  • Statuses a caller may see: ready-for-merge (only with a 40-hex reviewedHead and a verdict), merged, awaiting-human, escalated (an autonomy gate condition fired, distinct from the review's escalate), target-ready, escalate, failed-preparation, failed-contract, failed-seal, failed-implement, failed-fix, failed-verify, failed-custody, failed-resume, incompatible, and four added in 4.0.0: awaiting-scope-decision (quality converged, one or more scope proposals still pending a human decision), failed-publication (the final summary could not be confirmed, retry publication only, quality evidence stays intact), interrupted and abandoned (an explicit developer abandonment).

pair-loop halts on every one but the first.

PM tool and code host: one adapter layer

The cycle's scripts do not spawn gh or az themselves. Every write to a card or a pull request goes through the adapter in scripts/host/, which ships inside each workflow skill. That covers the card hash, PR comments, the pair-review check, the pr-state:* label, scope decisions, merge and close. Two adapters are implemented: GitHub (gh, the default) and Azure DevOps (az boards, az repos, az devops invoke).

  • Which one. It is read from way-of-working.md, using the existing pm-tool / code-host split. Card operations go to the PM tool and PR operations go to the code host. A project that declares nothing stays on GitHub.
  • Resolved once. The coordinator runs cycle-state.mjs bind-hosts --dir <run/story dir> at its start. Every later call against that run reuses the binding, even if way-of-working.md changes mid-cycle.
  • An unknown host stops the run. A declared tool without an adapter HALTs with host-unsupported, naming the declared value and the adapters that exist. It never falls back to GitHub.
  • Azure DevOps. The adapter supports squash and merge only, with no rebase. New cards are created as User Story. The pair-review check is a PR status bound to the reviewed head's iteration.
  • Adding a host. Write one new scripts/host/<id>.mjs file and add one way-of-working.md line. The steps are in the KB's host-adapter-extension-guide.md: the eight methods, how an adapter registers, and a worked filesystem stub.

Scope proposals are never defects

A finding that violates an accepted obligation (an AC, an adoption rule, a supported boundary) is a defect and is fixed or absorbed into the story like before.

Something demonstrably absent from the baseline (a genuinely new requirement, not a violation) is instead recorded as a scopeChanges[] entry (sc-<n>, stable across rounds): it never carries a severity, is never blocking, never enters a remediation plan and is never counted as a defect.

Once quality converges (zero blocking findings) with one or more proposals still pending, the cycle halts at awaiting-scope-decision (never ready-for-merge) and the reviewer posts the consolidated packet (<!-- pair:scope-decision #<story> PR#<n> -->): each proposal's evidence that it is new scope, the affected AC, and the three permitted choices.

The maintainer's decision is applied mechanically, never generated by an agent.

cycle-state.mjs apply-scope-decisions --dir <run/story dir> --decision-ref <PR comment URL> --repo <owner/name> --pr <n> reads the ACTUAL comment through gh, verifies its author is a User equal to the adopted maintainer, code-host-assignee, else default-assignee, read from way-of-working.md (never a login literal in shipped code, never a bot; --maintainer is the explicit override and an unresolvable principal is the typed refusal maintainer-unresolved:*), parses one fenced JSON object ({schemaVersion: 1, scopeBaselineHash, decisions: [{id, action, rationale?, targetIssueUrl?, approvedDelta?}]}, action one of ignore | new-card | extend-current-card), and rejects a stale baseline, a duplicate id, an unknown action or an unauthenticated comment outright.

From 4.0.1 the scopeBaselineHash covers the pending proposals' ids and types, never their wording (a new cycle's reviewer re-authors the text in a fresh run directory, which used to stale the maintainer's standing decision), and the same command with --decision-ref omitted discovers the decision on the PR: the reviewer runs it before declaring awaiting-scope-decision, so a decision already posted is applied instead of asked again. ignore and new-card disposition the proposal without touching source or severity; extend-current-card names the exact new/changed AC, bumps scopeEpoch exactly once, and opens a targeted remediation round for that delta through the ordinary pipeline: no parallel path. The whole exchange is idempotent: a replayed decisionRef is a no-op, never a duplicate epoch.

extend-current-card resolves each approved AC against the card's ACTUAL body, in the adopted formats only: the colon convention (AC-1: text, - **AC-1**: text), the checkbox convention (- [ ] **AC-01: Title.** Description., and the title-less shape the script emits itself) and the delivery template's numbered **Given** / **When** / **Then** blocks.

An id matched exactly is rewritten in place, its bullet, checkbox state and title preserved, the rest of the card byte-identical; an id defined twice, whether inside one format or across two, is ambiguous-ac-id and refused.

A parser miss is never taken as proof the AC is new: an id a recognized card cannot match is ac-id-unresolvable, and a card speaking none of these formats is refused outright as unsupported-card-format (no edit, no extended, no scopeEpoch bump) whether or not the id appears in its text. Restate the AC section in an adopted format, or use new-card.

Metrics, host runtime and the final PR summary

Two scripts, shipped beside cycle-state.mjs in review-phase's (and, where imported, other phases') scripts folder:

  • cycle-metrics.mjs: a pure, schema-1 reducer over the durable handoffs plus optional normalized runtime observations: cycle/review/defect/scope counts, token and time aggregation (known/partial coverage, never a fabricated zero), a cohort aggregator (completion rate, mean/ median/p90 completed cycles), and the render/publish functions for the final summary.
  • cycle-runtime.mjs: a narrow HOST process (never the Workflow sandbox, which still has no clock, filesystem or usage API): entry prepares a cache-hint capsule, observe tails ONLY the journal/usage sources it is given on a 5-second default interval and updates metrics.json/ metrics.md at every observed boundary, stopping on a durable terminal result once reconciled or after a 30-second grace period leaves it partial; finalize combines the last verdict with the accumulated metrics and posts the ONE synthesis comment.

From 4.0.1 (canary v9), when no host runtime is present (no .runtime-checkpoint.json / .run-terminal.json in the run directory, the case whenever the Workflow sandbox alone drives the cycle) the final reviewer runs finalize itself after its converging publish: same script, same marker, metrics reduced from the handoffs alone (completeness partial, never fabricated).

The engine result's metricsRef names metrics.json only on that evidence (or a present host runtime) and is absent otherwise; a reviewer that owned the synthesis and could not confirm it by read-back ends the story failed-publication, not ready-for-merge.

The final summary (one comment at <!-- pair:synthesis #<story> PR#<n> run:<runId> -->, upserted, never a second one per run/step/scope-decision/metrics-correction) carries, in order: workflow identity/ versions/models/reviewed head; quality vs. delivery status; effective cycles and review executions (retries, redirects, contract revisions kept separate); tokens and time with explicit coverage; late defects by origin (preexisting-missed, introduced-by-remediation, unknown) and a separate scope-proposal table; a compact schema-1 JSON tail (sourceDigest, asOf, metricsRevision, completeness).

Readiness is reported only after that candidate summary is read back and confirmed, until then delivery is failed-publication even when quality already converged. A confirmation-only republish is not a new semantic revision; human text written after the comment's owned section survives every republish.

Residual limit at 4.0.0: execution.redirects / engineRecoveries / administrativeDispatches / nestedDispatches are derived from an optional --dispatchStats JSON input the host's own launch recipe supplies to cycle-runtime.mjs's observe/reconcile/finalize (no journal/usage line encodes this: only the host that actually dispatched knows it); real counts when supplied, explicit null (never a claimed zero) when absent.

It persists in the runtime checkpoint, so a later tick or a finalize run after the observer stopped still reports the last host-supplied values, never silently reverting to null. usage.sharedOverhead works the same way via --sharedCost.

usage.byRole is real once usage observations carry a role; dispatches/startedWithoutResult are derived from observed executions themselves and need no external input. Every quantity's known/partial coverage is exact; none of these are placeholders presented as measurements.

Timing/completeness account for an execution observed on only one side (started with no result, or a result with no observed start) as genuine missing coverage (time.incomplete and snapshot.completeness: 'partial') never a silent complete from an unresolved execution.

Delta-usage dedup ((executionId, eventId)) survives a checkpoint/restart round trip: the applied-event ledger is itself persisted and reseeded on resume, so a replayed delta stays a no-op across a real process restart, not only within one reducer call.

A supplied entryCapsules map (per story id) is accepted and schema-validated (workflow major, schema version, story, PR, the 40-hex reviewed head), but it is a cache hint, NEVER approval: the sandbox has no filesystem/network access to confirm a capsule's claim itself, so it can never skip a dispatch.

Readiness always comes from the dispatched phase's own cycle-state.mjs resolve() call against the real durable state; a mismatched OR a fully matching capsule changes nothing about what gets dispatched (a cache hint is never a readiness proof).

model remains a legacy global override. For a measured trial use models, for example { models: { green: "fable" } }: that changes only GREEN, leaving the independent verifier, the preparation author and the validator on their declared models. Roles: implementation, reviewer, red, redVerifier, green.

The retired roles (planner, seal, preflight, pr) and the retired skill keys (pipeline.skills.remediationPlan, redSeal, p3Verify, cycleComments, prPhase) are rejected with a message naming what absorbed them, never mapped silently.

Every result carries workflowVersion and metrics (dispatches, retries, redirects; wall time and token counters are not exposed to a workflow script (the sandbox has no clock and no usage API) and are reported as unknown, never as zero: read both from the harness run summary), and every stage receives $workflowVersion, $policy and the effective-inputs digest so each handoff records which coordinator, policy and inputs produced it.

An incompatible workflow major or schema found in a run directory is incompatible; prior approval is never reused or overwritten silently.

runId (optional, one safe path segment) names the run directory .pair/working/runs/<runId>/; absent, each card uses story-<id>. The six phase skills are pipeline configuration like the others (pipeline.skills.contractPhase, redSpec, redVerify, implementPhase, greenFix, reviewPhase) and pipeline.reviewers (default 1, the KB tier default) is the number of independent final verifiers the tier requires.

Carrying verified evidence

A card may pass requiredFindings: a compact verified observation with observedHead, finding fields, oracle, probe and observed result. The final verifier re-proves it on the same reviewed SHA (kind: regression if the defect is still present). A different SHA is failed-verify; old evidence never silently becomes a new specification.

History rewrites and rebases

If a review finding can only be fixed by rewriting existing Git history, the verifier types it humanDecisionKind: "history-rewrite" and the engine escalates to a human before any RED, seal or GREEN. Nothing in the engine can accept or waive such a finding; the human decides out of band and re-runs.

A rebase is never repaired. An in-flight attempt whose base moved fails closed where it is measured (the sealer refuses a HEAD that is not its base, the custody check refuses a snapshot that is not an ancestor) and the trusted snapshot is preserved, never reset. There is no card-level reset, no supersedes field and no SHA-scoped waiver to carry across runs; unknown dirty state in a worktree is reported (dirty), never cleaned.

Regression-risk rewind

A defect the independent review proves was introduced by a remediation does more than reopen: it invalidates that remediation and sends the cycle back to the same batch's preparation.

"Rewind" is a workflow-state transition, not a Git operation. The branch stays on its current head, every commit and seal stays where it is, and the fix goes forward. The last clean reviewed head is only the behavioural baseline the guard is compared against: nothing is reverted, reset, rebased or force-pushed, and no seal or evidence is deleted.

The claim is only admissible with all of: the approved obligation it violates, an executable reproducer, the head where that reproducer passes, the head where it fails, the remediation batch that produced the failing head, closure assertions and affected boundary references. Anything less is an ordinary finding of unknown origin; a new or changed requirement stays a scope proposal.

The risk then lives on the existing finding entry with a script-derived riskId (story/PR + stable finding id + introducing batch), so replay, restart and a second observation reuse it. The active matrix is derived from the ledger, never stored, never supplied by a caller. The ordinary path runs unchanged (preparation → red verification → green fix → independent delta review), with every active guard inside the one complete contract.

Only an independent review bound to the exact new head discharges a risk: it executes the closure assertions, shows the obligation passing, confirms the batch's original findings closed and re-tests the affected boundaries. A discharged risk leaves the active matrix and stays in the append-only history; a reintroduction reopens the same finding rather than counting as a new discovery.

quality-converged, scope escalation and ready-for-merge are impossible while any risk is active, and a scope decision can neither waive nor discharge one. Four derived counters (invalidatedRemediations, regressionRepairs, activeRegressionRisks, dischargedRegressionRisks) reach the step metrics and the one PR summary, with active and historical views separated. An invalidated remediation is an attempted batch; its round becomes a completed cycle only once a review closes the original findings and leaves no active risk. maxFixRounds is reused as it stands.

Illegal transitions are contracted, not discovered late

A change to a persisted state, a ledger transition, a terminal gate or an evidence identity is contracted with its whole finite matrix before any production edit: the positive path plus every applicable negative family.

  • a direct jump to a terminal state without its predecessor
  • a missing predecessor or matching repair
  • mutation of immutable evidence
  • a payload that disagrees with persisted history
  • an event bound to another head, finding, batch or run
  • replay and restart
  • a stale writer
  • an authority omitted from one participant
  • a mandatory human decision combined with an otherwise automatic transition
  • a multi-group batch whose producing group differs
  • convergence with something still active
  • the interaction between a finding, its remediation and a regression that remediation introduced

The preparation owns the matrix; the independent verifier receives the same authoritative state and guard set the resolver holds and rejects an incomplete or inconsistent matrix before the seal (contract-incomplete:<transition>:<family>); implementation refuses to start without the complete seal and may not reduce a sealed row; the final review samples the sealed rows and the changed interactions. One green end-to-end chain is necessary and never sufficient.

For the regression-risk ledger specifically, one authority validates every transition against the persisted ledger: a discharge needs its active predecessor, a matching repair whose fix produced the exact reviewed head, byte-equal immutable evidence and the batch's own obligations closed in that same review, and the risk state, finding transition and blocking flag must describe one transition.

The repaired group is derived from the group that produced the failing head (never assumed to be -g1) and ambiguous provenance is a typed refusal.