Mission Specification: Mission Status contract v1

Mission Branch: issue-5558-mission-status-contract-v1 Created: 2026-10-01 Status: Draft Input: Issue #5558 - "Mission Status contract v1: contracts/mission-status/openapi.yaml (split layout, Node-free tooling, CI)". Part of #5528; takes over the transport question from the closed #460; implements the content specified in #5529 (overview) and #957 (detail). Design: the maintainer design comment on #5528 (comment 5937187090).

Summary

The Mission Status Read API is built contract-first. A versioned OpenAPI 3.1 document is the seam between a future read service and the dashboard and Desktop screens, so both sides can be built in parallel against one file. This Mission delivers that file, the conventions that let it scale past one module, and the CI that keeps it honest:

  • a split, multi-file contract under contracts/ (_shared/ plus mission-status/), covering a read-only v1 slice of five resources;
  • Node-free validate, bundle, lint and breaking-change checks in a new path-filtered workflow;
  • a reality-check test that proves the contract fits this repository's own Missions as today's readers see them;
  • a release workflow that publishes the bundled document and its checksum, with a dry run proven in PR 4;
  • CODEOWNERS for contracts/.

It adds a contract and CI only. No Python product code under src/ changes, no status event schema changes, and no CLI behaviour changes.

Terminology (binding for this spec and for the contract)

  • Mission, never "feature" (charter Terminology Canon). Contract schema names, examples and descriptions follow the same rule.
  • Status lane: one of the work-package lifecycle states of the status domain (glossary term lane: planned, claimed, in_progress, for_review, in_review, approved, done, blocked, canceled; plus the non-display values genesis and uninitialized). This is the only meaning the contract gives the word lane. The canonical list of the nine values is the lane entry of .kittify/glossaries/spec_kitty_core.yaml; the Lane entry in docs/context/orchestration.md still lists seven values and is marked applicable to 1.x and 2.x. That is known drift, cited here and not fixed by this Mission.
  • Code lane, repo-root lane, execution lane: the glossary (docs/context/topology.md) defines Code lane (a lane that resolves to a .worktrees/ lane worktree and a lane branch; only lanes and lanes_with_coord Missions have them) and Repo-root lane (the one-lane manifest of a single_branch Mission). Execution lane is kept only as the umbrella phrase for what a Mission's lanes.json records across both (docs/architecture/execution-lanes.md); it is not itself a glossary term. None of these is part of v1 and none appears in the contract's schemas. Where the contract, README or an example must name one, it says code lane or repo-root lane, and uses execution lane only as the umbrella. This spec never uses the bare word "lane" for either.
  • Stream cursor: the event-log tail position {offset, invariant} of the CLI's tail reader (a byte offset on a line boundary plus a SHA-256 of the last consumed line). Page cursor: the opaque pagination token in _shared/. The two are different schemas with different names; the bare word "cursor" is not used in contract field names.
  • Lifecycle status: the Mission-level value (active, planned, done, draft, discarded). Mission phase (contract field phases): a step of the Mission workflow (specify, plan, tasks, implement, review). It is not the glossary term phase (.kittify/glossaries/spec_kitty_core.yaml: a grouping container for mission primitives, not the enforcement unit); the contract and the README say so wherever phases is described, and the field keeps the name phases because #5558 uses it. Phase label: the optional free-text phase grouping field in a work package's frontmatter (contract field phaseLabel). Three different things with three different names, none of them the glossary phase.
  • Bundle: the single resolved openapi.yaml produced from a module's split files by CI. It is a build product and is never committed.
  • Provisional: an element marked x-provisional that may change without a major version bump until decided on #5528.

Clarifications

The following are recorded decisions for this Mission. They bind the spec, the plan and the implementation; a reviewer checks against them rather than re-opening them, with one stated exception: the realised form of CL-2's third bullet and of CL-4's dry-run placement diverges from the literal reading, and is recorded below as the flagged deviations DEV-1 and DEV-2, which need maintainer confirmation (OQ-4) and are not settled decisions. They come from two sources that are kept apart here. CL-1 to CL-4 are maintainer decisions made for this Mission about its process and its CI placement. They are recorded in this spec and are not quotes of #5558 or of the #5528 design comment, which do not contain them; the PR bodies repeat them so the maintainers can confirm them when the design is acknowledged (CL-1), and PR 1 lists DEV-1 and DEV-2 as explicit confirmation items (they are also asked on #5528). CL-1 and C-001 are amended by the operator ruling of 2026-10-02 (DD-21): the single Mission PR is replaced by six stacked seam PRs. CL-5 to CL-9 restate the design in #5558 and in the #5528 design comment, with one exception: the CODEOWNERS handles in CL-6 are also a maintainer decision made for this Mission. Everything the spec author derived from the charter, the code and the data (D-1 to D-19 below, including the silent-success rule D-15, the CI gate analysis D-16 and the release dry-run placement D-17) is a derived decision and open to reviewer challenge.

CL-1 - Build now; every PR of the stack stays a draft until the design is acknowledged (maintainer decision for this Mission, shape amended by the operator ruling of 2026-10-02, DD-21)

The Mission is built now. By the operator ruling of 2026-10-02 (DD-21, which supersedes the earlier "single Mission PR" wording of this clarification and of C-001) it is delivered as six stacked seam PRs: (1) WP01 and WP02; (2) WP03 to WP05 (the contract content, ending at preview point p0, the UI early-start unit); (3) WP06 and WP07; (4) WP08 and WP09; (5) WP10; (6) WP11 and WP12. PR 1 targets main; PR n (n = 2 to 6) targets the head branch of seam n-1 (plan, "PR stack"). Every PR is opened as a draft and all six stay drafts until a maintainer acknowledges the design on #5528 (one acknowledgement covers the stack). Until then none is marked ready for review and none is labelled ready-for-squad. Each PR body links the acknowledgement once it exists. Reading rule for this spec: where it says "the PR", "the PR body" or "green in the PR", it means the PR of the seam that carries that evidence, as assigned by the per-PR evidence table in the plan ("PR stack"); the stack as a whole is meant only where the text says "the stack". This is how the contributing guide's "large changes are agreed first" rule is satisfied for a change that introduces a new top-level tree, a new toolchain and a new release stream.

