* test(#1949): add failing-first tests for <precondition> element Red phase for issue #1949 (Design by Contract: <precondition> element asserted before task execution). Tests assert: - docs/reference/plan-md.md documents the new <precondition> element - agents/gsd-planner.md @-references planner-preconditions.md and stays under the 49152-char cap (progressive-disclosure requirement) - gsd-core/references/planner-preconditions.md exists and documents the three emission cases mandated by the issue (user_setup / prior-phase artifact / env-var) and the contract triad mapping - agents/gsd-executor.md asserts <precondition> before task execution and routes unmet preconditions through existing checkpoint machinery - cmdVerifyPlanStructure (behavioral via runGsdTools) accepts plans both with and without <precondition> — the additive-validation guarantee - Parity assertion: plan-md.md and planner-preconditions.md agree on the canonical tag spelling (DEFECT.GENERATIVE-FIX-DIVERGENCE guard) Most prose-contract assertions are Red until the implementation lands. The behavioral validator assertions pass immediately (regression guards proving the validator already accepts unknown optional tags). * feat(#1949): <precondition> task element — Design by Contract Add an optional <precondition> element to <task> in PLAN.md (issue #1949, The Pragmatic Programmer Topic 23). The front-of-task side of the plan contract — preconditions (before) ↔ postconditions (<verify>/<done>/ <acceptance_criteria>, after) ↔ invariants (must_haves.truths, across the whole plan). Together with the tracer-bullet proposal (#1945), this closes both ends of the 'outrunning your headlights' failure mode for an autonomous AI executor. Acceptance criteria met: - <precondition> is an optional element on <task>; plans that omit it validate unchanged (cmdVerifyPlanStructure checks for presence of required tags, does not reject unknown optional tags). - gsd-executor evaluates the precondition before any other task work. Unmet halts execution with a checkpoint:human-verify and no partial commit; met or absent produces no visible change to execution flow. Unmet is never auto-approved under AUTO_CFG=true — a missing prerequisite is a fact the executor cannot establish on its own. - gsd-planner emits <precondition> in exactly the three cases the issue mandates: user_setup consumption, prior-phase artifact dependency, and env-var/runtime-config dependency. - Tests cover met, unmet, and absent preconditions plus the additive- validator guarantee. Files: - gsd-core/references/planner-preconditions.md (NEW): full emission rules, the three cases with worked examples, format guidance, anti-patterns, the contract triad mapping, and the executor assertion contract. Progressive disclosure. - agents/gsd-planner.md: slim <precondition> note in Task Anatomy with @-reference to the new file. To stay under the 49152-char agent-file cap (27-char headroom before this change), the inline <comment_text_discipline> and <region_scoped_negative_gate> summaries are compressed to one-line pointers — their full rules already live in planner-antipatterns.md, so no content is lost. - agents/gsd-executor.md: new step 0 'Precondition check' in the execute_tasks loop, before the type dispatch, routing unmet through checkpoint_return_format. - docs/reference/plan-md.md: new Preconditions section in the schema reference, with the canonical example and the three emission cases. - CONTEXT.md: Precondition glossary entry as a sibling of Tracer Bullet. - docs/INVENTORY.md + INVENTORY-MANIFEST.json: row for the new references/planner-preconditions.md (regen via gen-inventory-manifest). - tests/precondition-element.test.cjs: failing-first tests covering schema docs, planner emission contract, executor assertion contract, reference-file presence + the three cases, behavioral additive- validator guarantee, and a parity assertion (DEFECT.GENERATIVE-FIX- DIVERGENCE guard). - .changeset/quick-hawks-bark.md: Added fragment. Companion to #1945 (tracer bullets). * chore(#1949): regen agent-size baseline + install-tree goldens Documented baseline regenerations required by the feat(#1949) prose changes (RULESET.AGENT_SIZE_BUDGET + golden-install-parity): - npm run size:baseline — locks in the new gsd-executor.md size (+1050 bytes: the precondition-check step 0 block). gsd-planner.md is net smaller (-142 bytes: compressed two inline summary blocks whose full rules already lived in planner-antipatterns.md to make room for the slim <precondition> pointer). No hard-cap breach. - npm run gen:golden — pick up the new references/planner-preconditions.md + the two changed agent files across all 18 runtime install trees. Both regens are CI-mandated after intentional agent/reference changes; see CLAUDE.md 'RULESET.AGENT_SIZE_BUDGET' and the comments in tests/golden-install-parity.test.cjs. * fix(#1949): bound <precondition> checks to read-only (security review) Apply the security-review finding (LOW, isolated /security-review subagent): the executor's 'run the cheapest check' phrasing for a plan-author-controlled prose line was broader than ideal — a hostile plan author could craft a <precondition> whose 'cheapest check' is side-effecting (curl to an attacker host under the guise of verification, rm -rf before checking, secret emission). The risk is inherited from GSD's existing plan-trust model (<verify>, <action>, <done> already direct the executor to run arbitrary shell), so <precondition> does not materially expand it. But the new prose actively directs execution ('run the check') rather than passively consuming the element, so the bound is worth making explicit. Tightened across all four surfaces that describe the check shape: - agents/gsd-executor.md step 0: '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.' - gsd-core/references/planner-preconditions.md Format section: same bound, plus the halt-and-surface escape hatch. - docs/reference/plan-md.md Preconditions section: mirrored. - CONTEXT.md Precondition glossary entry: mirrored. Regenerated agent-size baseline (executor grew 46186 -> 46440; still under the 49152 cap) and install-tree goldens. * chore(#1949): backfill changeset pr number 2422 Per CONTRIBUTING.md changeset workflow + feature-builder directive Step 8.7: backfill the placeholder pr:0 with the real PR number immediately after gh pr create returns. Avoids the fail_invalid_fragment gate. * fix(#1949): cite [#1949] on allow-test-rule exemption (ADR-456) CI's lint:ci runs lint-allow-test-rule-refs which per ADR-456 requires every // allow-test-rule: exemption on a NEW test file to carry an issue reference (#NNN or URL). My earlier push omitted it. Local 'npm run lint' (eslint) does NOT run this check — only 'npm run lint:ci' does. CLAUDE.md explicitly warns: 'lint:ci ≠ lint — CI runs lint:ci; a local pass is not the gate.' I should have run lint:ci before pushing; correcting now. Pattern matches the companion feature's test file: tests/tracer-bullet.test.cjs:1 // allow-test-rule: source-text-is-the-product [#1945]
7.4 KiB
Planner Preconditions — <precondition> Element
Progressive-disclosure reference for
agents/gsd-planner.md. The planner agent reads this file when it needs the full emission rules for the<precondition>task element (issue #1949, The Pragmatic Programmer Topic 23 — Design by Contract). The slim pointer inagents/gsd-planner.md→<task_breakdown>routes here; the canonical schema row lives indocs/reference/plan-md.md.
The contract triad
Every task in a PLAN.md participates in a three-sided contract:
| Contract side | GSD element | When it binds |
|---|---|---|
| Precondition | <precondition> (optional element on <task>) |
Before the task begins. What must already be true for the task to run safely. |
| Postcondition | <verify> + <done> + <acceptance_criteria> |
After the task ends. What the task guarantees on return. |
| Invariant | must_haves.truths (plan frontmatter) |
Across the whole plan/phase. What always holds. |
GSD already models postconditions and invariants well. <precondition> closes
the missing side: it states, in runnable/checkable terms, what must be true
before a task begins — so an autonomous executor stops the instant an
assumption is false, instead of building ten atomic commits on top of a
migration that never ran.
This is the front-of-task companion to the tracer-bullet proposal (#1945): tracers prove the architecture end-to-end before expansion; preconditions prove each expansion task's assumptions before it runs. Together they close both ends of the "outrunning your headlights" failure mode.
When to emit <precondition>
Emit <precondition> ONLY when a task relies on state the plan's own depends_on
ordering does not already guarantee. Three cases cover every legitimate use; if
the task's prerequisite is intra-plan sequencing, use depends_on, NOT
<precondition>.
Case 1 — External service setup (user_setup)
The task depends on an external service the developer must set up (account
creation, secret retrieval, dashboard configuration, billing activation). The
user_setup frontmatter field already enumerates these steps; <precondition>
on the consuming task ties a specific setup step to a specific task so the
executor halts if the setup was skipped.
<task type="auto">
<name>Send welcome email via SendGrid</name>
<precondition>SENDGRID_API_KEY is set (user_setup step 1 complete)</precondition>
<files>src/email/welcome.ts</files>
<action>...</action>
<verify>...</verify>
<done>Welcome email dispatched for a test user</done>
</task>
Case 2 — Prior-phase artifact dependency
The task consumes an artifact a prior phase promised (a generated schema, a
migration's dist output, a contract file). Cross-phase depends_on does not
cross phase boundaries, so a <precondition> is the explicit pointer.
<task type="auto">
<name>Generate TypeScript client from schema</name>
<precondition>dist/schema.json from Phase 02 exists and is non-empty</precondition>
<files>src/client/generated.ts</files>
<action>...</action>
<verify>...</verify>
<done>Client generated and compiles</done>
</task>
Case 3 — Environment variable / runtime configuration
The task shells out to a tool, hits an API, or runs a script that requires an environment variable or runtime config that exists now (not at plan time).
<task type="auto">
<name>Add /reveal endpoint handler</name>
<precondition>server bootstraps and responds to GET /health (from the tracer slice)</precondition>
<files>server/reveal.ts</files>
<action>...</action>
<verify>curl /reveal?path=... opens the OS file manager</verify>
<done>Endpoint committed and manually verified</done>
</task>
Format
<precondition> is a single line of prose inside the <task> element, placed right after <name> and before <files>. It is prose, not a structured block — concrete enough that the executor agent can run a read-only check (file existence, env var presence, idempotent GET /health-style ping), prose enough not to require a parser extension. The executor MUST verify with read-only checks only: no writes, no network POSTs, no secret emission. If a side-effecting check seems required, the executor halts and surfaces a checkpoint rather than running it.
<task type="auto">
<name>...</name>
<precondition>...</precondition>
<files>...</files>
<action>...</action>
<verify>...</verify>
<done>...</done>
</task>
What NOT to put in a <precondition>
- Vague readiness checks. "The system is ready" is not checkable. Name the
concrete signal: a
curlresponse, a file path, an env var name. - Intra-plan ordering. "Task 1 has completed" — that is what
depends_onis for. Reserve<precondition>for state the plan's wave/dependency graph cannot express. - Implementation choices. "We have chosen library X" — that belongs in the
<action>body or a## Decisionsrow, not a runtime fact. - Things the task itself creates. A precondition names a fact the task
assumes; if the task produces it, it is a postcondition (
<done>).
Executor behavior (assertion contract)
The executor agent reads <precondition> before any other task work:
| State | Executor behavior |
|---|---|
| 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: <precondition text>. 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
cmdVerifyPlanStructure checks for the presence of required tags (<name>,
<action>, etc.) and warns on missing recommended tags (<verify>, <done>,
<files>). It does not reject unknown optional tags, so adding
<precondition> to a plan passes validation unchanged. A future ADR may add
structured validation if drift emerges; v1 ships prose-only to keep the surface
minimal (Hyrum's Law: the smaller the observable surface, the less the system
depends on by accident).
Out of scope
The following are explicitly NOT part of v1:
- Structured precondition DSL (e.g.
<precondition kind="env" var="X"/>). Prose-first keeps complexity flat; structured validation can land in a later PR if prose proves insufficient. - Automatic precondition emission for every task. The three cases above are a hard ceiling (Zawinski's Law guard). Most tasks do not need a precondition.
- Cross-task preconditions. A precondition binds one task to one fact. Use
depends_onor a parent plan'smust_havesfor multi-task contracts.
See also
- The Pragmatic Programmer, Topic 23 — "Design by Contract" (Hunt & Thomas).
docs/reference/plan-md.md— canonical PLAN.md schema reference (where<precondition>appears in the task-element table).- Tracer-bullet proposal (#1945) — the architectural-end companion to this front-of-task contract.
agents/gsd-executor.md→<execution_flow>→ precondition check step — the assertion surface that consumes what this reference defines.