This chapter documents the file format for AI-DLC stage definitions — the
YAML frontmatter contract, the three-compartment body model, and the compile
pipeline that turns those sources into stage-graph.json. It complements
Stage Protocol, which covers the runtime behavioural
contract (approval gates, question flow, state tracking). This chapter is
about what a stage file contains; the Stage Protocol chapter is about what
a stage does.
Contributors read this to understand the format. When writing or editing a
stage file, refer to the authoritative contract at
core/aidlc-common/protocols/stage-definition.md. That file is
the canonical spec — this chapter adds narrative and "when to use" guidance.
Every stage .md file serves two readers:
- The parser (
parseStageFrontmatterincore/tools/aidlc-lib.ts). Reads the YAML frontmatter into a plain record thatvalidateStageFrontmatter(core/tools/aidlc-stage-schema.ts) then checks. Doesn't touch the body. - The LLM agent executing the stage. Reads the body, follows the prose instructions, produces artifacts. Doesn't touch the frontmatter.
Keeping both in one file means contributors see the graph edges and the execution steps side by side. Splitting them across separate files (one YAML for the graph, one prose for the agent) would break the inline visibility that makes stages reviewable.
The format is called "Variant A3", the third of three authorship variants that were weighed for it:
- One file beats split files. Frontmatter plus prose in one
.mdkeeps graph structure and execution steps together. A reviewer reading a stage sees both. - Grep-friendly. Plain text. No binary format, no YAML-vs-JSON translation at author time.
- Diff-friendly. Field additions, renames, and body edits all show up cleanly in a code review.
The rejected alternative was a central graph file (stage-graph.json
hand-edited) with prose-only stages. Loses the inline visibility of knowing
which artifacts a stage produces while editing its prose.
Edit stage YAML
|
v
bun scripts/package.ts
|
+--> ignored dist/<harness>/<harness-dir>/tools/data/stage-graph.json
|
+--> ignored dist-release/<harness>/<harness-dir>/tools/data/stage-graph.json
|
v
loadStageGraph()
The YAML is authoritative. The JSON is a generated build artifact. Repository packaging regenerates it from source; installed runtimes may also recompile it after plugin composition.
aidlc-graph compile and compile --check remain runtime and diagnostic
subcommands, but repository contributors do not preserve a compiled JSON copy
in source control. Edit the YAML and run bun scripts/package.ts; packaging
creates fresh ignored projections, and bun scripts/package.ts --check
rebuilds twice in temporary roots to check generator determinism.
The authoritative spec has a complete field table with types and constraints. This section adds narrative on the fields that need judgment calls.
Encodes dependency edges. Two roles:
- Semantic data dependency. "I consume artifact X, which stage Y
produces" → add
Ytorequires_stage. - Presentation-order edge. Two stages in the same phase with no
semantic dependency but a fixed ordering (e.g.,
market-researchbeforefeasibilityin Ideation). Add the weak edge so the computed stagenumberlands stably.
The compile step's slug-alphabetical tiebreak is a safety net. For stages that must land in a specific order, author the edge explicitly rather than relying on alphabetical accident.
Names an artifact whose instances drive iteration. The stage runs once per instance.
Today's use case: five Construction stages (functional-design,
nfr-requirements, nfr-design, infrastructure-design, code-generation)
run once per Unit — they each declare for_each: unit-of-work (the artifact
units-generation produces).
Tomorrow's use cases: a stage that runs per environment, per tenant, per region, per compliance jurisdiction. The primitive is workflow-engine generic; Construction happens to exercise it first.
Aggregation is inferred, not declared. A stage that consumes an artifact
produced by a for_each stage, without declaring its own for_each, is an
aggregation step by definition. build-and-test is the canonical example —
it runs once after all five Construction for_each stages have iterated
across Units, consuming their aggregated outputs. No explicit fan_in or
aggregation field — graph traversal figures it out.
Optional enum controlling the deterministic pre-generation checkpoint for a stage's question flow:
requiredmeans every execution must create a questions file and obtain the consolidated Looks correct confirmation before artifact generation.if-presentapplies the same enforcement only when a conditional question flow created a questions file. Once its summary decision or confirmation is recorded in the current attempt, deleting that file does not remove the obligation. Restore the questions and confirmation, or use an explicit lifecycle reset to start a new attempt.
The receipt is not inferred from markdown alone. aidlc-log.ts records the
reserved SUMMARY_CONFIRMATION_RECORDED event after a matching prompt record
and a reply of the person's since their last answer (before or after that
record), with their words as Person Reply, binding it to the questions-file digest and its recorded
Hash Scope. New receipts use confirmed-content-v2: the unchanged raw-content
SHA-256 algorithm normalizes CRLF/lone CR to LF, retains sections in file order,
and trims trailing whitespace once from the resulting content. All visible
Q and feedback sections remain bound; one post-summary
Assumption Confirmation section is excluded, up to any line spelled as a
top-level ## Q<n> or feedback heading. A Q<n> or
Assumption Confirmation heading counts with or without a leading emoji
decoration, by the claim-sources sensor's rule. Comments, code, HTML, and a
leading BOM in retained content still affect the digest. Heading and answer
recognition now uses the built-in Bun.markdown parser through markdownBlocks
and visibleMarkdownLines; raw HTML block content is never a heading, answer,
or tag. A Looks correct receipt also carries a
Summary Authorization Id (a digest of the attempt, stage, Unit, workflow,
questions path, confirmed content, and choice) that becomes the scope's active
authorization; the write-audit hook stamps that id on every later
ARTIFACT_CREATED/ARTIFACT_UPDATED row for the stage's outputs. Completion
refuses a missing or stale receipt, a changed confirmed section or forbidden
heading, or a declared artifact whose newest native write does not carry the
current receipt's id (the output does not descend from the current
confirmation). Identical re-confirmations mint the same id, so a repeated
Looks correct reaffirms instead of revoking; changed answers mint a new id, so
the outputs must be saved again under it; the order in which the receipt and the
writes landed decides nothing. A legacy receipt without an id still requires a
native write after the receipt. An unscoped in-flight receipt must be
re-confirmed to create a scoped receipt before a permitted append can be
accepted. Per-unit stages require one unit-scoped receipt per applicable Unit;
isolated runs use the same check with their single-stage:<slug> workflow
identity.
confirmed-content-v1 remains a supported legacy scope with the same digest
algorithm and the former hand-written visibility rules. A v1 receipt is accepted
when its recorded digest equals the current v2 digest, so unaffected documents
retain byte-identical identities without reconfirmation. A v1 mismatch refuses
with SUMMARY_CONTENT_SEMANTICS_CHANGED: the receipt predates the Markdown-parser
upgrade, so either the confirmed content changed after confirmation or raw HTML
content that v1 treated as confirmed text is no longer part of it. Raw HTML
headings and control tags are now excluded from Markdown recognition. Re-present
the summary and reconfirm to record v2; the refusal never asserts an edit.
A v2 mismatch keeps the existing SUMMARY_CONTENT_STALE “changed after
confirmation” refusal. Unscoped receipts keep whole-file SHA-256 and existing
recovery text; unknown scopes still refuse with SUMMARY_HASH_SCOPE_INVALID.
The earlier-identical-confirmation path also compares v1 receipts using the v2
hash function; stored receipts are not rewritten.
Boolean, default false. Set true on stages that must write source code to
the workspace root, not just planning documents under the per-intent record dir.
Why it exists: a stage's produces[] artifacts normally resolve under the
record dir, except for space-level stores such as Reverse Engineering's
per-repository codekb. So a "do the produces exist?" check is satisfied by a
code-generation stage that wrote its code-generation-plan.md,
unit-test-instructions.md, and code-summary.md but never emitted a line of
actual code (issue #366).
workspace_requires: true closes that gap: the
stage-completion artifact guard (aidlc-state.ts approve/advance/finalize/
complete-workflow) additionally requires evidence of real source work outside
the aidlc/ workspace tree and the harness directory before the stage may
complete.
For codekb stages, the same guard follows the active intent's recorded
repository set. Every recorded repository must have at least one declared
artifact in its canonical aidlc/spaces/<space>/codekb/<repo>/ directory;
misnamed or unrecorded directories do not count. An intent with no recorded
repositories keeps the legacy any-repo-directory fallback.
How "source work" is detected depends on the workspace:
- Git workspace - the guard asks git, so it can tell this session's code
from a brownfield repo's pre-existing
src/. It passes when there is an uncommitted or untracked non-doc change (git status --porcelain) or the last commit touched a non-doc path (git diff --name-only HEAD~1 HEAD). The second clause means commit-then-approve (a clean working tree, code in the last commit) still passes, closing the clean-tree false-block from #366 Update 3. - Non-git workspace (or any git error) - the guard falls back to a shell-free
filesystem-existence check: at least one file must exist outside the
aidlc/workspace tree and the harness dirs. - Code kept outside the workspace's own git - the code may live in one of
the intent's recorded repos (the
reposof its registry row), in a child repo, clone or worktree made after the work started, in a linked worktree of any of those (asgit worktree listnames them), or in a checkout the workspace's git does not track, such as a worktree of a repo kept outside the project and placed in a gitignored folder inside it (found up to three levels below each untracked or ignored folder, without following links). The guard asks each of them the same question it asks the workspace, so a brownfield checkout with no new code does not pass either. A checkout found when the check runs counts only where the review can see its code change, that is where the source walk lists files below it outside AI-DLC's own: code kept in a harness folder such as Claude Code's.claude/worktrees/, outside the project or on another drive is not counted, so it goes to the person with the line below, unless the work records that checkout (intent add-checkout <path>, which the agent runs before the review, or a Unit's claim inside a worktree of the harness folder, recorded at the review request). A recorded checkout is walked by the review as a root of its own, its files listed as<label>@checkout/<path>, so it is asked and found, and a change there after the review is a stale review. Git is asked only in a folder with a real.gitentry (a clone, submodule, init or worktree the person made); a committed folder merely shaped like a repo gets the filesystem check, so its committed git config never runs. AI-DLC's own Bolt worktrees under.aidlc/are left to the swarm's own evidence. - Code committed, then the records - a records-only last commit hides code
committed earlier in the stage, so the guard also compares the source with
the stage attempt's
Source Baseline: a change outside AI-DLC's own files since the stage began is source work.
When the guard finds no source work anywhere it looked, Guard Policy off lets
the work carry on, and the person hears once "AI-DLC found no source changes
under ". Under strict or relaxed the agent's own first report is
refused, naming the way on that always works: report again, and the approval
question goes to the person with that line ("If your code is somewhere else,
approve as usual; if it is missing, ask for changes."). Their approval records
it as Source Work beside their words. Under strict or relaxed, where no
approval would be the person's reply after the gate (an autonomous Construction
gate, the human-presence check switched off, an unattended run) and for the
agent's own advance, finalize and complete-workflow, the refusal stays. Each outcome is
recorded once per stage attempt as a SOURCE_WORK_NOT_FOUND row.
Today only code-generation declares it. Its per-unit reviews additionally
require <record>/construction/<unit>/code-generation/source-manifest.json:
a strict attribution index of every created, modified, or deleted application-
source path. The engine binds manifest bytes and claimed content into the unit
receipt, compares every fresh unit against a content-addressed stage-entry
baseline, and refuses changed paths outside the fresh claims union. Directory
claims cover later additions; in a main multi-repo workspace every entry names
its recorded repo, while a Bolt's manifest is relative to its one selected repo.
In a workspace that records no repos, an entry inside a checkout nested in the
project (a worktree or clone its git does not track) is judged by that
checkout's own git, so the workspace's .gitignore for that folder does not
refuse it. So is an entry through a link or junction to a checkout outside the
project (wt-link/src/, or wt-link/ for all of it), and it covers what the
source walk lists through that link. An entry inside one of the work's
recorded checkouts is judged by that checkout's git too and keyed by its
listing name; a checkout outside the project is named that way
(<label>@checkout/src/), with no repo even in work that records repos,
and a listing name the work has not recorded is refused with the step to
record it.
Missing pre-upgrade fields fail open only as documented migration evidence;
present-but-unbindable or destroyed modern evidence fails closed under Guard
Policy strict; under relaxed or off it is recorded once and the work continues. A team that
adds its own code- or config-emitting stage (a contract generator, an IaC
executor) should set workspace_requires: true so the workspace guard applies.
Bypass it for CI with AIDLC_SKIP_ARTIFACT_GUARD=1; that switch also bypasses
the review-time required-output existence check. Bypass source binding and
attribution separately with AIDLC_SKIP_SOURCE_FRESHNESS=1.
Optional map on a for_each: unit-of-work stage: each key is one of the stage's
produces or optional_produces artifact names, each value the list of Unit
kinds that artifact applies to. Kinds are declared per Unit in
units-generation's edge block (see Runtime graph
bolt_dag.units[].kind) and are one of
service | spec | ui | packaging | library.
produces:
- performance-requirements
- security-requirements
- scalability-requirements
- observability-requirements
produces_kinds:
performance-requirements: [service, ui]
scalability-requirements: [service]
observability-requirements: [service]Why it exists: the four construction design stages ran with a fixed produces
list applied identically to every Unit, so a spec Unit owed a scalability doc
and a packaging Unit a business-logic model - N/A stubs the human had to write.
produces_kinds lets the engine prune the matrix per Unit: when a Unit carries
a kind, the engine keeps only the produces entries whose kind list includes
that kind. An artifact not listed in the map applies to all kinds (annotate
only the kind-specific ones). A Unit with no kind, or a stage with no
produces_kinds map at all, keeps the full matrix - so this is inert for every
existing workflow.
The pruning is symmetric: it filters both the run-stage directive's produces
paths (what the conductor writes) and the per-unit coverage check (what the
approve-path guard requires). It composes with optional_produces: the
directive's paths are the kind-filtered union of both lists, while coverage
stays keyed off the required produces only (an optional artifact is pruned
from the directive for kinds it does not apply to, and remains coverage-exempt
either way). A Unit whose required set prunes to empty is
covered by definition - the stage does not apply to it - and a per-unit stage
where every Unit prunes to empty approves as a no-op rather than deadlocking
at the artifact guard. The default kind matrix for the four stages is stage
frontmatter data, reviewable and revertible per entry; removing a wrong entry
restores the full matrix for that artifact.
The stock NFR stages map both observability-requirements and
observability-design to service units.
One trust note: the kind: value is enum-checked at the units-generation gate
(the required-sections sensor fails loud on a typo), but the compiled
runtime graph is trusted afterwards - the engine only shape-checks the kind
when reading bolt_dag.units[].kind (matching how it trusts the compiled
batches). A unit hand-edited in the compiled graph to a valid-but-wrong kind
prunes to that wrong kind's set silently.
Boolean per consume entry. Semantically scoped to the active plan, not a global assertion that the artifact always exists somewhere:
required: truemeans "if the producing stage runs in the active plan, this consume must be satisfied." It does not mean "the producer always runs." When a scope excludes the producer (e.g.,bugfixskipsunits-generation), everyrequired: trueconsume of that producer's artifacts becomes moot — there is nothing to require.
Why the scoped reading. Only enterprise and feature execute every stage;
all other scopes deliberately skip upstream stages. A flat global
required: true would make those scopes structurally invalid, which is wrong — they're legitimate operating
modes. The real contract is conditional: "if upstream runs, feed me
downstream." The stage body already handles the absence case
gracefully (prose instructions like "if available" or fallbacks from
context).
What this means for the doctor lint. The lint walks each
active scope and reports "stage X's required: true consume for
artifact Y is moot because Y's producer is SKIP in this scope." That's
advisory, not blocking — the user has already opted into the
truncation by picking the scope.
when: (accepted, not yet evaluated). A stage may declare a when:
predicate; today the schema accepts only producer-in-plan, checks its shape,
and nothing acts on it yet, so the stage runs under its scopes: as if it were
absent. The required: true + conditional_on: brownfield|greenfield pair
covers the two dimensions the engine evaluates today; when: is meant to
generalise it.
Captures the brownfield/greenfield split. Example:
reverse-engineering produces artifacts only in brownfield mode; stages
that consume those artifacts mark the consume
conditional_on: brownfield to tell the scope resolver "this consume is
only required when we're in brownfield".
For unconditional consumes, omit the field entirely. There is no
always value — an unconditional consume simply has no conditional_on
key.
An optional boolean on a consume: cited: true says the stage's deliverables
cite that input whenever it exists. The upstream-coverage check asks the
deliverables to cite every required consume that exists; it leaves an optional
one (required: false) unchecked, because an optional input may not have
informed the output, unless the consume is marked cited: true. Requirements
Analysis marks intent-statement and scope-document: both are optional
there, because scopes without Ideation produce neither, and when they exist the
requirements build on them.
The compile lists the marked consumes as the stage's cites in
stage-graph.json and keeps the compiled consumes entries as they were, so
marking or unmarking one does not change the record of a stage that already
finished. The flag sits on the consume, not in a top-level list, because a
project on 2.8.0 to 2.11.0 refreshes to a newer release with its own tool,
which accepts a key inside a consume and refuses a top-level key it does not
know.
consumes:
- artifact: intent-statement
required: false
cited: trueA plain kebab-case string list, parallel to produces:. It names artifacts
the stage may write per unit but is not required to. Absent means
none; only the one stage that needs it (functional-design, for
frontend-components) declares it, so the compiled stage-graph.json stays
minimal.
Why it exists: a per-unit Construction stage (for_each: unit-of-work) is
COVERED for a unit only when every produces[] artifact exists on disk under
that unit's record dir (the per-unit coverage check in
aidlc-orchestrate.ts). Some artifacts are genuinely conditional on the unit
-
functional-designwritesfrontend-componentsonly when the unit has a UI. Listing that underproduces:forced a backend-only unit to write an N/A stub just to satisfy coverage, and left the stage gate unreachable until it did. Moving them tooptional_produces:exempts them: -
Coverage exemption.
optional_producesentries are ignored by the per-unit coverage loop. A unit is covered once its requiredproduces[]artifacts exist; the optional ones never blocknextfrom advancing orapprovefrom committing. -
Still resolved for the conductor. The run-stage directive's
producespaths unionproduces+optional_produces, so when the unit DOES write the conditional artifact the conductor still knows where it lands. When the stage also declaresproduces_kinds, that union is kind-filtered before it resolves (seeproduces_kindsabove), so a kind an optional artifact does not apply to never sees its path. -
Still in the vocabulary.
artifactsRegistry()andproducersOf()union both lists, so the artifact name and its producer stage stay registered.
Pairing convention. Every optional_produces entry MUST have a
(CONDITIONAL - ...) marker in the stage body prose (and the outputs:
string) that tells the agent when to write it. The frontmatter key is the
engine's coverage view; the prose is the agent's instruction. Keep them in
sync.
Warning. An artifact marked optional_produces is invisible to the
per-unit coverage ledger - the engine cannot prove the unit produced it. Use
it only for artifacts that are legitimately conditional on the unit, never to
quietly relax coverage for an artifact a stage should always write per unit.
The stage's communication topology — who talks to whom while the body runs. Five values, four active:
inline— the conductor runs the stage in its own context; support agents are perspectives it adopts (voices). Zero dispatches. Short stages, fast to execute, no context pressure.subagent— hub-and-spoke. The lead is dispatched to a fresh subagent context (long stages, e.g. Construction code generation, that would blow out the conductor's context). When the stage also declaressupport_agents, each one is dispatched as a real spoke against the lead's returned draft (mutually blind briefs carrying artifacts by path, with the stage's rules delivered perstage-protocol.md§ "For subagent stages" step 2) and the lead is dispatched once more to integrate.pipeline— chain. The lead drafts; each support agent enriches in declared order, every link seeing the draft plus all earlier contributions. Order is the point. Requires non-emptysupport_agents, and every agent across the lead plus support chain must be unique.mob— mesh, run as bounded rounds: all support agents contribute in parallel against the lead's draft (mutually blind), the lead integrates, and unresolved objectors get one confirm-or-maintain round with the other participants' positions. Maintained dissent is quoted verbatim at the gate. Requires non-emptysupport_agents. The shipped showcase isuser-stories(Product Manager lead; Design, Developer, Quality collaborators; Product Lead reviewer — the mob-elaboration ritual).agent-team— reserved. The future native-bus transport for mesh collaboration: when Anthropic's experimentalCLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMSprimitive stabilises, a live peer-messaging room can carrymob's semantics without conductor-carried rounds.mobis the portable mode; no stage declaresagent-team.
On every topology the conductor is the bus: agents never invoke each other —
only the conductor delegates. The writing model mirrors a real working
session: everyone writes their own work, the owner collates and edits. Each
dispatched support agent writes a contribution file
(contributions/<agent-slug>.md, stage-protocol-collaborators.md shape with the
identity-marker first line); the lead alone edits the stage's produces[]
artifacts; pipeline links advance the artifacts directly instead. On mob and
subagent-with-supports stages the contribution files are the completion
evidence — the engine refuses approval while one is missing. The review loop
is NOT a mode: reviewer + reviewer_max_iterations deliver the two-party
critique topology on top of any mode, and a NOT-READY re-invokes the lead
alone.
Consumer contract. Orchestrator code reading the mode field must
handle agent-team explicitly — at minimum throw "mode agent-team not yet
implemented". Do not fall through to a default execution path. Silent
fallthrough on enum extension is a known foot-gun.
Swarm-trigger coupling. The autonomous Construction swarm fires on
for_each: unit-of-work + mode: subagent. Re-moding the per-unit build
stage silently takes it off the swarm path; aidlc-graph compile emits a
stderr advisory when it sees that shape.
The lead agent owns the stage. The lead's persona (skills, knowledge, tool
allowlist) is loaded at stage start. Support agents add perspective — a
stage might lead with aidlc-product-agent for requirements work but load
aidlc-delivery-agent as support for capacity reality-checking.
Both fields validate dynamically against .claude/agents/*.md via
loadAgents(), which reads the active harness's agents directory; aidlc-graph.ts compile passes the
discovered agent slugs into validateStageFrontmatter, so a lead_agent or
support_agents value naming an agent with no matching file fails the
compile loudly (lead_agent "<name>" has no matching .claude/agents/*.md)
rather than surfacing at run time as an unregistered-subagent Task error.
The one exemption is the reserved orchestrator pseudo-agent (the conductor
itself, named as lead_agent on the bootstrap initialization stages); it has
no agent file by design. No hardcoded enum in the schema — adding an agent
means dropping its .md file in .claude/agents/ with the required
frontmatter. See
Contributing: Adding an Agent.
Pipeline stages additionally reject a support agent repeated elsewhere in the
chain, including the lead repeated as support, because link receipts identify
one declared position by its unique agent.
Optional. reviewer names a quality-gate agent invoked after the stage body
produces its artifacts and before the approval gate (see Stage
Protocol). Two reviewers ship today —
aidlc-product-lead-agent and aidlc-architecture-reviewer-agent — and the
compile validates the value against the discovered agent roster the same way
lead_agent is validated.
Every reviewer-bearing stage must also declare review_artifact, naming one
required Markdown entry from produces[]: the artifact the review is about.
The review record is keyed to it, the gate names it, and
--reject-finding / --reopen-finding <artifact>#R-NN address its findings; the reviewer never
writes to it. List ordering and plugin-added outputs cannot change it. On a per-Unit stage the target must remain applicable for every Unit
kind on which any required output is applicable, otherwise graph compilation
fails. Structured outputs such as traceability.json cannot be review targets.
reviewer_max_iterations caps the review/revise loop before the workflow proceeds
to the gate with unresolved findings. It defaults to 2 when reviewer is
declared but no cap is given; a value you give must be a positive integer, and
any other value fails schema validation before the compile. Omit the field on a stage that declares no reviewer: the compiler rejects
a reviewer_max_iterations declared without a reviewer (the schema error
reviewer_max_iterations requires a reviewer fails the graph compile), so it is
never silently ignored.
review_class selects the review contract: adversarial (the refute-and-repair
loop above — the default when a reviewer is declared without a class) or
advisory (one normal-flow pass whose findings the human approval gate shows
from the engine-owned findings list, no repair loop; the effective iteration budget is 1). A later
write that invalidates its terminal receipt permits one bounded recovery request
at the next ordinal, and a review the person asks for is never refused. The shipped split:
the 8 human-gated ideation/inception prose stages declare advisory; the 5
Construction design/build stages default adversarial. none is deliberately
not a stage value — a stage that wants no review deletes its reviewer: line;
none exists on the scope review_cap and the per-run --review override,
which can silence a declared reviewer without editing stages. The effective
class at runtime is the stage declaration lowered by one ceiling: the per-work
--review override when one is set, otherwise the active scope's review_cap
(the shipped bugfix, poc, classic, and workshop scopes cap to
advisory, while express caps to none). A ceiling can lower a class but
never raise one past the stage's declaration; an override set for a piece of
work replaces the scope's ceiling, so --review adversarial on a capped scope
runs each stage's own class. Autonomous swarm reviews are exempt from caps and overrides:
inside a Bolt the reviewer is the only pre-merge verification, so the declared
class always applies there. Like the cap, review_class requires a reviewer
(schema error review_class requires a reviewer).
Scope frontmatter also accepts five ceremony switches, each on | off
(absent means on, except collaborators, which ships on only on enterprise):
sensors, learnings, summary_confirmation, plan_approval, and
collaborators (off runs every stage lead-only; the reviewer is unaffected).
The last is distinct from a stage's summary_confirmation: required | if-present:
the scope/intent policy decides whether that checkpoint applies at all.
/aidlc --sensors on|off, /aidlc --learnings on|off,
/aidlc --summary-confirmation on|off, and /aidlc --plan-approval on|off
override an intent's scope default (only the person turns plan approval off).
AIDLC_DISABLE_SENSORS=1, AIDLC_DISABLE_LEARNINGS=1,
AIDLC_DISABLE_SUMMARY_CONFIRMATION=1, and AIDLC_DISABLE_PLAN_APPROVAL_GUARD=1
force the respective ceremony off. Classic enables sensors, learnings, and plan
approval and disables summary confirmation; bugfix disables learnings and
summary confirmation; express and poc disable plan approval. Apart from plan
approval off, which builds each code plan without asking, stage approvals,
human-turn authority, audit, and team write protection remain in force.
Optional. The stage's composer screening prior — the same four facts as
one entry of tools/data/ars-priors.json, written in the stage's own
frontmatter:
ars:
targets: [ve, r] # ARS components the stage reduces: iae | csu | ve | r | ua
cost: 4 # 1 (trivial) .. 5 (heavy); null = never numerically screened
role: structural # optional: initialization | core | phase-gate | structural
project_types: [brownfield] # optional: mirror a condition: that restricts the project kindLists are inline ([a, b]). The block runs until the next top-level key, so
blank lines and # comments inside it, including trailing ones, are allowed.
Why it exists: aidlc-graph ars (see CLI commands)
screens each stage EXECUTE/SKIP from a cost prior and the components it
targets. The shipped priors name core stages only and a plugin cannot edit
that file, so a plugin stage used to land as a no-prior row the composer
could only decide by judgment. The schema validates the block like a
priors-file entry, aidlc-graph compile copies it onto the node, and the ars
subcommand screens it exactly like a file entry: role: core always executes,
structural and a null cost are left to judgment at the gate,
project_types screens the stage out on the other project kind, and a
--completed stage outranks everything. The priors file has one role a stage
block cannot declare, project-type (Reverse Engineering): it runs the stage
on its own project kind whatever it scores, so on that kind a plugin stage's
project_types still goes by its components.
Three rules keep the screen predictable:
- The priors file wins. When a slug has both a file entry and an
ars:block, the file entry is used, so core screening never changes under a stage-side edit. Core stages declare noars:. - An empty
targetsnever executes mechanically.targets: []with a numeric cost is legal, but without arolenothing can clear the threshold: the row is SKIP, with a reason that says so. - A cost must have a threshold.
coststays on the composer persona's1..5scale, andaidlc-graph arsexits 1 naming the stage when the value has noevThresholdsentry in the priors file it loaded.
Each screen row's priorSource (shipped, stage or null) says where its
prior came from.
Stages and agents follow the same YAML-first discipline. Agent frontmatter (see Agent System) declares who — the agent's name, allowed tools, tier. Stage frontmatter declares what — which artifacts the stage produces and consumes, which agents it delegates to, how it executes.
Both formats:
- Are authoritative sources for their domain (no parallel hardcoded maps).
- Ship with a
loadX()helper that returns typed structures. - Validate dynamically against the filesystem rather than against a hardcoded enum.
Adding a new stage is the same shape as adding a new agent: drop an .md
file, add the required frontmatter, and the helpers pick it up at runtime.
The canonical example is scope-definition. The normative YAML block lives
in core/aidlc-common/protocols/stage-definition.md — refer
there rather than duplicating here.
The example encodes, in structured form, what today's prose describes:
requires_stage: [intent-capture]encodes the prose instruction "Read intent statement from the intent'sideation/intent-capture/(under its record dir)". The parser does not care about the prose — it just sees the graph edge — but human readers should keep them in sync.consumes: [{artifact: intent-statement, required: true}]says this stage is blocked untilintent-statementexists. If the scope's resolver can't find a producer forintent-statement, the doctor's missing-producer check fails.produces: [scope-document, intent-backlog, scope-definition-questions]is the forward edge — other stages looking for "who producesscope-document?" find this one viaaidlc-graph.ts producersOf().- No
for_eachfield —scope-definitionruns once per workflow.
The body of a stage file has three compartments, declared in this order, and every shipped stage populates all three:
| Compartment | What goes here |
|---|---|
## Steps |
Imperative prose the agent follows |
## Sensors |
Compact output-location, imported-sensor, and upstream-target summary; stage-specific exceptions remain local |
## Learn |
Compact pointer to the shared section 13 learning-loop contract and its bootstrap exception |
The fixed order means a stage's sensor bindings and learning-loop pointer
always have a place of their own. See Sensor
System for the ## Sensors binding semantics and
the pull-import model. Shared sensor behavior is defined once in
stage-protocol-sensors.md, while stage-protocol-learnings.md section 13 defines the diary and ritual only when directive.protocol_modules lists learnings; otherwise skip both.
The ## Sensors and ## Learn headings are checked by
t87-stage-compartment-headers across all 33 stages; ## Steps is in every
stage but has no test of its own.
parseStageFrontmatter and emitStageFrontmatter live in
core/tools/aidlc-lib.ts, YAML-only. aidlc-graph.ts compile, which
bun scripts/package.ts runs for every generated tree, compiles the YAML into
stage-graph.json. The repository generates that file only inside ignored
projections; package determinism, not a committed compiled-file drift check, is
the source-tree gate.
for_eachis used by Construction only. The five per-Unit Construction stages declarefor_each: unit-of-work; the other 28 stages omit the field.when:is accepted but not evaluated. See the field note above.- Only the frontmatter is validated at compile. The body prose is not
parsed; its
## Sensorsand## Learnheadings are checked by the test suite, and the engine's runtime guards (for example the completion checks on a stage's required work) act on what the stage produces, not on its prose.
The spec reserves names for primitives AI-DLC will likely add in later releases. The schema rejects unknown keys — reserving the names here prevents future contributions from colliding with ad-hoc additions.
| Key | Reserved for | What it would do |
|---|---|---|
on_failure |
the loop driver | Declarative error recovery: "if this stage fails, jump back to X" or "retry with adjusted inputs". Would move revision semantics out of stage-protocol-recovery.md prose |
blocks_on |
Construction worktrees | Completion dependency without data read: would split today's overloaded requires_stage (which conflates "I consume your output" with "I run after you") |
timeout |
sensor binding | Execution budget (deadline). Homed in sensor bindings, not stage frontmatter |
retry |
the loop driver | Retry policy on failure. Homed in loop config, not stage frontmatter |
when is no longer reserved: the schema accepts it with the producer-in-plan
predicate and checks its shape, and its evaluation is still to come (see
consumes[].required above).
Design rationale: Claude Code's own task primitives (TaskCreate family plus
/loop and cron) omit dependency, blocks, retry, and timeout — all
multi-step orchestration is pushed to client-side code. This implementation mirrors that
choice by homing execution behaviour (retries, timeouts, failure handling)
in the loop and sensor subsystems rather than the stage spec. The fields
above would be modest structural extensions if consumers emerge, not new
paradigms.
The reserved-namespace pattern has precedent in the audit taxonomy
(State Machine), which can pre-register an event name
with a Reserved Emitter cell until the code that emits it ships, and keeps a
retired name as Reserved (retired name) (CHANGE_CONTROL_SET today).
core/aidlc-common/protocols/stage-definition.md— the authoritative spec this chapter narrates.- Stage Protocol — runtime execution behaviour.
- Agent System — parallel YAML-first contract for agent files.
- State Machine — where stage execution emits audit events.