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:
| Field | Type | Source | Required |
|---|---|---|---|
repo_root | pathlib.Path | bridge's resolved repo root | yes |
mission | str | runtime DAG mission key ("software-dev") | yes |
action | str | normalized step ID (see normalization rule) | yes |
actor | str | bridge'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_depth | int | default 2 (executor's default) | no |
Normalization rule
Legacy DAG step IDs map to the new composition action IDs:
| Legacy step ID | Normalized action |
|---|---|
specify | specify |
plan | plan |
tasks_outline | tasks |
tasks_packages | tasks |
tasks_finalize | tasks |
implement | implement |
review | review |
| anything else | passes 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:
| Action | Pass condition |
|---|---|
specify | feature_dir / "spec.md" exists |
plan | feature_dir / "plan.md" exists |
tasks | tasks.md exists AND ≥1 tasks/WP.md AND every WP.md has raw dependencies: frontmatter (reuse _has_raw_dependencies_field) |
implement | existing _should_advance_wp_step("implement", feature_dir) returns true |
review | existing _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
| Outcome | Bridge return |
|---|---|
| Composition succeeded AND guard passed | success exit; emit invocation_id chain to trail |
Composition raised StepContractExecutionError | non-zero exit; structured error with executor message |
| Composition succeeded but guard failed | non-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
StepContractExecutionErroras a non-zero CLI exit. - Guard parity: each post-action guard's failure cases match the legacy
_check_step_guardsfailure 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 ID | Required | Has command | Has delegations | Notes |
|---|---|---|---|---|
bootstrap | yes | yes (charter context) | no | Mirrors specify/plan/implement/review contracts. |
outline | yes | no | yes (tactics: problem-decomposition, requirements-validation-workflow) | Produces kitty-specs/<mission>/tasks.md. |
packages | yes | no | yes (directives: 010, 024) | Produces kitty-specs/<mission>/tasks/WP##.md files. |
finalize | yes | yes (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-missionMUST fail repository validation. - Loading with a
delegates_to.kindoutside the supported set MUST fail repository validation. - Loading with a duplicate step
idwithin the contract MUST fail repository validation.