Skip to Content
ArchitectureHand-off contracts

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.

Last updated on