Files
msd-core/gsd-core/references/planner-preconditions.md
Tom Boucher 8fc88f663d fix(#3210): gate unmet preconditions as blocking-human; cap blocker retries at needs_human (#3528)
* fix(#3210): gate unmet preconditions as blocking-human and cap blocker retries at needs_human

* chore(#3210): add changeset fragment for PR #3528

* fix(#3210): restore blocking-human carve-out and CRLF-safe split

---------

Co-authored-by: sim <sim@local>
2026-08-14 23:01:30 -04:00

157 lines
7.4 KiB
Markdown

# 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.
```xml
<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.
```xml
<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).
```xml
<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.
```xml
<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` reporting `**Gate:** blocking-human` (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.