In-turn gates: agents run the runner-owned gate CLI before stopping; SubagentStop shrinks to a presence check #477

Closed
opened 2026-08-22 11:37:46 +00:00 by jared · 3 comments
Owner

Problem Statement

Gate execution (tests, lint) currently runs after an agent stops, inside machinery spawned from the SubagentStop hook. Because gates take minutes and the hook has seconds, ADR-0140 added a detached settlement process: spawn, pid stamp, liveness probe, a gates_settling state, and an Unsettled error path. This solves a timing problem the design itself created. It also makes correction loops slow: a programmer whose change fails the gate only learns after its dispatch ends, and the fix costs a full dispatch round-trip. The operator maintains an async state machine whose only purpose is to move a verdict from one process to another.

Solution

Move gate execution inside the agent's turn. The producing agent runs a runner-owned gate subcommand as the final act before it stops, and iterates until it reaches its required outcome — the programmer until green, the test-writer until red for the right reason. The runner executes the gates and writes step results and the verdict to the pipeline database itself; the agent can invoke the gate but cannot author the result. SubagentStop keeps its role as the unconditional final record, but shrinks to a millisecond presence check: if a gate verdict is on the dispatch, route on it; if the agent stopped without gating, stamp a loud error verdict and let the map route the failure. The detached settlement machinery — spawn, pid, liveness, gates_settling — is deleted, not replaced. The async problem is not solved; it stops existing.

This supersedes ADR-0140 and inverts the "agents never run tests" contract for gated roles. Precedent already exists: the test-writer's red-assert/green-assert path is an agent invoking a runner-owned gate CLI today.

User Stories

  1. As the pipeline operator, I want gates to run inside the agent's turn, so that no process ever runs between an agent stopping and its verdict landing.
  2. As the pipeline operator, I want the detached settlement machinery deleted, so that I stop maintaining an async state machine (spawn, pid, liveness probe) that exists only to ferry a verdict.
  3. As the pipeline operator, I want the SubagentStop hook to finish in milliseconds, so that hook timeouts can never again truncate a verdict.
  4. As a programmer agent, I want to run the gate command and see the failure output in my own turn, so that I fix red tests immediately instead of stopping and waiting for a re-dispatch.
  5. As a test-writer agent, I want to run the gate command in-turn, so that I can confirm my test is red for the intended reason before I stop.
  6. As the pipeline operator, I want the runner — not the agent — to execute gate commands and write step results and the verdict to the pipeline database, so that the judged party can never author its own verdict.
  7. As the pipeline operator, I want SubagentStop to detect a missing gate record and stamp a loud error verdict, so that an agent skipping its gate is caught in milliseconds and routed by the map, never silently accepted.
  8. As the map author, I want gate-skip failures to route along the existing error edges, so that no new verdict vocabulary or map state is introduced.
  9. As the orchestrator (implement skill), I want next to route on a verdict that is already present, so that the gates_settling polling state and its retry logic disappear from my contract.
  10. As the pipeline operator, I want the gate command to reuse the target already bound on the implementation row (ADR-0139), so that in-turn gate calls resolve their test command without per-call target plumbing.
  11. As a reviewer agent, I want no gate obligation, so that judgment-only roles keep their current contract unchanged.
  12. As the wiring agent, I want my gate to remain externally run, so that I keep operating without a Bash tool and without expanded authority.
  13. As the pipeline operator, I want the dispatches table's settlement columns removed, so that the schema reflects only mechanisms that exist.
  14. As a future maintainer, I want a superseding ADR recorded and the affected ADRs amended, so that the decision history and the code assert the same design.
  15. As a future maintainer, I want the deterministic-gates reference, glossary, overview, and role definitions rewritten, so that no document still claims agents never run tests or that settlement is detached.
  16. As the pipeline operator, I want correction loops to complete within one dispatch where possible, so that a red test costs an in-turn iteration instead of a full dispatch round-trip.
  17. As the pipeline operator, I want an agent that cannot reach its required outcome to stop with its last failing verdict recorded, so that the map's fail edges and escalation paths still get exercised.
  18. As the operator of a supervised live drive, I want the first drive after this change observed end to end, so that the behavioral risk — agents negotiating with gates instead of iterating honestly — is tested where the mechanism cannot be.

