Contracts

runtime-bridge-composition-api.md

Contract — Runtime-bridge ↔ StepContractExecutor handoff

Mission: software-dev-composition-rewrite-01KQ26CY Call site: src/specify_cli/next/runtime_bridge.py Callee: specify_cli.mission_step_contracts.executor.StepContractExecutor.execute

Inputs

The bridge MUST construct a StepContractExecutionContext with the following fields:

FieldTypeSourceRequired
repo_rootpathlib.Pathbridge's resolved repo rootyes
missionstrruntime DAG mission key ("software-dev")yes
actionstrnormalized step ID (see normalization rule)yes
actorstrbridge's resolved actor; default "unknown"yes (default)
profile_hint`str \None`operator --profile if present
request_text`str \None`invocation prompt text if present
mode_of_work`ModeOfWork \None`from CLI if present
resolution_depthintdefault 2 (executor's default)no

Normalization rule

Legacy DAG step IDs map to the new composition action IDs:

Legacy step IDNormalized action
specifyspecify
planplan
tasks_outlinetasks
tasks_packagestasks
tasks_finalizetasks
implementimplement
reviewreview
anything elsepasses through; bridge does NOT enter composition branch

Dispatch decision

IF mission == "software-dev"
AND normalized_action ∈ {"specify","plan","tasks","implement","review"}:
    result = StepContractExecutor.execute(context)
    run_post_action_guard(action, feature_dir)
ELSE:
    fall through to existing legacy DAG dispatch unchanged

Post-action guard contract

After a successful composed run, the bridge MUST invoke a guard equivalent to today's _check_step_guards for the same artifacts. Per-action guard inputs and pass conditions:

ActionPass condition
specifyfeature_dir / "spec.md" exists
planfeature_dir / "plan.md" exists
taskstasks.md exists AND ≥1 tasks/WP.md AND every WP.md has raw dependencies: frontmatter (reuse _has_raw_dependencies_field)
implementexisting _should_advance_wp_step("implement", feature_dir) returns true
reviewexisting _should_advance_wp_step("review", feature_dir) returns true

Guard failure MUST surface as a non-zero CLI exit with a clear message (same UX as today).

Outputs

OutcomeBridge return
Composition succeeded AND guard passedsuccess exit; emit invocation_id chain to trail
Composition raised StepContractExecutionErrornon-zero exit; structured error with executor message
Composition succeeded but guard failednon-zero exit; guard failure message

Invariants

1. C-001: Bridge MUST NOT call ProfileInvocationExecutor directly for composed actions; only via StepContractExecutor.execute. 2. C-002: Bridge MUST NOT generate text or call models. Composition produces invocation payloads; the host harness interprets them. 3. C-003 / FR-007: Lane-state writes inside any composed step or post-action guard MUST go through emit_status_transition. No raw lane string writes. 4. C-008: Composition dispatch MUST NOT fire for any mission key other than "software-dev" in this slice. Other missions stay on the legacy DAG path.

Test coverage requirements

  • Positive: each of the five composed actions routes through the executor and produces an invocation_id chain.
  • Negative: action outside the composed set falls through to the legacy DAG path (asserted by mocking the executor and verifying no call).
  • Negative: missing contract for a composed action surfaces StepContractExecutionError as a non-zero CLI exit.
  • Guard parity: each post-action guard's failure cases match the legacy _check_step_guards failure cases for the same artifacts.

tasks-step-contract-schema.md

Contract — tasks.step-contract.yaml schema

Mission: software-dev-composition-rewrite-01KQ26CY Artifact: src/doctrine/mission_step_contracts/shipped/tasks.step-contract.yaml Schema source of truth: doctrine.mission_step_contracts.models.MissionStepContract

YAML shape (binding)

schema_version: "1.0"      # exact string required by repository loader
id: tasks                   # uniquely names this contract within the shipped set
action: tasks               # routes via repo.get_by_action(mission, action)
mission: software-dev       # mission key (exact match)

steps:                      # ordered; executor walks each in sequence
  - id: <step-id>           # unique within this contract; namespaces with contract_id externally
    description: <text>     # human-readable; reproduced in invocation request_text
    command: <string|null>  # optional declared command; executor declares only, never runs
    inputs:                 # optional; CLI-style flag/source pairs
      - flag: --profile
        source: wp.agent_profile
        optional: true
    delegates_to:           # optional; selects DRG artifacts via action context
      kind: <directive|tactic|paradigm|styleguide|toolguide|procedure|agent_profile>
      candidates: [<id-or-name>, ...]
    guidance: <text>        # optional; appended to invocation request_text

Required steps for this contract

Step IDRequiredHas commandHas delegationsNotes
bootstrapyesyes (charter context)noMirrors specify/plan/implement/review contracts.
outlineyesnoyes (tactics: problem-decomposition, requirements-validation-workflow)Produces kitty-specs/<mission>/tasks.md.
packagesyesnoyes (directives: 010, 024)Produces kitty-specs/<mission>/tasks/WP##.md files.
finalizeyesyes (spec-kitty agent mission finalize-tasks)yes (directive: 024)Validates dependencies; finalizes WP frontmatter.

Validation rules (enforced by executor + tests)

1. MissionStepContractRepository.get_by_action("software-dev", "tasks") must return the loaded contract. 2. Every delegates_to.candidates value must, after action-context resolution, appear in the ResolvedContext.artifact_urns set produced by resolve_context(graph, "action:software-dev/tasks", depth=2). Any unresolved candidate goes into unresolved_candidates on the step result and is logged but does not fail the run. 3. The bootstrap step's command is declared only. The host owns execution. (Same convention as the four shipped contracts.) 4. The finalize step's declared command is spec-kitty agent mission finalize-tasks; this is the canonical existing CLI surface for finalize semantics. 5. No step may write lane state directly. Any lane-state side-effect must invoke emit_status_transition. (Cross-cutting; not a YAML rule but a runtime invariant.)

Negative tests

  • Loading the contract with mission: other-mission MUST fail repository validation.
  • Loading with a delegates_to.kind outside the supported set MUST fail repository validation.
  • Loading with a duplicate step id within the contract MUST fail repository validation.