Hand-off contracts
The three orchestrators own every contract in the pipeline. No specialist, critic, formatter or generator defines one — each of them consumes a shape an orchestrator declared and returns a shape an orchestrator declared, which is what makes an agent replaceable without renegotiating anything around it.
The blocks below are the same bytes as the agent files, extracted from the
stage sections that define them. Comments included: the # ← TRANSIENT
markers are part of the contract, and re-serialising these shapes through a
parser would drop them.
The requirements stage
generation_brief
Stage 4 of requirements-orchestrator.
generation_brief:
context: { ...full clarification_context... } # shared, read-only
target_category: functional | non_functional | constraint_and_business_rule
assigned_needs: # only the needs this specialist owns
- tier: business | stakeholder | solution | transition
summary: string
category: functional | non_functional | constraint | business_rule | use_case
source_field: string # which context field this need came from
id_block: # reserved IDs this specialist must draw from, in order
prefix: FR | NFR | CON | BR | UC
start: 1 # next sequence number to use
created_at: "YYYY-MM-DD" # today's date, passed so all files agree
global_id_index: [ ...all IDs allocated so far across categories... ] # for cross-references
draft_requirements
Stage 5 of requirements-orchestrator. Transient: applies_to — carried between agents, never written to disk.
draft_requirements:
- id: FR-001
type: functional # functional | non_functional | constraint | business_rule | use_case
tier: solution # business | stakeholder | solution | transition
title: string
description: string # EARS-phrased for FRs
rationale: string
fit_criterion: string
priority: must | should | could | wont
confidence: high | medium | low
verification_method: test | inspection | analysis | demonstration
ears_pattern: ubiquitous | event | state | unwanted | optional | complex # FRs only; omit otherwise
status: draft
created_at: "YYYY-MM-DD"
traces_from: [ ...IDs... ]
traces_to: { design: [], tests: [], code: [] }
applies_to: [ ...IDs... ] # ← TRANSIENT, constraint/business_rule only
scope: project | epic | story # reserved; default project
parent_scope: null # reserved
body_markdown: |
# ...rendered body: EARS description + Gherkin AC (FR), or six-part QAS (NFR)...
critique_report
Stage 6 of requirements-orchestrator.
critique_report:
gate: pass | fail
validator: # structural-gate result, folded back in from the formatter (Stage 7)
command: "python3 <scripts>/validate_requirements.py .sdlc/requirements"
exit_code: 0
summary: string
per_requirement:
- id: FR-001
verdict: pass | revise
findings: [ ...INCOSE/29148 + anti-pattern notes... ]
coverage:
iso_25010_gaps: [ ...characteristics with no NFR + justification... ]
glossary_findings:
- term: Decay
issue: undefined | circular | vacuous | padding
note: string # what's wrong, and (for undefined) a proposed definition
context_artifact
Stage 6.5 of requirements-orchestrator.
context_artifact:
assumptions: # statements believed true without proof
- id: A-1
statement: string
dependencies: # external conditions the project relies on
- id: D-1
statement: string
open_questions: # unresolved items for human follow-up
- id: Q-1
statement: string
owner: string # who should resolve it (or "unassigned")
glossary: # domain vocabulary — the M2/M3 anchor
- term: Decay
definition: The reduction of a pet's stat values over elapsed time, applied whether or not the app was running.
aliases: [stat decay]
formatter_result
Stage 7 of requirements-orchestrator.
formatter_result:
files_written: [ ".sdlc/requirements/functional/FR-001-...md", ... ]
index: ".sdlc/requirements/index.yaml"
review_queue_count: 0
context_artifact: ".sdlc/requirements/assumptions.md"
glossary: ".sdlc/requirements/glossary.md"
validator_rerun: { exit_code: 0 }
applies_to_backfill:
- from: "CON-001"
into: [ "NFR-002" ]
The design stage
generation_brief
Stage 5 of design-orchestrator.
generation_brief:
context: { ...full design_context... }
requirements_digest: # the FULL requirement set, not just the ASRs.
# The orchestrator reads the files once; specialists never re-read.
# asr_analysis marks the significant subset within it — the
# component specialist still needs every FR to decompose against.
- id: NFR-002
type: non_functional
title: string
description: string
measure: string # fit_criterion (FR), response measure (NFR), or a CON/BR's
# own fit_criterion where it states one — omitted when the
# requirement genuinely has no measure
priority: must | should | could | wont
confidence: high | medium | low
terms: # inherited verbatim from requirements/glossary.md — the design
# stage does not author vocabulary, only consumes it (STO-197 A.2)
- term: string
definition: string
aliases: [ string ]
asr_analysis: [ ...see Stage 3... ]
target_category: component | interface
id_block: { prefix: CMP | IF, start: 1 }
created_at: "YYYY-MM-DD"
component_set: [ ... ] # INTERFACE BRIEF ONLY: components + their capabilities
draft_components
Stage 6 of design-orchestrator. Transient: required_capabilities — carried between agents, never written to disk.
draft_components:
- id: CMP-001
type: component
title: string
description: string
responsibility: string
boundary: internal | external
traces_from: [ FR-001, NFR-002 ]
traces_to: { adr: [], diagrams: [], code: [], tests: [] }
depends_on: [] # left empty — the orchestrator fills it
required_capabilities: # ← TRANSIENT
- capability: "take card payments"
rationale: string
status: draft
confidence: high | medium | low
created_at: "YYYY-MM-DD"
body_markdown: |
# ...rendered body...
draft_interfaces
Stage 6 of design-orchestrator. Transient: consumed_by, satisfies_capabilities — carried between agents, never written to disk.
draft_interfaces:
- id: IF-001
type: interface
title: string
description: string
provider: CMP-002
operations: [ { name, summary, interaction } ] # interaction is PER OPERATION
error_modes: [ ... ]
consumed_by: [ CMP-001 ] # ← TRANSIENT, drives the back-fill
satisfies_capabilities: # ← TRANSIENT, proves nothing was dropped
- { component: CMP-001, capability: "take card payments" }
traces_from: [ ... ]
traces_to: { adr: [], diagrams: [], code: [], tests: [] }
status: draft
confidence: high | medium | low
created_at: "YYYY-MM-DD"
body_markdown: |
# ...rendered body...
critique_report
Stage 8 of design-orchestrator.
critique_report:
gate: pass | fail
validator:
command: "python3 <scripts>/validate_design.py .sdlc/design"
exit_code: 0
summary: string
per_artifact:
- { id: CMP-001, verdict: pass | revise, findings: [ ...42010 notes... ] }
asr_coverage:
- requirement_id: NFR-002
addressed_by: [ CMP-001, IF-001 ]
verdict: addressed | deferred_to_decision | unaddressed
note: string # optional — the evidence for this verdict
tradeoffs:
- { decision: string, gains: string, costs: string, affected: [ NFR-002, CON-001 ] }
sensitivity_points:
- { point: string, affected_requirements: [ NFR-002 ], note: string }
design_context_artifact
Stage 9 of design-orchestrator.
design_context_artifact:
assumptions: [ { id: A-1, statement } ] # architecture's own, e.g. single-writer DB
dependencies: [ { id: D-1, statement } ]
open_questions: [ { id: Q-4, statement, owner } ] # inherited IDs preserved + newly raised
drivers:
asrs: [ { requirement_id, driver_type, significance } ]
tradeoffs: [ { decision, gains, costs, affected: [] } ]
sensitivity_points: [ { point, affected_requirements: [], note } ]
formatter_result
Stage 10 of design-orchestrator.
formatter_result:
files_written: [ ".sdlc/design/components/CMP-001-...md", ... ]
index: ".sdlc/design/index.yaml"
review_queue_count: 0
context_artifact: ".sdlc/design/assumptions.md"
drivers: ".sdlc/design/drivers.md"
diagrams:
generated: [ "DIA-001", "DIA-002", "DIA-003" ]
exit_code: 0
validator_rerun: { exit_code: 0 }
traceability_rerun: { exit_code: 0, warnings: [] }
The QA stage
generation_brief
Stage 4 of qa-orchestrator.
generation_brief:
qa_context: { ...the Stage 1 object, forwarded verbatim... }
scripts_dir: string # absolute; supplied by the skill, threaded to the formatter
requirement_digest: # what Stage 2 read, so specialists do not re-read
- id: FR-001
type: functional
title: string
tier: string
priority: string
verification_method: test | inspection | analysis | demonstration
acceptance_criteria: string # BODY — "## Acceptance Criteria"
- id: NFR-004
type: non_functional
title: string
verification_method: test | inspection | analysis | demonstration
quality_attribute: string # BODY — "## ISO 25010 Characteristic"
fit_criterion: string # the threshold — carried so the specialist can cite it, never restate it
scenario: # BODY — the six-part QAS under "## Quality Attribute Scenario"
source: string
stimulus: string
environment: string
artifact: string
response: string
response_measure: string
- id: CON-002 # and BR- entries, in the same shape
type: constraint # or business_rule
title: string
tier: string
priority: string
description: string # the normative rule itself, from frontmatter
fit_criterion: string # the countable or binary check — cite it, never restate it
verification_method: test | inspection | analysis | demonstration
bounds: [ FR-009, NFR-005 ] # BODY — "## Bounds / Implemented by" or "## Implemented by"
design_digest:
- id: CMP-013
title: string
responsibility: string
boundary: string
- id: IF-007
title: string
provider: CMP-013
operations: [string, ...]
id_block:
behavioural: [TS-001, TS-002, TS-003, TS-004] # allocated to the behavioural specialist
quality_attribute: [TS-005]
created_at: "YYYY-MM-DD" # today's date, passed so all files agree
assigned:
behavioural: [FR-001, FR-002, CON-002, BR-001]
quality_attribute: [NFR-004]
draft_test_strategies
Stage 5 of qa-orchestrator.
draft_test_strategies:
items:
- id: TS-001
type: test_strategy
title: string
description: string
test_level: unit | integration | contract | e2e | performance | security # required when verification_mode is test
risk_level: high | medium | low
risk_rationale: string
enforcement: ci | manual | none
verification_mode: test | inspection | analysis | demonstration
traces_from: [ FR-001, CMP-013 ]
traces_to: { tests: [], code: [] }
status: draft
confidence: high | medium | low
created_at: "YYYY-MM-DD"
body_markdown: |
# ...rendered body...
assumptions: [ ...optional sibling statements... ]
dependencies: [ ...optional sibling statements... ]
critique_report
Stage 6 of qa-orchestrator.
critique_report:
gate: pass | fail
validator: # structural-gate result, folded back in from the formatter (Stage 7)
command: "python3 <scripts>/validate_qa.py .sdlc/qa"
exit_code: 0
summary: string
per_item:
- id: TS-001
verdict: pass | revise
findings: [ ...quality and altitude notes... ]
coverage:
uncovered_asrs: [ ...requirement IDs no item covers, with justification... ]
level_gaps: [ ...test levels the set omits, with justification... ]
qa_context_artifact
Stage 6.5 of qa-orchestrator.
qa_context_artifact:
assumptions:
- id: A-1
statement: string
dependencies:
- id: D-1
statement: string
open_questions:
- id: Q-1
statement: string
owner: string
accepted_risks: # the "what we are not testing" register
- id: AR-1
statement: string
requirement: FR-007 # or a design ID
rationale: string
unenforced: # the "written but not gated" register
- id: UE-1
item: TS-014 # the strategy item, not a requirement
title: string # the item's own title, carried through
covers: [BR-002] # the item's traces_from, carried through
rationale: string # the item's own risk_rationale
formatter_result
Stage 7 of qa-orchestrator.
formatter_result:
files_written: [ ".sdlc/qa/strategy/TS-001-...md", ... ]
strategy: ".sdlc/qa/qa-strategy.md"
index: ".sdlc/qa/index.yaml"
review_queue_count: 0
validator_rerun: { exit_code: 0 }
traceability_rerun: { exit_code: 0, warnings: [] }
Why the shapes are these shapes
Two things the blocks cannot say for themselves.
Why capabilities travel as prose, not IDs
The design stage’s component and interface authoring is circular by nature. A
component declares depends_on: [IF-…]; an interface declares
provider: CMP-…. Whichever specialist runs first would have to cite IDs the
other has not allocated yet.
The transient fields are how the cycle is broken. component-specialist
declares what a component needs as prose, never as an IF- ID — the
example in plugin/agents/component-specialist.md is literally
capability: "take card payments" with a rationale beside it. Every other
field it returns goes into the component’s frontmatter verbatim, so
depends_on comes back present and empty. interface-specialist then turns
each capability into an interface and reports, in consumed_by, which
components asked for it. The orchestrator’s Stage 7 does the wiring, and does
it mechanically: for each interface, for each component in consumed_by,
append the interface ID to that component’s depends_on, deduplicate, sort.
No judgment, because there is none left to apply.
The check that follows matters as much as the wiring. Every declared capability
must be satisfied by exactly one interface. An unsatisfied or duplicated
capability is a re-dispatch, not an accepted gap — and an IF- ID appearing in
a component draft at all means the specialist guessed at the graph, which is
also a re-dispatch.
Where each transient dies, and what breaks if it survives
The two stages strip their transients at different points, and the difference is not an inconsistency.
The design stage’s three — required_capabilities on components,
consumed_by and satisfies_capabilities on interfaces — are consumed and
dropped at Stage 7, before the critique gate. By the time design-critic
sees the set, the graph is already wired as IDs, which is what the critic needs
to judge.
The requirements stage’s one, applies_to, survives further on purpose. It
names the requirements each constraint or business rule bounds, and the
requirements stage has no back-fill stage of its own — so the orchestrator
carries it through the merge and through the critic, and
requirements-formatter performs the back-fill at Stage 7, writing each
constraint’s ID into the named requirements’ traces_from and stripping
applies_to before writing anything.
What breaks if one survives is the structural gate, immediately. Neither field
is in its schema, requirement.schema.json sets additionalProperties: false
and design.schema.json sets unevaluatedProperties: false, so an artifact
carrying a transient fails validation on the formatter’s own re-run. The
failure is loud and local, which is the intended outcome — a transient that
leaked silently would put a field in the frontmatter that nothing downstream
knows how to read.