Implementation Decisions

  • New runner subcommand gate <dispatch_id> on the runner CLI, replacing settle-gates one for one. It resolves the dispatch, its implementation, and its map node; builds the gate command hash; runs each gate; records step results; and stamps the verdict — synchronously, returning before the agent's turn ends. The rewritten settlement class is its body; the gate-verdict runner, gate-command builder, step-result recorder, and verdict stamper are reused unchanged.
  • Verdict authority stays with the runner. The agent invokes the subcommand; the runner writes the database rows. The agent's only possible defections are not running the gate (caught by the presence check) or stopping on a failing verdict (routed by the map's fail edges).
  • Completion intake becomes a presence check. For gate-sourced nodes: verdict present → done; verdict absent → stamp error with a gate-skip reason recorded on the dispatch, routed by the map's existing error edge. No new verdict value, no new state. The agent-sourced-verdict branch is unchanged.
  • next loses settlement resolution. The gates_settling payload, the settlement model (queue, resolve, pid liveness, the unsettled error), the detached and inline settlement classes, and the settlement seam are deleted. next routes on a present verdict; an absent verdict on a completed dispatch is impossible by construction after intake runs.
  • Schema reverts. The settlement pid and started-at columns on dispatches are dropped in a new schema version, restoring the pre-ADR-0140 shape.
  • Hook script unchanged in mechanics. SubagentStop remains a thin courier; its stale detached-settlement docstring is corrected. The hook timeout stays at a small value since intake is now milliseconds.
  • Role contracts invert for gated producers only. Programmer: must run the gate and iterate until green before stopping. Test-writer: must run the gate until red for the right reason (its existing red-assert path is the precedent and remains). Reviewer: no gate (consistent with its existing exemption, ADR-0089). Wiring: keeps its external gate and gains no Bash tool — the presence check does not apply to it.
  • Iteration is bounded by the turn, not the CLI. The gate command is stateless per invocation. An agent that cannot reach its outcome stops anyway; the last recorded verdict is a fail and the map routes it. No attempt counter is added.
  • Target resolution reuses the binding persisted on the implementation row at implementation-open (ADR-0139); no per-call target plumbing.
  • ADR moves: one new ADR supersedes ADR-0140. ADR-0128 (SubagentStop-driven advancement) and ADR-0139 (target binding) stand intact. ADR-0097 (gate logic in tested Ruby), ADR-0107, ADR-0134 (map-total routing), and ADR-0137 (gate evidence as brief fact) are amended: only the caller of gate logic changes, fail edges now fire on stop-with-fail rather than every red run, and gate evidence originates inside the turn.
  • Docs: the deterministic-gates reference's core rule ("agents never run tests/lint/format — hooks do") is rewritten; the glossary entries for Gate and Settlement, the overview's hook-run-tests claims, the implement skill's settlement description, and the implementation-status leaf (current shape plus one changelog row) are updated.
  • Ticket #470 (nil-verdict recovery design) is closed as superseded: the state it recovers from no longer exists.

Testing Decisions

  • One seam, already existing: the runner CLI against a fixture pipeline database, with gate commands injected as a stubbed command hash. All tests observe external behavior — database rows and CLI output — never internals. This is the same seam the settlement tests use today.
  • Gate command tests (rewrite of the settlement tests): invoke gate with stubbed pass and fail commands; assert step-result rows and the stamped verdict.
  • Presence-check tests (rewrite of the subagent-stop CLI tests): pipe a synthetic SubagentStop event for a dispatch with and without a gate record; assert route-on-verdict versus loud error verdict with the gate-skip reason.
  • Routing tests: assert next routes on a present verdict and that the gates_settling state is gone.
  • Deleted with their subjects: queue/resolve/liveness/detached-spawn tests and their support helpers.
  • Deliberately untestable at this seam: whether agents iterate honestly against the gate in-turn. That is behavioral, assigned to the supervised live drive, and is the residual risk this design accepts.

Out of Scope

  • Bulk changes to map topology or edge semantics beyond what the ADR-0134 amendment requires.
  • Any Bash-tool grant to wiring, or changes to reviewer/judgment-role contracts.
  • Attempt-counting, retry budgets, or negotiation detection in the gate CLI — the live drive decides whether these are needed.
  • Ticket #475 (regression test for the settled flow) — re-scope or close it against the new design in its own right, not here.
  • Harness feature requests (hook respond-early semantics).

