Files
msd-core/gsd-core/references/planner-preconditions.md
Tom Boucher 1720aacf0c feat(#1949): <precondition> task element — Design by Contract (#2422)
* 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]
2026-07-19 07:52:36 -04:00

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 in agents/gsd-planner.md → <task_breakdown> routes here; the canonical schema row lives in docs/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 curl response, a file path, an env var name.
  • Intra-plan ordering. "Task 1 has completed" — that is what depends_on is 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 ## Decisions row, 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_on or a parent plan's must_haves for 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.