| name | plan-breakdown |
| description | Break a reviewed markdown plan into a Guardrails task folder — a dependency DAG of
tasks, each with an action (script or prompt) and deterministic-first guardrails —
executable by the `guardrails` CLI. Use when the user says "break down this plan",
"generate tasks for <plan>.md", or hands you a plan path with that intent.
Input: path to a REVIEWED `.md` plan. Output: a `<plan-name>/` folder next to the
plan, self-validated with `guardrails validate`, presented as a DRAFT for human
review. The skill leans deterministic (tests/regex/exit codes) over prompt-judges,
and INSERTS guardrail-enabling tasks the plan never mentioned (e.g. "author the
unit tests" before "implement the feature").
|
Plan Breakdown
Turn a reviewed plan into an executable task DAG whose guardrails a human approves
once — instead of reviewing every agent output forever. The output is always a
draft: the human edits it, then /guardrails-review runs an adversarial pass,
and only then does guardrails run execute it.
References (load as needed):
references/guardrail-catalogue.md — archetypes, decision tree, demotion gate,
anti-patterns (UNIVERSAL doctrine). Read before Step 4, every time.
references/stacks/<stack>.md — the STACK-SPECIFIC idioms (build-descriptor
registration, cross-module reference, structural impl regex, canonical build command,
grep-scope traps). Load the one matching the detected stack in Step 0 (only
stacks/dotnet.md ships today). The catalogue holds the universal rule; the stack file
holds the exact regex/command.
references/stacks/ui.md — the two-level UI-verification methodology (#41/#78):
Level A liveness smoke vs Level B behavioral interaction-flow, the $e2eStack detection
ladder, and the v2 boundary. Read when the plan is UI-facing, alongside the
Step 4b / 5c — Two-level UI verification section below.
references/schemas.md — exact file formats to emit (excerpt of the SSOT,
docs/plans/02-schemas-and-contracts.md), including the waved nested layout (§14).
references/example-breakdown.md — a complete worked breakdown including an
inserted task, plus a negative example. Read when in doubt about output shape.
references/example-breakdown-waved.md — the worked WAVED breakdown (2 ordered stages →
nested <plan>/<wave>/… layout), the JIT staged-breakdown flow, and its closing report.
Read alongside Step 9 whenever the plan is authored as ordered stages.
Step 0 — Preconditions
Interactive Charter .charter.md input — check this FIRST (#390–393): if the input filename ends
.charter.md (or — only when ATTENDED — a .md whose column-0 ::: blocks you confirm are
Charter, not a Mermaid classDef or a fenced example), run Step 0c (discover charter-format → gate
the format-version marker → interpret ::: → fold a resolved :::question / surface an open one) BEFORE
the preconditions below, then continue with the interpreted content. A plain .md (no confirmed :::)
skips Step 0c (existing path, unchanged). Step 0c needs the Skill tool AND — for its prompts — an attended
human; the headless/autonomous path consumes Charter's flattened handoff markdown and never triggers it.
-
Resolve the plan path. If the file doesn't exist, stop and say so. The <plan-name>/ task
folder is generated beside the source .md by default; a repo that prefers one consolidated
footprint MAY instead keep plan folders under a .guardrails/ home (the same optional home
guardrails-patterns.md documents). Post-#266 the location no longer affects runnability, so this
is aesthetic — the default stays beside the .md (issue #275).
-
If <plan-name>/ already exists next to the plan, never silently clobber —
ask: merge (default, preserves human guardrail edits), overwrite, or abort. A human
may have edited that folder. On merge, follow the regeneration flow in Step 8.
-
Confirm the plan is reviewed (ask if unclear). Breaking down an unreviewed plan
multiplies its errors into N tasks.
-
Check guardrails --version works. If not on PATH, warn that Step 7
self-validation will be skipped and the output is unverified.
-
Identify the workspace (the repo the plan operates on — normally the folder
containing the plan) and what already exists there: test framework, linter,
build system. Guardrail selection depends on what's real. Record WHICH test
framework is present, not merely whether one is — set $testFramework by scanning
existing test projects for the framework dependency (.NET: a PackageReference to
xunit / NUnit / MSTest.TestFramework in any *.csproj; node: jest / vitest
/ mocha in package.json; python: pytest; etc.). If no test project exists, set
$testFramework = none — that is the trigger for the framework-selection rule in
Step 5, not a licence to pick one silently.
Also decide brownfield vs greenfield FOR THE TOUCHED AREA — it gates the Step 5
positive-baseline <plan>/preflights/ check (#181). Beyond "is there a test framework at all", record whether
the projects/modules the plan will MODIFY already have existing tests covering them:
- Brownfield = the plan modifies project(s)/module(s) that ALREADY have existing tests
in the touched area. Set
$baselineArea to the existing test project(s), each scoped by a
--filter that selects the CURRENTLY-GREEN existing tests of that area (e.g.
tests/Inventory.Tests filtered to the pre-existing tests — --filter "Category!=Stats" if a
later author-tests task will add a Stats category to that project). Never a whole-project
dotnet test in the preflight — that hits the #165/#176 compile-coupling trap (Step 5). Record ONE
entry per distinct touched test project (the baseline is deduped one-per-area in Step 5). This is
the trigger to EMIT the baseline <plan>/preflights/ check(s) in Step 5 — subject to the worth-it gate there.
- Greenfield = a new project, or no existing tests in the touched area. Set
$baselineArea = none. Step 5 SKIPS the baseline preflight (nothing to baseline) and the
Step 7 report states the reason. Do NOT emit a vacuous baseline that runs zero tests or
asserts "0 failed" over an empty set.
A plan can be brownfield in one area and greenfield in another (it extends an existing
project AND adds a new one); scope $baselineArea to the EXISTING-tests portion only, one entry
per touched test project.
-
Detect the stack from the workspace and load the matching stack file
(references/stacks/<stack>.md) BEFORE guardrail selection (Steps 4–6):
| Workspace signal (any match) | Stack | Stack file |
|---|
*.slnx · *.sln · *.csproj | dotnet | references/stacks/dotnet.md (ships) |
build.gradle · build.gradle.kts · pom.xml | jvm | (not authored yet) |
package.json | node | (not authored yet) |
go.mod | go | (not authored yet) |
pyproject.toml · requirements.txt | python | (not authored yet) |
- Ambiguous (mixed monorepo, multiple signals) or no stack file exists yet for
the detected stack → FALL BACK to the core catalogue and warn the user explicitly:
"stack detected but no stack file ships yet (or the workspace mixes stacks); I'll
use only the universal catalogue, so stack-specific guardrails (build-descriptor
registration, cross-module references, structural impl checks) may be incomplete —
review those especially." Never silently emit stack-agnostic guardrails as if complete.
- When exactly one stack is detected and its file exists, load it and use its idioms
wherever the catalogue points to the stack file (Steps 4–6).
- A
## Stack declared in guardrails-patterns.md (substep 7) overrides
auto-detection — a human declaring the stack resolves an otherwise-ambiguous
monorepo, so load that stack's file directly instead of falling back.
Future stacks: jvm / node / go / python — add as real projects on those stacks surface
gaps (issue #13's sequencing). The routing above is generic, so a new stacks/<stack>.md
is drop-in: author the file, and detection already routes to it.
-
Read the repo pattern file, if present (guardrails-patterns.md at the workspace
root or under .guardrails/). It is an OPTIONAL, human-authored topology file — the
project's CLAUDE.md-analogue for breakdowns — naming repo specifics no stack file can
infer: the stack, the build-descriptor path, the shared-abstraction project name + its
consumers, and project-layout notes. When present, its specifics OVERRIDE/augment the
stack file's generic guidance (use the real solution path, the real abstraction project
name in the pattern-2/3 guardrails). When absent, proceed with the stack file alone —
it is high-value but never required. Expected shape:
# guardrails-patterns.md (repo root or .guardrails/)
## Stack
dotnet
## Build descriptor
PoC/ConformedSources/WorksoftMigrator.slnx
## Shared abstractions project
MigrationAbstractions — consumed by WorksoftMigrator.Desktop and WorksoftMigrator.Cli
## Project layout notes
New UI projects live under PoC/ConformedSources/. Each new .csproj must be registered in
WorksoftMigrator.slnx and have a <ProjectReference> from its consumer before the solution
build guardrail is meaningful.
-
Decide FLAT vs WAVED (the layout fork) — set $waved (#254). Is the plan authored as
ordered STAGES, each building on the materialized artifacts of the prior stage? The tells:
explicit "Wave 0..N" / "Stage 1..N" / "Phase 1..N" headings whose later stages say "builds on
Stage N-1's output", reference real file paths / signatures a prior stage produces, or are
undesignable up front because their evidence points at artifacts that don't exist until an
upstream stage runs. If yes → $waved = true: the breakdown emits the nested layout (Step 9),
not the flat tasks/ layout. If the plan is a single stage / a plain feature (no staged milestones
whose downstream tasks depend on upstream materialization) → $waved = false: the flat layout
(Steps 1–8), unchanged. Do NOT wave a flat plan — fine-grained parallelism is a task DAG
inside ONE wave, not multiple waves (waves are the COARSE ordering for stages whose downstream
tasks can't be authored until the upstream is real; a wave barrier destroys cross-wave parallelism,
SSOT §14 C5). When $waved, Steps 1–8 still run — once per wave — but Step 9 governs the
layout, the wave gates, the wave-qualified identity, and the JIT staged-breakdown mode; read it
before proceeding.
Step 1 — Parse the plan into candidate work items
Read the whole plan. Extract numbered steps, deliverable-shaped headings, acceptance
criteria, "done when" language, and dependency words ("after", "requires", "once X
exists"). Build a scratch table:
| item | deliverable artifact(s) | completion evidence available in the plan | hinted deps |
Charter input ($charter, Step 0c): if the plan was a .charter.md, you already interpreted its
::: blocks and gated the format version in Step 0c — build this table from the interpreted content: a
resolved :::question's folded answer is a settled decision (an input/constraint, not a work item),
an open one was already asked (AskUserQuestion) or became an agent-needs-human task, and the
:::note/warn/comparison/diagram/diff/custom-html blocks ride along as context/rationale for the rows
they inform. Everything else on this page is unchanged.
Anything with no observable deliverable ("think about performance", "consider
edge cases") is flagged: it either merges into a neighboring task's guardrail or is
reported to the human as non-executable plan content. Never invent a task for it.
A user-facing UI outcome IS a deliverable — record the UI surface, not just the
backend that would serve it. When a plan describes something the user sees or
operates — "the user sees…", "a page that…", "served to the browser", "wizard
screen", "master/detail view", "tri-state tree", "next/back navigation", "renders…",
"a form/dashboard/grid" — that screen/page/component is a first-class deliverable in
its own right, NOT decoration on a backend route. The failure this guards against
(issue #66) is silent: UI language maps onto the nearest backend capability (the
route/handler/DTO that would feed the screen), that backend gets decomposed, and the
UI surface is dropped — the run goes fully green producing a JSON API with no
human-facing frontend. So for every UI-facing phrase, add a distinct row for the
UI artifact (wizard.html + its client JS/CSS, or the framework component) ALONGSIDE
any backend row that serves it — never collapse the two into one backend row. The
backend that serves a screen and the screen itself are two deliverables with two
different completion evidences; Step 4's UI-facing doctrine check and Step 5's
UI-implementation insertion act on these rows.
Step 2 — Size the tasks
A task is right-sized when ALL hold:
-
One verifiable outcome. One primary artifact/behavior a guardrail can check.
If describing the outcome needs "and", consider splitting.
-
Guardrail-boundary rule (load-bearing): split exactly where verification
changes character. "Implement parser + write its tests" splits because the
test-task's guardrails (the tests build + fail on stubs) differ from the
parser-task's (tests pass). Conversely "create the file AND register it in the
index" stays one task if a single guardrail checks both.
-
One-session rule: a competent agent finishes it in one focused session
(≈ ≤ 30–45 min of agent work).
-
Retry-cheapness: a failed guardrail re-runs the whole action. If a one-line
fix would redo an hour of work, the task is too coarse.
-
TDD default for code deliverables. When the primary deliverable is code (a
library, feature, service behavior, or algorithm), the guardrail-boundary rule (rule
2) almost always fires: a test-author task's "red" guardrail (build-passes +
tests-fail-on-stubs for a behavioral type, or tests-fail-on-current-code for a
data model — Step 5's stub-based TDD rule) and the implementation task's guardrail
(specific-tests-pass) are different in character. Default to splitting into two
consecutive tasks:
NN-author-tests-<feature> — writes tests encoding the behavior BEFORE it exists
NM-implement-<feature> — makes those tests pass without modifying them
Collapse to a single task only when (a) tests for this behavior already exist in
the repo, (b) the behavior is too simple to have meaningful unit tests, or (c) the
deliverable is a pure data model (an enum/record/value type with no behavioral stub
possible — the type declaration IS the implementation, so the TDD "red" has no stub-vs-real
distinction; Step 5's stub-based TDD rule) — state the reason explicitly in the task
description or breakdown report. When in doubt, split: the test-author task is cheap and
its anti-tautology guardrails (build-passes + tests-fail-on-stubs for a behavioral
type — Step 5) are the strongest anti-tautology check the skill has.
Over-size split-check — a CHECK WITH TEETH, not advice (#111)
The right-sizing rules above describe the target; this check ENFORCES it. Before emitting any
task, run it through the split-trigger. If ANY trigger fires, you MUST split the task and re-run the
triggers on each piece — do NOT emit the over-sized task and "note it." A milestone-sized chunk that
maps to one task thrashes at run time: every failed guardrail re-runs the whole oversized action, and
it is the single most likely needs-human in a run (the exact retry-cheapness anti-pattern).
Split-trigger — split when ANY holds:
- (a) Bundles multiple distinct deliverables. The description reads "do X and Y and Z"
with the conjuncts being separately-verifiable outcomes (add a gate and delete three classes
and re-baseline the suite). Each distinct deliverable is its own task. (One outcome that needs
"and" only to describe it — "create the file and register it in the index", checked by a single
guardrail — is NOT this trigger; that is rule 2's single-guardrail case.)
- (b) Wide blast radius. The task creates/deletes/renames many files, or re-baselines many test
references (a rough line: deleting ≥3 source files, or touching ≳10 files / test references in
one action). A wide-blast task fails the retry-cheapness rule by construction: a one-line guardrail
miss re-does the entire multi-file change. Split so each task's diff — and therefore its retry — is
bounded. Turn-budget lowers this threshold sharply (#378). For a task already near the
maxTurns
ceiling, the blast-radius threshold DROPS — flag at writeScope ≥ ~3–4 paths, not ≥10; near-max
maxTurns AND a multi-file surface is the exact thrash-and-timeout profile (the author's own max-budget
bump is an admission the task is turn-heavy). validate emits GR2042 (WARN) on precisely this
co-occurrence — treat that warning as a fired trigger, not noise to wave through.
- (c) Maps 1:1 to a design milestone. A plan milestone / phase / numbered section is NOT a task —
it is a bundle of deliverables. If a candidate task is "implement Milestone M4," decompose it into
the deliverables inside M4; never size a milestone 1:1 to a task.
- (d) Retry re-runs expensive work. Estimate what a single failed guardrail forces to redo. If a
retry re-runs an hour of refactoring (a multi-deletion, a 100+-ref re-baseline), the task is
mis-sized by definition. Split so each task's retry is cheap.
- (e) Fan-in-sink / composition-root wiring (#378). "Wire the components together" is NOT one
deliverable when it spans multiple collaborators or the composition root — each wire-up (factory
registration, scheduler call-site, CLI exit-code plumbing) is a separately-verifiable integration
point; "it's just wiring" is a rationalization that dodges the split (it reads as a single outcome
precisely because it is described as one). Treat a task that composes the outputs of ≥2 upstream
producers into a factory /
Program.cs / dispatch site as N tasks (one collaborator wiring each),
isolating the turn-expensive composition-root proof (drive the REAL factory, #120) to a thin sink.
This is the archetype the older triggers missed: it is not "milestone-sized," creates/deletes nothing,
and sits at half the ≳10-file line — yet each file is a distinct integration surface the retry re-runs.
Its structural tell is the co-occurrence in (b) — a near-max maxTurns plus a multi-file writeScope
plus a wide dependsOn fan-in — the same fingerprint validate flags GR2042. This trigger shares a
root with the passing-but-blind check (#382, Step 4 analysis): the sink is over-scoped because it
concentrates real-seam integration proof that should be distributed to each collaborator's own task.
Carry the plan's own feasibility signals into sizing (#111). When the plan's
feasibility / self-critique / risk section flags a milestone as heavy, over-packed, or
high-churn ("~147 test refs", "over-packed", "large blast radius", "risky to do in one pass"),
treat that as a fired trigger: the breakdown MUST split that milestone rather than size it 1:1.
This signal already exists in the plan — do not let it die between the plan's risk section and your
task sizing.
Every sized task declares its write surface (#389). Sizing bounds a task's blast radius; the
writeScope MAKES that bound explicit and machine-checked. Once a task is sized, it MUST carry a
writeScope — real paths for a writing task, or [] when it writes nothing to the repo (a
configure-a-database task, a verification/read-only check, a state-only task whose only output is a
GUARDRAILS_STATE_OUT fragment). Omitting it is a validation ERROR (GR2041) that Step 7's
guardrails validate will catch — see the writeScope schema quick-ref in Step 6.
Corrective action when a trigger fires: decompose the task into the smallest pieces that each
(i) carry one verifiable outcome, (ii) land in one session, and (iii) retry cheaply — scoping each
piece's test re-baseline to that piece. Worked split: a task bundling "add the git-required
validation gate + new error codes, delete CapturedFileStore + FileHashCapture +
RestoreAncestorCaptures + two validators, and re-baseline ~147 test refs" fires (a), (b), and (d).
Split it into e.g. (1) add the validation gate + error codes; (2) remove the two validators; (3)
delete the three capture classes + the retry-loop change — each with its test re-baseline scoped to
that piece, so each lands in a session and retries cheaply.
Heuristic: a typical feature plan yields 5–15 tasks. TDD splitting doubles code
tasks (each code item becomes two tasks); this does not count against the threshold.
Under 3 or over 25 tasks after applying TDD → re-examine, and tell the user why if
it stands. A count under the floor after splitting over-sized milestones is itself a signal
that a milestone was sized 1:1 — re-run the split-trigger before settling on a small task count.
Large/unbounded fan-out → scripted ETL, NOT an agent-per-item loop (#100)
The over-size split-trigger sizes by deliverable count and blast radius; this rule sizes by
iteration cardinality. When a task's deliverable is "process N items where N is unknown and
potentially large" — a web crawl/scrape, a bulk transform over an unknown-size glob, a mass API
fetch, a dataset ETL — the wrong model is an agent-iterated loop (one agent turn-budget covering
N fetch+convert+write cycles). Agent turns are the wrong unit for bulk work: a few hundred items blow
any reasonable turn budget, the action hits max-turns and is killed, and the retry hits the same wall
identically — a hard dead-end (action-failed → retries fail → needs-human) on a task that is
perfectly doable when structured as a script. Raising maxTurns does not fix it; it only moves the
wall.
Detection heuristic — flag during sizing when a task fans out over an external or unknown-size set:
a website / section / sitemap, a recursive glob, an API listing, "every page under…", "all files
matching…", "each record in…". The tell is cardinality the plan cannot bound at breakdown time
("8 expected" can turn out to be 409 actual). A retry-cheapness / one-session check on "could this be
hundreds of items?" trips the rule.
When it fires, structure the work as a scripted bulk operation — three moves:
- Scripted-ETL action (the volume happens off the turn budget). The agent authors and runs one
script that does the N-item work in a single execution (e.g. Playwright + HTML→markdown; a glob
walk + transform). The agent's turns go to writing, verifying, and running the script — NOT to
iterating items. This is a
script action, not a .prompt.md that loops. Guard it with the
ordinary script archetypes (file-exists on the output dir + command-exit-code / a count check), and
verify the recorded output, not a replay.
- Discover-size-first. When the set size is unknown, enumerate/count before committing to an
approach, so sizing and any curation are calibrated to reality. This is its own cheap probe
(enumerate the in-scope set, write the count to state or a manifest) and may be a separate upstream
task feeding the ETL task.
- Split bulk-capture from per-item derivation. Make the cheap, complete, scripted capture one
task (deterministic, fits a session — dump all N items locally), and any agent derivation/curation
a separate, bounded task over a selected subset — never "derive all N." "Crawl all 409 pages to
local markdown" (scripted capture) then "curate a high-value committed subset" (bounded agent
derivation) is the shape, not one agent told to "crawl and curate 409 pages."
The catalogue's scripted-ETL section holds the archetype detail and the decision-tree leaf
(references/guardrail-catalogue.md → "Bulk/unbounded fan-out"). Relation to siblings: this is
necessary but distinct from maxTurns budgeting (#94 — bulk fan-out does not scale with turns at all)
and from corpus-completeness guardrails (#99 — those verify the output; this structures the task so
it can be produced at all).
Step 3 — Determine the DAG (dependsOn)
Edge sources, in priority order:
- (a) artifact dependency — task B consumes a file/state key task A produces;
- (b) guardrail dependency — B's guardrail executes A's artifact (tests, scripts);
- (c) explicit plan ordering ("after", "requires").
Default to the sparsest correct DAG. Plan prose order alone is NOT an edge —
parallelism is free, false edges serialize the run. Record a one-line justification
per edge (in the task description or the breakdown report). Verify acyclicity.
(d) transitive COMPILATION dependency — a verified task compiles a test file that references
another task's type (#176). Edge source (a) covers a task that reads another's FILE; this covers
a task that compiles a file referencing another's TYPE. When a task B's verification runs
dotnet build / dotnet test (filtered or whole-suite), it compiles the entire test project —
including .cs test files authored by other tasks. If an ancestor test-author task A wrote a
test file that references a type produced by an implementation task C, then B's compilation
depends on C even though B never reads C's file directly — and if C is not already in B's
ancestor set, B's working tree lacks C's output and the compile fails on an error B cannot fix
(the type lives in C's writeScope, not B's). The trapped agent then redefines the missing type in
its OWN scope to make the compile pass, colliding with C's copy at the AI-merge → a duplicate-class
CS0101 (the plan-0009 #176/#175/#174 failure chain). Rule: when a task's verification compiles a
test project containing an ancestor test-author task's tests that reference types from another
implementation task, add that implementation task to the verifying task's dependsOn — so its output
is present in the working tree and the test project compiles. (Sparsest-DAG caveat still applies:
add the edge only when an ancestor test file actually references the other task's type; do not couple
to every implementation task defensively. The guardrails-review "Transitive compilation dependency"
probe flags the case you miss.)
Step 4 — Select guardrails (read references/guardrail-catalogue.md first)
Apply the decision tree per task, using BOTH layers loaded in Step 0: the universal
catalogue for the archetype, and the stack file (references/stacks/<stack>.md, plus
any guardrails-patterns.md topology) for the exact regex/command. Rules that are never
optional:
-
1–4 guardrails per task, cheapest-first filename order (01-exists,
02-builds, 03-tests, 04-review).
-
Every guardrail file opens with # catches: <the wrong implementation it catches>.
Can't write the sentence → delete the guardrail.
-
Every candidate prompt-judge passes the 4-question demotion gate or is demoted.
A judge is never a task's only guardrail.
-
Deterministic guardrails print ONE actionable failure line to stdout (it becomes
retry feedback). Write the failure branch as a multi-line if block — the
Write-Output reason and its exit 1 each on their own indented line; never collapse
the body onto the if line with ; (if (...) { Write-Output "..."; exit 1 }). That
reason line is what a human reviews and what the next attempt reads as feedback, so it
must stand on its own line and be easy to scan. Applies to every archetype, build /
exit-code checks included.
-
A tests-pass guardrail MUST re-emit the failure DETAIL at the END of stdout (#179).
The harness feeds back only the tail of a failed guardrail's stdout (last ~60 lines).
Default dotnet test prints each failure's assertion/exception text mid-run and ends with
only [FAIL] <name> + a count — so a bare dotnet test; if ($LASTEXITCODE -ne 0) {…} puts
only the test NAMES in the tail and the next attempt sees WHAT failed, not WHY (it then
retries blind — plan-0009 burned 12 attempts). For any guardrail that asserts tests PASS, use
the catalogue's capture → emit-full-log → re-emit-failure-lines-at-the-end pattern (catalogue
→ "Failure detail must reach the retry tail"; .NET regex in stacks/dotnet.md §4.2). The
INVERSE TDD-red checks (tests-fail-on-stubs, where a non-zero exit is success) do NOT
re-emit. This is in addition to — not a replacement for — the single actionable reason line.
-
"All tests pass" appears ONLY in the terminal <plan>/guardrails/ folder (the terminal gate).
-
A full build / whole-suite test guardrail in the terminal <plan>/guardrails/ folder is a
terminal postcondition → keep it LOCAL (#165). Do NOT mark 01-solution-builds /
02-all-tests-pass (the whole-repo build and full test suite) scope: "integration".
Omit the scope key (default "local") so they run ONLY at the terminal gate — once, on the
merged plan-branch HEAD, AFTER every upstream task has merged.
That is the correct and ONLY moment a full build / full suite should run. A scope: "integration" guardrail re-runs at every union point (every fan-in / non-FF
integration), on partial merges where downstream tasks have NOT run yet. In a TDD plan a
Wave-2 union contains test files that reference types implemented in Wave 3+, so a whole
build / whole suite FAILS at that intermediate union and the harness rolls the wave back —
even though every per-task guardrail passed. That is exactly the #125
terminal-postcondition-at-integration-scope anti-pattern (decision test: "would this
pass on a partial merge with a downstream task unsettled?" — a full build/suite would
NOT). Marking it integration-scoped red-halts a correct run. (Catalogue → "A
scope:"integration" guardrail MUST be UNION-SAFE".)
-
The terminal <plan>/guardrails/ folder MUST still carry ≥1 scope: "integration" UNION-SAFE
invariant guardrail (GR2028, the re-homed GR2018 content teeth). GR2028 requires the terminal
folder to carry at least one real integration-set re-run; when that re-run is a union invariant it is
the conditional union invariant,
NOT the build/suite. It asserts something true of any valid intermediate union — the
GR2028-crediting core is "every produced file present is non-empty and conflict-marker-free";
"every contribution PRESENT in the union is intact" is the additive contribution-present tightening
layered on top (not GR2028-satisfying on its own, #343 — see below) — so it passes trivially BEFORE a
contributing task has run. Its content checks MUST be UNION-SAFE = CONDITIONAL: IF contribution X is present, verify it's real, never REQUIRE X present. The conditional pattern (the parallel-hello
template; examples/parallel-hello/.../01-whole-repo-greeting.ps1):
# Union-safe: gate on the artifact being present, then verify it — pass trivially when absent.
$outDir = Join-Path $ws 'out'
if (-not (Test-Path $outDir)) { exit 0 } # nothing produced at this union yet — fine
foreach ($f in Get-ChildItem -Path $outDir -Filter '*.txt' -File) {
$content = Get-Content -Raw -Path $f.FullName
if ($content -match '(?m)^<<<<<<<' -or $content -match '(?m)^>>>>>>>') { # line-anchored ours/theirs — false-positive-free (#187)
Write-Output ("out/" + $f.Name + " contains git conflict markers — the union did not cleanly integrate")
exit 1
}
}
exit 0
Line-anchor the marker regex (#187). Match (?m)^<<<<<<< / (?m)^>>>>>>> — a real conflict
writes both at column 0 — and DROP a bare ======= check: the unanchored form false-fires on a
==== banner / Markdown setext underline / ASCII table rule and red-halts a correct run.
A contribution-present check uses the same conditional shape — if ($content -match "<token>") { if ($content -notmatch '<real-construct>') { $failures += "<token> present only as comment — construct missing" } } — so it false-passes (correctly) before the
contributing task has run, and tightens once that task's hunk lands. But a
contribution-present check does NOT satisfy GR2028 on its own — it is ADDITIVE, layered on top
of the union-soundness proof, never the sole content of the terminal gate (#343). GR2028 is
credited only by a conflict-marker-freedom check (the line-anchored <<<<<<</>>>>>>> scan
above) or a recognized whole-repo build/test/suite invocation — these are ungameable,
whereas a content grep is vacuous exactly where the terminal gate matters most: the union-safe
CONDITIONAL form can never FAIL when a merge DROPPED a contribution entirely (the gate goes
false → pass), so it certifies nothing about union soundness by itself. The
overlapping-writeScope union-guardrail (next bullet, #132) satisfies GR2028 because it ALSO
carries the conflict-marker-freedom check — its contribution-present checks are the additive
tightening layered on top, not the GR2028-satisfying content.
-
Overlapping writeScopes → author a scope:"integration" union-guardrail on the shared file
(#132). When ≥2 tasks have OVERLAPPING writeScopes on a shared file (colliding siblings the
AI-merge unions), emit one scope:"integration" guardrail on the integration / fan-in task
asserting the shared file's UNION invariant — the merged file still holds every sibling's
contribution (each distinctive marker present, conflict-marker-free), union-safe (#125), as the
texttools showcase does with components-union-verified. The union re-verify is integration-set-only
(SSOT §4.3), so a dropped hunk on the shared file is re-verified at the union ONLY by an
integration-scoped guardrail. Prefer disjoint scopes (a collision is usually a plan-shape smell);
emit the union-guardrail when the overlap is genuine. (Catalogue → overlapping-writeScope
union-guardrail.)
- Shared CODE file both tasks define into → add a DUPLICATE-DEFINITION check (#175). When the
overlapping-
writeScope file is a CODE file and both colliding tasks could ADD a
type/member DEFINITION to it (a class/record/interface/enum/method), the union-guardrail's
conflict-marker + contribution-present checks are NOT enough: a 3-way / AI-merge of two branches
that each appended the same new definition to different regions keeps both copies with
no textual conflict marker, so the merged file holds a duplicate definition (CS0101) that
only the build catches — the exact #175 trap that red-halted plan-0009's terminal gate. Add a
duplicate-definition count check to the same scope:"integration" union-guardrail: count
occurrences of each definition both siblings could add and fail when >1, naming the AI-merge
duplicate. Keep it union-safe/conditional (#165) — place it inside the existing file-present
gate so it passes trivially at a union where the file hasn't landed. The .NET realization is
[regex]::Matches($content,'class\s+<Name>').Count -gt 1 (stacks/dotnet.md §19). The harness can
only attribute this collision at the gate (name the colliding writeScope pairs, SSOT §3.3); the
duplicate-definition check is the authoring-side PREVENTION. (Catalogue → overlapping-writeScope
union-guardrail, duplicate-definition sub-check.)
- On removing a dependency edge, re-evaluate the union guardrail's expected contributions (#159).
When a regeneration (Step 8) or a hand-edit removes a dependency edge (task B no longer
dependsOn task A — e.g. a mode deferred, a producer demoted to a disconnected leaf), re-examine
every scope:"integration" union guardrail on the fan-in task. If any of them still checks for
a contribution token that only task A could produce and A is no longer in the fan-in task's
ancestor set (no directed path A → fan-in), the guardrail has gone stale: it now implicitly
requires a disconnected task to stay in the plan, and if A is later removed the integration gate
fails spuriously with a confusing "shared file is missing <token>" — a stale-guardrail bug, not
a merge failure. Resolve it one of two ways: (a) add an alternative DAG path from A to the
fan-in task (make the dependency the guardrail relies on explicit), or (b) remove the now-stale
contribution check for A's token from the union guardrail. This is the authoring-side complement to
the guardrails-review "Union guardrail ancestor staleness" probe (#159).
-
Two-scope preflights/guardrails — the four-folder model REPLACES the integrationGate: true
task + no-op ROOT/END scaffolding (deliverable 9, SSOT §1/§3.3). On a harness version whose
loader understands the four folders, do NOT emit an integrationGate: true sink task — a plan
still declaring one gets a hard validation error (GR2029), no coexistence window. Emit these
four first-class folders instead:
<plan>/preflights/ — the plan-root "Full Flight Checks", a sibling of tasks/,
guardrails.json, state/. Evaluated ONCE, BEFORE the Scheduler builds waves, against the
starting repo. This is where the #181 positive baseline (REFRAMED, not replaced) now
lives: instead of a no-op ROOT task + --filter-scoped guardrail, emit a positive check
file in this folder (e.g. 01-all-repo-tests-green) asserting the currently-green
precondition. Also the home for a negative assert-absent baseline (a one-shot,
plan-level-only check that a not-yet-introduced artifact is genuinely absent at the start) —
this cross-references the existing tests-fail-on-current-code/tests-fail-on-stubs
anti-tautology archetype rather than forking a new one. Remove the no-op ROOT/END task
scaffolding and its #174/#182 short-circuit dependence from the baseline story (the
short-circuit remains a general §7 rule for any REAL task that no-ops elsewhere, untouched —
it simply no longer participates in the baseline/preflight story), and remove any simulated
"precondition" scope value (no third scope value exists under this model — only "local"
(default) and "integration").
<plan>/guardrails/ — the plan-root "Terminal Gate", also a plan-root sibling. Evaluated
ONCE, at run end, on the merged plan-branch HEAD. The re-homed GR2018 authoring rule: a
multi-leaf/fan-in plan's <plan>/guardrails/ folder MUST carry ≥1 real integration-set
re-run — a genuine whole-repo build/test/suite invocation, or a union invariant — NOT a
tautological exit 0 file; content teeth survive the move from task to folder
(validate enforces this as GR2028 on a multi-leaf/fan-in plan). scope: "integration"
itself is unchanged — it remains the per-union tag driving the §4.3 per-union re-verify;
only the terminal-sink TASK kind was retired.
tasks/<id>/preflights/ — task-level, a sibling of the existing tasks/<id>/guardrails/.
JIT dependency-delivery: evaluated in the consumer's own segment worktree at taskBase,
BEFORE its attempt loop. Emit one whenever a dependsOn edge delivers a type/route/symbol/
artifact a downstream task needs inside its own segment — confirming the producer's
contribution actually landed before the attempt loop spends a turn building against
possibly-absent bytes. Polarity here is positive-monotone-safe (never negative — a
task-level check runs per-attempt against a segment that only grows, so a negative
"not yet present" assertion would flip false as soon as an unrelated file lands).
tasks/<id>/guardrails/ — the existing per-task postcondition folder, unchanged.
All four folders share one guardrail-file parser/grammar with the existing
tasks/<id>/guardrails/ shape (NN-name.ps1/.sh/.py + optional .json sidecar, or
NN-name.prompt.md; catches: comment required; ordinal sort) — they differ only in WHERE
they live and WHEN they run.
Superseded — the rule below describes the RETIRED integrationGate: true task mechanism
(now a hard validation error, GR2029). It applies only when authoring for a harness version
that predates the four-folder loader, or a plan's own named, documented bootstrap exemption —
never silently. For a plan targeting a current harness, use the <plan>/guardrails/ folder
above instead.
A terminal integration task must declare integrationGate: true in its task.json — it
marks the terminal whole-repo integration gate, the final soundness boundary run once on
the fully merged plan-branch HEAD (SSOT §3.3, pre-four-folder). Validation enforced this: a
plan with ≥2 leaf tasks or any fan-in had to declare exactly one integrationGate: true
sink (GR2017, retired), and that sink had to carry at least one scope: "integration"
guardrail (GR2018, re-homed onto the folder above) — an empty gate verified nothing.
Route through these doctrine checks every task (the decision tree's newer leaves):
-
State output — does this task's action write a state key (to GUARDRAILS_STATE_OUT)
that a downstream task reads (via GUARDRAILS_STATE_IN)? Add the fragment-key-present
guardrail (catalogue → state-output leaf): read GUARDRAILS_STATE_FRAGMENT, parse JSON,
assert the key non-null + non-empty (+ allowed-set if a downstream task branches on it).
A task's action may write state ONLY under its own task FOLDER NAME as the single top-level
key — single-writer-per-key is enforced (SSOT §6.2). The key is the directory name the
task.json lives in (e.g. 04-author-tests-tcapi-local), NOT the task's stableId (an
internal regeneration token — the harness rejects a fragment keyed by the stableId as a
foreign/unowned key). Writing under another task's folder name or any shared key likewise
rejects the fragment and fails the attempt every retry. The generated prompt must state this
rule with a concrete { "<folder-name>": { … } } example (Step 6 authoring rule).
-
Build-descriptor registration — does the task add a module/project to a build
descriptor (a .csproj to a .slnx)? Add the stack file's registration guardrail on the
DESCRIPTOR, not just the new file (stacks/dotnet.md §1). A descriptor build passes with
an unregistered project — file-exists + build-passes do NOT cover this.
-
Cross-module reference — does this task create an abstraction a later task must
consume? Add the stack file's reference-chain guardrail on the CONSUMER's project file
(stacks/dotnet.md §2). Builds pass independently, so without this an agent can define a
local copy of the interface and pass.
-
Composition-root wiring (#120 — the recurring lesson) — does the plan add a component
that must be CONSTRUCTED and INJECTED at a production composition root or entry point to do
anything (an IFoo + FooImpl pair injected into a factory / Program.cs / DI registration /
dispatch site / RunCommand)? The per-component tasks author-test + implement FooImpl against
an injected constructor seam — each green — and the terminal whole-suite build + test passes,
yet nothing constructs FooImpl and hands it to the production assembler, so the real entry
point never takes the new branch and the feature is inert (reachable only from xUnit, which
injects the seam itself). This is the highest-impact false-green the skill emits — it recurred 3×
in one plan (engine, AI-merge, triage — all built, all dead from the CLI at the SchedulerFactory
composition root). Two artifacts close it, generated in Step 5: an explicit wiring task (a
named deliverable: construct FooImpl and inject it into the assembler, with a DAG sink depending
on it) and a composition-root guardrail asserting the component is ACTUALLY wired in
production — drive the REAL assembler and assert observable output the wired-only feature produces
(strongest), or reflect on the constructed object for the non-null collaborator WITH a contrast
case (the Factory_Wires* shape; catalogue → composition-root section, stacks/dotnet.md §10).
The guardrail MUST NOT inject the seam itself, and the terminal whole-suite gate does NOT cover
this (it is necessary but not sufficient). The signals (any one):
- the plan introduces an
IFoo + FooImpl pair (heuristic: every such pair needs a "wire
FooImpl into the composition root" deliverable);
- the component is reachable only via a constructor/DI seam the unit tests inject themselves;
- the plan names a factory /
Program.cs / Startup / DI registration / dispatch site /
RunCommand that must construct, branch on, or inject the new component;
- the feature activates only under a mode/flag (e.g.
maxParallelism > 1) the production dispatch
must honour — "machinery reachable only from xUnit" is the tell.
(This is a sibling of the executable-entry-point-wiring check below but at the assembly layer:
that one greps Program.cs + smoke-tests a route for a server serving over a port; this one
asserts a factory/container constructs and injects an internal collaborator. A plan can need
both — wire the entry point to the launcher AND wire a collaborator into the factory.)
-
Faked-seam ⇒ paired real-seam proof (#382 — passing but blind) — does an author-tests-* task
fake an in-process seam the real run drives (an IPromptRunner, the executor, the scheduler, a
factory) via DI? A unit test that injects a fake of the very seam the production path exercises can go
GREEN over a component that is broken through the real composition root — a green light over a
broken wire. Where #120 asks "is the component wired at all?", this asks "is the component proven
through the seam the run actually drives, or only against a fake of it?". Two authoring moves close it:
- The implement task carries a real-seam contract test that drives the ACTUAL seam. A fake
CLI / process / subprocess is fine (the #120 wiring test already fakes the process boundary),
but a fake of the in-process seam itself is NOT — the test must exercise the real
IPromptRunner / executor / scheduler / factory the run will use.
- Distribute the composition-root proof. Prove each component through the real factory AT the
task that builds it, where feasible, so an integration bug surfaces in-scope and early. Keep only
a thin join-check in the terminal
<plan>/guardrails/ gate over already-integration-proven
parts. Do NOT concentrate all real-path proof in one over-scoped end-of-wave sink — that sink IS
the #378 fingerprint (over-scoped because it concentrates deferred integration risk, and it
cannot fix a cross-file bug it finds), and the two issues share one root. (Catalogue →
"drive-the-real-seam"; stacks/dotnet.md §10e.)
-
Dispatch / factory pairing (#158 — is the RIGHT impl wired to the RIGHT mode?) — does this task
dispatch from an enum / discriminated value to one of ≥2 concrete implementations
(ImportMode.TcApiLocal → new TcApiLocalImporter(), a switch/if selecting a handler per mode)?
The build passes with the pairings swapped (either concrete type satisfies the interface in either
branch), and if the dispatch tests inject a substituted fake (RecordingImporter / FakeHandler
via DI) they assert only that an importer was called, never which concrete type — so an inverted
wiring (Mode B → the wrong importer) ships fully green. A bare keyword check that all enum values AND
all type names appear somewhere in the file does NOT catch it (all are present regardless of
pairing). Add one proximity check per pairing (catalogue → "Dispatch / factory wiring";
stacks/dotnet.md §10d): assert <EnumValue> sits within a bounded window ([\s\S]{0,300},
multiline-dotall, both orders) of <ConcreteType> in the dispatch file, scoped to that one file.
Decision gate: if the dispatch tests already assert the concrete TYPE NAME
(Assert.IsType<TcApiLocalImporter> on the resolved object), the test catches the swap — OMIT the
proximity check and say so in the covering guardrail's # catches: comment. Distinct from #120
composition-root wiring (which asks whether the impl is constructed/injected at all); this asks
whether each mode got the right one. Fire only when both hold: ≥2 concrete impls selected by an
enum, AND the dispatch tests use seam-injection (not type assertions).
-
Executable entry-point wiring — does the plan describe a server or CLI executable
outcome (signals below)? Component tasks (scaffold, handler, routes) each compile and
unit-test green, and the terminal whole-solution build passes — yet nothing wires the
entry point to the handler, so the binary builds and serves nothing. Unit tests cannot
catch a missing new Launcher().StartAsync(). Two artifacts close this, generated in
Step 5: a wiring task guarded by a static grep that the entry point references the
launcher (catalogue → entry-point-wiring; stacks/dotnet.md §7), and after it a live
smoke-test task that actually starts the binary, hits a route, and asserts a response
(archetype #7 port/endpoint-answers; the start/poll/assert/teardown script in
stacks/dotnet.md §8). The signals (any one):
- plan phrases: "CLI entrypoint", "starts a server", "serves … to the browser",
"loopback HTTP", "prints a URL", "listens on", "health endpoint";
- a
.csproj using Microsoft.NET.Sdk.Web or declaring <OutputType>Exe</OutputType>;
- an explicit smoke-test statement in the plan (see Step 5's authoring note).
This catches "the exe does what the plan says" vs merely "the code compiles" — the one
gap a green build and passing unit tests leave open. (Scope: starting-and-serving ONLY;
whether the described UI was actually built and is served is the next doctrine check —
the two compose: this one proves the exe serves something, the UI-facing check proves
the something is the UI the plan described.)
-
UI-facing deliverable — does the plan describe a user-facing screen/page/visual
component served to the browser (the Step 1 UI signals: "the user sees…", "a page
that…", "served to the browser", "wizard screen", "master/detail view", "tri-state
tree", "renders…", a form/dashboard/grid)? The component tasks decompose to backend
routes/handlers/DTOs and unit tests — each green — and (with the entry-point-wiring check
above) the binary even starts and serves. Yet no task built the UI itself: there is no
HTML page, stylesheet, client JS, or wwwroot, and the served root returns JSON or a
placeholder. A green build + passing unit tests + a 200 from / cannot catch a missing
frontend — the route answers, it just answers with no UI. Two artifacts close this,
generated in Step 5: a UI-implementation task per described screen (produces the
HTML/JS/CSS or framework component that renders it and binds to the backend contract) and
a pair of UI-presence guardrails — (a) a static asset-exists check that the page/asset
file is present (catalogue → UI-presence; stacks/dotnet.md §9), and (b) a served-markup
assertion that EXTENDS the §8 smoke-test (the same start/poll/teardown lifecycle, with an
added assertion that the response body contains a known UI element/string from the page —
not merely HTTP 200). Both are deterministic (asset grep; served-markup contains a known
string) — never a prompt-judge "does this look like a good UI"; visual quality is out of
scope, presence and wiring of the described UI is the deliverable. The exit-criteria
self-review in Step 7 is the backstop: a plan promising a frontend that decomposed to zero
UI tasks fails its own review.
-
Positive-effect / non-hollow output assertion (#73) — does this task's action claim a
non-empty quantity of output (a "how many items were processed" result: migration
moved-count, items written, rows produced, entities created)? Typically the terminal/
integration e2e task. A keyword-presence regex on the assertion
(Assert.*\([^)]*(Moved|Written|Count|Entities)), a bare Assert.NotNull(...), or a
non-error exit 0 is hollow — it passes on Assert.Equal(0, writer.Count), certifying
a no-op (a migration that moved zero entities goes green). Emit the positivity check
instead: require a strictly positive value
((>\s*0|>=\s*1|NotEmpty\s*\(|True\s*\([^)]*Count\s*>\s*0)), or better, read the
runner-recorded count / state key and assert > 0. Catalogue → positive-effect / non-hollow
assertion.
-
Negative assertion — an EXCLUDED scenario must be verified ABSENT (#176) — does this task's
action prompt explicitly exclude a scenario/keyword the deliverable must NOT contain ("Mode C /
CommanderRest is wizard-blocked — do NOT include it in the dispatch tests"; "the importer must NOT
call X directly")? The positive covers-key-behaviors guardrail only checks that the kept
scenarios are PRESENT — it says nothing about the excluded one, so the agent can include the removed
scenario undetected (which is how the excluded CommanderRest slipped into plan-0009's dispatch
tests and fed the #176 compile trap). Emit a negative-assertion guardrail — a fail-on-present
check that the excluded keyword is ABSENT: if ($content -match "CommanderRest") { Write-Output "…"; exit 1 } (scoped to the one file the task owns). It is a legitimate, deterministic archetype, the
mirror of covers-key-behaviors; pair it with the positive coverage check (catalogue → negative
assertion; stacks/dotnet.md §20). Note guardrails validate's GR2026 stays silent on this
guardrail's keyword — correctly, post-#177: GR2026 flags only POSITIVE require-present coverage tokens
(SSOT §4.4), so a fail-on-present keyword intentionally absent from the prompt is NOT a stale-coverage
warning. Do not omit or weaken the negative assertion to silence a (now non-existent) GR2026 warning.
-
A cross-cutting-output task OWNS the re-baseline of every golden it feeds (#193) — the runtime
mirror of the transitive-compilation edge (Step 3d, #176). When a task changes a cross-cutting
output shape — a renderer, a hash, a serializer, a formatter, a message/wire schema, any code
whose bytes flow into a pinned literal / golden file / snapshot / approved output that an
EXISTING test asserts against — that task's tests-pass guardrail must not be allowed to sweep in a
pre-existing golden the task cannot own. Two coupled authoring moves:
- Scope the
tests-pass --filter to THIS task's own tests (a class-name / trait filter, not
a broad FullyQualifiedName~<substring> that also matches pre-existing golden/snapshot tests) —
the same "filter to THIS task's tests" rule as archetype #4, sharpened: a broad substring filter
that pulls in a pre-existing golden test whose fixture this task's change invalidates traps the
task on a test it can't edit (its writeScope excludes the fixture → write-scope check red-halts
the fix → needsHuman loop).
- If the change genuinely re-bakes a shared golden, OWN the re-baseline. Widen this task's
writeScope to include the affected golden fixture(s) + their pinned test, so regenerating them
is in-scope; OR, when the re-baseline is large / distinct enough to be its own deliverable (Step
2 over-size trigger — a 100+-golden re-bake), insert a dedicated re-baseline task (an ancestor
this task dependsOn) that owns and regenerates every golden the cross-cutting change feeds. Do
NOT leave a golden orphaned — owned by no task's writeScope yet asserted by a test the change
breaks. (The guardrails-review "Orphaned golden swept in by a broad tests-pass --filter"
probe, §2, flags the case you miss; catalogue → orphaned-golden / broad-filter trap.)
-
Structural impl / keyword match — any "implements/extends/declares" check uses the
stack file's declaration regex (stacks/dotnet.md §3), never a bare type-name grep. A
property-declaration check must be accessor-order-insensitive (#112) — key on the
declaration up to the brace (public\s+TYPE\s+NAME\s*\{), never a fixed leading \{\s*get,
which false-passes on { init; get; } (catalogue → structural-vs-keyword; stacks/dotnet.md §3).
-
Grep scope — every file-content guardrail is scoped to the one file this task owns
(catalogue → grep-scope contamination anti-pattern; .NET traps in stacks/dotnet.md §5).
-
Test-author needs a production testability seam (#84) — while routing a test-author task,
check each behavior: does expressing it as a test that can eventually PASS require a
production-code injection seam that does not exist yet (a DI constructor overload, a factory
delegate, an injectable interface, a fixture source)? The tell: the behavior injects a fake/double
(RecordingX, FakeX, InMemoryX, a fixture source) into a type currently constructed only via a
production constructor with no injection point. If yes, insert an upstream production-seam task
(Step 5's #84 bullet) the test-author task dependsOn — do NOT let the test-author task invent the
seam or rely on its needsHuman escape hatch. Distinct from the compile-coupled-DTO case (where the
missing symbol is a type the test constructs) and from composition-root wiring #120 (which injects
the real impl in production); the seam only opens the injection point so tests can supply a double.
-
Positive baseline (preflight) — a BROWNFIELD plan needs a green START before the DAG runs (#181).
This is the general positive-baseline / preflight archetype: a plan-level Full Flight Check that
asserts a positive precondition ALREADY holding on the starting state ("never build on red"). Under the
four-folder model it is a positive check FILE in the plan-root <plan>/preflights/ folder (a
sibling of tasks/, evaluated ONCE before the DAG against the starting repo) — NOT a no-op ROOT task.
The existing-area-tests-green baseline is the canonical worked instance and the ONLY one the skill
emits today; the same shape extends to other positive baselines (build-green, endpoint-up) by the same
preflight-file pattern, none emitted yet. Is this a brownfield plan (Step 0 set $baselineArea ≠ none —
it modifies project(s) that already have existing tests)? Before any inserted author-tests task adds
its intentionally-FAILING new tests, and before any implementation task runs, the EXISTING unit tests in
the touched area must pass on the CURRENT code. Without a green start, a work task's tests-pass
guardrail can fail from PRE-EXISTING breakage (misattributed to the task → wasted retries → late
needsHuman), and a new test's "red" is ambiguous (red-because-missing vs red-because-already-broken).
When it fires (brownfield AND the worth-it gate passes), emit the positive preflight check in Step 5
(<plan>/preflights/01-baseline-<area>-tests-green.ps1), one per touched area.
- Scope via
--filter to the CURRENTLY-GREEN existing tests of the touched area — NEVER the whole
suite/project. Load-bearing: a whole-project dotnet test in the preflight hits the #165/#176
compile-coupling trap (a mid-TDD project does not compile — its test project references types
later implementation tasks have not produced yet), manufacturing a FALSE RED no work task can fix.
The rule: the baseline targets the existing, currently-passing tests of the touched area ONLY.
- One baseline per AREA, deduped — one preflight file per distinct touched test project, each scoped
to its area, NOT a single global whole-suite preflight.
- The worth-it gate (a check with teeth) — emit ONLY when ALL hold: the target pre-exists; the plan
MODIFIES not creates it; the check is deterministic + cheap (a bounded, filtered command — a
filtered
dotnet test is fine; no live-service boot or network poll, which flakes); strictly narrower
than the terminal <plan>/guardrails/ gate; ≥2 work tasks build on the area; deduped per area.
Under-fire when unsure — a missed baseline is just the status quo, a false baseline halts a
correct plan before the DAG.
- Greenfield (
$baselineArea = none) or worth-it gate fails → SKIP it and state why in the report
(nothing to baseline). Distinct from the terminal gate: the baseline preflight is a green START on
EXISTING tests, evaluated once BEFORE the DAG; the terminal <plan>/guardrails/ folder is a green END
on the merged HEAD — complementary, state both. A RED baseline preflight halts the run before the DAG
(the general Full-Flight-Check semantics), and #179 (re-emit form) makes its WHY reach the feedback.
The negative "not yet present" baseline is NOT a new archetype — it already IS
tests-fail-on-current-code/tests-fail-on-stubs (cross-reference, don't fork), and when emitted at
plan level it is likewise a <plan>/preflights/ check (assert-absent, plan-level-only). (Catalogue →
"Baseline-green / start-from-green (preflight)"; the .NET realization is stacks/dotnet.md §21.)
Step 5 — Insert guardrail-enabling tasks (the generative step)
For every selected guardrail whose precondition doesn't exist yet, generate the
upstream task that creates it:
-
Brownfield plan (Step 0 set $baselineArea ≠ none) → emit a positive-baseline (preflight) CHECK
in <plan>/preflights/ per touched area (#181). "Never build on red": establish that the EXISTING
tests in the touched area pass on the CURRENT code BEFORE the DAG runs. This is the general
positive-baseline/preflight shape (a plan-level Full Flight Check evaluated once, before the DAG,
against the starting repo — one cheap deterministic positive-precondition guardrail); the
existing-area-tests-green instance below is the ONLY one emitted today, but the shape extends to other
positive baselines (build-green, endpoint-up) unchanged. Emit
<plan>/preflights/01-baseline-<area>-tests-green.ps1 (use the real area name, e.g.
01-baseline-inventory-tests-green; the plan-level <plan>/preflights/ folder is a sibling of
tasks/). This REPLACES the retired no-op ROOT task model — do NOT emit a 00-baseline-* task with
a dependsOn: [] no-op action; the preflight folder runs before the DAG with no task, no edges:
- First, run the worth-it gate (a check with teeth) — emit ONLY when ALL hold: the target
pre-exists; the plan MODIFIES not creates it; the check is deterministic + cheap (a bounded,
filtered command — a filtered
dotnet test is fine; no live-service boot or network poll); strictly
narrower than the terminal <plan>/guardrails/ gate; ≥2 work tasks build on the area; deduped per
area. Under-fire when unsure — a missed baseline is just the status quo (work tasks attribute
their own failures the slow way); a false baseline halts a correct plan before the DAG. If the gate
fails, SKIP and say why in the report.
- It is a guardrail-shaped preflight FILE, not a task — a
.ps1/.sh/.py file in
<plan>/preflights/ (same parser as tasks/<id>/guardrails/), opening with # catches:, that runs
the check and exits 0/non-zero. There is no action to make a no-op of — the preflight folder is
evaluated by the pre-DAG phase directly, so the retired "TRUE no-op exit 0 action" scaffolding and
its #174/#182 short-circuit dependence are GONE from the baseline story (a RED preflight simply halts
the run before scheduling any task). The file IS the verification.
- The check: the EXISTING area tests PASS on the current code, scoped via
--filter. Run the
EXISTING unit test project(s) covering the projects the plan modifies — $baselineArea from Step 0
— and assert they ALL PASS (exit 0). Scope to the CURRENTLY-GREEN existing tests of the touched
area via --filter — NEVER the whole suite/project. This is load-bearing: a whole-project
dotnet test in the preflight hits the #165/#176 compile-coupling trap — a mid-TDD project does
not compile (its test project references types later implementation tasks have not produced yet), so
a whole-project test manufactures a FALSE RED no work task can fix, dead-ending the run. The rule:
target the existing, currently-passing tests of the touched area ONLY. Keep it bounded — a too-wide
scope also re-imports unrelated flakiness into the pre-DAG phase.
- One baseline per AREA, deduped. Emit one preflight file per distinct touched test project, each
scoped to its area — NOT a single global whole-repo preflight. Two independent touched test projects
→ two area preflight files; one area → one. Never collapse N areas into one whole-suite preflight,
never two for the same area.
- It runs BEFORE the DAG — no
dependsOn, no edges. The preflight folder is evaluated once against
the starting repo before the Scheduler builds any wave, so every task in the plan is implicitly gated
on it — you do NOT wire work tasks to it (the retired model made every area work task
dependsOn a no-op root; that scaffolding is gone). Acyclicity (Step 3) is unaffected — a preflight
file is not a DAG node.
- Scope = EXISTING tests ONLY (the load-bearing constraint). The preflight asserts the PRE-PLAN
tests pass. It runs on the STARTING workspace state, BEFORE any inserted
author-tests task adds its
intentionally-FAILING new tests. So it must target the existing test project(s)/area and must NOT
accidentally run (and fail on) the about-to-be-authored red tests. The pre-DAG phase evaluates it
against the starting bytes (no new tests yet), which makes this natural; if $baselineArea is a whole
test project that a later author-tests task will ALSO add failing tests into, prefer a --filter
(or category) that selects only the pre-existing tests, so the baseline can never go red on tests that
don't exist yet.
- The PASS check is a tests-pass archetype → it MUST use the #179 failure-detail-re-emit
pattern (capture → emit full log → re-emit failure-signal lines at the END), so a RED baseline's
WHY (the failing assertion/exception) reaches the halt feedback, not just
[FAIL] <name>. The
.NET realization is stacks/dotnet.md §21 (it reuses §4.2's re-emit form).
- A RED baseline preflight halts the run BEFORE the DAG. A failing pre-DAG preflight stops the run
before any task is scheduled (the general Full-Flight-Check semantics) — no retry budget is burned on
a no-op, because there is no task. Make the check's final actionable line say so plainly, e.g. "the
area's existing tests are already failing on the starting code — fix the pre-existing breakage before
this plan builds on it" — that fast, actionable halt IS the correct outcome.
- The negative "not yet present" baseline is NOT a new archetype — cross-reference, don't fork. The
mirror ("a precondition that should be ABSENT is genuinely absent at the start") already IS
tests-fail-on-current-code/tests-fail-on-stubs (and the #120 wired/not-wired contrast); reach for
those. When emitted at plan level it is likewise a <plan>/preflights/ check (an assert-absent,
plan-level-only one-shot) — do not author a parallel "negative preflight" task.
- Greenfield → DO NOT emit it. When
$baselineArea = none (a new project / no existing tests in
the touched area) there is nothing to baseline. SKIP the preflight and state the reason in the Step 7
report. A vacuous baseline (running zero tests, or dotnet test over a project with no tests, which
trivially "passes") is worse than none — it certifies nothing while looking like a gate.
-
Code task and tests do not yet exist → insert NN-author-tests-<feature> BEFORE the
implementation task (the TDD default in Step 2 means this fires for most code tasks).
Three things follow automatically:
Test-author task guardrails — the "red" must COMPILE and FAIL, not just exit non-zero
(#155). A guardrail that accepts ANY non-zero dotnet test exit as the TDD "red" is
gameable: a test file that does not compile exits non-zero identically to one that
compiles and fails, so garbage passes — and the implementation task (whose writeScope
excludes the test file) can't fix the compile error, dead-ending the run at needsHuman.
True TDD red = the tests compile and fail. The guardrail form splits on the type
under test (catalogue → "Stub-based TDD" is the SSOT; stacks/dotnet.md §4.1):
- Behavioral type (a class with methods/logic) → the test-author task ALSO writes the
minimal STUBS. The task produces two artifacts: the test file AND the minimal skeleton
stubs the tests need to COMPILE (interface decls / classes whose members throw
NotImplementedException or return default). Its guardrails are the TWO-guardrail pair,
cheapest-first: build-passes (archetype #3 — with the stubs the test project compiles,
so garbage fails HERE unambiguously) then tests-fail-on-stubs (the #8 form — the build
being green means a non-zero dotnet test now unambiguously means the tests ran and
FAILED against the throwing stubs = TDD red). The implementation task fills real logic over
the stubs (its scope TARGETS them; see below).
- Data model (enum/record/value type — no behavioral stub possible) → COLLAPSE by default.
The type declaration IS the implementation, so there is no stub-vs-real distinction. Default
to a single task (define the type + assert
tests-pass) and state the reason explicitly:
"data model — no behavioral stub possible". If you keep the split, note the anti-tautology is
weaker, keep tests-fail-on-current-code (the test references the not-yet-existing type, so a
compile failure IS the red — omit a separate tests-build, which would fail at the same
moment), and strengthen covers-key-behaviors STRUCTURALLY — assert a real
[Fact]/[Theory] attribute is present (stacks/dotnet.md §17.1), not just that the
enum-value tokens appear (a comment satisfies a bare keyword grep).
- Mixed task (data + behavioral) → lean BEHAVIORAL. Stub the behavioral parts so the whole
test file compiles, and use the
build-passes + tests-fail-on-stubs pair; the data-model
members come along inside the same compiling file.
writeScope test-exclusion — the deterministic TDD test-protection (SSOT §3.4).
Tests are protected by (i) physical worktree isolation and (ii) the harness's
write-scope check: a deterministic, read-only git diff membership test that runs
after the action and before the task's own guardrails. It asserts every path the task's
diff adds/modifies/deletes/renames is inside the task's declared writeScope; an
out-of-scope edit is a guardrail-class failure (retry with feedback naming the offending
paths, eventual needs-human). The check never reverts the in-scope work, and it
writes nothing — it only inspects the diff. Set the two scopes so the implementation
cannot author the tests:
-
Test-author task.json: declare a writeScope covering the test file(s) AND, for a
behavioral type, the STUB file(s) it authors (#155). List each test file and each stub
file (or their directories) workspace-relative — the surface this task is permitted to
write. For a behavioral type the test-author task writes both the test and the minimal
NotImplementedException stubs the tests compile against, so BOTH belong in scope:
{
"description": "Author failing tests + minimal stubs for <feature>",
"dependsOn": ["..."],
"stableId": "…",
"writeScope": ["tests/MyProject/MyFeatureTests.cs", "src/MyProject/MyFeature.cs"]
}
(For a data-model task with no stub, the scope is just the test file, as before.)
-
Implementation task.json: declare a writeScope that EXCLUDES the test file but
TARGETS the stub file(s) (#155). Scope it to the implementation surface (e.g.
src/MyProject/, which COVERS the stub the test-author created) and do NOT list the test
file. The implementation fills real logic over the skeleton stubs; the write-scope check
then deterministically enforces "the implementation may not write the tests" — an edit to
a test file falls outside the implementation's scope and fails the check. If a stub lives
OUTSIDE the implementation's directory surface, list that stub file explicitly so the impl
may overwrite it. This is the replacement for the removed
captureHashes/tests-untouched/restoreOnRetry triad; no hashing, no restore, no
downstream tests-untouched guardrail.
{
"description": "Implement <feature> so the tests pass (fill logic over the stubs)",
"dependsOn": ["NN-author-tests-<feature>"],
"stableId": "…",
"writeScope": ["src/MyProject/"]
}
writeScope is REQUIRED on EVERY task (#389); NEVER omit it, NEVER emit a vacuous **.
Every emitted task.json MUST declare a writeScope — omitting it is now a validation ERROR
(GR2041), and Step 7's guardrails validate FAILS a breakdown that omits any writeScope
(self-validation closure). The three forms:
- a task that writes to the repo → list its real surface (paths/globs/dirs), e.g.
["src/MyProject/"] or ["tests/Foo/Tests.cs", "src/Foo/Feature.cs"];
- a task that writes NOTHING to the repo → emit
"writeScope": [] (the deliberate
"writes nothing" declaration — VALID, never flagged). This is the correct form for:
configure-a-database / provisioning task, a verification / read-only check task, and a
state-only task whose only output is a GUARDRAILS_STATE_OUT fragment (a state fragment is
NOT a repo write and never appears in the segment diff). DECLARE [] — do not omit;
- a genuinely broad / cross-cutting change (a sweeping refactor, a terminal whole-suite gate)
still declares its surface EXPLICITLY (name the directories), never a vacuous
**.
validate rejects a scope that escapes the workspace (GR2019, error) and warns on a
vacuous/over-broad scope (GR2020) — so emit a real surface or [], never omit and never **.
Action prompt for both tasks. The declared scope is injected into the action prompt as
advisory context, but the harness ALSO enforces it mechanically — so every test-author prompt
must carry a Scope boundary (harness-enforced) paragraph (#154; Step 6 has the authoring
rule and exact shape). The test-author ## Task section must tell the agent: (a) the exact
test file path(s), and — for a behavioral type — the exact STUB file path(s) to create with
NotImplementedException skeletons so the test project COMPILES (#155), plus any category/trait
convention the repo uses; (b) the tests MUST COMPILE and FAIL against the stubs — failing is
intentional, NOT compiling is a mistake to fix; (c) do NOT implement the behavior — write the
tests and only the minimal throwing stubs. The implementation ## Task must say plainly: fill
real logic over the stub file(s); do NOT edit the authored tests; make them pass by fixing the
implementation; if the authored tests are genuinely wrong or incompatible, emit
{"needsHuman": "<why>"} rather than changing them — an out-of-scope edit to a test file fails
the write-scope check and burns a retry. Neither task needs to compute or write any hash. See
references/example-breakdown.md for the complete worked action.prompt.md (including the Scope
boundary paragraph and the stub file).
-
A test-author behavior needs a production-code testability SEAM that doesn't exist yet →
insert an upstream production-seam task (#84). Distinct from the compile-coupled-DTO case
above: there the missing symbol is a type the test constructs, so forcing the whole test
file red via a compile failure is correct. The seam case is different — only one behavior of
several needs an injection point (a DI constructor overload, a factory delegate, an injectable
interface, a fixture source) for that behavior to be expressible as a test that can eventually
PASS. The other behaviors are runtime-testable against the existing surface and must keep
compiling and failing as their own clean red; folding the seam into the test file (or vaguely
gesturing at it from the implementation task) leaves the test-author task unable to verify its own
behavior will ever go green — so it correctly halts needsHuman mid-run and forces a human to
hand-edit production code. The seam belongs in its own small upstream task the test-author
task dependsOn, generated at breakdown time so the run stays autonomous.
Detection heuristic (apply while parsing each test-author behavior, Step 4 routing). A behavior
requires a seam when it injects a fake/double — RecordingX, FakeX, InMemoryX, a fixture
source — into a type that is currently constructed only via a production constructor with no
injection point. That is the signal. The action prompt's "if no seam exists, write needsHuman
and stop" escape hatch must be the last resort, not the default: by run start the seam task
should already exist.
Insert NN-add-<component>-<seam>-seam — a pure structural production change: add the
constructor overload / factory delegate / injectable interface + its DI registration. No behavior,
no endpoint — the seam only opens an injection point. Edge direction: the test-author task
dependsOn this seam task (the seam is upstream; the tests compile against it), never the reverse.
- Guardrails: the stack build (
build-passes, archetype #3 / stacks/dotnet.md §4) + a
structural check that the seam exists — the stack file's declaration regex (the new
constructor signature / factory delegate / interface), never a bare name grep (catalogue →
structural-vs-keyword; the .NET seam realizations are stacks/dotnet.md §11). Scope the grep to
the one production file the seam task owns.
- TDD-exempt: a seam is a too-simple structural change with no meaningful unit-test behavior —
state the exemption reason in the task description (rule (b) of the Step 2 TDD-collapse criteria).
- DAG: the test-author task
dependsOn the seam task (artifact dependency: the tests compile
against the real seam). With the seam present, the test-author task authors all behaviors
against the real injection point — every behavior fails at runtime (the endpoint/feature is still
absent) as a clean red, with no needsHuman.
Compose with the TDD pair above (the seam task is upstream of NN-author-tests-<feature>) and with
the composition-root wiring bullet below when the same seam must later be wired in production
(#120): the seam task only opens the injection point for tests; a wiring task still constructs and
injects the real collaborator at the composition root. Two distinct deliverables — do not conflate
"a seam exists so tests can inject a fake" with "production injects the real impl."
-
Test framework is not yet chosen ($testFramework = none from Step 0 and no test