Further Notes

  • Honest accounting: this reverses ADR-0140 days after it shipped and deletes most of its commit. What it buys: the entire async state machine gone, shorter correction loops, and code deleted rather than added.
  • The residual ~25% doubt is behavioral, not mechanical: whether gated agents iterate honestly or develop negotiation habits ("tests mostly pass"). The runner-owned CLI mitigates; the first supervised live drive proves.
  • A stray settlement-era worktree under the pipeline's worktrees directory duplicates several affected files; confirm it is stale before the deletion pass.

Decomposition

VERDICT: CREATE — hard floor, explicit user-approved work (skeptic gate 2026-08-22).

  • Supersession ADR recorded in docs/adr/ (done on main thread)
  • #478 gate CLI
  • #479 presence check + routing (blocked by #478)
  • #480 settlement deletion + schema revert (blocked by #479)
  • #481 role contracts + docs (blocked by ADR only; parallel with #478-#480)
  • Close-out: blocked by all children; close #470 as superseded; verify no gates_settling references remain
## Problem Statement Gate execution (tests, lint) currently runs after an agent stops, inside machinery spawned from the SubagentStop hook. Because gates take minutes and the hook has seconds, ADR-0140 added a detached settlement process: spawn, pid stamp, liveness probe, a `gates_settling` state, and an `Unsettled` error path. This solves a timing problem the design itself created. It also makes correction loops slow: a programmer whose change fails the gate only learns after its dispatch ends, and the fix costs a full dispatch round-trip. The operator maintains an async state machine whose only purpose is to move a verdict from one process to another. ## Solution Move gate execution inside the agent's turn. The producing agent runs a runner-owned `gate` subcommand as the final act before it stops, and iterates until it reaches its required outcome — the programmer until green, the test-writer until red for the right reason. The runner executes the gates and writes step results and the verdict to the pipeline database itself; the agent can invoke the gate but cannot author the result. SubagentStop keeps its role as the unconditional final record, but shrinks to a millisecond presence check: if a gate verdict is on the dispatch, route on it; if the agent stopped without gating, stamp a loud error verdict and let the map route the failure. The detached settlement machinery — spawn, pid, liveness, `gates_settling` — is deleted, not replaced. The async problem is not solved; it stops existing. This supersedes ADR-0140 and inverts the "agents never run tests" contract for gated roles. Precedent already exists: the test-writer's red-assert/green-assert path is an agent invoking a runner-owned gate CLI today. ## User Stories 1. As the pipeline operator, I want gates to run inside the agent's turn, so that no process ever runs between an agent stopping and its verdict landing. 2. As the pipeline operator, I want the detached settlement machinery deleted, so that I stop maintaining an async state machine (spawn, pid, liveness probe) that exists only to ferry a verdict. 3. As the pipeline operator, I want the SubagentStop hook to finish in milliseconds, so that hook timeouts can never again truncate a verdict. 4. As a programmer agent, I want to run the gate command and see the failure output in my own turn, so that I fix red tests immediately instead of stopping and waiting for a re-dispatch. 5. As a test-writer agent, I want to run the gate command in-turn, so that I can confirm my test is red for the intended reason before I stop. 6. As the pipeline operator, I want the runner — not the agent — to execute gate commands and write step results and the verdict to the pipeline database, so that the judged party can never author its own verdict. 7. As the pipeline operator, I want SubagentStop to detect a missing gate record and stamp a loud error verdict, so that an agent skipping its gate is caught in milliseconds and routed by the map, never silently accepted. 8. As the map author, I want gate-skip failures to route along the existing error edges, so that no new verdict vocabulary or map state is introduced. 9. As the orchestrator (implement skill), I want `next` to route on a verdict that is already present, so that the `gates_settling` polling state and its retry logic disappear from my contract. 10. As the pipeline operator, I want the gate command to reuse the target already bound on the implementation row (ADR-0139), so that in-turn gate calls resolve their test command without per-call target plumbing. 11. As a reviewer agent, I want no gate obligation, so that judgment-only roles keep their current contract unchanged. 12. As the wiring agent, I want my gate to remain externally run, so that I keep operating without a Bash tool and without expanded authority. 13. As the pipeline operator, I want the dispatches table's settlement columns removed, so that the schema reflects only mechanisms that exist. 14. As a future maintainer, I want a superseding ADR recorded and the affected ADRs amended, so that the decision history and the code assert the same design. 15. As a future maintainer, I want the deterministic-gates reference, glossary, overview, and role definitions rewritten, so that no document still claims agents never run tests or that settlement is detached. 16. As the pipeline operator, I want correction loops to complete within one dispatch where possible, so that a red test costs an in-turn iteration instead of a full dispatch round-trip. 17. As the pipeline operator, I want an agent that cannot reach its required outcome to stop with its last failing verdict recorded, so that the map's fail edges and escalation paths still get exercised. 18. As the operator of a supervised live drive, I want the first drive after this change observed end to end, so that the behavioral risk — agents negotiating with gates instead of iterating honestly — is tested where the mechanism cannot be. ## Implementation Decisions - **New runner subcommand `gate <dispatch_id>`** on the runner CLI, replacing `settle-gates` one for one. It resolves the dispatch, its implementation, and its map node; builds the gate command hash; runs each gate; records step results; and stamps the verdict — synchronously, returning before the agent's turn ends. The rewritten settlement class is its body; the gate-verdict runner, gate-command builder, step-result recorder, and verdict stamper are reused unchanged. - **Verdict authority stays with the runner.** The agent invokes the subcommand; the runner writes the database rows. The agent's only possible defections are not running the gate (caught by the presence check) or stopping on a failing verdict (routed by the map's fail edges). - **Completion intake becomes a presence check.** For gate-sourced nodes: verdict present → done; verdict absent → stamp `error` with a gate-skip reason recorded on the dispatch, routed by the map's existing error edge. No new verdict value, no new state. The agent-sourced-verdict branch is unchanged. - **`next` loses settlement resolution.** The `gates_settling` payload, the settlement model (queue, resolve, pid liveness, the unsettled error), the detached and inline settlement classes, and the settlement seam are deleted. `next` routes on a present verdict; an absent verdict on a completed dispatch is impossible by construction after intake runs. - **Schema reverts.** The settlement pid and started-at columns on dispatches are dropped in a new schema version, restoring the pre-ADR-0140 shape. - **Hook script unchanged in mechanics.** SubagentStop remains a thin courier; its stale detached-settlement docstring is corrected. The hook timeout stays at a small value since intake is now milliseconds. - **Role contracts invert for gated producers only.** Programmer: must run the gate and iterate until green before stopping. Test-writer: must run the gate until red for the right reason (its existing red-assert path is the precedent and remains). Reviewer: no gate (consistent with its existing exemption, ADR-0089). Wiring: keeps its external gate and gains no Bash tool — the presence check does not apply to it. - **Iteration is bounded by the turn, not the CLI.** The gate command is stateless per invocation. An agent that cannot reach its outcome stops anyway; the last recorded verdict is a fail and the map routes it. No attempt counter is added. - **Target resolution** reuses the binding persisted on the implementation row at implementation-open (ADR-0139); no per-call target plumbing. - **ADR moves:** one new ADR supersedes ADR-0140. ADR-0128 (SubagentStop-driven advancement) and ADR-0139 (target binding) stand intact. ADR-0097 (gate logic in tested Ruby), ADR-0107, ADR-0134 (map-total routing), and ADR-0137 (gate evidence as brief fact) are amended: only the caller of gate logic changes, fail edges now fire on stop-with-fail rather than every red run, and gate evidence originates inside the turn. - **Docs:** the deterministic-gates reference's core rule ("agents never run tests/lint/format — hooks do") is rewritten; the glossary entries for Gate and Settlement, the overview's hook-run-tests claims, the implement skill's settlement description, and the implementation-status leaf (current shape plus one changelog row) are updated. - **Ticket #470 (nil-verdict recovery design)** is closed as superseded: the state it recovers from no longer exists. ## Testing Decisions - **One seam, already existing: the runner CLI against a fixture pipeline database**, with gate commands injected as a stubbed command hash. All tests observe external behavior — database rows and CLI output — never internals. This is the same seam the settlement tests use today. - **Gate command tests** (rewrite of the settlement tests): invoke `gate` with stubbed pass and fail commands; assert step-result rows and the stamped verdict. - **Presence-check tests** (rewrite of the subagent-stop CLI tests): pipe a synthetic SubagentStop event for a dispatch with and without a gate record; assert route-on-verdict versus loud error verdict with the gate-skip reason. - **Routing tests**: assert `next` routes on a present verdict and that the `gates_settling` state is gone. - **Deleted with their subjects**: queue/resolve/liveness/detached-spawn tests and their support helpers. - **Deliberately untestable at this seam**: whether agents iterate honestly against the gate in-turn. That is behavioral, assigned to the supervised live drive, and is the residual risk this design accepts. ## Out of Scope - Bulk changes to map topology or edge semantics beyond what the ADR-0134 amendment requires. - Any Bash-tool grant to wiring, or changes to reviewer/judgment-role contracts. - Attempt-counting, retry budgets, or negotiation detection in the gate CLI — the live drive decides whether these are needed. - Ticket #475 (regression test for the settled flow) — re-scope or close it against the new design in its own right, not here. - Harness feature requests (hook respond-early semantics). ## Further Notes - Honest accounting: this reverses ADR-0140 days after it shipped and deletes most of its commit. What it buys: the entire async state machine gone, shorter correction loops, and code deleted rather than added. - The residual ~25% doubt is behavioral, not mechanical: whether gated agents iterate honestly or develop negotiation habits ("tests mostly pass"). The runner-owned CLI mitigates; the first supervised live drive proves. - A stray settlement-era worktree under the pipeline's worktrees directory duplicates several affected files; confirm it is stale before the deletion pass. ## Decomposition VERDICT: CREATE — hard floor, explicit user-approved work (skeptic gate 2026-08-22). - [ ] Supersession ADR recorded in docs/adr/ (done on main thread) - [ ] #478 gate CLI - [ ] #479 presence check + routing (blocked by #478) - [ ] #480 settlement deletion + schema revert (blocked by #479) - [ ] #481 role contracts + docs (blocked by ADR only; parallel with #478-#480) - [ ] Close-out: blocked by all children; close #470 as superseded; verify no gates_settling references remain
Author
Owner