CL-2 - The reality-check test, its marker, and its CI placement (maintainer decision for this Mission)

  • The reality-check test is tests/contract/test_mission_status_reality.py (a new path) and carries pytest.mark.corpus as a real marker application (a module-level pytestmark entry or a decorator), not a prose mention.
  • contracts/** is added to the CI router's corpus path filter in .github/workflows/ci-router.yml, so a contracts-only change selects the router's marker-selected corpus job.
  • The new path-filtered contracts workflow ties into the reality check without running it twice per change. This is the maintainer decision, read literally: the contracts workflow also exercises the reality check against the contract it builds, and the reality check never runs twice per change.
  • DEV-1 (flagged deviation from the literal reading; maintainer confirmation requested, non-blocking, OQ-4). The form this spec builds is not the literal one. The contracts workflow proves, with a non-pytest resolver-parity script, that the reality check's resolution of the contract equals the CI bundle, and the reality check itself runs once, in the router's tests-corpus job (D-16). The intent (the reality check is exercised against the contract the contracts workflow builds, and never runs twice) is kept; a pytest run inside the contracts workflow is not built. The alternative is a literal pytest run of the reality check in the contracts workflow, which would need a priced ledger row, a WORKFLOW_FILES row and a deselection from tests-corpus (D-16). If the maintainer picks the literal reading, these change: FR-017 (no-pytest bullet), FR-019 (last two bullets), FR-020, FR-025 (guard-tests row and runner table), D-16, D-19, OQ-1, SC-007 and SC-008, and C-003. The PR body carries DEV-1 as a confirmation request, in these words.

How these three decisions are realised against the repository's architectural gates, and which job is the single blocking home, is derived in D-16 and specified in FR-017 and FR-020.

CL-3 - Release tag namespace (maintainer decision for this Mission)

The release tag is contract-mission-status-v1.0.0: no leading v, so the CLI's v.. release machinery never sees it. This was verified on this checkout: .github/workflows/release.yml has a single tag trigger, v.. (plus a manual dispatch that takes an existing release tag); scripts/release/validate_release.py (discover_release_tags) keeps only tags that start with v and match the release version pattern; src/specify_cli/release/changelog.py lists tags with the pattern v*. Non-v tags already exist in the repository (crossrepo-bugfix-pack-2026-02-24, saas-hardening-2026-02-24). The contract release workflow's tag trigger must not match any CLI release tag, and the CLI release trigger must not match a contract tag; this is asserted (FR-022).

CL-4 - Release workflow ships in the stack (PR 4); the tag is pushed after merge (maintainer decision for this Mission)

The PR ships the release workflow with a dry run that builds the bundle and its sha256 as a workflow artifact, and that dry run is green in PR 4 (the seam that carries the release workflow and the dry-run job). After merge, a maintainer pushes the tag contract-mission-status-v1.0.0. That tag push is recorded as a close-out step of this Mission (in the PR body and the Mission close-out record), not as a follow-up issue. Because GitHub resolves a manually dispatched workflow from the default branch, the dry run's evidence in the PR comes from the release-dry-run job of the contracts workflow, a pull_request run on the PR branch that executes the same contracts/tools/ release script as the release workflow (D-17, FR-022). The release workflow itself has no pull_request trigger (DEV-2).

CL-5 - Contract-first, split layout, conventions

  • OpenAPI 3.1, contract-first. Humans edit split files; CI produces the bundle; the bundle is never committed.
  • Layout: contracts/README.md (conventions); contracts/_shared/ (cross-module pieces only: Problem as application/problem+json, the page cursor, PageInfo; schemas/, parameters/, responses/, each with an _index.yaml registry); contracts/mission-status/ with a root openapi.yaml (info, servers, tags, and a path-to-file map only), paths/ (one file per path item, named after the path with / turned into _ and {param} kept, for example missions_{missionId}.yaml), schemas/ (one file per schema, named like the schema, plus _index.yaml), parameters/, responses/, examples/, and CHANGELOG.md.
  • Each module is one document with its own info.version and the /api/v1 prefix. Later modules (for example write surfaces) are sibling folders versioned independently.
  • $refs between files are relative file paths; there are no hand-written ~1 JSON pointers; the root maps paths to files.
  • contracts/fixtures/ (the existing handoff fixtures read by tests/contract/test_handoff_fixtures.py) is left untouched.

CL-6 - Node-free tooling and CI shape

  • Validate and bundle: a small Gradle build under contracts/ using the openapi-generator Gradle plugin (JVM), which reads multi-file specs; it validates each module and bundles it with the openapi-yaml generator.
  • Lint: vacuum (a Go binary) with a Spectral-format ruleset kept in contracts/.
  • Breaking changes: oasdiff (a Go binary) compares each module's bundle with its last release; a breaking change fails unless the major version moves.
  • Tool versions are pinned, downloaded artefacts are checksum-verified, and third-party GitHub Actions are pinned to full commit SHAs (the repository's existing workflows do the same).
  • The workflow is path-filtered on contracts/** and separate from the Python suites, so the Python suites' critical path is unchanged.
  • CODEOWNERS for contracts/ names @stijn-dejongh and @MOES-Media (#5558 says only "CODEOWNERS for contracts/"; the two handles are a maintainer decision made for this Mission). No .github/CODEOWNERS exists today; it is a new file. The charter records that nothing on GitHub enforces review, so this is advisory routing, and the PR body says so.
  • No Node toolchain is introduced anywhere.

CL-7 - The v1 slice

Read-only resources, all under /api/v1: GET /project, GET /missions, GET /missions/{missionId}, GET /missions/{missionId}/work-packages/{wpId}, and GET /events (text/event-stream, one schema per event's data, documented as a convention because OpenAPI has no native event-stream schema).

CL-8 - Data decisions

  • Raw nine-status-lane counts, with no grouping. Board columns are a documented consumer mapping, not part of the contract.
  • Identity is the ULID missionId. displayNumber is display-only (numbers repeat).
  • Lifecycle status uses today's five values: active, planned, done, draft, discarded.
  • Topology has five values: lanes, single_branch, coord, lanes_with_coord, unknown.
  • Staleness and next action are marked x-provisional until decided on #5528.
  • Mission phases: specify, plan and tasks come from artifacts and lifecycle events; implement and review are derived from status lanes and are marked as derived.
  • No absolute paths (feedback_path, record_path, prompt_path, feature_dir) and no e-mail addresses in any payload or example. Actors are agent, profile and role handles.
  • Every v1 schema field cites its source in the status domain.

CL-9 - Scope exclusions

Out of v1: artifact reads (#5533) and write surfaces (#5231). Out of this Mission: the read service implementation and the dashboard and Desktop screens (separate Missions against this contract).

Flagged deviations (maintainer confirmation requested, OQ-4)

  • DEV-1 is recorded in CL-2 (third and fourth bullets): the reality check runs once in tests-corpus, and the contracts workflow proves resolver parity, instead of running pytest itself.
  • DEV-2 (CL-4 read literally). Read literally, CL-4 has the release workflow carry its own dry run that is green in the PR. The form built here keeps the release workflow to a tag push and a workflow_dispatch (with a dry-run mode that never publishes), and puts the PR-green dry run in the contracts workflow as the release-dry-run job, which runs the same release script. The artifact CL-4 asks for (the bundle and its sha256, built, verified and uploaded, green in the PR) is produced; only the workflow file that holds the PR-triggered job differs. The reason is recorded in D-17. If the maintainer picks the literal reading, these change: FR-017 (fleet-verdict bullet: a second PR_WORKFLOWS entry, a second workflow_run name, and an edit to scripts/ci/fleet_main.py), FR-022, D-16 item 6, D-17, R-12, C-003 and SC-008.

Derived decisions (spec author; reviewable)

These follow from the grounded data shapes, from the code and charter as read on this checkout, and from the decisions above. A reviewer may overturn any of them; if so, the affected FRs change with them.

1. tests/architectural/test_ci_corpus_trigger_completeness.py pins the set of corpus-marked test modules to a hand-maintained registry (_CORPUS_MARKED_MODULES) with equality in both directions, so every new tests/contract module that carries the marker (the reality check and any helper unit-test module) needs its own registry row in the same change. The marker is detected by a single-line regular expression: pytestmark = ...pytest.mark.corpus must be on one line (as in tests/contract/test_example_round_trip.py), or the marker is a decorator; a multi-line pytestmark = [ list is not detected. The file's _CORPUS_GLOBS and _CORPUS_DATA_ROOTS are documentation of the corpus path set, not a mirror of the router: today _CORPUS_GLOBS holds .kittify/release/downstream-verified.json, which the router filter does not, and no test compares the two (test_every_corpus_data_root_is_covered_by_a_trigger_glob compares the roots only with the file's own globs). This Mission does not edit them: the gate needs only the registry rows, and open PR #5557 rewrites this same file so that _CORPUS_GLOBS is read from the live router (load_router().filters['corpus']), so a documentation edit would be a pure conflict risk with no gate behind it. A third transcription, non_src_router_groups.corpus in tests/release/ci_retirement_scrub.json, is not gated either and is left unedited. 2. tests/architectural/test_no_duplicate_suite_execution.py classifies a whole workflow as change-triggered when it has a pull_request trigger or a push with branch or path filters; only a workflow listed in NON_CHANGE_TRIGGERED_WORKFLOWS (or a tags-only push) is exempt, so a workflow_dispatch or tag-push job inside a workflow that also has a pull_request trigger still counts as per-change. Each (workflow, job) that reaches pytest needs a row in AUTHORIZED_PER_CHANGE_SUITE_JOBS; under the charter's Standing Order 5 such a row is priced debt that needs an owner, a rationale and an exit condition, and the empty-allowlist shape is preferred. 3. tests/architectural/test_workflow_coherence.py requires the set of workflows that invoke pytest to equal WORKFLOW_FILES in tests/architectural/_gate_coverage.py, and requires every router filter glob to match at least one tracked path (contracts/ does once contracts/ holds files). 4. .github/ci-module-registry.yml lists tests/contract as a registry-excluded directory whose only per-PR home is the marker-selected router corpus job and records that claiming it in a module row "would double-run it". The registry is not edited. 5. Two per-change jobs already select the corpus marker: tests-corpus in ci-router.yml (-m "corpus and not windows_ci", 10 minute timeout) and built-in-corpus-suite in packs.yml (the same expression over the whole tree with --cov=src/doctrine, 20 minute timeout, forced on for every push to main and in full mode, and on for pull requests touching its built_in filter paths such as packs/built-in/ and tests/doctrine/*). Both are authorised in the ledger and neither has a JVM. The reality-check test is therefore collected by both on a push to main and on a mixed diff, by existing design, and must pass in both. 6. Per-workflow gates the new workflow files meet: tests/ci/test_fork_guard.py (a workflow with a push, schedule or workflow_run trigger needs the canonical fork guard on every root job unless it is in EXEMPT_WORKFLOWS, today only release.yml); the fleet verdict (applicable_workflows in scripts/ci/fleet_verdict.py collects every workflow with a pull_request trigger and raises ValueError when that set differs from PR_WORKFLOWS; tests/ci/test_fleet_verdict.py pins PR_WORKFLOWS and pins the workflow_run list of ci-fleet-verdict.yml to the name: of every member; main_workflows in scripts/ci/fleet_main.py reads only PR_WORKFLOWS members and raises on any push trigger key other than branches and paths). Only a workflow with a pull_request trigger is a member, so the contracts workflow (which has one) is added to PR_WORKFLOWS and to that workflow_run list with the listed edits of FR-017, and the release workflow (tag push and workflow_dispatch only, D-17) is not a member and needs none of the fleet edits, in particular none to scripts/ci/fleet_main.py; and test_reusable_workflow_ceiling_respected in tests/architectural/test_module_shard_registry.py (all .yml files under .github/workflows/ against a ceiling of 20; 17 exist at this base).

  • D-1 Status-lane field names. Contract properties that carry a status lane use the stem statusLane (statusLane, statusLaneCounts, fromStatusLane, toStatusLane) and the enum schema is StatusLane, so a reader of the contract cannot confuse a status lane with an execution lane. The lane stem is not used.
  • D-2 Closed response schemas. Response object schemas are closed (additionalProperties: false, or unevaluatedProperties: false where composed). Documented open maps are the only exceptions (for example subtask id to status lane). Without this the reality check would pass on payloads carrying any junk, which is the vacuous-pass failure this Mission exists to avoid. Adding a response property is still a non-breaking, minor-version change.
  • D-3 Citation extensions. Every property of every resource schema carries x-source (repo-relative file path plus symbol, optionally a line number) or, where the value is computed from several sources or by a rule no existing reader implements, x-derived. x-derived is a structured value, {rule: <non-empty prose>, inputs: [<non-empty list>]}, where each input is either a code citation of the same form as x-source (repo-relative path plus symbol) or a contract-field reference (a property path in this module that must exist). The citation check resolves every x-derived input exactly as it resolves an x-source, and fails an x-derived that is free text, has an empty rule, or has an empty or missing inputs list (FR-010). Line numbers rot; the path and symbol are what the citation check resolves (FR-010). A cited path must exist in the checkout the PR targets. A derivation whose only implementation lived in the dashboard code removed by #5545 is therefore x-derived with its rule written in the schema description and its inputs cited (contract fields or surviving status-domain symbols), and carries no x-source to the removed file.
  • D-4 wpTotal definition. wpTotal is the number of work packages in the reduced status snapshot (len(snapshot.work_packages)): event-sourced, not a count of tasks/WP*.md files. Measured on this checkout (539 Missions with meta.json, scaffold of this Mission included): 3125 work package files; 2936 snapshot work packages when materialize_snapshot reads each Mission's own directory; and 52 Missions where the two sets differ (45 with fewer snapshot work packages than files, 7 with more). All of these figures are counted in own-directory mode (each Mission's own kitty-specs/<slug>/ directory), a pure function of the checked-out tree, so they are identical in a developer clone and in CI; they stay indicative until re-measured from a tests-corpus job log at implement start (FR-019). The reducer folds status transitions and annotations only, so a work package that has a file and perhaps a WPCreated lifecycle row but no transition events (or a Mission with no log at all) is not in the snapshot. That is a pre-existing reader limit which v1 pins as behaviour; it is not hidden: FR-019 lists and counts the disagreeing Missions, wpTotal may differ from the length of the detail workPackages array, and the PR reports the limit under the charter's pre-existing failure rule (C-009), citing a public upstream issue for it (filed by the implementer if none exists).
  • D-5 Lifecycle derivation. lifecycleStatus is derived from statusLaneCounts, wpTotal, discardedAt and acceptedAt by an explicit rule written in the schema description (x-derived), taken from the dashboard derivation being replaced: discarded when discardedAt is set; draft when wpTotal is 0 (an unreadable event log also reads as draft, with zero counts, and the reality check lists any such Mission); active when any work package is claimed, in_progress, for_review, in_review or approved; planned when none of those holds and any work package is planned or blocked; otherwise (every work package done or canceled) active until acceptedAt is set and done after. v1 pins today's quirks (a Mission whose only remaining work packages are blocked reads as planned; all-canceled with acceptedAt set reads as done). The dashboard function counted five board columns taken from tasks/WP*.md files with a non-genesis lane (claimed and in_progress as doing, for_review and in_review as for_review, blocked as planned, canceled as done), not snapshot work packages, so the rule above equals the old behaviour only where the file set and the snapshot set agree. The pinned expected values in FR-019 sample only such Missions; the disagreeing Missions are a listed, counted difference, never a silent one.
  • D-6 Mission-level fields without a reader are excluded. openDecisionCount (no reader exists), retrospective (the reducer matches an event_name envelope that does not occur in the corpus, so it is effectively never populated, and its record path is an absolute host path), done evidence (free-text commands and file lists), claim.shellPid (host-local), and any promptPath are not in v1.
  • D-7 Provisional semantics. Changes to an x-provisional element are recorded in the module CHANGELOG.md under a "Provisional" heading and do not by themselves require a major version bump. Removing x-provisional is a minor-version change, also recorded. The breaking-change check still runs over provisional elements and reports them separately; it never excludes them silently.
  • D-8 Event stream scope. Every event's data carries missionId and the stream cursor of that Mission's own log. A stream cursor is valid for exactly one physical event log (the primary and the coordination copy of a Mission's log are different files with different offsets). The event stream therefore never forwards raw log rows: it emits named, projected kinds only, by the allow-list projection in FR-007, and drops every other row kind. v1 has exactly three kinds: a status-lane transition kind, a Mission lifecycle marker kind, and a log-truncated (resync) kind. Annotation, decision-point, retrospective, reviewer-self-approval and unknown rows are not exposed (free-text notes and absolute record paths), and work package history exposes transitions only (FR-006), so the rule is the same in both places.
  • D-9 Event projection is mandatory. The CLI's tail reader passes every JSON row through (status transitions, annotations, lifecycle rows, retrospective rows), and the corpus contains absolute paths and e-mail addresses in exactly those rows. The contract's event schemas therefore define the projected shape, and the leak rules (FR-012) apply to event data.
  • D-10 Actor handles. #5558 and CL-8 say actors are agent, profile and role handles, so an actor projects to {tool, role, profile}, where tool is the agent handle (the key the status domain stores it under). The stored actor can also carry a model key; it is not part of the actor projection (a fourth handle would widen the exposed surface beyond the decision). The model of a work package's authored plan and resolved assignment is a separate field (authored.model, assignment.model); it is subject to the same handle pattern, the same null-on-mismatch rule and the same leak scan, and the model sentinel __resolved_model_absent__ projects to null. Each handle is a nullable ActorHandle string constrained to a conservative pattern (letters, digits, ., _, :, -; no @, no /, no whitespace; at most 128 characters). The mapping from the stored forms is fixed: (a) a structured actor (the four-key dict the status domain stores) is copied field by field for tool, role and profile through the handle filter, each becoming null when it is null or fails the pattern; (b) a plain string that matches the pattern becomes tool, with role and profile null, and colon-encoded forms such as claude:sonnet:implementer:implementer are not split; (c) any other string (an e-mail address, anything with @, / or whitespace, or longer than 128 characters) becomes all-null. The reality check asserts an e-mail actor projects to all-null and user or finalize-tasks project to tool with the others null, and examples/ holds one example per source form.
  • D-11 topology. A Mission with no stored topology (49 of 539 on this checkout), or with a stored value outside the four known shapes, projects to unknown. It does not inherit the runtime's "unstamped means lanes" default, because that would state something the metadata does not say.
  • D-12 First-release baseline. For the breaking-change check, "no previous release tag exists" is a distinct, loudly reported state, allowed only for the module's initial version; see FR-016.
  • D-13 missionType is an open string. Mission types can come from organisation packs, so the contract does not close the set to the three observed built-in values. A typeless Mission is null.
  • D-14 Free text, field classes and the leak scan. String values fall into three classes, and the leak scan (FR-012) treats each explicitly. (1) Authored markdown (promptMarkdown, returned only on request) passes through as authored; it is excluded from the scan of corpus-derived payloads and says so in its schema description, but it is scanned in examples/, which we author. (2) Strict fields (identifiers, handles and path-like values: missionId, mid8, slug, wpId, targetBranch, mergeTargetBranch, missionType, every ActorHandle, eventType, dependencies, ownedFiles, requirementRefs, subtask ids, trackerRefs): a value that fails the strict patterns projects to null where the field is nullable, and otherwise is a projection error that fails the reality check (an identifier cannot be redacted). (3) Human text fields (title, friendlyName, phaseLabel, every reason, cancellation.reason, review reference, nextAction): real titles and names legitimately contain /tmp or start with ~/, so these fields are scanned with the narrower "real host path" pattern and the e-mail pattern, pass through unchanged when clean, and when they fail have only the matching substring replaced by the fixed token [path] or [email] (the field stays non-null). Every redaction in the corpus is printed with its Mission and field, never silent. The two real cases ~/.kittify Runtime Centralization (a friendly name) and /tmp burn-down: sync (a work package title) are named positive-pass controls of the human text class.
  • D-15 Silent success is forbidden. Every check this Mission adds (validate, bundle, lint, breaking-change diff, citation check, leak scan, layout check, example and event-mapping checks, enum pinning, pin and checksum verification, resolver parity, no-pytest scan, reality check, release dry run, and every guard test) must fail loudly when it cannot do its job: missing tool, empty corpus, missing release tag, checksum mismatch, zero inputs. A check that passes because it examined nothing is a defect. Authority: the charter's Standing Order 5 (non-vacuous gates: a concrete floor plus a self-mutation test; architectural-gate-non-vacuity) and the acceptance-criteria non-vacuity tactic. FR-021 and the matrix in FR-025 make it testable.
  • D-16 CI gate analysis and placement. Read from this checkout, and binding on the plan through FR-017 and FR-020:

Chosen shape. The invariant is that the reality-check test body runs exactly once per change event class that matters, accounting for the packs job (item 5): on a pull_request diff that touches contracts/** and no path of the packs built_in filter, it executes in exactly one blocking per-change job, the router's tests-corpus; on a push to main, and on a diff that also matches the packs built_in filter, built-in-corpus-suite runs it as well, by existing design. The home is tests-corpus, selected by the one added router glob, so neither the router marker expression nor ci-module-registry.yml changes (C-003, SC-008). Neither new workflow invokes pytest on any event: the contracts workflow runs validate, bundle, lint, breaking-change and negative-test jobs plus the non-pytest contracts/tools/ check scripts (D-19) and a resolver-parity script that proves the reality check's resolution of the contract equals the CI bundle (FR-019, OQ-1; the script has its own planted-violation proof, FR-021, FR-025); the release dry run is a job of the contracts workflow, which is change-triggered as a whole and is counted as such, which is a further reason it runs no pytest; the release workflow has only a tags-only push and workflow_dispatch, which test_no_duplicate_suite_execution.py exempts as publication and manual events, and it runs no pytest either. Neither workflow therefore needs a ledger row or a WORKFLOW_FILES row. This is the realisation of "the contracts workflow also runs the reality check without running it twice" (CL-2): a literal pytest execution inside the contracts workflow on a pull request (DEV-1) would be a second per-change execution and would need a priced ledger row, which the "without running it twice" decision rules out. If a maintainer later wants one, it needs the owner, rationale and exit condition above, a WORKFLOW_FILES row and a deselection from tests-corpus, and is outside this Mission.

Where the dry run lives, and the alternative weighed (DEV-2). Two placements were weighed against CL-4 ("a dry run that builds the bundle and its sha256 as an artifact, green in the PR"). (A) A pull_request trigger on the release workflow: it makes the publishing workflow a PR_WORKFLOWS member, so it needs a second PR_WORKFLOWS entry, a second workflow_run name, an edit to production CI code (scripts/ci/fleet_main.py, whose main_workflows raises on any push key other than branches and paths, so the release workflow's tags key needs a skip) and a tags-only test case, and it puts a pull-request trigger on a workflow that publishes. (B, chosen) The release workflow is a tag push plus workflow_dispatch only (with a dry-run mode that never publishes; a manually dispatched workflow resolves from the default branch, so it cannot be the PR evidence); the PR-green dry run is the release-dry-run job of the contracts workflow, which is already a PR_WORKFLOWS member, and both workflows call the same release_check.py and one shared build wrapper under contracts/tools/, so they differ in triggers and publish steps only. B is a strict subset of A's shared-CI edits: no second PR_WORKFLOWS entry or workflow_run name, no fleet_main.py edit, one fewer test edit. A PR-green dry run needs a pull_request-triggered job somewhere; B places it in the one workflow that already has the trigger. Accepted cost: the PR proves the script and the build, not the release workflow's own YAML, whose tag-rule, namespace and publish-step guards (FR-022) are tested separately and whose publication step is proven only after merge (R-12). Planted fixtures. The planted dry-run fixture module (a module whose CHANGELOG.md lacks the version heading, and one with a mismatched version) lives under contracts/tools/fixtures/, outside module discovery, so the real dry run, the layout, validate and lint checks never see it. The negative-test job points release_check.py at that fixture root with the module-root parameter and asserts the failure message, with a clean control module on the same fixture root. The other planted fixtures of the contracts workflow (FR-016's baseline and candidate pair, the layout, citation and leak cases) live in the same place; the text-level leak scan's allowlist of planted-fixture directories (FR-012) is this directory. contracts/fixtures/ is not used (C-004).

  • D-17 Release dry run on a pull request. A pull_request event has no tag, so the dry run does not take one as input: contracts/tools/release_check.py (a D-19 script, with a module-root parameter) loops over every module discovered under contracts/ (the direct subdirectories of contracts/ holding a root openapi.yaml; _shared, fixtures, gradle and tools are not modules), derives the candidate tag contract-<module>-v<info.version> from each module's info.version, applies the tag rules to that candidate, and builds, checksums and verifies the bundle. On a tag push the pushed tag is the input and must equal the derived tag of the module it names. Publication is reachable only on a push of a tag ref (FR-022).
  • D-18 Event stream request shape. GET /events takes an optional missionId query parameter. A resume cursor requires missionId and is carried as the query parameter streamCursor in the string form <offset>:<invariant> (decimal byte offset, a colon, the 64 hex characters); the same string is the SSE id: field of every event and is also accepted in the Last-Event-ID request header, so a browser's automatic reconnect resumes. The JSON object {offset, invariant} remains the form inside data. An unscoped stream (no missionId) is live-only: it never replays, and a cursor sent without missionId is refused with a Problem. A new connection with no cursor is live-only from the moment of connection. Ordering is per Mission only (log order); there is no cross-Mission order guarantee, and a log-truncated event for one Mission may interleave with events of others. The stream sends a heartbeat: an SSE comment line (: heartbeat) every 30 seconds, which carries no data and is not an event. On log-truncated a consumer discards that Mission's stored cursor, re-reads the Mission detail, and resumes from the detail's streamCursor (the event's own cursor is the reset one: offset 0 with the empty digest). A consumer treats an unknown enum value inside a known event kind as an unknown value and does not fail; the contract's schemas stay closed, so a producer must not emit one.
  • D-19 Runner for every check. The new workflows run no pytest (D-16), so every Python-implemented check has a named runner that is not a second pytest execution of the corpus. Contract-owned checks are plain Python scripts under contracts/tools/ (new path; not a module, because a module is a directory with a root openapi.yaml, and the layout check ignores tools/), run by the contracts workflow as python contracts/tools/<script>.py using only the standard library and the repository's already-locked dependencies (PyYAML and jsonschema are declared in pyproject.toml). They never import or invoke pytest, and they never import from tests/. The shared contract resolver is a library of that directory, contracts/tools/contract_resolver.py: the reality check and the resolver-parity script both import it, so there is one resolution authority (FR-019). The no-pytest scan of contracts/tools/ is itself a contracts/tools/ script (no_pytest_scan.py, with a unit-test module in tests/contract/), so it runs on exactly the edits it guards: a tests/ci/ placement would not, because the module-matrix ci row selects only scripts/ci/ and .github/workflows/, and an edit confined to contracts/tools/ selects neither. It detects an import, a python -m invocation or a subprocess or shell string that reaches pytest, building its own search strings from fragments so it does not flag itself, and it scans every script in the directory including itself. The contracts workflow's contracts/ path filter selects it on every edit under contracts/, README and CHANGELOG included, so each check runs on exactly the edits it guards. Each script has a unit-test module under tests/contract/ that carries the single-line corpus marker and a registry row (D-16 item 1); the router selects it through the added contracts/ glob and tests-corpus runs it, once. On a push to main built-in-corpus-suite collects it too by existing design (D-16 item 5), so it is fast and needs no JVM. The contracts workflow's negative-test job runs each script against committed planted fixtures and asserts the failure message; that is the class A CI evidence of FR-021. Tests that guard the workflow files, scripts/ci/ and the fleet machinery stay in tests/ci/, where the module-matrix ci row selects them on every edit to .github/workflows/** (the workflow-file half of the no-pytest guard included). Nothing is placed in tests/ci/ that must run on an edit confined to contracts/. The per-check assignment is the runner table after the FR-025 matrix.

User Scenarios & Testing (mandatory)

User Story 1 - Build a screen or a service against one pinned, trustworthy file (Priority: P1)

A developer building the dashboard or Desktop screens, and a developer building the read service, each work against the same versioned contract. They take the bundled openapi.yaml from a release, verify its sha256, and generate or mock from it. Every field they see names where it comes from in the status domain, and fields that are not settled are marked provisional.

Why this priority: it is the reason the Mission exists; both sides can only proceed in parallel if the file is trustworthy and pinnable.

Independent Test: build the bundle from a clean checkout, verify that it contains the five v1 paths, that every property of every resource schema carries a source citation, that every provisional element carries x-provisional, and that every cited path resolves in the checkout.

Acceptance Scenarios:

1. Given a clean checkout, When the contract build runs, Then one bundled openapi.yaml is produced containing exactly the five v1 path items and no unresolved reference. 2. Given a resource schema property with no citation, When the citation check runs, Then it fails naming the property (and a property citing a path that does not exist also fails). 3. Given the bundle, When a consumer reads GET /missions and GET /missions/{missionId}, Then the status-lane vocabulary is the raw nine values, lifecycle status has five values and topology has five values, with no board-column grouping anywhere.

User Story 2 - A maintainer is protected from drift and accidental breaking changes (Priority: P1)

A maintainer opens a PR that touches contracts/**. A dedicated workflow validates, bundles, lints and breaking-change-checks every module, without touching the Python suites' critical path. Each check has been shown to fail on a planted violation, so a green check means something.

Why this priority: an unguarded contract drifts from reality and from its consumers; a guard that cannot fail is worse than none.

Independent Test: for each of validate, bundle, lint and breaking-change diff, a planted violation makes that check (and only that check's job) fail with a message naming the cause; the same fixture without the violation passes.

Acceptance Scenarios:

1. Given a module whose path file references a schema file that does not exist, When validate runs, Then it fails and names the unresolved reference. 2. Given a response schema that violates a ruleset rule, When lint runs, Then it fails; the clean fixture passes under the same ruleset (positive control). 3. Given a baseline bundle and a candidate that removes a response property or an enum value, When the breaking-change check runs without a major version bump, Then it fails; with a major bump it passes. 4. Given a PR that touches only contracts/, When CI runs, Then** the contracts workflow runs and none of the Python module-shard jobs is newly selected by this Mission's edits beyond the corpus job named in CL-2.

User Story 3 - The contract is proven against this repository's own Missions (Priority: P1)

Before anything is built on the contract, a test builds the overview and detail payloads for every Mission in this repository's kitty-specs/ with today's readers and validates each against the contract. A contract that does not match the status ledger fails here.

Why this priority: a contract written from prototypes or memory will not match the stored data; this is the gate that makes the "grounded in what the domain actually stores" claim checkable.

Independent Test: run tests/contract/test_mission_status_reality.py; it validates at least the floor number of Missions and every work package of those Missions, with zero exclusions, and fails on a planted mismatch.

Acceptance Scenarios:

1. Given the repository's committed Missions, When the reality check runs, Then every Mission's overview and detail payload, and every work package's detail payload, validates against the contract. 2. Given a planted payload carrying an e-mail address, an absolute path, an unknown status lane, or an extra property, When the same validation runs, Then it fails (and an identical clean payload passes). 3. Given a Mission with no stored topology, no event log, a null display number or a null status lane, When its payload is built, Then it validates using the documented nullable or unknown forms rather than being skipped.

User Story 4 - Cut a release consumers can pin (Priority: P2)

A maintainer pushes the tag contract-mission-status-v1.0.0 after merge. The release workflow publishes the bundled openapi.yaml and its sha256, and consumers pin by checksum. Before merge, a dry run of the same release script, as a job of the contracts workflow, builds the same two files as an artifact in the PR.

Why this priority: the pin is what consumers rely on, but it cannot be exercised for real until after merge, so the PR proves everything except publication.

Independent Test: the dry run's artifact contains a non-empty bundle and a checksum file, and sha256sum -c over them succeeds; a corrupted checksum makes the dry run fail.

Acceptance Scenarios:

1. Given the PR, When the contracts workflow's release-dry-run job runs, Then it uploads the bundle and its sha256 as an artifact, verifies them, and does not publish anything. 2. Given a tag whose version differs from the module's info.version, When the release workflow runs, Then it fails before publishing. 3. Given the contract tag and the CLI release tags, When the workflow trigger patterns are compared, Then neither matches the other's tags.

User Story 5 - Future modules follow the same conventions, and the right people review (Priority: P2)

A contributor adding a second module (for example write surfaces) reads contracts/README.md, copies the layout, and the same workflow covers it. CODEOWNERS routes contracts/ changes to the contract owners.

Why this priority: the conventions are the long-term value; this Mission only needs to make them discoverable and enforced.

Independent Test: add a throwaway second module folder following the README and confirm the workflow picks it up with no workflow edit; remove its root file and confirm the discovery check fails rather than skipping.

Acceptance Scenarios:

1. Given contracts/README.md, When a reader looks for naming, $ref, versioning and provisional conventions, Then each is stated with an example. 2. Given .github/CODEOWNERS, When it is parsed, Then contracts/ is owned by both named handles.

Edge Cases

  • Empty or shrunken corpus. The reality check requires a floor of at least 500 Missions with meta.json (539 measured on this checkout, scaffold of this Mission included). Zero Missions, or fewer than the floor, fails loudly; it never passes vacuously. It also fails if it validated zero work packages, or if a status lane, topology value or lifecycle value that must be exercised never occurs and no fixture covers it.
  • Values the corpus cannot exercise. discarded_at occurs in zero committed Missions, so discarded cannot be proven from the corpus. A fixture-built payload (positive control) proves the branch, and the examples validate.
  • No previous release tag. The first release has no baseline. The breaking-change check reports that state explicitly (FR-016), allows it only for the initial module version, and fails in every other case, including a shallow checkout that cannot see tags.
  • Field absent from real data. topology is absent on about 9 percent of Missions (49 of 539); mission_number is null on about half (255 of 539); mid8 resolves to an empty string when there is no mission_id; a work package with no events has a null status lane; some Missions have no event log on the primary checkout (21 of 539; coordination or single-branch Missions among them) and read as draft; resolved-binding fields exist for only about a quarter of work packages. Each projects to a documented nullable, empty or unknown form. A required property absent from real data is a contract defect to fix, never a reason to skip the Mission.
  • Unknown enum values. A status lane, lifecycle status or topology value from real data outside the contract's enum fails the reality check loudly (the contract lags the domain); it is neither coerced silently nor dropped. Topology is the one deliberate exception: unstamped and unrecognised values map to unknown (D-11).
  • Sentinels and variants in real data. The model sentinel __resolved_model_absent__ projects to null; frontmatter agent values can be dicts, plain strings or colon-encoded strings; observed execution_mode values include a quoted "code_change"; actors can be an e-mail address, user, finalize-tasks, migration. The projection normalises each explicitly and documents it; it does not skip the record.
  • Duplicate display numbers. Display numbers repeat (41, 45, 65, 80 and 83 repeat in the corpus). displayNumber is never used as an identifier, and the reality check asserts missionId is unique across the corpus.
  • Missions without a usable ULID. The dashboard's legacy:<slug> and orphan:<slug> pseudo-keys are dormant in this corpus and are not part of v1. A Mission directory with no valid ULID mission_id is not representable; the reality check fails on one rather than inventing an id.
  • Absolute paths and e-mail in source rows. About 383 event lines contain an absolute path (feedback_path on 271 events, record_path on retrospective rows) and e-mail addresses occur in MissionCreated payload.actor and accepted_by. None may reach a payload, an event or an example.
  • Readers that write. Some existing read paths write to disk (the dashboard's encoding auto-fix; a past query-only next call that rewrote a tracked status.json). The reality check proves the readers it uses left every tracked Mission artifact byte-identical.
  • Router reshaping while this PR is open. An open CI-rework PR (#5557) re-homes the corpus job (a blocking router job running named node ids, with the marker-selected remainder run elsewhere). After rebase the test must still be selected by exactly one blocking job on a contracts-only pull request (D-16); this is re-verified, not assumed.
  • Dangling citations. PR #5545, which removes the CLI-bundled dashboard that many catalogue citations name, has merged on upstream main after this branch's base. A citation that stops resolving fails the citation check loudly after rebase. Derivations that existed only in that dashboard code become contract-owned x-derived rules (D-3, FR-010), and the plan rebases onto current main before implementation starts.
  • Unknown or pre-existing red tests. The reality check is judged against a recorded baseline (see Risks), so a pre-existing red elsewhere on the base is neither blamed on nor hidden by this Mission.
  • Mid-flight Missions. See Reflexivity below.

Reflexivity: Missions in flight

This Mission adds a contract tree, CI workflows, one CODEOWNERS file and tests. It does not change src/, the status event schema, meta.json handling, any CLI command, or any migration. Preserved surfaces, unchanged and not re-described by the new contract, with no new write path to any of them: the vendored spec_kitty_events upstream contract (specify_cli/core/upstream_contract.json, loaded by tests/contract/conftest.py), the orchestrator-api command surface, the doctrine schemas and the mission step contracts. Missions already in flight are unaffected in how they run. One coupling is new and is stated plainly. The router's corpus path filter already includes kitty-specs//spec.md, plan.md, kitty-specs//tasks/, kitty-specs//contracts/ and acceptance-matrix.json (it deliberately excludes only bare status.events.jsonl and a bare kitty-specs/, so routine status churn does not trigger the corpus job). Every Mission work-package PR in this repository therefore already selects the corpus job, and after this Mission that job runs the whole-corpus reality check against the committed data of every other Mission, including Missions mid-flight with partial frontmatter or a mission_number sentinel (the identity reader raises on a non-numeric string). A data defect in an unrelated in-flight Mission can block a different Mission's PR, and this Mission's own scaffold directory is part of that corpus. Triage of such a failure follows the baseline, attribution and blast-radius rules in Risks (R-3, R-4).

Requirements (mandatory)

<!-- Legend (tactic acceptance-criteria-non-vacuity): delivery [build] = new behaviour. The last column of the FR table names the evidence a do-nothing change cannot produce, and the success criteria it feeds (the full SC-to-FR map is in the Traceability section). The Acceptance Criteria that follow name how each fails. -->

Functional Requirements

IDTitleUser StoryPriorityStatusDeliveryEvidence a no-op cannot produce (SC)
FR-001Contracts layout and README conventionsAs a contributor, I want one documented layout and naming scheme so that every later module is built the same way.HighOpen[build]layout check fails on each planted violation; README and CHANGELOG structural check (SC-001, SC-008)
FR-002Shared building blocksAs a module author, I want Problem, the page cursor and PageInfo (plus shared parameters and responses) defined once so that modules do not diverge.HighOpen[build]lint fails a planted error response with another content type; shared pieces listed in _index.yaml (SC-001)
FR-003GET /project resourceAs a screen developer, I want the project name and Mission count so that I can render a project header.HighOpen[build]reality check asserts missionCount equals the number of overview records built (SC-002)
FR-004GET /missions overview resourceAs a screen developer, I want one overview record per Mission, paginated and deterministically ordered, so that I can render the Mission list without reading any work package.HighOpen[build]assertion that the built list is strictly ordered by the stated sort key; builder test with tasks/WP*.md reads trapped (SC-001, SC-002)
FR-005GET /missions/{missionId} detail resourceAs a screen developer, I want a Mission's phases and work package summaries so that I can render a Mission page.HighOpen[build]phase-basis validation with a planted bad entry; unit tests for every phase-rule branch (SC-002)
FR-006GET /missions/{missionId}/work-packages/{wpId} resourceAs a screen developer, I want one work package's authored plan, resolved state, review evidence, readiness and history, with the prompt body only on request, so that I can render a work package page.HighOpen[build]snapshot-equality assertion with a positive and a negative control (SC-002)
FR-007GET /events event stream resource (projection: D-8, D-9)As a screen developer, I want a documented change stream with one schema per event kind so that I can update views without polling.HighOpen[build]event-name-to-schema mapping check with planted name and schema; examples validate (SC-001)
FR-008Fixed v1 vocabulariesAs a consumer, I want the status-lane, lifecycle, topology and phase vocabularies fixed and ungrouped so that my own mapping is stable.HighOpen[build]enum-pinning check plus breaking-change failure on a changed enum (SC-001, SC-002)
FR-009Identity and display rulesAs a consumer, I want a ULID missionId as the only identifier and displayNumber as display-only so that repeated numbers cannot collide.HighOpen[build]reality check fails on a duplicate missionId and passes on duplicate display numbers (SC-002)
FR-010Source citation on every fieldAs a reviewer, I want each v1 field to cite where the status domain stores or derives it so that I can check the contract against the code.HighOpen[build]citation check by AST symbol resolution with cited == total (SC-004)
FR-011Provisional markingAs a consumer, I want unsettled fields marked x-provisional so that I do not build on them as if they were stable.HighOpen[build]provisional-marker check fails on a missing or undescribed marker (SC-004)
FR-012No host paths, no e-mail addresses (event data: D-9)As a maintainer of a public repository, or of a private one, I want no absolute path and no e-mail address in any payload, event or example so that the API cannot leak local identity.HighOpen[build]leak scan and text-level scan with planted values of each kind and floors (SC-005)
FR-013Closed schemas and validating examplesAs a consumer and as the reality check, I want closed response schemas and examples that validate so that a payload with a wrong or extra field fails.HighOpen[build]example validation and orphan-example check on planted violations (SC-001)
FR-014Validate and bundle checkAs a maintainer, I want every module validated and bundled to one document in CI so that structural errors and unresolved references fail the PR.HighOpen[build]validate and bundle planted failures; byte-identical double build (SC-001, SC-003)
FR-015Lint checkAs a maintainer, I want a Spectral-format ruleset enforced by vacuum so that conventions are machine-checked.HighOpen[build]lint planted-violation fixtures with a passing clean control (SC-003)
FR-016Breaking-change check (first-release baseline: D-12)As a consumer, I want a breaking change to fail CI unless the major version moves so that I can pin a major version safely.HighOpen[build]baseline and candidate fixture pair, plus the loud no-baseline state (SC-003)
FR-017Contracts workflow shapeAs a maintainer, I want a path-filtered workflow separate from the Python suites so that contract work does not slow the Python critical path.HighOpen[build]guard test over the new workflow files (no Node, path filter, fork guard, no pytest) (SC-007, SC-008)
FR-018Pinned and verified toolingAs a maintainer, I want pinned tool versions, verified checksums and SHA-pinned actions so that the build is reproducible and supply-chain safe.HighOpen[build]SHA-pin guard, checksum tamper fixtures, Gradle dependency verification (SC-003)
FR-019Reality-check testAs a maintainer, I want the contract validated against payloads built from this repository's own Missions with today's readers so that a contract that does not match the ledger fails before anything is built on it.HighOpen[build]floors, controls, read-only hash and snapshot-equality assertions (SC-002)
FR-020CI placement of the reality checkAs a maintainer, I want the reality check selected by exactly one blocking per-change job and the architectural gates kept green so that it neither goes unrun nor runs twice.HighOpen[build]collect-only run, a named green job, and the named architectural gate files (SC-007, SC-008)
FR-021Planted-violation proof for every checkAs a reviewer, I want each check shown failing on a planted violation, with a clean positive control, so that a green check is evidence.HighOpen[build]linked failing runs with asserted failure messages (SC-003)
FR-022Release workflow, dry run and tag namespaceAs a consumer, I want a release that publishes the bundle and its checksum under a tag the CLI release machinery cannot see, and a maintainer wants that proven in the PR except for publication.HighOpen[build]dry-run artifact with sha256sum -c; publish-step and namespace guards (SC-006, SC-009)
FR-023CODEOWNERS for contracts/As a maintainer, I want contract changes routed to the contract owners so that the contract has accountable reviewers.MediumOpen[build]CODEOWNERS check; PR 3 (WP07) states review is advisory (SC-010)
FR-024Module versioning and CHANGELOGAs a consumer, I want a module info.version, a CHANGELOG.md and stated versioning rules so that I can tell what changed between pins.MediumOpen[build]CHANGELOG structural check; breaking-change job fails on a bundle change without a version change (SC-003)
FR-025Loud failure when a check cannot do its jobAs a maintainer, I want every check to fail loudly rather than pass vacuously so that a green run always means the check examined something.HighOpen[build]one planted-violation test per matrix row, each asserting a minimum input count (SC-003)

Acceptance Criteria

Each criterion names the observable that changes and how it fails. "Planted" means a fixture or throwaway change committed only to demonstrate failure (see FR-021 for where the evidence lives).

FR-001 - Layout and conventions

  • A layout check over contracts/ (a contracts/tools/ script, D-19) fails (naming the offending path) when: a file under a module's paths/ is not named after a path item per the / to _, {param} kept rule; the root openapi.yaml maps a path to a file that does not exist; a paths/ file is referenced by no root entry (orphan); a schema file's name differs from the schema it defines; an _index.yaml lists a file that does not exist or omits one that does; a $ref is a URL, an absolute path, or contains a ~1 pointer; _shared/ is referenced for a module-specific piece; or a bundled document (a file containing the resolved, inlined component set of a module) is tracked under contracts/. Each is a planted violation with a clean positive control on the same fixture.
  • contracts/README.md states, with one example each: the layout, the path-file naming rule, relative $ref rules, _shared/ admission criteria, per-module info.version and the /api/v1 prefix, the x-source, x-derived and x-provisional extensions, the status lane versus code lane and repo-root lane terminology (with execution lane as the umbrella phrase), the workflow phase versus glossary phase distinction, that the Topology enum is the stored meta.json shape and is unrelated to MissionStatus.topology in the status aggregate (a two-value legacy or coordination vocabulary outside this contract), and the rule that the bundle is a build product. A Node-free structural check (a contracts/tools/ script run by the contracts workflow, D-19, so that an edit confined to either file triggers it) over contracts/README.md and contracts/mission-status/CHANGELOG.md fails when a required heading is absent (one README heading per topic above; the CHANGELOG headings for added, changed, removed and provisional, and a heading for the module's current info.version), and fails when it read zero headings; a planted fixture with a heading removed fails and the clean fixture passes. The repository's .markdownlint-cli2.jsonc is enforced only by the router's existing advisory markdownlint job (npx, ending in || true, so it cannot fail); this Mission neither relies on that job as evidence nor adds a Node markdownlint step (C-005), and the README says markdown lint of these files is advisory.
  • git diff --stat for the PR shows no change under contracts/fixtures/.

FR-002 - Shared building blocks

  • contracts/_shared/ defines Problem (media type application/problem+json), a page cursor schema, PageInfo, and the shared parameters and responses the five resources use, each listed in its _index.yaml. Every non-2xx response in the module references the shared Problem response. A planted path file declaring an error response with a different content type fails lint.
  • The page cursor is opaque to consumers and has no schema relationship to the stream cursor.

FR-003 - GET /project

  • Returns name (the project slug, cited to .kittify/config.yaml project.slug and ProjectIdentity.project_slug in src/specify_cli/identity/project.py) and missionCount (x-derived: the number of overview records, that is, of Missions the service lists). There is no separate display name in the domain, and the response has no project path, no worktree path and no tokens.
  • The reality check asserts missionCount equals the number of overview records it built. The Mission registry of the removed dashboard (build_mission_registry) counted pseudo-key orphans, scanned .worktrees copies and collapsed duplicate ids, so it is at most a cross-check on the clean corpus and never the definition.

FR-004 - GET /missions

  • Each record carries the fields in the Contract Field Catalogue (Mission overview). A response schema missing any catalogue field, or carrying one not in the catalogue, fails the reality check (closed schema, D-2).
  • Ordering is a documented total order: the operation description names the sort key createdAt descending, ties broken by missionId ascending (a total key, because missionId is unique). The reality check asserts that the overview list it built is strictly ordered by that key with no ties. "Two requests return the same order" is not testable in a Mission that implements no service, so it is not claimed; the total key is the checkable form. The list is paginated with the shared page cursor; the first page, a middle page and an exhausted cursor each have an example.
  • Overview payloads are built without reading any work package file (tasks/WP.md): the overview-builder test runs with reads of those files trapped (patched open and Path.read_text raising on any path matching tasks/WP.md) and fails if the trap is hit; wpTotal is the snapshot's work package count (D-4).

FR-005 - Mission detail

  • Returns the overview fields plus phases and workPackages. phases always contains exactly the five Mission phases (specify, plan, tasks, implement, review); each entry carries a status (pending, in_progress, complete) and a basis that is artifact, lifecycle_event or derived_from_status_lanes. A phase entry without a basis, or an implement or review entry claiming an artifact basis, fails validation.
  • The phase rules are contract-owned (x-derived, written in the schema description), because no existing reader computes them: the dashboard's workflow reader returned four keys and set implement to in_progress whenever the tasks directory existed, never complete, and had no review key. Specify, plan and tasks: complete with basis artifact when spec.md, plan.md or tasks.md respectively exists in the Mission directory; else complete with basis lifecycle_event when the matching SpecifyCompleted, PlanCompleted or TasksCompleted lifecycle event exists; else in_progress (basis lifecycle_event) when the matching ...Started event exists; else pending (basis artifact). Implement and review (basis derived_from_status_lanes, counts from statusLaneCounts, evaluated in this order). Implement: complete when wpTotal is above 0 and every work package is for_review, in_review, approved, done or canceled; else pending when no work package has left planned or blocked (including zero work packages); else in_progress. Review: complete when wpTotal is above 0 and every work package is done or canceled; else pending when no work package is for_review, in_review, approved or done; else in_progress. A unit test with planted lane-count fixtures covers every branch.
  • workPackages entries are the file-backed work packages of the Mission (each joined with its snapshot state; a null status lane when it has no events), as summaries (identifier, title, phase label, status lane, dependencies, readiness, subtask progress, last transition time), and never include the prompt body. A work package present in the snapshot with no file has no authored plan and is not representable; it is counted in the disagreement list of FR-019, not dropped silently.

FR-006 - Work package

  • Returns the fields in the Contract Field Catalogue (Work package). The prompt body (promptMarkdown) is returned only when the request asks for it with an explicit query parameter, and an example proves both the absent and the present case. No property named promptPath or prompt_path exists.
  • Resolved state is read from the status snapshot, never from stale runtime keys left in WP frontmatter (agent, shell_pid, lane, assignee, review_status). Schema validation cannot tell a snapshot value from a stale frontmatter value, so the reality check asserts equality instead: for every work package it builds, the payload's statusLane, assignment and actor equal the values derived from an independent read of materialize_snapshot for that Mission (the assertion re-reads the snapshot; it does not reuse the helper's intermediate). Positive control: a fixture work package whose frontmatter lane, agent and assignee deliberately differ from its event log must project the snapshot values. Negative control: the same payload patched with the frontmatter value fails the equality assertion.
  • Work package history lists status transitions only (kind is transition); annotation and lifecycle rows are not exposed there (D-8), and reason is a human text field (D-14).
  • The review block exposes the review reference (review-cycle://... or the legacy free text reference), the verdict (approved or changes_requested) and the reviewer handle; it never exposes feedback_path.

FR-007 - Event stream

  • events.yaml declares a 200 response with content type text/event-stream, documents the stream convention in prose (event name, id, resume, heartbeat, per-Mission ordering, consumer actions; D-18), and maps each event name to one data schema. The mapping is checked: every event name referenced in the description has a schema, every schema in the mapping is referenced by exactly one name, and the check fails when it finds zero event names. A planted event name with no schema, or a schema with no name, fails the check.
  • Request shape (D-18): optional missionId query parameter; optional streamCursor query parameter (<offset>:<invariant>), which requires missionId; the Last-Event-ID header carries the same string; an unscoped stream is live-only and heartbeats are SSE comment lines every 30 seconds.
  • Exactly three kinds are defined (D-8): status-transition, mission-lifecycle and log-truncated (resync, with reason limited to size_shrink and content_mismatch). Every event's data carries missionId and a stream cursor. The projection is an allow-list:
Source row in status.events.jsonlStream kindmissionIdateventId
status transition (a row with to_lane)status-transitionthe Mission's meta.json mission_idthe row's atthe row's event_id
lifecycle row (envelope timestamp, aggregate_id, event_type, payload) whose event_type is one of MissionCreated, SpecifyStarted, SpecifyCompleted, PlanStarted, PlanCompleted, TasksStarted, TasksCompletedmission-lifecyclethe same meta.json identitythe row's timestampthe row's event_id
anything else: annotation rows, WPCreated, ReviewerSelfApproval, DecisionPointOpened and DecisionPointResolved, RetrospectiveCaptured and RetrospectiveCaptureFailed (which uses type), and every unknown kinddropped, never forwarded

missionId never comes from aggregate_id: it is the Mission ULID only for MissionCreated, the slug for several other lifecycle rows and the work package id for WPCreated. The reality check projects every committed log, validates each projected event, asserts at least one event of each of the two row-derived kinds, prints the count of dropped rows with each dropped event type, and a planted log row of an unknown kind is dropped, not forwarded.

  • Resume: the request carries a stream cursor with missionId; the documented refusal reasons are exactly negative, out_of_range, misaligned and content_mismatch (the reason values of the tail reader's ResumeRefused), each mapped to a Problem response, plus the refusal of a cursor sent without missionId. An example exists for each. On log-truncated the consumer discards the Mission's cursor and re-reads the Mission detail (D-18).
  • Raw log rows never appear (D-9): a planted event example carrying an absolute path or e-mail fails the leak scan (FR-012).

FR-008 - Vocabularies

  • StatusLane lists exactly the nine display values: planned, claimed, in_progress, for_review, in_review, approved, done, blocked, canceled. statusLaneCounts has exactly those nine keys, each required, none grouped, and neither genesis nor uninitialized is a key (a Mission's event-less work package reads as a null statusLane).
  • LifecycleStatus is exactly active, planned, done, draft, discarded. Topology is exactly lanes, single_branch, coord, lanes_with_coord, unknown. Adding or removing a value in any of the three fails the breaking-change check without a version move and fails an enum-pinning check in the reality check's fixtures.
  • The README documents the board-column mapping as a consumer convention and the contract contains no columns or board grouping schema.

FR-009 - Identity

  • missionId is a 26 character Crockford ULID (pattern ^[0-9A-HJKMNP-TV-Z]{26}$, the pattern of the status domain's identifier), and is the only identifier in every path parameter that selects a Mission. displayNumber is nullable, described as display-only, and is not accepted as a path parameter anywhere. mid8 is nullable (null when there is no Mission id), never an empty string.
  • The reality check fails if two corpus Missions share a missionId, and passes when Missions share a displayNumber.

FR-010 - Citations

  • A citation check (a contracts/tools/ script, D-19) fails when a property of a resource schema has neither x-source nor x-derived, when an x-derived is not the structured form of D-3, has an empty rule, or has an empty or missing inputs list, when an x-derived input does not resolve (a code input by the rules below; a contract-field input as a property path that must exist in the bundle), when a cited file does not exist in the checkout, or when a cited symbol does not resolve in the cited file. Resolution is by defined name, not by text match: in a .py file the symbol must be a def, a class, or an assignment target (module or class level) found by parsing the file's syntax tree, and a name that appears only in a comment, a string or as a substring of another name does not resolve; in a YAML or JSON file the symbol is a key path that must exist. Line numbers are optional and are not checked. The check fails by naming the property and the unresolved citation.
  • Traversal: every property at any depth under every resource schema of the bundle (nested objects, array items, allOf, oneOf and anyOf branches, additionalProperties value schemas, and properties reached only through $ref) is checked. The check computes the total property count itself, prints it beside the number of properties carrying x-source, the number carrying x-derived and the number of x-derived inputs resolved, and fails unless the x-source and x-derived counts sum to the total and the total is above zero. A planted nested property without a citation fails, a planted symbol that occurs only in a comment fails, a planted x-derived with no inputs fails, and a planted x-derived input naming a missing symbol fails. Whether a resolving citation is the right one is a review judgment no check can make; to bound reuse of one real citation, the check reports any citation used by more than five properties, and the PR body lists them together with every x-derived property, its rule and its inputs, so the set of properties exempt from code grounding is visible and cannot grow unnoticed.
  • Where a citation can name a status-domain reader (src/specify_cli/status/, src/specify_cli/mission_metadata.py, src/mission_runtime/), it does so. A derivation that existed only in the dashboard code (lifecycle derivation, workflow phase, next action, mission count) is stated as an x-derived rule in the schema description (D-3, D-5, FR-005), with no x-source to code that the base no longer contains, so the contract stands on its own.

FR-011 - Provisional

  • Staleness (per work package; the domain's StaleState: fresh, stale or not applicable, plus reason and minutes since last commit, which needs git and worktrees and so is not computable from events alone) and next action (a nullable prose string; the derivation returns one of two prose strings or null, the review-then-accept string or the consolidate string, both of which embed the Mission slug) carry x-provisional: true with a description naming the open decision on #5528. A check fails if either lacks the marker, and fails if any element carries x-provisional without a description that names what is undecided.
  • Provisional fields are nullable. Because the reality check cannot compute staleness (it has no worktrees), its payloads carry null there; examples/ carries populated provisional values that validate, so the non-null branches are exercised.

FR-012 - No leaks

  • No property in the contract tree is named feedback_path, record_path, prompt_path, feature_dir, worktree path, project_path, or camel-case forms of those (feedbackPath, recordPath, promptPath, featureDir), and no property carries an absolute path. A planted property with any of those names fails a name check.
  • A value scan runs over every example and every payload the reality check builds, per field class (D-14), with these patterns (Python regular expressions). Strict fields: a value is a host path if it matches ^(?:/|~/|[A-Za-z]:[\\/]), or (?:^|/)(?:home|Users)/[^/\s]+/, or [A-Za-z]:\\Users\\. Human text fields: a value contains a host path if it matches (?<![\w.~-])/(?:home|Users|root)/[^\s/]+, or (?<![\w.~-])/(?:tmp|var/tmp|var/folders)/\S, or [A-Za-z]:\\Users\\[^\s\\]+ (so a leading ~/ and a bare /tmp word are not host paths there). All string fields except authored promptMarkdown in corpus-derived payloads: a value is an e-mail address if it matches [A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+. These patterns were run over the corpus's work package titles and phase labels, owned files, dependencies, requirement references, subtasks, friendly names, slugs and target branches (6231 values at measurement) with zero hits. Behaviour on a hit follows D-14 (null, projection error, or substring redaction by field class). The scan has same-fixture positive controls (a planted value of each kind in each class is detected), the two named real pass controls of D-14, and floors per class (it fails when it scanned zero values in a class).
  • Actor values are ActorHandle strings (D-10). The reality check's corpus contains source actors that are e-mail addresses or free text; the check asserts they projected to all-null handles, so the redaction is exercised, not assumed.
  • A text-level scan runs the human-text and e-mail patterns over every file under contracts/ (descriptions, x-source and x-derived strings, README, CHANGELOG, examples including the promptMarkdown of examples, which we author) and over the new tests' fixture files, with an explicit allowlist limited to the directory of deliberately planted fixtures (each planted file carries a marker line and is itself asserted to be detected). It fails when it scanned zero files. Test source files are not scanned (they hold the patterns); planted values in test code are assembled at run time from fragments.
  • Residual risk, stated: a handle-shaped string can still be a person's account name (the status domain has recorded a user's OS or git user name in actor-like fields). The contract cannot prove a handle is not a person's name; the shape rule and the value scan reduce, not eliminate, that risk (see Risks).

FR-013 - Closed schemas and examples

  • Response object schemas are closed (D-2). examples/ holds at least one validating example per resource and per event kind, plus the provisional-populated and discarded-Mission examples. A validate step fails when an example does not validate against the schema it is attached to, and when examples/ contains a file no schema or operation references.

FR-014 - Validate and bundle

  • The workflow validates every module found under contracts/ (a module is a directory with a root openapi.yaml) and bundles each to one openapi.yaml. It reports the number of modules validated and fails when that number is zero.
  • The bundle contains the five v1 path items and no remaining relative file $ref. A planted dangling reference, a planted schema that breaks OpenAPI 3.1, and a planted module whose root file is missing each fail the job.
  • Bundling is deterministic: two consecutive builds of the same checkout produce byte-identical files (the sha256 of both is compared and the job fails on a difference).
  • The bundle is uploaded as a workflow artifact and is never committed (a tracked bundle fails FR-001's check).

FR-015 - Lint

  • The ruleset is a Spectral-format file kept under contracts/. It enforces at least: operationId present and unique, every operation tagged, every non-2xx response referencing Problem, every schema property described, x-source or x-derived on resource schema properties (FR-010), no property with a forbidden name (FR-012), enum values in a fixed case convention, and closed response schemas (D-2). Each rule has a planted-violation fixture and the clean fixture passes under the same ruleset.
  • Lint fails when the ruleset file is missing or empty, when vacuum loads zero rules, or when it lints zero files.

FR-016 - Breaking-change check

  • For each module the check compares the freshly built bundle with the bundle of the module's last release (the most recent tag matching contract-<module>-v<semver>), rebuilt or downloaded as a verified release asset. A breaking change (removed path, removed or renamed response property, narrowed enum, newly required request parameter, changed type) fails unless the candidate's info.version has a higher major version than the baseline. A non-breaking addition passes with a minor or patch bump and fails if info.version did not change at all while the bundle did (CHANGELOG discipline, FR-024).
  • First release (D-12): when no tag matching the module's pattern exists, the job prints an explicit "no baseline" notice and records it in the job summary; this is allowed only when info.version is the module's initial version and the module's CHANGELOG.md carries the initial entry, and fails otherwise. The job also fails (it does not report "no baseline") when it cannot list tags reliably, for example in a shallow checkout (git rev-parse --is-shallow-repository is true) or when tag listing errors.
  • Provisional elements are diffed and reported in their own section (D-7); they are never dropped from the comparison.
  • Because no real release exists yet, the negative proof of this check runs against a committed pair of small baseline and candidate fixtures, under contracts/tools/fixtures/ (outside module discovery, D-17), with a clean positive control.

FR-017 - Workflow shape

  • A new workflow file (new path, for example .github/workflows/contracts.yml) triggers on pull requests and on pushes to main, both filtered to exactly three paths (the push trigger carries branches: [main]; the pull_request trigger carries no branches key, because PRs 2 to 6 of the stack target seam branches and a branches: [main] filter would never fire on them; the same absence keeps the fleet-verdict applicability test, which defaults a missing branches to every base, true for them; DD-7, DD-21): contracts/**, .github/CODEOWNERS (so the CODEOWNERS check of FR-023 runs on the edits it guards) and its own file. It has least-privilege permissions, shares no job with the Python suites, and holds the validate, bundle, lint, breaking-change, negative-test and release-dry-run jobs (FR-022). The release workflow (FR-022) has only a tag push and workflow_dispatch: a tag push cannot be path-filtered, and it has no pull_request or branch-push trigger. A guard test over the two new workflow files fails when (i) the contracts workflow has a pull_request or push trigger without exactly that path set, or its pull_request trigger carries a branches key, or its push trigger lacks branches: [main], (ii) the release workflow has any pull_request trigger or any push trigger other than tags, or (iii) either file invokes a Node toolchain (node, npm, npx, yarn, pnpm, setup-node). The release workflow's tag glob is checked by the namespace guard and its publication by the publish-step guard (FR-022), not by the path-filter rule. Like every guard test (D-15) it first asserts it parsed at least one workflow file and one job.
  • The Mission's edits to the Python suites' selection are limited to the one corpus glob in the router (FR-020), plus the registry rows that the corpus-trigger gate requires (D-16: the gate requires the registry rows; nothing reads the marker description, so pytest.ini is not edited, and the file's glob and data-root documentation is not edited, because #5557 rewrites it). The listed fleet-verdict edits below are the only other edits to shared CI. No existing job's needs, matrix or marker expression changes, and ci-module-registry.yml is untouched.
  • Neither new workflow invokes pytest, directly or through make, a script or a composite action, on any event (D-16). The guard has two halves, each placed where it runs on the edits it protects (D-19): a guard test in tests/ci/ parses both workflow files and fails if any step reaches pytest (selected by the ci row on any workflow edit), and the contracts/tools/no_pytest_scan.py check, run by the contracts workflow, fails if any script under contracts/tools/ imports or invokes pytest (selected by contracts/**). No ledger row and no WORKFLOW_FILES row is therefore added; if a plan change ever adds one, FR-020's ledger rules (owner, rationale, exit condition) apply and the change is called out in the PR.
  • Fork guard: both new workflows have a push trigger (branch and paths for the contracts workflow, tags for the release workflow), so tests/ci/test_fork_guard.py treats them as automated workflows. Every root job (no needs) carries the canonical guard as the first top-level && conjunct of its if: ((github.repository == 'spec-kitty/spec-kitty' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch')), so pull-request and manual runs stay live on forks. No entry is added to EXEMPT_WORKFLOWS (only release.yml is exempt). The named file is run and passes.
  • Fleet verdict (explicit, minimal, listed edits; the alternative that needs more is weighed in D-17). applicable_workflows in scripts/ci/fleet_verdict.py adds every workflow that has a pull_request trigger to the inventory it checks and raises ValueError ("PR workflow inventory changed") when that set differs from PR_WORKFLOWS; tests/ci/test_fleet_verdict.py pins PR_WORKFLOWS (test_all_existing_pr_workflows_are_registered, test_new_pr_workflow_fails_closed) and pins the workflow_run list of ci-fleet-verdict.yml to the name: of every member plus the aggregate (test_reporter_trigger_covers_every_registered_workflow_and_reruns). Only the contracts workflow has a pull_request trigger, so only it is added; leaving it out would fail those tests and, once merged, would make the fleet verdict computation raise for every PR. The release workflow has no pull_request trigger, so it is not in the inventory, main_workflows in scripts/ci/fleet_main.py never reads it, and that file is not edited. This Mission makes exactly these edits to the fleet machinery and no others: (a) the contracts workflow file name is added to PR_WORKFLOWS (scripts/ci/fleet_verdict.py); (b) its name: is added to the workflow_run list of ci-fleet-verdict.yml; (c) tests/ci/test_fleet_verdict.py is updated: its workflow-id fixture gains the new entry, and its existing expected sets additionally exclude the path-filtered contracts workflow (the pyproject.toml diff expects PR_WORKFLOWS minus the contracts workflow; the docs/example.md diff keeps its existing exclusion of release-readiness.yml and check-spec-kitty-events-alignment.yml and also excludes the contracts workflow; a case with a contracts/ path expects it); (d) tests/ci/test_fleet_main.py: the contracts workflow has a push trigger with branches and paths, which main_workflows classifies as a conditional main gate, so the conditional-gate expectations of that file (test_absent_conditional_gate_is_explicit_and_its_failure_is_observed lists the unobserved conditional gates) gain it; the plan reads the exact lines. Each pull_request trigger uses only the keys applicable_workflows supports (types, branches, paths); the contracts workflow's pull_request trigger uses paths only (no branches), so it stays applicable to a stacked PR whose base is a seam branch (a missing branches defaults to every base) and the ci-router.yml pull_request trigger, which also has no branches filter, behaves the same way. Consequence: a red contracts check on a PR that touches a path in its filter appears in the fleet PR verdict comment; the contracts workflow's push trigger is path-filtered, so it is a conditional main gate. The contracts checks are still not in router-gate. C-003 and SC-008 list these edits, and the PR body repeats them. tests/ci/test_fleet_verdict.py and tests/ci/test_fleet_main.py are run as named gate files (FR-020).
  • Workflow count: 17 workflow files exist at this base and the two new ones (the contracts workflow and the release workflow) make 19, within the ceiling of 20 pinned by test_reusable_workflow_ceiling_respected. The plan records the count and re-verifies it after rebasing on current main, because other CI changes (#5557 is open) may consume the remaining slot.

FR-018 - Pinned and verified tooling

  • Gradle (distribution checksum verified through distributionSha256Sum), the openapi-generator plugin, vacuum and oasdiff each have an exact version; downloaded binaries and distributions have a recorded sha256 in a committed manifest, and the workflow verifies each checksum before the binary or distribution is executed. A mismatching or missing checksum fails the job before execution (planted: an altered checksum fails). Every uses: in the new workflow files is pinned to a full 40 character commit SHA with a version comment; a guard test fails on a floating tag or branch ref (planted: an unpinned uses: is detected, the pinned one passes).
  • The plugin and its transitive JVM dependencies are resolved under Gradle dependency verification: a committed verification-metadata.xml (under contracts/gradle/) records a sha256 for every resolved artefact, and the build runs with strict verification, so an unverified artefact, a missing metadata file or a tampered checksum fails the build naming the artefact (planted: a copy of the metadata with one altered checksum fails). A missing JVM, a missing Gradle distribution and a plugin or repository resolution failure each fail the job naming the cause (FR-025).
  • No tool is installed through an unpinned package-manager command in the workflow. Downloads use HTTPS only.

FR-019 - Reality check

  • tests/contract/test_mission_status_reality.py builds, for every Mission directory under kitty-specs/ that has a meta.json, the overview payload and the detail payload, and for every work package in that Mission the work package payload. It reads each Mission through the status domain's coordination-aware read surface (MissionStatus.load and its resolved read directory, ADR D-3: the API composes the existing read models and never rebuilds paths) and then uses the existing public readers: materialize_snapshot for the status snapshot (the read-only function; materialize also writes status.json into the Mission directory, which this check must never call; on this checkout it rewrites tracked snapshots and creates missing ones), the meta.json identity reader, resolve_mid8, compute_weighted_progress, reconstruct_wp_view (WPView), dependency_readiness_for_wp and the tail reader for the stream cursor. MissionStatus.load (specify_cli/status/aggregate.py) reads the coordination copy when the Mission's coordination branch exists and raises CoordinationBranchDeleted when it does not, so which status source a Mission is read from depends on the branches present in the clone: a developer clone with local coordination branches and a CI clone can differ. On this checkout 36 Missions cannot be loaded that way (indicative only). For those the check reads the Mission's own directory, lists each such Mission in its output and prints the count. That fallback count is environment-dependent, so it is reported and never floored or ceilinged. This is the recorded deviation from ADR D-3 and it is counted, not hidden.
  • Environment rule. Every pinned number (the floors below, the disagreement-list ceiling, and the pinned expected-output fixture) is computed in own-directory mode: a second, cheap pass that reads each Mission's own kitty-specs/<slug>/ directory with materialize_snapshot and builds no payload. That pass is a pure function of the checked-out tree, so the same figure holds in a developer clone and in CI. The coordination-aware read is used for building and validating the payloads, and the equality assertions re-read the same resolved directory as the payload, so they hold in either environment. The ceilings and floors are nevertheless confirmed from a tests-corpus job log (the CI checkout) at implement start and recorded in the PR; every figure in this spec is indicative until then, and a figure that differs between the developer clone and the CI log is a defect in the pass, fixed before the number is pinned.
  • Derivations that have no public reader (lifecycle status, phases, readyToStart, missionCount; D-3, D-5, FR-005) are implemented in the test-side helper (a module under tests/contract/, not collected as a test; new path; no module under src/) as the executable form of the rules written in the contract's schema descriptions. Because the helper then holds its own copy, three things keep the check from testing only itself: the rule lives in the contract description, which is the single authority; the helper's unit tests cover every branch with planted lane-count fixtures; and a pinned expected-output fixture holds the derived values for a named sample of Missions chosen to cover each lifecycle value and each documented quirk, produced once by the original dashboard derivation at the recorded commit d78aa2345 and committed, and the reality check compares the helper's output with it. That fixture always runs; it is not skipped when the original code is gone.
  • It validates every built payload against the contract, one parametrised test case per Mission (the case id is the Mission directory name, so a failure names the Mission) plus a small number of corpus-level tests. A payload failing validation fails the case and names the Mission, the work package and the JSON pointer of the first failure; a Mission is never silently skipped, and there is no exclusion list at delivery (a later exclusion needs an issue, an owner and a drain date, per the charter's allowlist rule, and is printed in the test output). Corpus-level tests that must see every Mission carry an explicit timeout mark with the rationale recorded beside it, because the pytest.ini default of 240 seconds applies to each test including fixture setup (NFR-001).
  • Non-vacuity floors: at least 500 Missions, at least 3000 file-backed work package payloads (3125 tasks/WP.md files measured) and a summed own-directory snapshot work package count of at least 2800 (2936 measured); at least one Mission for each of the nine status lanes is represented in statusLaneCounts; each of active, planned, done and draft occurs; each of the four stamped topology values and unknown occurs (these four floors are evaluated over the own-directory pass); the test fails if any floor is not met and no fixture covers it. The floors are set below counts measured by a short, documented method (glob kitty-specs//meta.json, count tasks/WP*.md, sum len(materialize_snapshot(dir).work_packages)), and the numbers are re-measured at implement start and recorded in the PR, never copied from this spec.
  • Snapshot and files disagree: for every Mission the check asserts wpTotal equals len(snapshot.work_packages), and lists every Mission whose snapshot work package count differs from its tasks/WP*.md count (52 at measurement: 45 with fewer snapshot work packages than files, 7 with more), with both numbers. The list is computed in own-directory mode and its length is asserted at or below a pinned shrink-only ceiling, so the difference cannot grow unnoticed (D-4). The lifecycleStatus comparison with the pinned expected values samples only Missions where the two sets agree, and compares the helper's output over the own-directory snapshot.
  • Positive and negative controls on one shared fixture: a payload of a real Mission validates; the same payload with a planted e-mail, absolute path, unknown status lane, or extra property fails. The snapshot-equality assertion of FR-006 has its own positive and negative control.
  • The test is read-only, which the reader choice above makes necessary and the hash check proves: it records a content hash of every tracked file under kitty-specs/ before and after the build and fails if any differ.
  • The contract it validates against is the contract the CI build resolves, not a hand copy: it either consumes the bundle the build produces or resolves the split files with a reference resolver whose output is proven equal to the bundle by a non-pytest parity script that the contracts workflow runs and that fails on divergence (OQ-1 chooses the mechanism; the requirement is that there is one resolution authority or a proven-equal pair). The resolver-parity pair is specified as follows. The resolver is contracts/tools/contract_resolver.py (D-19): the reality check imports it from there (a test importing from contracts/tools/ is the allowed direction; the tool never imports from tests/ and never imports pytest), and contracts/tools/resolver_parity.py imports the same module, so the two cannot diverge by being two copies. The parity script fully dereferences both the resolver's output and the CI bundle and compares the two trees, key order ignored. It fails, naming the first differing JSON pointer, on any divergence; it fails when the resolver cannot be imported, when the bundle is missing or empty, and when it compared fewer than the five v1 path items, zero schemas or zero resolved $refs (it prints the three counts). Its negative proof is a planted divergent resolver (a copy that drops one $ref target or alters one schema, under contracts/tools/fixtures/) which the negative-test job runs the script against, asserting the failure message and the pointer, with the real resolver as the clean control; it has a unit-test module like every contracts/tools/ script (D-19).
  • It gives a real verdict in every job that selects the corpus marker, which are the router's tests-corpus and the packs built-in-corpus-suite (--cov=src/doctrine, D-16), both without a JVM; if the contract or its resolver is unavailable it fails (it never skips and never passes).

FR-020 - CI placement of the reality check

  • contracts/** is added to the corpus filter group in .github/workflows/ci-router.yml as an additive, single-line change; the existing globs are untouched. The router job tests-corpus is the reality check's blocking per-change home on a contracts-only pull request (D-16); the router's marker expression and ci-module-registry.yml are not edited.
  • tests/architectural/test_ci_corpus_trigger_completeness.py is updated only so that its curated registry lists tests/contract/test_mission_status_reality.py and every other new tests/contract module that carries the marker (including the helper's unit-test module and the unit-test module of each contracts/tools/ script). Its _CORPUS_GLOBS and _CORPUS_DATA_ROOTS are not edited (D-16: documentation that no gate reads, and #5557 rewrites it). Every new module applies the marker on a single line, pytestmark = [pytest.mark.contract, pytest.mark.fast, pytest.mark.corpus], or as a decorator, because a multi-line list is not detected by the gate's regular expression. pytest.ini is not edited: the gate asserts only that the corpus marker is registered (test_corpus_marker_is_registered_in_pytest_ini) and no test reads the marker's description text, so a description edit would have no failing test behind it. Running that test file passes (it fails if the registry and the marked modules differ in either direction).
  • Selection is demonstrated, not assumed. After rebasing on the latest main, pytest --collect-only -m "corpus and not windows_ci" tests/contract/test_mission_status_reality.py collects the test, and the body of PR 5 names tests-corpus as the test's blocking home with a link to a green run of it on a contracts-touching diff. The evidence step lists every job whose pytest expression selects corpus at that moment (at this base, tests-corpus in ci-router.yml and built-in-corpus-suite in packs.yml) and verifies each expression with a collect-only run; the PR body records, per router shape on main at that moment, which jobs select the test.
  • Exactly-once, scoped (D-16): on a pull_request diff touching contracts/** and no path of the packs built_in filter, the number of blocking per-change jobs whose logs show the reality check's test id executing is exactly one (tests-corpus). The contracts workflow's pull_request runs, its release-dry-run job included, are change-triggered events and are counted: they run no pytest, so they add none; the release workflow has no change trigger. On a push to main and on a diff that also matches the packs built_in filter, built-in-corpus-suite runs the test as well by existing design, and the module passes there (no JVM, --cov=src/doctrine). tests/architectural/test_no_duplicate_suite_execution.py, tests/architectural/test_workflow_coherence.py, tests/architectural/test_ci_corpus_trigger_completeness.py, tests/ci/test_fork_guard.py, tests/ci/test_fleet_verdict.py, tests/ci/test_fleet_main.py and test_reusable_workflow_ceiling_respected stay passing. They are run as named files only (the charter forbids full architectural sweeps in mission work). Any ledger or allowlist addition, expected to be none, is justified in the plan with an owner, a rationale and an exit condition.
  • .github/ci-module-registry.yml is unchanged by this Mission; if the plan finds a reason to edit it, the reason and the conflict handling with the open CI PRs are recorded in the plan and SC-008 is amended.
  • Every other new test names its marker and the CI job that collects it, verified by a collect-only run. A test in tests/contract/ with only fast or contract markers is collected by no per-PR job; any such new test in that directory carries corpus (and a registry row), and tests for the workflow files go in tests/ci/, whose module-matrix row already selects changes under .github/workflows/**. Checks that must run on an edit confined to contracts/ are contracts/tools/ scripts, not tests/ci/ tests (D-19).

FR-021 - Planted-violation proof

  • For each of validate, bundle, lint, breaking-change diff, citation check, leak scan, layout check, example and event-mapping checks, enum pinning, pin and checksum verification, resolver parity, the no-pytest scan, the README and CHANGELOG structural check, the CODEOWNERS check, reality check, the release dry run, and each guard test, there is at least one planted violation that makes that check fail, with the failure message naming the cause, and a clean control on the same fixture that passes. The evidence has two classes, fixed by where the check runs (the runner table), and no check is in both: class A (linked real failing run) covers every check run by the contracts workflow (validate, bundle, lint, breaking-change, pin and checksum verification, every contracts/tools/ script including resolver parity and the release script): a negative-test job runs it against committed planted fixtures and asserts the failure message, or throwaway commits whose failing runs are linked in the PR body. Class B (in-test planted case) covers the checks that are pytest tests, namely the reality check and the guard tests in tests/ci/: the test itself runs the check against a planted input and asserts the failure and its message, and the evidence is the green run of that test, whose test ids are listed in the PR body (a deliberately red throwaway commit is not required, because the assertion that the planted input fails is what passes). A check with no evidence of its class is treated as not delivered. For the contracts/tools/ checks the workflow's negative-test job is the runner of record (D-19).
  • Where a negative-test job (class A) or an in-test case (class B) runs a check against a planted fixture, the job passes only if the check failed for the expected reason (it asserts the message), not merely because it exited non-zero.

FR-022 - Release workflow and dry run

  • A new release workflow (new path, for example .github/workflows/contracts-release.yml) triggers on tags matching contract--v.. and on workflow_dispatch (an input selects dry-run mode, which builds and verifies but never publishes; a manually dispatched workflow runs from the default branch, so it is not PR evidence). It has no pull_request trigger and no branch push (DEV-2, D-17), so it is not change-triggered and not a PR_WORKFLOWS member, and it invokes no pytest on any event (D-16, FR-017). Its root jobs carry the repository fork guard (FR-017). The PR-green dry run is the release-dry-run job of the contracts workflow (FR-017, D-17). Both call contracts/tools/release_check.py and the shared build wrapper: for every discovered module the script derives the candidate tag from info.version, applies the tag rules, builds the bundle, writes openapi.yaml.sha256, verifies it with sha256sum -c, and the job uploads both as a workflow artifact and executes no publication step. The dry run's module-root parameter lets the negative-test job point it at the planted fixture root (D-17).
  • Tag rules, checked before publication and in the dry run (against the derived candidate on a pull request, against the pushed tag on a tag push): the tag matches contract-<module>-v<semver>; <module> names an existing module; <semver> equals that module's info.version; the module's CHANGELOG.md has a heading for that version. Each violation fails the job (planted, under contracts/tools/fixtures/: a fixture module whose version has no CHANGELOG heading, and a mismatched version, each fail, with a clean control module on the same root).
  • Publication is reachable only on a tag push. Every publish step (creating the release, uploading an asset) carries an if condition restricted to a push of a tag ref (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')). A guard test parses the workflow, fails when it finds zero publish steps, fails when any publish step lacks that restriction, and evaluates the condition for a pull_request, a workflow_dispatch, a push to main and a push of refs/tags/contract-mission-status-v1.0.0, which must yield false, false, false and true (planted: a publish step without the condition fails).
  • Namespace guard: a test evaluates the release workflow's tag glob against the CLI release tags and the CLI release trigger glob against contract-mission-status-v1.0.0; neither matches. It implements GitHub's filter-pattern semantics, not fnmatch ( matches any characters except /, also matches /), so a tag that differs from a matching one only by a slash (for example contract-mission/status-v1.0.0) is not matched; it fails when its list of CLI release tags is empty, and fails when a contract tag would match v... It does not call the CLI release scripts with the contract tag as a live tag.
  • The release workflow is not wired into .github/workflows/release-readiness.yml, release.yml or any CLI release gate. The contract release does not alter release.yml.
  • After merge (close-out step, CL-4): a maintainer pushes contract-mission-status-v1.0.0; the evidence is a published release with exactly the bundle and its sha256, the downloaded checksum verifying, and the digest matching a locally rebuilt bundle. This evidence is recorded in the Mission close-out record and cannot be produced in the PR.

FR-023 - CODEOWNERS

  • .github/CODEOWNERS (new) contains a rule for contracts/ owned by @stijn-dejongh and @MOES-Media. A check (a contracts/tools/ script run by the contracts workflow, whose path filter includes .github/CODEOWNERS, D-19) parses the file and fails if the rule is missing, if either handle is missing, or if the pattern does not cover contracts/mission-status/. The PR body states that review is not enforced by branch protection today.

FR-024 - Versioning and CHANGELOG

  • The mission-status module has info.version set to its first release value and a CHANGELOG.md with an initial entry and sections for added, changed, removed and provisional elements. contracts/README.md states the versioning rule: breaking changes move the major version, additive changes the minor version, documentation-only changes the patch version; a bundle change with no info.version change fails the breaking-change job. The README and the CHANGELOG satisfy the structural check in FR-001 (markdown lint of them is advisory, FR-001).

FR-025 - Fail-loud matrix

The table below is binding; each row has a planted-violation test (FR-021). General rule (D-15): every check and every guard test first asserts a minimum input count (files globbed, uses: lines inspected, tags evaluated, publish steps found, examples validated, event names mapped, properties traversed) and only then asserts its properties, so none can pass on zero inputs.

CheckCannot do its job whenRequired behaviour
Validateno module found; a module has no root openapi.yaml; the JVM, Gradle or the validator is missing or its checksum mismatches; plugin or repository resolution fails; dependency verification fails; a $ref does not resolveNon-zero exit; message names the module and the cause; reports module count; zero modules fails
Bundleoutput missing, empty, or has fewer than the five v1 paths; two builds differ; JVM, Gradle or plugin resolution failureNon-zero exit naming the missing or differing artefact
Lintruleset missing or empty; zero rules loaded; zero files linted; binary missing or checksum mismatchNon-zero exit naming the cause
Breaking-change diffno baseline and not the initial version; shallow checkout or tag listing error; baseline cannot be fetched or its checksum mismatches; oasdiff missingNon-zero exit; the one allowed no-baseline state is printed loudly and recorded in the job summary
Layout checkno module found; zero files under any paths/ inspected; zero _index.yaml readNon-zero exit naming the path and the rule; reports the counts inspected
Citation checkzero citations found; the x-source and x-derived counts do not sum to the computed property count; a cited path or symbol does not resolve; an x-derived is free text, has an empty rule, has empty or missing inputs, or has an input that does not resolveNon-zero exit naming the property; prints the x-source, x-derived and resolved-input counts
Example validation and orphan-example checkexamples/ empty or zero examples validated; an example validates against nothing; a file in examples/ is referenced by nothingNon-zero exit naming the example
Event-mapping checkzero event names found in the stream description; a name with no schema; a schema with no nameNon-zero exit naming the name or schema
Enum pinninga pinned enum list is empty or the schema's enum cannot be read; a value added or removedNon-zero exit naming the enum and value
Leak scan and text-level scanzero values scanned in a class; zero files scanned; a planted value of any kind not detectedNon-zero exit
Pin and checksum verificationthe manifest is empty; zero tools verified; zero uses: lines inspected; a checksum mismatch; an unpinned uses:; Gradle dependency metadata missing or mismatchingNon-zero exit before any tool executes, naming the artefact
Resolver parityzero documents; fewer than the five v1 path items, zero schemas or zero resolved $refs compared; the resolver cannot be imported; the bundle missing or empty; the two trees differNon-zero exit; prints the three counts; on divergence names the first differing JSON pointer
No-pytest scan of contracts/tools/zero scripts scanned; a script imports or invokes pytest or reaches it through a shell string or makeNon-zero exit naming the script and the line; prints the count scanned
Guard tests (workflow shape and no Node, no pytest in the new workflow files, fork guard, SHA pin, namespace, publish-step condition)zero workflow files, jobs, uses: lines, CLI release tags or publish steps foundTest failure naming the missing input, before any property is asserted
CODEOWNERS checkthe file is missing or holds zero rulesNon-zero exit naming the missing input before any rule is asserted
Reality checkfewer than the floor of Missions; zero work packages; contract or resolver unavailable; payload invalid; readers modified a tracked fileTest failure, never skip, never pass
Release dry run and releasebundle empty; sha256 mismatch; tag not matching the rules; module or version missing; zero modules discovered; the module-root parameter names a directory with no moduleNon-zero exit before any publication step

Runner assignment (binding, D-19). Every check has one runner, and none is a second pytest execution of the corpus.

CheckRunnerCI evidence of failure (FR-021)
Validate, bundle, lint, breaking-change diff, tool pin and checksum verificationcontracts workflow jobs (Gradle, vacuum, oasdiff, checksum steps)class A: negative-test job over planted fixtures, message asserted
Layout check, citation check, example and orphan-example check, event-mapping check, enum pinning, leak scan and text-level scan, README and CHANGELOG structural check, CODEOWNERS check, resolver parity, the no-pytest scan of contracts/tools/, release script (tag rules, dry run)contracts/tools/ scripts run by the contracts workflow (path filter contracts/** and .github/CODEOWNERS); each has a unit-test module in tests/contract/ (single-line corpus marker, registry row) run once by tests-corpusclass A: negative-test job over planted fixtures, message asserted
Reality checkpytest in tests-corpus (and built-in-corpus-suite by existing design, D-16)class B: the controls inside the module, run green with the failure asserted
Guard tests on workflow files (workflow shape, no Node, no pytest in workflow files, fork guard, SHA pin, publish-step condition, namespace)pytest in tests/ci/, selected by the module-matrix ci row on any workflow editclass B: each guard's own planted cases
Release dry runthe release-dry-run job of the contracts workflow (pull_request), and the release workflow's workflow_dispatch dry-run mode after merge; both call release_check.pyclass A: negative-test job running release_check.py over the planted fixture module root

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001Reality-check runtimeThe reality check over the corpus at delivery completes in at most 120 seconds wall-clock for the whole module on a standard hosted runner (539 Missions and 34148 event lines on this checkout), and no single test case, fixture setup included, takes more than 60 seconds (a quarter of the 240 second per-test ceiling in pytest.ini). Both numbers, and the duration of the whole tests-corpus job against its 10 minute timeout (at least 50 percent headroom), are measured from the CI job log, recorded in the body of PR 5 (WP10), and checked there by the reviewer. WP12 T076 measures the numbers and relays them; the orchestrator then updates PR 5's draft body with them (a body edit on a draft PR, not a merge); they are not asserted inside the test (no timing flake).PerformanceMediumOpen
NFR-002Deterministic bundleTwo builds of the same commit produce byte-identical bundles (equal sha256), checked in CI.ReliabilityHighOpen
NFR-003Supply chain100 percent of downloaded tool artefacts have a pinned version and a verified sha256; 100 percent of uses: references in the new workflows are SHA-pinned; zero floating references.SecurityHighOpen
NFR-004Leak-free artefactsZero absolute host paths and zero e-mail addresses in the contract tree (text-level scan, FR-012), in examples/ including their promptMarkdown, and in every payload the reality check builds (excluding authored promptMarkdown of corpus-derived payloads).SecurityHighOpen
NFR-005No new Python critical-path costThe contracts workflow adds no job to the Python module-shard matrix and changes no existing job's needs; the only router edit is the single corpus glob. Measured by diffing the workflow files.PerformanceMediumOpen
NFR-006Reproducible locallyA contributor with a JDK and network access can run validate and bundle locally with one documented command; the README states the prerequisite and the command. The Mission's own required checks run in CI and do not depend on local runs.UsabilityLowOpen
NFR-007Test disciplineNew Python test code passes ruff check and ruff format (both hard-enforced). No existing CI job measures coverage of test-helper code (the coverage configuration maps src/ and the router corpus job runs without --cov), so the criterion is checkable behaviour: every projection helper function has a direct unit test, and the planted positive and negative controls exercise each branch. The implementer also runs a non-gating local pytest --cov=<helper module> --cov-branch --cov-fail-under=90 over the helper's unit-test file and pastes the output in the body of PR 5. Mission work runs targeted test files and the named architectural gate files only, never a full architectural, end-to-end or full-suite sweep; commands and counts are recorded in the PR.QualityMediumOpen

Constraints

IDTitleConstraintCategoryPriorityStatus
C-001Draft PRs until design acknowledgedSix stacked seam PRs (operator ruling of 2026-10-02, DD-21; supersedes "one PR for the Mission"): (1) WP01+WP02 onto main; (2) WP03-WP05; (3) WP06+WP07; (4) WP08+WP09; (5) WP10; (6) WP11+WP12, each onto the previous seam's head branch. Stack-level rules: all six are opened as drafts and stay drafts until the single acknowledgement on #5528 (CL-1); the ready-for-squad label goes on none until then; they merge bottom-up (PR 1 first) and only by the operator or a maintainer; the orchestrator and implementers never merge a PR and never mark one ready; retargeting an upper PR after a lower one merges is a maintainer act.ProcessHighOpen
C-002Contract and CI onlyNo file under src/ changes; no status event schema, meta.json handling, CLI command or migration changes. Test helpers live under tests/.TechnicalHighOpen
C-003Minimal shared-CI edits.github/workflows/ci-router.yml receives only the additive corpus glob (FR-020); .github/ci-module-registry.yml, pytest.ini and the glob and data-root sets of tests/architectural/test_ci_corpus_trigger_completeness.py are not edited (that test gains registry rows only). The only other shared-CI edits are the fleet-verdict edits listed in FR-017: scripts/ci/fleet_verdict.py (one PR_WORKFLOWS entry), .github/workflows/ci-fleet-verdict.yml (one name in the workflow_run list), tests/ci/test_fleet_verdict.py and tests/ci/test_fleet_main.py; scripts/ci/fleet_main.py is not edited. Neither #5557 nor #5545 touches any of these four fleet files (checked with gh pr diff --name-only). The real conflict surface: open PR #5557 edits ci-router.yml and .github/ci-module-registry.yml, rewrites tests/architectural/test_ci_corpus_trigger_completeness.py (which this Mission also edits for its registry rows), edits _gate_coverage.py, test_no_duplicate_suite_execution.py, test_workflow_coherence.py and test_module_shard_registry.py (gates this Mission runs), and adds tests/ci/test_corpus_blocking_home.py; PR #5545 (merged after this branch's base) edited ci-router.yml, ci-module-registry.yml, ci-windows.yml, ci-nightly.yml and other CI files. Edits are additive, rebased last, and the named gates re-run after rebase.TechnicalHighOpen
C-004contracts/fixtures/ untouchedThe existing handoff fixtures and their reader tests/contract/test_handoff_fixtures.py are not modified.TechnicalHighOpen
C-005Node-freeNo Node, npm, npx, yarn or pnpm in the contracts tooling, workflows or tests.TechnicalHighOpen
C-006Public repository hygieneNothing in the spec, plan, contract, examples, tests or commit messages contains an absolute host path, an e-mail address, a credential, or a reference to a private discussion. The standard co-author trailer that tooling appends to a commit message is the one exception to the e-mail rule. Upstream issue and PR numbers may be cited.SecurityHighOpen
C-007Terminology"Mission", never "feature", in all new text and identifiers; "status lane" is never conflated with "code lane", "repo-root lane" or the umbrella "execution lane", and "workflow phase" is never conflated with the glossary phase. The glossary was checked for the terms used: lane (.kittify/glossaries/spec_kitty_core.yaml, nine values), Code lane and Repo-root lane (docs/context/topology.md), phase (grouping container); execution lane is not a glossary term.GovernanceHighOpen
C-008No version numbers in scopeThe release stream's own version (contract-mission-status-v1.0.0, info.version) is the artefact's identity, not a CLI release scope; nothing in this Mission names or alters a CLI release version.GovernanceMediumOpen
C-009Pre-existing failures reported, not absorbedPre-existing red tests encountered are recorded against a baseline taken on the base commit and reported as the charter's pre-existing failure rule requires; they are never made green by this Mission, and never blamed on it without the baseline.GovernanceHighOpen
C-010ATDD orderThe reality-check test (and each check's planted-violation fixture) is committed first and shown red against an empty or wrong contract before the contract makes it green; reviewers verify red then green.QualityHighOpen
C-011Docs and ADRNo ADR is authored here. The open ADR amendment on the read API's placement and transport (the ADR still records an in-process module and CLI JSON verbs, with no HTTP server in the CLI) is a separate decision on #5528, not this Mission's scope.GovernanceMediumOpen

Key Entities (include if the mission involves data)

  • Contract module: a self-contained OpenAPI 3.1 document split across files, with its own info.version, CHANGELOG.md and release stream (mission-status first).
  • Shared building blocks: cross-module pieces in contracts/_shared/ (Problem, page cursor, PageInfo, shared parameters and responses).
  • Bundle: the single resolved document built by CI from a module; the release asset; never committed.
  • Release asset pair: the bundle openapi.yaml and its openapi.yaml.sha256.
  • Mission overview: one record per Mission with identity, vocabulary values, raw status-lane counts, progress, timestamps and the stream cursor.
  • Mission detail: the overview plus the five Mission phases and work package summaries.
  • Work package detail: authored plan, resolved state, readiness, review evidence and history for one work package; prompt body only on request.
  • Event stream: the text/event-stream change feed of projected, named event kinds, each with a data schema, a missionId and a stream cursor.
  • Corpus: this repository's own kitty-specs/ Missions, the data the reality check validates against.

Contract Field Catalogue (requirements)

Evidence is cited as path:line at this checkout (branch HEAD fefa3bc73 on a base descending from d78aa2345). The symbol is what the citation check resolves (D-3, FR-010); line numbers are indicative and rot. src/ is implied. "Null" means the schema allows null. "Provisional" means x-provisional: true. Types are the contract's types, not the source's. Rows that cite specify_cli/dashboard/scanner.py record historic evidence: PR #5545 has merged and removed that module upstream, so after the required rebase those derivations are contract-owned x-derived rules stated in the schema description and the scanner is no longer cited (D-3, R-2).

Project (GET /project)

FieldTypeNullSource
namestringno.kittify/config.yaml project.slug; specify_cli/identity/project.py:44-58 (ProjectIdentity)
missionCountintegernox-derived: the number of overview records, that is, of Missions the service lists (the removed dashboard's build_mission_registry is a cross-check on the clean corpus only)

Mission overview (items of GET /missions; also the head of detail)

FieldTypeNullSource
missionIdstring, ULIDnometa.json mission_id; specify_cli/mission_metadata.py:226-269; ULID pattern specify_cli/status/models.py:99
mid8stringyesmission_runtime/identity.py:64-96 (resolve_mid8; empty becomes null)
slugstringnospecify_cli/mission_metadata.py:254-256
friendlyNamestringnometa.json friendly_name; specify_cli/dashboard/scanner.py:730-737 (falls back to slug)
displayNumberintegeryesmeta.json mission_number; specify_cli/mission_metadata.py:107-116. Display-only, not unique
missionTypestringyesmeta.json mission_type; specify_cli/mission_metadata.py:262. Open set (D-13)
targetBranchstringnometa.json target_branch; specify_cli/mission_metadata.py:61
topologyenum (5)nometa.json topology; mission_runtime/context.py:44-56; unstamped or unknown is unknown (D-11)
createdAtstring, date-timenometa.json created_at; specify_cli/mission_metadata.py:62
statusLaneCountsobject, nine required integer keysnoStatusSnapshot.summary, specify_cli/status/models.py:740-765; nine keys at :29-68
wpTotalintegernolen(snapshot.work_packages); ProgressResult.total_count in specify_cli/status/progress.py (compute_weighted_progress) (D-4). May differ from the number of work package files
blockedCountintegernoderived: statusLaneCounts.blocked (x-derived)
progressobject: weightedPercentage, donePercentage, doneCount, semantics (const weighted_readiness)nospecify_cli/status/progress.py:79-116, :119-206, :48; weights :34-46
lifecycleStatusenum (5)nox-derived: the rule in D-5 over statusLaneCounts, wpTotal, discardedAt and acceptedAt; historic implementation _derive_mission_status in specify_cli/dashboard/scanner.py (board columns, file-count based)
acceptedAt, mergedAt, discardedAtstring, date-timeyesmeta.json; specify_cli/dashboard/scanner.py:235,244; specify_cli/mission_metadata.py:705-731
lastEventIdstringyessnapshot.last_event_id; specify_cli/status/models.py:749
lastActivityAtstring, date-timeyesderived: maximum work package last_transition_at (x-derived; there is no Mission-level event time)
eventCountintegernosnapshot.event_count; specify_cli/status/models.py:748
streamCursorobject: offset (integer), invariant (64 hex characters)nospecify_cli/status/tail_reader.py:59-73, :52 (empty digest at offset 0), :327-339; bound to one log file (D-8)
nextActionstringyesProvisional. Two prose strings or null: the review-then-accept string (when the Mission carries a merge baseline and is not accepted) or the consolidate string (all work packages terminal with some done, not accepted); both embed the Mission slug; null otherwise. Historic implementation _derive_next_action in specify_cli/dashboard/scanner.py; the runtime's Decision (runtime/next/decision.py:94-131) is per-agent and is not v1

Mission detail (adds to the overview)

FieldTypeNullSource
phasesarray of exactly five: name (specify, plan, tasks, implement, review), status (pending, in_progress, complete), basis (artifact, lifecycle_event, derived_from_status_lanes)nox-derived, contract-owned rules in FR-005. Inputs: presence of spec.md, plan.md, tasks.md; the lifecycle event type constants in specify_cli/status/lifecycle_events.py; statusLaneCounts. There is no implement or review lifecycle event, and the historic get_workflow_status returned only four keys
workPackagesarray of summariesnoper work package fields below

Work package (GET .../work-packages/{wpId}; summaries reuse a subset)

FieldTypeNullSource
wpIdstringnospecify_cli/status/wp_metadata.py:222
titlestringnospecify_cli/dashboard/scanner.py:953-962; wp_metadata.py:223
phaseLabelstringyesfrontmatter phase; specify_cli/status/wp_metadata.py:269
authored.role, authored.agentProfile, authored.modelstringyesspecify_cli/status/wp_view.py:117-135
authored.subtasks, ownedFiles, dependencies, requirementRefsarray of stringno (may be empty)specify_cli/status/wp_view.py:132-135, wp_metadata.py:226
executionMode, taskType, priority, mergeTargetBranch, trackerRefsstring or arrayyesspecify_cli/status/wp_metadata.py:235-265 (normalisation of quoted values documented)
statusLaneStatusLaneyessnapshot lane; specify_cli/status/wp_view.py:80,181 (null when no events)
assignment (agent, assignee, role, agentProfile, agentProfileVersion, model, provider)handle stringsyes eachspecify_cli/status/wp_view.py:66-91,159-195; sentinels to null :176-194
subtasksobject: subtask id to StatusLanenospecify_cli/status/wp_view.py:85
subtaskProgressobject: done, totalnospecify_cli/dashboard/scanner.py:1042-1057
implementerOfRecordhandleyesspecify_cli/status/reducer.py:123-172 (schema 2 snapshots only)
lastTransitionAt, forceCount, lastEventIdstring, integer, stringnosnapshot keys present on every entry (specify_cli/status/reducer.py, survey of committed status.json)
actorobject: tool, role, profile (handles)nodecode_actor and actor_identity_str in specify_cli/status/models.py (D-10: mapping from the stored dict and string forms; model is not part of the actor)
cancellationobject: reasonSource, reasonyesspecify_cli/status/reducer.py:114-115
readinessobject: satisfied (boolean), unsatisfied (array of wpId)nospecify_cli/core/dependency_graph.py:34-72 (a missing dependency's status lane defaults to planned, :87)
readyToStartbooleannox-derived: true only when statusLane is planned and readiness.satisfied is true; false for every other status lane, including null. Inputs: dependency_readiness_for_wp in specify_cli/core/dependency_graph.py and the snapshot lane
review.latestResultobject: reviewer (handle), verdict (approved, changes_requested), reference (string)yesspecify_cli/status/reducer.py:201-216; specify_cli/status/models.py:285-305
review.overrideobject: at, actor (handle), reasonyesspecify_cli/status/wp_review.py:28-53; models.py:428-445
historyarray of: eventId, at, kind (transition only), fromStatusLane, toStatusLane, actor, force, reason, reviewVerdictnoStatusEvent in specify_cli/status/models.py. Annotation (InnerStateChanged) and lifecycle rows are not exposed (D-8); reason is a human text field (D-14)
stalenessobject: status (fresh, stale, not_applicable), reason, minutesSinceCommityesProvisional. specify_cli/core/stale_detection.py:38-53
promptMarkdownstringyeson request only; specify_cli/dashboard/scanner.py:1126. No promptPath (D-6, D-14)

Events (GET /events)

Event nameData fieldsSource
status-transitionmissionId, eventId, wpId, fromStatusLane, toStatusLane, at, actor, force, streamCursorStatusEvent in specify_cli/status/models.py; reader-injected cursor keys in poll_once (specify_cli/status/tail_reader.py); projection table in FR-007
mission-lifecyclemissionId, eventId, eventType (one of the seven allow-listed lifecycle event types), at, streamCursorevent type constants in specify_cli/status/lifecycle_events.py; allow-list, missionId and at sources in FR-007
log-truncatedmissionId, reason (size_shrink, content_mismatch), detectedAtOffset, streamCursor (reset to offset 0 with the empty digest)_truncation_signal in specify_cli/status/tail_reader.py

Citation drift check. The data-shape evidence this catalogue builds on was re-verified on this checkout (branch HEAD fefa3bc73, base descending from d78aa2345). The status-domain citations held; the MissionStatus class is declared at specify_cli/status/aggregate.py:168. Upstream main has since moved: #5545 merged and removed specify_cli/dashboard/, so the scanner-bound rows above are historic until the rebase (R-2).

Out of Scope

  • The read service implementation (separate Mission, built against this contract) and the dashboard and Desktop screens (built in the UI repository against this contract).
  • Artifact reads (#5533) and write surfaces (#5231).
  • Execution-lane data (lanes.json: code lanes and the repo-root lane), board-column grouping, retrospective data, done evidence, host-local claim data (shell process ids), the runtime's per-agent next-step decision, open decision counts, and MissionStatus.topology in the status aggregate (a two-value legacy or coordination vocabulary, unrelated to the contract's Topology).
  • Any change under src/, any status or meta.json schema change, any CLI behaviour change, any migration.
  • Authoring or amending an ADR (see C-011), fixing the quirks of the existing lifecycle derivation (D-5), fixing the existing readers' defects (including the reducer limit in D-4), or adding the glossary term execution lane (the glossary was checked: see Terminology and R-9). The preserved surfaces named in Reflexivity are not touched.
  • Publishing the release inside the PR (the tag is pushed after merge, CL-4).
  • Changes to contracts/fixtures/ or to the CLI's own release process.

Assumptions

  • The maintainer-ratified design in the #5528 design comment stands; all six PRs of the stack are drafts until the design is acknowledged (CL-1, C-001).
  • The status domain's readers (materialize_snapshot, reconstruct_wp_view, compute_weighted_progress, dependency_readiness_for_wp, the lifecycle derivation) are stable enough to cite and to call from a test; they are not modified here.
  • The corpus (kitty-specs/ on main) is large enough (on this checkout: 539 Missions with meta.json, 518 with event logs, 3125 work package files, 2936 snapshot work packages, 34148 event lines; the earlier survey counted 538, 517 and 34146, the difference being this Mission's own scaffold) that the floors in FR-019 are meaningful, and its stored data is representative of what a read service will serve. Floors are re-measured at implement start.
  • A JDK, Gradle and network access are available on the CI runner for the contracts workflow; the corpus-marker jobs (the router's tests-corpus and the packs built-in-corpus-suite) do not have a JVM and are not given one (OQ-1, D-16).
  • pytest.ini keeps the corpus marker; the CI router keeps a marker or node-id based corpus selection in some shape (the open CI-rework PR changes the shape, not the existence).

Risks and Dependencies

#Risk or dependencyEffectMitigation
R-1Open PR #5557 (CI rework) edits ci-router.yml, ci-modules.yml, module-tests.yml, ci-aggregate.yml, ci-nightly.yml, packs.yml and .github/ci-module-registry.yml; rewrites tests/architectural/test_ci_corpus_trigger_completeness.py (its glob set becomes the live router's corpus group) and edits tests/architectural/_gate_coverage.py, test_no_duplicate_suite_execution.py, test_workflow_coherence.py and test_module_shard_registry.py, plus many tests/ci/ files; adds tests/ci/test_corpus_blocking_home.py; and re-homes the corpus job to a named-node-id blocking router job with the marker-selected remainder elsewhere.A marker-only corpus test could become advisory or orphaned after that merges; the one-line glob edit and the registry rows in test_ci_corpus_trigger_completeness.py can conflict; the gate files this Mission runs change under it; the workflow-file count ceiling (FR-017) may be consumed.Additive single-line edit; no edit to the glob and data-root sets (D-16); rebase last and re-read each gate file; re-verify selection by collect-only plus a named job link (FR-020); if the blocking home is node-id based, the reality check's id is added to it (a router edit, recorded in the plan with SC-008 amended) or the plan records why it stays marker-selected.
R-2PR #5545 (remove the bundled dashboard) has merged on upstream main (after this branch's base, which still contains the dashboard code). It edited ci-router.yml, ci-module-registry.yml, ci-windows.yml, ci-nightly.yml and the ADR, and removed the dashboard code that many catalogue citations name.Citations to specify_cli/dashboard/ dangle after the rebase, and the derivations that lived only there have no reader left.Rebase onto current main before implementation; the citation check fails loudly on a dangling citation (FR-010); derivations that existed only in the dashboard become x-derived rules in the schema descriptions (D-3, D-5, FR-005) and are covered by the helper unit tests and the pinned expected-output fixture (FR-019); status-domain citations are re-verified after the rebase.
R-3Baseline of known-red tests on main. The corpus and contract tests have failed on main for unrelated reasons before.Misattributing a red.Record, at implement start, a baseline run of the targeted files (tests/contract corpus-marked tests, the three named architectural gates, tests/ci) on the base commit; judge the reality check and the gates against it; report pre-existing reds as the charter requires and never absorb them (C-009).
R-4The reality check couples contract validity to live kitty-specs/ data, and the router corpus glob already includes kitty-specs//tasks/, so every Mission work-package PR runs it against every other Mission's data.A flawed stored Mission, including one mid-flight, turns a corpus-marked job red on an unrelated PR.Zero exclusions at delivery. A failure naming a Mission the PR does not touch is first classified as contract gap, reader defect or data defect and routed to that Mission's owner in the PR conversation; the exclusion path (an issue, an owner and a drain date, printed in the output) is pre-approved for it so a stranger's PR is not stuck. It is never silenced.
R-5A JVM-less corpus job must still render a real verdict (FR-019).A tooling dependency leaks into an unrelated job, or the test skips.OQ-1 resolves the mechanism within the constraint that the existing corpus jobs are not edited; never skip; a non-pytest parity script in the contracts workflow proves any second resolution path equal.
R-6The leak guarantee relies on shape and scan rules, and a handle-shaped string can still be an account name; the status domain has recorded OS and git user names in actor-like fields before.Residual identity exposure through handles.Handle pattern plus value scan plus corpus assertion that known e-mail sources project to null; residual risk stated in the README; no claim of a stronger guarantee.
R-7The issue-matrix scaffold collects #N tokens from the spec and plan prose as issue rows.Approval refusals for prose such as clarification numbers.The spec cites only real upstream issue and PR numbers (#5528, #5558, #5529, #957, #460, #5533, #5231, #5545, #5557; each was checked against the upstream tracker) and names its own decisions CL-n and D-n; each cited number gets an honest verdict row, with a not-applicable verdict for the two PRs.
R-8The requirement parser reads every FR- token in the spec as a local requirement.A false unmapped requirement at finalize.Only FR ids defined in this spec appear in the prose; no other Mission's requirement ids are cited.
R-9execution lane is not a glossary term; the glossary defines Code lane and Repo-root lane (docs/context/topology.md), and its lane is the status meaning (with two glossary surfaces disagreeing on the lane list; see Terminology).Residual terminology ambiguity in code and docs outside this Mission.The contract and README use the glossary terms and keep execution lane only as a stated umbrella phrase tied to docs/architecture/execution-lanes.md; the glossary was checked for the terms used; adding the term, or fixing the stale Lane entry, is out of scope and noted for the maintainers.
R-10Gradle wrapper binaries are committed binaries, a Go binary download needs network, and the Gradle build resolves the generator plugin and its transitive JVM artefacts at run time.Supply-chain and reproducibility concerns; an unverified transitive artefact would make the sha256-pinned bundle depend on unverified resolution.Prefer a pinned Gradle distribution with distributionSha256Sum and a pinned setup action over a committed wrapper jar; Gradle dependency verification with a committed metadata file in strict mode, with a tampered-checksum fixture; FR-018.
R-11prose_only PR classification in the router treats corpus globs as doc paths.Adding contracts/** to the corpus group changes how a mixed contracts plus docstring-only Python change is classified.The plan reads scripts/ci/prose_only.py and scripts/ci/gate_selection.py and records the effect; a guard or test pins any behaviour that changes.
R-12The release job cannot be fully exercised from a PR (publication needs a tag; a manually dispatched workflow resolves from the default branch), and the PR dry run is a job of the contracts workflow, not of the release workflow (DEV-2).An untested publication step, and release-workflow YAML that the PR run does not execute.The dry run on the PR branch proves the shared release script and the build, everything except the publication call; the release workflow's steps are thin calls to the same script and wrapper, and its tag, namespace and publish-step conditions are guard-tested; the post-merge close-out step verifies the real release (FR-022).
R-14Reader drift is not selected by the corpus job. The router's corpus filter (ci-router.yml, the corpus: group) names packs/, Mission spec, plan, tasks, contracts and acceptance-matrix paths and .kittify governance paths; with the added contracts/ glob it still names none of the readers the contract tracks (src/specify_cli/status/, src/specify_cli/mission_metadata.py, src/mission_runtime/, src/specify_cli/core/dependency_graph.py), nor the reality check, its helper or the contracts/tools/ library under tests/contract/**.A PR that edits a reader, or only the reality check or its helper, selects module shards, not tests-corpus; reader drift that breaks the contract is caught on the next push to main (the packs built-in-corpus-suite is forced on for every push to main, D-16 item 5) or on the next contract or Mission-data PR, not before merge. This is the failure the reality check exists to prevent, caught late.Accepted residual, because C-003 and SC-008 forbid widening the router beyond the one glob and a wider filter would run a whole-corpus check on most src/ PRs. Mitigations: the push-to-main run; the README tells reader authors to run the reality check locally (one command) when they change a status reader; the PR body names the uncovered reader paths so a maintainer can choose to add them, with the test files, to the corpus group later (a router edit outside this Mission).
R-13Existing quirks (blocked-only reads as planned; all-canceled and accepted reads as done; tasks status counts differently) are pinned, not fixed.A consumer may expect saner semantics.Documented in the schema descriptions and the CHANGELOG as v1 behaviour; a later minor or major version may change them.

Open Questions (plan phase; none block the spec)

Two items remain open; neither blocks the spec or its review. OQ-1 is a plan-level mechanism with a constraint the plan must satisfy; OQ-4 is a maintainer confirmation request. No requirement depends on either answer beyond that constraint and the DEV notes. The two questions that were open in an earlier draft are resolved and recorded as decisions: where the release dry run's evidence comes from (D-17) and the unscoped event stream (D-18); the numbering OQ-2 and OQ-3 is retired, not reused.

  • OQ-1 Contract resolution in the reality check. The reality check must give a real verdict in the jobs that select the corpus marker, which have no JVM, but the bundle comes from the JVM build. Giving those jobs a JDK and a Gradle step is excluded: it would edit existing jobs (the router's tests-corpus, the packs workflow's built-in-corpus-suite), against C-003, FR-017 and SC-008. The plan therefore chooses between resolving the split files in Python with a reference resolver whose output is proven equal to the CI bundle by the non-pytest parity script in the contracts workflow, or another route that edits no existing job. Whichever it chooses, the resolver is the one contracts/tools/contract_resolver.py that the parity script also imports (FR-019). Constraints: no skip; no second unproven resolution authority; the verdict is real in every job that selects the corpus marker.
  • OQ-4 Maintainer confirmation of DEV-1 and DEV-2 (non-blocking). The maintainer is asked, in the body of PR 1 and on #5528, to confirm or overturn two realised readings: DEV-1 (CL-2: the reality check runs once in tests-corpus, and the contracts workflow proves resolver parity instead of running pytest) and DEV-2 (CL-4: the PR-green release dry run is a job of the contracts workflow, and the release workflow has no pull_request trigger). Work proceeds on the realised forms; the affected requirements are listed in CL-2 and in the DEV-2 note, so an overturn is a bounded edit. It does not block the spec, the plan or the draft PRs.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001 (FR-001 to FR-009, FR-013, FR-014, NFR-002): The bundle built from a clean checkout contains exactly the five v1 path items and zero unresolved file references; two consecutive builds have equal sha256. — [build] · evidence a no-op cannot produce: the bundle itself and its two builds' digests
  • SC-002 (FR-003, FR-004, FR-005, FR-006, FR-008, FR-009, FR-019, NFR-001): The reality check validates at least the floors of FR-019 (500 Missions, 3000 file-backed work package payloads, a summed own-directory snapshot work package count of 2800; measured at delivery from a tests-corpus log and recorded, the survey counts 538 Missions and 3125 files), with zero failures and zero exclusions, each other floor in FR-019 holds, and the snapshot-versus-files disagreement list is printed and at or below its pinned ceiling, and the resolver-parity script (FR-019) passes in the contracts workflow with its three counts above their floors. — [build] · evidence a no-op cannot produce: floors, controls and the equality assertions
  • SC-003 (FR-014, FR-015, FR-016, FR-018, FR-019, FR-021, FR-024, FR-025, NFR-003): Every check listed in FR-021 has evidence of its class, with one passing control run and an asserted failure message: class A (every check run by the contracts workflow, resolver parity included) has at least one linked, real failing run on a planted violation; class B (the reality check and the tests/ci/ guard tests, which are pytest tests) has a green run of the in-test planted case, whose test ids are listed in the PR body. None is demonstrated only by a local run. — [build] · evidence a no-op cannot produce: the linked failing runs and the listed in-test cases
  • SC-004 (FR-010, FR-011): 100 percent of properties in resource schemas, at any depth, carry x-source or x-derived (the x-source and x-derived counts sum to the computed property count, and both counts are printed), every x-derived has a non-empty rule and non-empty inputs, 100 percent of cited paths, symbols and x-derived inputs resolve in the checkout, and every provisional element carries x-provisional. — [build] · evidence a no-op cannot produce: the printed checked and total counts
  • SC-005 (FR-012, NFR-004): Zero absolute host paths and zero e-mail addresses across the contract tree, examples/ and all reality-check payloads (excluding authored promptMarkdown of corpus-derived payloads), with the scan's positive controls detecting planted values of each kind in each field class. — [build] · evidence a no-op cannot produce: the scan floors and controls
  • SC-006 (FR-022): The release dry run (the contracts workflow's release-dry-run job) is green in PR 4 (WP08 and WP09) and its artifact contains a non-empty bundle and an openapi.yaml.sha256 that passes sha256sum -c; the publish-step guard shows publication unreachable on a pull_request. — [build] · evidence a no-op cannot produce: the dry-run artifact
  • SC-007 (FR-017, FR-020, NFR-005): On a pull_request diff touching contracts/** and no packs built_in path, the reality check's test id executes in exactly one blocking per-change job (tests-corpus); on a push to main and a mixed diff the packs built-in-corpus-suite also runs it by existing design and it passes there; and the named gate files (corpus trigger completeness, no duplicate suite execution, workflow coherence, fork guard, fleet verdict, fleet main, workflow-count ceiling) pass with every ledger or allowlist addition, expected to be none, carrying an owner, a rationale and an exit condition. — [build] · evidence a no-op cannot produce: the named job log and the gate runs
  • SC-008 (FR-017, FR-020, C-002 to C-005): Checked twice, over the stack's cumulative diff against origin/main and over each PR's own diff against its base (origin/main for PR 1, the previous seam head for PRs 2 to 6; the router glob line appears in PR 1 only). The diff touches zero files under src/, zero files under contracts/fixtures/, zero lines of .github/ci-module-registry.yml and of pytest.ini, and adds exactly one glob line to .github/workflows/ci-router.yml; its only other edits to shared CI are the four fleet-verdict files of C-003, limited to the changes listed in FR-017 (one PR_WORKFLOWS entry, one workflow_run name, and the two test files), with scripts/ci/fleet_main.py, _CORPUS_GLOBS and _CORPUS_DATA_ROOTS untouched; no Node tooling appears anywhere in the diff. — [build] · no-op passable: yes on its own, because an empty diff satisfies it; it is evidence only in conjunction with SC-007 and SC-002, which a diff that does nothing cannot satisfy (see Traceability)
  • SC-009 (FR-022): Post-merge close-out: the tag contract-mission-status-v1.0.0 exists, its release has exactly the bundle and its sha256, the downloaded checksum verifies, and the digest equals a locally rebuilt bundle; recorded in the Mission close-out record, not a follow-up issue. — [build] · evidence a no-op cannot produce: the published release
  • SC-010 (FR-023, C-001): All six PRs are drafts until a maintainer acknowledgement is linked on #5528, .github/CODEOWNERS routes contracts/ to both handles (PR 3 states review is advisory), and every PR body follows the repository's five-section PR body contract and lists the baseline of known-red tests used for judging; PR 1 (and #5528) carries DEV-1 and DEV-2 as confirmation requests. The per-PR evidence is assigned in the plan ("PR stack"). — [build] · evidence a no-op cannot produce: the linked acknowledgement and the CODEOWNERS check

Traceability

Every FR appears in at least one SC, or names the acceptance criterion that is its only evidence; the table is the check.

SCRequirements it evidences
SC-001FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-013, FR-014, NFR-002
SC-002FR-003, FR-004, FR-005, FR-006, FR-008, FR-009, FR-019, NFR-001
SC-003FR-014, FR-015, FR-016, FR-018, FR-019, FR-021, FR-024, FR-025, NFR-003
SC-004FR-010, FR-011
SC-005FR-012, NFR-004
SC-006FR-022
SC-007FR-017, FR-020, NFR-005
SC-008FR-017, FR-020, C-002, C-003, C-004, C-005
SC-009FR-022
SC-010FR-023, C-001

Requirements and constraints that are not an SC's subject have a named evidence: NFR-001 is a recorded measurement with thresholds the reviewer checks in the body of PR 5 (WP10), measured by WP12 T076 and written into that draft body by the orchestrator; NFR-006 is checked by the README's one documented command; NFR-007 by the recorded commands and the local coverage output pasted in PR 5; C-006 and C-007 by the text-level scan and the review.