diff --git a/.changeset/3210-precondition-blocking-human-retry-ceiling.md b/.changeset/3210-precondition-blocking-human-retry-ceiling.md new file mode 100644 index 000000000..172a30612 --- /dev/null +++ b/.changeset/3210-precondition-blocking-human-retry-ceiling.md @@ -0,0 +1,6 @@ +--- +type: Fixed +pr: 3528 +--- + +**Autonomous/auto-mode no longer auto-approves unmet `` checkpoints, and the blocker loop now halts `needs_human` instead of retrying forever** — the checkpoint an executor returns when a task's `` is unmet (an unmet `user_setup` step, a missing env var, an absent prior-phase artifact) now carries `gate="blocking-human"`, which both auto-mode bypass layers (executor checkpoint protocol and execute-phase checkpoint handling) honor, so it always stops for a human instead of being silently approved with a synthetic "approved" and then failing `` on the still-missing prerequisite. Independently, `/gsd:autonomous`'s blocker handler now counts "Fix and retry" attempts per phase step and, after 3 failed attempts, escalates to a terminal `needs_human` halt that surfaces the unmet items and records a `## Needs Human` STATE.md row, ending the observed multi-hour retry loops on operator-gated plans. (#3210) diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index d7f8c2786..5e3afef06 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -148,7 +148,7 @@ For each task: 0. **Precondition check (before any other task work):** If the task carries a `` element, evaluate that single prose line first — it names a runnable/checkable fact the task assumes (env var set, prior-phase artifact present, server responding to `/health`, `user_setup` step done). Verify with **read-only checks only** — file existence, env var presence (no value output), idempotent `GET /health`-style pings. Do NOT run commands with side effects (writes, network POSTs, secret emission) as the check; if a side-effecting check seems required, halt and surface via checkpoint instead. - **Met OR absent:** continue with no visible change to execution flow. The precondition is a no-op for the rest of the task loop. - - **Unmet:** STOP — return a `checkpoint:human-verify` (use `checkpoint_return_format`) with `**Blocked by:** Precondition not met: `. Do NOT partial-commit the task. Unmet preconditions are NEVER auto-approved, even under `AUTO_CFG=true` — a missing prerequisite is not a verification step a human can rubber-stamp; it is a fact the executor cannot establish on its own. The human either satisfies the precondition (sets the env var, completes the `user_setup` step, regenerates the artifact) or reruns `/gsd:plan-phase` to restructure. + - **Unmet:** STOP — return a `checkpoint:human-verify` reporting `**Gate:** blocking-human` (use `checkpoint_return_format`) with `**Blocked by:** Precondition not met: `. Do NOT partial-commit the task. Unmet preconditions are NEVER auto-approved, even under `AUTO_CFG=true` — a missing prerequisite is not a verification step a human can rubber-stamp; it is a fact the executor cannot establish on its own. The human either satisfies the precondition (sets the env var, completes the `user_setup` step, regenerates the artifact) or reruns `/gsd:plan-phase` to restructure. 1. **If `type="auto"`:** - Check for `tdd="true"` → follow TDD execution flow @@ -328,7 +328,7 @@ For full automation-first patterns, server lifecycle, CLI handling: **Auto-mode checkpoint behavior** (when `AUTO_CFG` is `"true"`): -- **checkpoint:human-verify** → Auto-approve **except package-legitimacy checkpoints**. If checkpoint has `gate="blocking-human"` OR its purpose indicates package legitimacy verification (`what-built` mentions `Package verification required before install` or `Package install failed — human verification required`), do **not** auto-approve. STOP and return checkpoint_return_format for explicit human confirmation. +- **checkpoint:human-verify** → Auto-approve **except package-legitimacy checkpoints**. If checkpoint has `gate="blocking-human"` OR its purpose indicates package legitimacy verification (`what-built` mentions `Package verification required before install` or `Package install failed — human verification required`), do **not** auto-approve. STOP and return checkpoint_return_format for explicit human confirmation. Precondition-unmet checkpoints report `blocking-human` — never auto-approved. - **checkpoint:decision** → If checkpoint has `gate="blocking-human"`, do **not** auto-select — STOP and return checkpoint_return_format for an explicit human decision (a `blocking-human` decision exists because its default answer would be wrong to assume). Otherwise auto-select first option (planners front-load the recommended choice), log `⚡ Auto-selected: [option name]`, continue to next task. - **checkpoint:human-action** → STOP normally. Auth gates cannot be automated — return structured checkpoint message using checkpoint_return_format. @@ -354,7 +354,7 @@ When hitting checkpoint or auth gate, return this structure: ## CHECKPOINT REACHED **Type:** [human-verify | decision | human-action] -**Gate:** [blocking | blocking-human] — copy the task's `gate` attribute verbatim so the orchestrator's carve-out sees it +**Gate:** [blocking | blocking-human] — copy the task's `gate` attribute verbatim (precondition-unmet checkpoints report `blocking-human`) **Plan:** {phase}-{plan} **Progress:** {completed}/{total} tasks complete diff --git a/gsd-core/references/checkpoints.md b/gsd-core/references/checkpoints.md index a40f85dfc..925c79bc7 100644 --- a/gsd-core/references/checkpoints.md +++ b/gsd-core/references/checkpoints.md @@ -9,14 +9,14 @@ Plans execute autonomously. Checkpoints formalize interaction points where human 3. **User only does what requires human judgment** - Visual checks, UX evaluation, "does this feel right?" 4. **Secrets come from user, automation comes from Claude** - Ask for API keys, then Claude uses them via CLI 5. **Auto-mode bypasses verification/decision checkpoints** — When `workflow._auto_chain_active` or `workflow.auto_advance` is true in config: human-verify auto-approves, decision auto-selects first option, human-action still stops (auth gates cannot be automated) -6. **`gate="blocking-human"` is never auto-approved** — a checkpoint carrying this gate stops for a human in *every* mode, including auto-mode, regardless of its type. Rule 5 does not apply to it. +6. **`gate="blocking-human"` is never auto-approved** — a checkpoint carrying this gate stops for a human in *every* mode, including auto-mode, regardless of its type. Rule 5 does not apply to it. The executor's precondition-unmet checkpoint (a task's `` evaluated false — unmet `user_setup` step, missing env var, absent prior-phase artifact) reports this gate (#3210). **The `gate` attribute:** | Value | Auto-mode behavior | Use for | |-------|--------------------|---------| | `gate="blocking"` | Bypassed per rule 5 (human-verify auto-approves, decision auto-selects) | The default. Post-hoc verification and implementation choices that are safe to take the recommended path on when unattended. | -| `gate="blocking-human"` | **Never bypassed.** Stops for a human in auto-mode too. | Irreversible or trust-establishing steps a human must actually see: package-legitimacy verification before install, and any decision whose default answer would be wrong to assume. | +| `gate="blocking-human"` | **Never bypassed.** Stops for a human in auto-mode too. | Irreversible or trust-establishing steps a human must actually see: package-legitimacy verification before install, any decision whose default answer would be wrong to assume, and unmet `` facts the executor cannot establish on its own (#3210). | Reach for `gate="blocking-human"` whenever auto-approving the checkpoint would defeat its purpose. If the checkpoint exists because a human must *decide* something, `blocking` is the wrong gate — auto-mode will decide it for them. diff --git a/gsd-core/references/planner-preconditions.md b/gsd-core/references/planner-preconditions.md index 92cbafd99..531520109 100644 --- a/gsd-core/references/planner-preconditions.md +++ b/gsd-core/references/planner-preconditions.md @@ -121,7 +121,7 @@ The executor agent reads `` before any other task work: |---|---| | **Absent** | No visible change — execute the task exactly as today. Back-compat for every existing plan. | | **Met** | No visible change — proceed with the task. The precondition is logged in the SUMMARY only if it was non-trivial to verify. | -| **Unmet** | STOP — return a `checkpoint:human-verify` (use `checkpoint_return_format`) with `**Blocked by:** Precondition not met: `. Do NOT partial-commit the task. Unmet preconditions are NEVER auto-approved — a missing prerequisite is not a verification step a human can rubber-stamp, it is a fact the executor cannot establish on its own. | +| **Unmet** | STOP — return a `checkpoint:human-verify` reporting `**Gate:** blocking-human` (use `checkpoint_return_format`) with `**Blocked by:** Precondition not met: `. Do NOT partial-commit the task. Unmet preconditions are NEVER auto-approved — a missing prerequisite is not a verification step a human can rubber-stamp, it is a fact the executor cannot establish on its own. | ## Plan-structure validation diff --git a/gsd-core/workflows/autonomous.md b/gsd-core/workflows/autonomous.md index a4bf6c800..bc546a3e5 100644 --- a/gsd-core/workflows/autonomous.md +++ b/gsd-core/workflows/autonomous.md @@ -781,7 +781,7 @@ When any phase operation fails or a blocker is detected, present 3 options via A 2. **"Skip this phase"** — Mark phase as skipped, continue to the next incomplete phase 3. **"Stop autonomous mode"** — Display summary of progress so far and exit cleanly -**On "Fix and retry":** Loop back to the failed step within execute_phase. If the same step fails again after retry, re-present these options. +**On "Fix and retry":** Loop back to the failed step within execute_phase. Track the retry count per phase + step (`RETRY_COUNT`, kept in memory for the run). If the same step fails again after retry, re-present these options. **Retry ceiling (#3210):** once the same phase step has failed 3 "Fix and retry" attempts, do NOT re-present the options — escalate to a terminal `needs_human` halt: display `Phase {N} ⛔ {Name} — needs_human`, list the unmet items (the blocker description from each attempt), append/update a `## Needs Human` section in STATE.md (`| ${PHASE_NUM} | needs_human | resolve blocker, then /gsd:autonomous --from ${PHASE_NUM} |`), and stop autonomous mode with the standard stopped-summary banner. A blocker that survives 3 fix attempts is an operator gate, not an executable gap — retrying it again just burns hours. **On "Skip this phase":** Log `Phase {N} ⏭ {Name} — Skipped by user` and proceed to iterate. diff --git a/gsd-core/workflows/execute-phase.md b/gsd-core/workflows/execute-phase.md index 0b7244656..59fc7e81a 100644 --- a/gsd-core/workflows/execute-phase.md +++ b/gsd-core/workflows/execute-phase.md @@ -1141,7 +1141,7 @@ When executor returns a checkpoint AND `AUTO_MODE` is `true`: - **decision** → Auto-spawn continuation agent with `{user_response}` = first option from checkpoint details. Log `⚡ Auto-selected: [option]`. **Except `blocking-human`.** - **human-action** → Present to user (existing behavior below). Auth gates cannot be automated. -**Carve-out — overrides all branches above.** If the returned `Gate:` is `blocking-human`, or its `` mentions `Package verification required before install` or `Package install failed — human verification required`, never auto-approve or auto-select, regardless of type. Present to user (standard flow below). Log `⛔ blocking-human gate — auto-mode suspended`. +**Carve-out — overrides all branches above.** If the returned `Gate:` is `blocking-human` (precondition-unmet, #3210), or its `` mentions `Package verification required before install` or `Package install failed — human verification required`, never auto-approve or auto-select. Present to user (standard flow). Log `⛔ blocking-human gate — auto-mode suspended`. **Standard flow (not auto-mode, human-action, or blocking-human):** diff --git a/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md b/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md index 6253463a2..1340bfaae 100644 --- a/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +++ b/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md @@ -63,7 +63,11 @@ On `checkpoint:human-verify` / `checkpoint:decision` tasks, `gate="blocking"` `` (`agents/gsd-executor.md`), and `checkpoints.md` (the full gate table) is embedded in the dispatch `` verbatim. Only `gate="blocking-human"` always surfaces to a human, regardless of -auto-mode. +auto-mode. An unmet `` checkpoint (executor step 0, `Blocked by: +Precondition not met` — unmet `user_setup` step, missing env var, absent +prior-phase artifact) reports `blocking-human` and therefore always surfaces +to a human, in every mode (#3210): the missing prerequisite is a fact only a +human can establish, not a verification step to rubber-stamp. When composing the `Agent()` prompt, do NOT add text refusing or overriding auto-approval for a `blocking` gate. Orchestrator-composed instructions that contradict the diff --git a/tests/autonomous-converge.test.cjs b/tests/autonomous-converge.test.cjs index fd349b80d..889729bf5 100644 --- a/tests/autonomous-converge.test.cjs +++ b/tests/autonomous-converge.test.cjs @@ -286,3 +286,42 @@ describe('autonomous verification deferral contract', () => { assert.match(iterateStep, /drop deferred phases from the autonomous queue/); }); }); + +// ─── Issue #3210: bounded blocker retries, needs_human escalation ──────────── +// +// handle_blocker's "Fix and retry" path had no attempt ceiling across +// invocations and no automatic escalation to a terminal needs_human state, so +// a non-converging blocker (e.g. an operator gate the executor cannot satisfy) +// looped indefinitely. Regression coverage lives here because this file owns +// the autonomous.md host-workflow contract. + +describe('issue #3210: autonomous handle_blocker has a retry ceiling with needs_human escalation', () => { + function stepOf(content, name) { + const open = ``; + const from = content.indexOf(open); + assert.ok(from !== -1, `step "${name}" not found`); + const to = content.indexOf('', from); + assert.ok(to !== -1, `step "${name}" has no closing tag`); + return content.slice(from, to); + } + + test('handle_blocker bounds "Fix and retry" attempts per phase step', () => { + const step = stepOf(read(WORKFLOW_PATH), 'handle_blocker'); + assert.match( + step, + /\b3\b.*retr|\bretr.*\b3\b|RETRY_COUNT|retry (ceiling|limit|count)/i, + 'handle_blocker must track a bounded retry count for the same phase step instead of ' + + 're-presenting "Fix and retry" indefinitely (#3210)' + ); + }); + + test('handle_blocker auto-escalates to a terminal needs_human halt once the ceiling is exceeded', () => { + const step = stepOf(read(WORKFLOW_PATH), 'handle_blocker'); + assert.match( + step, + /needs_human/, + 'once the retry ceiling is exceeded, handle_blocker must halt autonomously in a terminal ' + + 'needs_human state (surfacing the unmet items) instead of looping or asking again (#3210)' + ); + }); +}); diff --git a/tests/emitted-drift-acks/2943-context7-tool-name.json b/tests/emitted-drift-acks/2943-context7-tool-name.json index 401d92f86..446faafcf 100644 --- a/tests/emitted-drift-acks/2943-context7-tool-name.json +++ b/tests/emitted-drift-acks/2943-context7-tool-name.json @@ -1,6 +1,6 @@ { "version": 1, "paths": { - "gsd-executor.md": "#2943: the context7 doc-lookup block's ctx7 CLI-fallback rationale was rewritten to describe the real mechanism (custom subagents cannot see project-scoped .mcp.json; they inherit only user-scoped ~/.claude/mcp.json), replacing the wrong anthropics/claude-code#13898 'tools: frontmatter restriction' attribution. The tool name was also corrected (get-library-docs -> query-docs) and the params renamed (context7CompatibleLibraryId/topic -> libraryId/query). The +95 bytes is the longer-but-accurate mechanism description; it is the literal fix for the mis-attribution, not incidental prose growth, and the rationale must be correct because agents read it to decide when to fall back to the CLI. #3021 amendment: branch allow-list regex widened to accept worktree-wf_* (Workflow backend naming) alongside agent-*/worktree-agent-*." + "gsd-executor.md": "#2943: the context7 doc-lookup block's ctx7 CLI-fallback rationale was rewritten to describe the real mechanism (custom subagents cannot see project-scoped .mcp.json; they inherit only user-scoped ~/.claude/mcp.json), replacing the wrong anthropics/claude-code#13898 'tools: frontmatter restriction' attribution. The tool name was also corrected (get-library-docs -> query-docs) and the params renamed (context7CompatibleLibraryId/topic -> libraryId/query). The +95 bytes is the longer-but-accurate mechanism description; it is the literal fix for the mis-attribution, not incidental prose growth, and the rationale must be correct because agents read it to decide when to fall back to the CLI. #3021 amendment: branch allow-list regex widened to accept worktree-wf_* (Workflow backend naming) alongside agent-*/worktree-agent-*. — #3210 append: the unmet- branch now reports the returned checkpoint with **Gate:** blocking-human (both auto-mode bypass layers key on that gate; without it auto-mode silently auto-approved the checkpoint with a synthetic 'approved'), the auto-mode human-verify rule names precondition-unmet checkpoints as exempt, and checkpoint_return_format's Gate line notes precondition-unmet checkpoints report blocking-human; +134 bytes, still under the 49152 cap." } } diff --git a/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json b/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json new file mode 100644 index 000000000..357ee24b3 --- /dev/null +++ b/tests/emitted-drift-acks/3210-autonomous-precondition-gate.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "paths": { + "autonomous.md": "#3210: handle_blocker's 'Fix and retry' path gained a retry ceiling — a per-phase-step RETRY_COUNT that escalates to a terminal needs_human halt (STATE.md '## Needs Human' row + unmet items surfaced) after 3 failed fix attempts, instead of re-presenting the options indefinitely. This is the loop-fixing behavior the issue title names; +704 bytes is the ceiling rule itself, not incidental prose. (The matching gsd-executor.md growth is acknowledged in the 2943 fragment and the execute-phase.md growth in the 3370 fragment, which already name those paths.)" + } +} diff --git a/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json b/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json index b1d9e3d53..4e90a5601 100644 --- a/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json +++ b/tests/emitted-drift-acks/3370-execute-phase-gate-conflation.json @@ -1,7 +1,7 @@ { "version": 1, "paths": { - "execute-phase.md": "#3370: the step-3 executor-routing line now also cites #3370 — the checkpoint gate rule itself lives in the execute-phase/steps/per-plan-executor-routing.md fragment (loaded per plan in every isolation mode immediately before the dispatch prompt is composed), the same keep-the-host-lean pattern #1689/#3417 used, because the host sits under the frozen ADR-857 Phase 6 ceiling (≤93400). Net growth is 6 bytes (93386 -> 93392): the routing citation only; the rule text, which forbids the orchestrator from composing dispatch text that refuses or overrides auto-approval for the default gate=\"blocking\" (only blocking-human always surfaces), is in the fragment. Supersedes the spent #3324 fragment (merged into next), which also named execute-phase.md and would otherwise double-ack the same path. — #3423 append (epic #1891 F8): grew 12 bytes with the -> tag rename (4 tag tokens, +3 bytes each), then trimmed 8 bytes of redundant prose (dropped 'current' from the model-inheritance note; #3478's growth had left only 8 bytes of ADR-857 margin) to stay under the Phase 6 margin ceiling; net +4 bytes against the #3370 baseline.", + "execute-phase.md": "#3370: the step-3 executor-routing line now also cites #3370 — the checkpoint gate rule itself lives in the execute-phase/steps/per-plan-executor-routing.md fragment (loaded per plan in every isolation mode immediately before the dispatch prompt is composed), the same keep-the-host-lean pattern #1689/#3417 used, because the host sits under the frozen ADR-857 Phase 6 ceiling (≤93400). Net growth is 6 bytes (93386 -> 93392): the routing citation only; the rule text, which forbids the orchestrator from composing dispatch text that refuses or overrides auto-approval for the default gate=\"blocking\" (only blocking-human always surfaces), is in the fragment. Supersedes the spent #3324 fragment (merged into next), which also named execute-phase.md and would otherwise double-ack the same path. — #3423 append (epic #1891 F8): grew 12 bytes with the -> tag rename (4 tag tokens, +3 bytes each), then trimmed 8 bytes of redundant prose (dropped 'current' from the model-inheritance note; #3478's growth had left only 8 bytes of ADR-857 margin) to stay under the Phase 6 margin ceiling; net +4 bytes against the #3370 baseline. — #3210 append: the checkpoint_handling blocking-human carve-out names precondition-unmet checkpoints (+29-byte marker '(precondition-unmet, #3210)'), and the decision bullet's 'Except blocking-human' conditional — required by tests/package-legitimacy-gate.test.cjs — was restored (+29), funded by trimming ', regardless of type' (-20, the carve-out already overrides all branches) and '(standard flow below)' -> '(standard flow)' (-6); net +3 bytes (93395 -> 93398), still under the frozen ADR-857 Phase 6 ceiling (<=93400).", "execute-plan.md": "#3370: the Pattern A dispatch prompt spec gained the gate-semantics clause (gate=\"blocking\" (the default) is auto-approvable in auto-mode per the executor's own checkpoint protocol, gate=\"blocking-human\" always surfaces to a human; add no instruction overriding that protocol), closing the identically-shaped dispatch-time gap on the single-plan path named in the issue. Growth ~248 bytes (38913 -> 39161, still under the DEFAULT 40 KiB ceiling). Supersedes the spent #2652 fragment (merged into next), which also named execute-plan.md and would otherwise double-ack the same path." } } diff --git a/tests/execute-phase-wave.test.cjs b/tests/execute-phase-wave.test.cjs index 7f141e985..9e05c3982 100644 --- a/tests/execute-phase-wave.test.cjs +++ b/tests/execute-phase-wave.test.cjs @@ -908,3 +908,30 @@ describe('bug #3096: ai-integration-phase sequential ordering and Edit-only disc }); }); } + +// ─── Issue #3210: auto-mode carve-out exempts precondition-unmet checkpoints ─ +// +// The checkpoint_handling auto-spawn rule dispatched on checkpoint type alone; +// a checkpoint returned because a task's was unmet would have +// been auto-approved with a synthetic "approved" — re-approving the very +// checkpoint the executor refused to auto-approve (it reports Gate: +// blocking-human). This file owns the execute-phase.md host-workflow contract. + +describe('issue #3210: execute-phase auto-mode carve-out exempts precondition-unmet checkpoints', () => { + test('the checkpoint_handling auto-spawn rule names precondition-unmet checkpoints', () => { + const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8'); + const open = content.indexOf(''); + assert.ok(open !== -1, 'checkpoint_handling step not found'); + const close = content.indexOf('', open); + const step = content.slice(open, close); + const splitLines = require('../gsd-core/bin/lib/text-lines.cjs').splitLines; + const carveOut = splitLines(step).find((l) => l.includes('Carve-out')); + assert.ok(carveOut, 'checkpoint_handling must keep the blocking-human carve-out'); + assert.match( + carveOut, + /precondition/i, + 'the auto-mode carve-out must state that a precondition-unmet checkpoint reports ' + + 'blocking-human and is never auto-approved (#3210)' + ); + }); +}); diff --git a/tests/precondition-element.test.cjs b/tests/precondition-element.test.cjs index b1b6c2992..d2c76d6f4 100644 --- a/tests/precondition-element.test.cjs +++ b/tests/precondition-element.test.cjs @@ -231,3 +231,64 @@ describe('issue #1949: parity between plan-md.md and planner-preconditions.md', assert.ok(ref.includes(''), 'planner-preconditions.md must spell '); }); }); + +// ─── Issue #3210: the precondition-unmet checkpoint must be human-gated ────── +// +// The executor's own invariant ("unmet preconditions are NEVER auto-approved, +// even under AUTO_CFG=true") was unenforceable: the checkpoint it returns for an +// unmet carried no gate, and both auto-mode bypass rules only +// exempt `gate="blocking-human"`. Auto-mode therefore auto-approved it with a +// synthetic "approved", the task's failed closed, and the loop read +// the failure as an executable gap — retrying an operator gate forever (#3210). + +describe('issue #3210: executor gates the precondition-unmet checkpoint as blocking-human', () => { + const CHECKPOINTS_REF = path.join(ROOT, 'gsd-core', 'references', 'checkpoints.md'); + + test('unmet-precondition branch stamps the returned checkpoint with Gate: blocking-human', () => { + const exec = read(EXECUTOR); + const unmet = exec.split('\n').find((l) => l.includes('**Unmet:**')); + assert.ok(unmet, 'gsd-executor.md must keep the **Unmet:** precondition branch'); + assert.match( + unmet, + /blocking-human/, + 'the unmet-precondition branch must report the checkpoint with **Gate:** blocking-human — ' + + 'that gate is what both auto-mode bypass layers check (#3210)' + ); + }); + + test('auto-mode human-verify rule explicitly exempts precondition-unmet checkpoints', () => { + const exec = read(EXECUTOR); + const rule = exec.split('\n').find((l) => l.includes('**checkpoint:human-verify**') && /Auto-approve/.test(l)); + assert.ok(rule, 'gsd-executor.md must keep the auto-mode human-verify bypass rule'); + assert.match( + rule, + /precondition/i, + 'the executor auto-mode bypass rule must name precondition-unmet checkpoints as never ' + + 'auto-approved, not only gate="blocking-human" tasks (#3210)' + ); + }); + + test('checkpoints.md maps precondition-unmet checkpoints onto the blocking-human gate', () => { + const ref = read(CHECKPOINTS_REF); + const rule = ref.split('\n').find((l) => l.includes('never auto-approved')); + assert.ok(rule, 'checkpoints.md must keep golden rule 6 (blocking-human is never auto-approved)'); + assert.match( + rule, + /precondition/i, + 'golden rule 6 must name the executor\'s precondition-unmet checkpoint as a carrier of ' + + 'gate="blocking-human" (#3210)' + ); + }); + + test('planner-preconditions.md documents the blocking-human gate on the unmet path', () => { + const ref = read(PRECONDITIONS_REF); + const unmet = ref.split('\n').find((l) => l.includes('**Unmet**')); + assert.ok(unmet, 'planner-preconditions.md must keep the executor-behavior Unmet row'); + assert.match( + unmet, + /blocking-human/, + 'the Unmet row must state the returned checkpoint carries gate="blocking-human", matching ' + + 'agents/gsd-executor.md (#3210)' + ); + }); +});