Decomposed into children #478-#481; supersession ADR being recorded on main thread. Skeptic verdict: CREATE.

Decomposed into children #478-#481; supersession ADR being recorded on main thread. Skeptic verdict: CREATE.
Author
Owner

Work starting on branch worktree-477-in-turn-gates via os-sdlc pipeline.

Work starting on branch worktree-477-in-turn-gates via os-sdlc pipeline.
Author
Owner

Resolution

Done: In-turn gate execution shipped: gated producers run the runner-owned 'os-sdlc-runner gate DISPATCH_ID' in-turn and iterate until green; CompletionIntake is a fail-closed presence check stamping error/gate_skip on a missing verdict; detached-settlement machinery, settlement_pid/settlement_started_at columns (schema 15), and gates_settling routing removed; gated map nodes carry the inverted role contract. ADR-0141 committed, superseding ADR-0140; ADR-0139 untouched.

Evidence: Commits e578c1b (implementation) and 11d7e09 (ADR-0141) merged fast-forward to main. Full suite 1017 runs, 2593 assertions, 0 failures; rubocop 12 files, 0 offenses. Reviewer (opus) verdict pass after one correction round; gate tests pin in-turn synchrony, fail-closed gate_skip, and the crashing-gate error path.

Follow-ups: Existing ticket #481 carries the deferred items: stale docs (deterministic-gates.md, CONTEXT.md, implement/SKILL.md, pipeline-state-investigate/SKILL.md), GateSettlement->GateRun and settlement_step_output renames, gates_settling negative assertion, BUSY_TIMEOUT_MS decision, ADR-0097/0107/0134/0137 amendments. Closing #470 as superseded is left to the user. No new tickets captured.

## Resolution **Done:** In-turn gate execution shipped: gated producers run the runner-owned 'os-sdlc-runner gate DISPATCH_ID' in-turn and iterate until green; CompletionIntake is a fail-closed presence check stamping error/gate_skip on a missing verdict; detached-settlement machinery, settlement_pid/settlement_started_at columns (schema 15), and gates_settling routing removed; gated map nodes carry the inverted role contract. ADR-0141 committed, superseding ADR-0140; ADR-0139 untouched. **Evidence:** Commits e578c1b (implementation) and 11d7e09 (ADR-0141) merged fast-forward to main. Full suite 1017 runs, 2593 assertions, 0 failures; rubocop 12 files, 0 offenses. Reviewer (opus) verdict pass after one correction round; gate tests pin in-turn synchrony, fail-closed gate_skip, and the crashing-gate error path. **Follow-ups:** Existing ticket #481 carries the deferred items: stale docs (deterministic-gates.md, CONTEXT.md, implement/SKILL.md, pipeline-state-investigate/SKILL.md), GateSettlement->GateRun and settlement_step_output renames, gates_settling negative assertion, BUSY_TIMEOUT_MS decision, ADR-0097/0107/0134/0137 amendments. Closing #470 as superseded is left to the user. No new tickets captured.
jared closed this issue 2026-08-22 12:18:38 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#477
No description provided.