Mission Specification: Mission Status contract 1.1

Mission Branch: issue-5625-mission-status-contract-1-1 Created: 2026-10-04 Status: Draft Input: Issue #5625 - "Mission Status contract 1.1: work-package detail and artifact reads (additive)". Part of epic #5528. Binding scope decisions: the operator comment "Scope decisions before design" on #5625 (recorded below as CL-1 to CL-4).

Summary

The Mission Status contract v1 (contracts/mission-status/, #5558) carries the overview of a Mission, its work package summaries, one work package in full, and the change stream. Two of the next mission screens (the dashboard and the desktop app, called "the mission screens" below) need data v1 does not carry: a work-package detail screen and an artifacts screen. This Mission adds those reads as an additive slice, by new operations only. "1.1" is the working name of this slice (it names the Mission and the issue), not a version number: the contract is still unreleased on main (info.version: 1.0.0-SNAPSHOT, never tagged) and the additions ship in that same release (CL-2):

1. Work-package detail: a sub-resource of a work package carrying what v1 lacks: subtask titles, dependency titles with their current status lanes, review history per cycle, the workspace (lane, branches, worktree presence), the owned files each with a change state, and references to the work package's prompt and to the spec it implements. 2. Artifact listing: the files of one Mission as a flat, sorted list with kind, path relative to the Mission directory, size, last-modified time and whether the content is readable. 3. Artifact content: one artifact's text in a JSON envelope, bounded to 256 KiB, always redacted, refused loudly when too large, not text, or holding a credential (AD-17).

Nothing existing changes. The schemas WorkPackage, WorkPackageSummary, MissionHead and ReviewResult and every existing operation stay byte-identical. The contract is unreleased, so there is no new version: info.version stays 1.0.0-SNAPSHOT and the additions go into the same release as the previous API changes. The proof is observable, not asserted: a scripted comparison against the contract tree on main shows existing files unchanged and only new operations and schemas added, the breaking-change check run with that tree as baseline (its scratch copy carries a lower version, because the version is unchanged) exits successfully with breaking=0, and planted violations make each of those checks fail (FR-017). Cutting the release is not produced by this Mission (Out of Scope). The four known v1 gaps (per-Mission staleness, an actor on WorkPackageSummary, the project branch, the lane weights behind weightedPercentage) are decided out of this slice and recorded in the module CHANGELOG as deferred to the next major version.

The Mission delivers a contract change, its examples and its proof. It does not implement the read service (a separate Mission of the epic) and changes no product code under src/. It does extend the shared contract tooling under contracts/tools/ and its tests (FR-015), an acknowledged scope extension, because the new fields need a leak class the shared scanner lacks and the new enums need pins. The slice file set (Terminology) states every contract, tooling and test file the Mission may touch; the Mission's own planning directory is governed separately (Terminology). The proof that the contract fits this repository's own Missions is the extension of the reality check and its projector, which PR #5581 delivered and merged to main (tests/contract/test_mission_status_reality.py, tests/contract/_mission_status_payloads.py). This checkout predates that merge; AD-15 states what that means for the order of work.

Authentication and authorization are the serving layer's responsibility. The contract declares no security scheme, as in v1, and none of the three reads is an access-control surface: what the contract does guarantee is that a missionId and a path can reach only files of that one Mission (FR-014).

Terminology (binding for this spec and for the contract)

  • Mission, never "feature" (charter Terminology Canon). New schema names, examples and descriptions follow the same rule.
  • Status lane: one of the nine work-package lifecycle states of the v1 StatusLane vocabulary. It is the only meaning the contract gives the word lane. A code lane (the glossary term for the .worktrees/ lane worktree and lane branch recorded in a Mission's lanes.json) is named in the new fields laneId and laneBranch; the field names say "lane" for that sense only where the v1 term is already settled, and every description states which sense it uses.
  • Artifact: a regular file inside one Mission's directory on the primary planning surface (AD-2), other than the status records excluded by FR-011. Its kind is one value of a closed vocabulary (FR-010).
  • Artifact path: a path relative to the Mission directory, with / separators, never absolute.
  • Prompt file: a regular, non-symlink .md file directly under tasks/ whose name is a work package id followed by a hyphen (WP[0-9]{2,}-<anything>.md: the WpId pattern of contracts/mission-status/schemas/WpId.yaml, at least two digits, then -; the wpId of a prompt file is its name up to the first hyphen). A file such as tasks/WP1-x.md has one digit, is no prompt file and is classified other (FR-010). The prompt files of one work package are those whose name starts with that work package's wpId and a hyphen. When more than one file qualifies for one work package the first in byte order of the file name is the prompt file, and every use of "the work package's prompt file" in this spec (FR-005, FR-008, AD-18) means that one. A file such as tasks/WP-notes.md, or one of another work package's id, is not a prompt file of this work package; tasks/WP-notes.md is no prompt file at all and is classified other (FR-010). <wp-slug> is the stem of the prompt file (its name without .md).
  • Work package of a Mission (the universe of the detail read): the work packages that v1's workPackages list and getWorkPackage serve, selected the way the product selects work package files: the single selector wp_task_files (src/specify_cli/status/wp_metadata.py: the entries of tasks/ whose name starts with WP and ends with .md, case-sensitive), each identified by the work_package_id of its frontmatter as v1 reads it. This is deliberately wider than a Prompt file: a v1 work package whose file is not a Prompt file (for example tasks/WP01.md of sync-diagnose-canonical-allowlist-01KS4F8H) is a work package here, answers 200, and has artifactReferences.prompt null (a link is made only to a Prompt file) and kind other in the listing. The detail answers 200 for exactly these and 404 for any other id, including an id that appears only in the status log (v1 cannot represent it either; it is not served). A work package file with no status events is served, with null status lanes. A WP*.md file that wp_task_files selects but whose frontmatter does not parse (a frontmatter or validation error) or has no valid work_package_id (for example a stray tasks/WP-notes.md) is skipped, never a 500: it is not a work package, contributes nothing to the listing, to the detail universe or to the independent count, and raises no error (as the product's own bootstrap skips it, src/specify_cli/status/bootstrap.py: "Skipping ...: missing or invalid work_package_id", and skips a file whose frontmatter raises a parse or validation error; a work package file of unparseable frontmatter is therefore not the "file that cannot be read" of FR-020, which is an I/O failure); its stem answers 404 on the detail. The independent count of this set (FR-021) is the number of distinct work_package_id values of the files wp_task_files selects, counted by a direct scan, not by the reader under test.
  • Two selectors, stated once: the universe of work packages (the list, the detail's 200/404 rule, FR-021's floors) follows wp_task_files; the stricter Prompt file pattern governs only artifactReferences.prompt and the work_package_prompt kind (FR-010). Every other use of either term in this spec follows from this entry.
  • Malformed artifact path: one predicate, used by AD-9, AD-19, FR-010 and FR-014. A path is malformed when it is empty; is longer than 512 characters; is absolute (begins with /, begins with ~/, or begins with a drive letter and a colon); contains a backslash or a NUL; has an empty segment (a//b) or a trailing /; or has a . or .. segment. The artifact-path rule is its negation: a path that is not malformed. A malformed path in a request is a 400 (AD-9); an emitted path (listing, envelope, artifactPath, artifactReferences[].path) that is malformed fails the check; by the definition of eligible file below, none can occur for an eligible file.
  • Reference reader: the test-tree helper that implements the rules of this spec well enough to build payloads over this repository's own Missions: the extension of the projector tests/contract/_mission_status_payloads.py that #5581 delivered, built from public readers only. It is not a product surface and the service is not built from it; it is the executable statement of the rules and the thing the reality check runs.
  • Eligible file: a regular file under one Mission directory on the primary planning surface such that no component of its path below the Mission directory is a symlink (every component is lstat-ed, FR-014), it is not a root-level status record (AD-5), and its path is not malformed (the artifact-path rule above). Eligibility is a rule over one path, never membership of the listing (AD-8), whatever the 1000-entry cap does to the listing. It is the single rule that makes the listing, the content read, artifactPath and artifactReferences agree. A regular file whose name makes its path malformed (a backslash in the name, a path over 512 characters, a first-level directory named ~, a name beginning with a drive letter and a colon) is not eligible: it is neither listed nor readable, because no request can name it (a request for that path is a 400, AD-9). The Linux file system allows such names, which is why the rule is stated rather than assumed.
  • Credential pattern: the credential kinds a content read refuses and a title withholds (AD-17): a GitHub token (a classic gh token with prefix ghp, gho, ghu, ghs or ghr, or a fine-grained token with prefix github_pat), an AWS access key id (prefix AKIA or ASIA) and the header line of a PEM private key. The authority for the exact expressions is SECRET_PATTERNS in contracts/tools/leak_patterns.py; this spec adds none and weakens none. The descriptions of the 422 response and of readable list these kinds by name, so a service can be built from the contract alone, and a change of the list is a behaviour change of a released read, pinned by a test that compares the description with SECRET_PATTERNS (FR-019).
  • Slice file set: the statement of which contract, tooling and test files this Mission may change. FR-015, FR-017, Out of Scope, SC-008 and AC-VERSION refer to it and add nothing. (1) New files under contracts/mission-status/ (paths, schemas, parameters, responses, examples). (2) In existing files of contracts/mission-status/: only openapi.yaml (the version and the path map), the _index.yaml files (new entries) and CHANGELOG.md (the 1.1.0 entry). (3) In contracts/tools/: only leak_scan.py, fixture_builder.py (STRICT_FIELDS), negative_cases.json and enum_pins.json (pins for the new enums). (4) The test tree under tests/contract/. (5) Job registration under .github/ only if the plan adds a job (FR-023). Operator-decided exceptions (OD-1 = X-pure, operator 2026-10-04): the registrations of the new test modules, namely the --deselect entries in .github/workflows/packs.yml and the registry rows in tests/architectural/test_ci_corpus_trigger_completeness.py, are part of the slice file set; the scope check allows exactly these two paths. Nothing under src/ and nothing else under contracts/, contracts/_shared/ included. The Mission's own directory kitty-specs/<mission>/ (this spec, the plan, the tasks, the reviews and the status log) is not part of the slice file set: it is governed by C-003 (add-only, and no edit of a file already on main), and the scripted scope check excludes that one directory from its comparison and checks it only for that rule. The slice file set is therefore the statement of the contract, tooling and test files, not of every path the Mission's branch changes.
  • Host capability: the explicit input of the reference reader that says whether git and lane worktree lookups are available. The reader never discovers it from the machine it runs on (FR-016).
  • Provisional, x-source, x-derived: as defined by the v1 contract (contracts/README.md).
  • Planted negative: a deliberately violating input used to prove a check fails; each endpoint has one under the leak rules (AC section), and each rule that has a stated mutation has a test that applies the mutation (FR-022).

Clarifications / Decisions

Decisions acknowledged by the operator (consolidated)

Each row below is an author choice that extends or interprets the wording of an operator decision (CL-1 to CL-4). The operator acknowledged AD-5 through AD-22 as written on 2026-10-04 (CL-5), and each row and each AD entry below says so; the acknowledgement does not change their content. The table is complete for that class. The row FR-015 is a scope extension, not an AD; the operator acknowledged it on 2026-10-04 as a scope extension of the contract tools (CL-5). The author decisions that interpret no operator wording and are ordinary design choices open to squad scrutiny are: AD-1, AD-2, AD-3, AD-4. The operator had decided exactly these statuses and members before the acknowledgement: 413 and 415 for the content read, the envelope members {path, kind, mediaType, encoding, sizeBytes, redacted, content}, the always-redact rule with the tokens [path] and [email], the 256 KiB cap, and the owned-file object. Each acknowledged decision stands as the most conservative testable reading written in its decision, the plan builds nothing beyond it, and a reversal is priced next to it.

DecisionWhat the operator fixedWhat this spec adds or readsConservative reading keptCost of reversal once released
AD-5 status records are not artifacts (acknowledged by the operator 2026-10-04)"artifact listing" (CL-1)the root status.events.jsonl and status.json are not listed and not readableexcluded by exact root-level name onlya behaviour change of the reads, no schema change
AD-6 symlinks never followed (acknowledged by the operator 2026-10-04)"artifact listing" (CL-1), "artifact content" (CL-3)a symlink is neither listed nor readable (404), stricter than resolving and checking containmentnever followed, no resolutiona behaviour change of the reads, no schema change; admitting symlinks later widens a released read
AD-7 readable semantics and whole-file listing reads (acknowledged by the operator 2026-10-04)"the listing marks that file readable: false" for a file over the cap (CL-3)readable is false also for 415, 422 and 500, and the listing reads every returned file of at most 262144 bytes in full to decide it (worst case 262,144,000 bytes per listing)the stricter, checkable reading: readable: true implies a successful content readreadable is a stable member: narrowing it to oversize only changes the meaning of a released member (a behaviour change, not a schema change) and would let a screen offer a file that always fails; dropping the whole-file reads trades that guarantee for listing speed
AD-8 listing shape and 1000-entry cap (acknowledged by the operator 2026-10-04)"artifact listing" with kind, path, size, time, readable (CL-1)flat files only, byte-order sort, a cap of 1000 entries with truncated: true, no pagination; links by eligibility, not by listingcap 1000, truncated provisional (AD-14)the cap is behaviour, not a schema element: moving it changes what a screen sees without a schema change; a later pagination would add request members (a minor addition)
AD-9 status 400 invalid_artifact_path (acknowledged by the operator 2026-10-04)413 and 415 only (CL-3)a malformed, missing or repeated path is a 400400 declared on getArtifactContenta status removed from a released operation is breaking (C-001): major version
AD-10 meaning of redacted (acknowledged by the operator 2026-10-04)the member exists and content is always redacted; the meaning is not stated, and the wording could be read as "always true"true only when a substitution changed the contentthat reading, with redacted marked x-provisional and named in the CHANGELOG Provisional sectionnone within 1.x (provisional); a stable member would need a major version
AD-11 mediaType table (acknowledged by the operator 2026-10-04)the member mediaType (CL-3)the fixed extension table and the text/plain default, never sniffedthe table of AD-11a table row change is a behaviour change of a stable member's values, no schema change
AD-12 worktreePresent for a planning-lane work package (acknowledged by the operator 2026-10-04)"worktree presence" in workspace (CL-1)true only for a lane worktree: a planning-artifact work package, whose workspace is the repository checkout, reports falsetrue only when runs_in_checkout_root is false and exists is trueworkspace is provisional (AD-14): none within 1.x
AD-13 review cycle is a review-cycle file (acknowledged by the operator 2026-10-04)"review cycles" (CL-1)one entry per review-cycle-<N>.md; a review without a file is not a cycle; the verdict is matched from status events by the full pointerfile-based cycles, no guessed verdictreviewCycles is provisional: none within 1.x
AD-14 provisional set (acknowledged by the operator 2026-10-04)the envelope, the owned-file object, the cap, 413 and 415member shapes of reviewCycles and workspace, ArtifactKind, truncated, redacted, the refusal code valuesthose elements are x-provisionalnone within 1.x
AD-15 two deliverables, one order of work (acknowledged by the operator 2026-10-04)the slice is contract change, examples and proof (CL-1, CL-2)the Mission branch is brought level with main before the plan or any implementation (an orchestrator step); the reference reader and the synthetic tests follow the projector's orderthe order stated in AD-15process only, no contract member
AD-16 no version number in scope is not violated (acknowledged by the operator 2026-10-04; amended by the operator ruling of 2026-10-05)the module version (CL-1, CL-2)there is no new version: info.version stays 1.0.0-SNAPSHOT, a property of the deliverable, not a release scope for the CLI productthe reading in AD-16none, a reading of the charter rule
AD-19 artifact path is its own leak class (acknowledged by the operator 2026-10-04)the strict leak classes of v1 (D-14) apply to new fieldsa path with a home/<x>/ segment or an at sign is listed with its real name, checked by the artifact-path rule, not the strict patterns; a file whose path is malformed under that rule is not eligible, so it is not listed and not readablethe artifact-path rule of Terminologya scanner rule change, no schema change
AD-17 status 422 artifact_secret, credentials in titles (acknowledged by the operator 2026-10-04)the tokens [path] and [email] only (CL-3)the credential kinds are those of the Terminology entry and are listed in the contract descriptions; a content file holding a credential is refused (422); a title holding a credential is withheld (subtask null, dependency the work package id); no [secret] token is introducedno new token; credentials are refused or withheld, never maskedthe 422 status is stable once released: removing it is a major version. The alternatives are a [secret] token for titles (a new token) or folding 422 into 415 (stretches the operator's code); both are the operator's call
AD-18 review cycles from the primary planning surface only (acknowledged by the operator 2026-10-04)nothing (CL-1 lists "review cycles")a cycle held only on a coordination surface is not shownprimary only, no src/ change; OQ-2 is resolved: this option is kept (2026-10-04)reviewCycles is provisional: none within 1.x
AD-22 modifiedAt format (acknowledged by the operator 2026-10-04)"last-modified time" (CL-1)RFC 3339 UTC, Z, whole seconds, floored; host modification timethat formata format change of a stable member is a behaviour change: consumers parse it
FR-015 tooling edit (scope extension; acknowledged by the operator 2026-10-04)the slice is "new operations and new schemas" (CL-1)contracts/tools/ (leak_scan.py, fixture_builder.py STRICT_FIELDS, negative_cases.json, enum_pins.json: the slice file set of Terminology) and the contract-tools tests are edited to give the new fields their leak class and artifact-path ruleedited only as FR-015 states; every existing contract file must still pass; the alternative is to scan the new fields in the reference reader only and leave the shared scanner unchanged, which weakens FR-015tooling and tests, no schema or version effect: reverting removes the guard, not a contract member
AD-20 changeState inputs and glob rule (acknowledged by the operator 2026-10-04)derived with git against the lane branch; unknown without a worktree (CL-4)base, committed-only, rename and deletion handling, detached HEAD, glob matcher (exactly the repository's owned-files matching: fnmatch or the directory-prefix rule, plain directory entries included; operator ruling 2026-10-04), brace patterns unknownthe rules of FR-007changeState is a stable closed enum, but its derivation is not a schema element: a rule change is not a schema change
AD-21 statuses 404 and 500 and their codes (acknowledged by the operator 2026-10-04)413 and 415 only (CL-3)404 (with code not_found on all three reads; an ill-formed missionId or wpId is a 404) and 500 (artifact_unreadable, also for an unreadable Mission directory on the content read; artifact_listing_unreadable; source_unreadable on the detail read)as written in AD-21the statuses are stable once released (only the code values are provisional): a change is a major version

Operator decisions (binding; the four decisions of the #5625 comment "Scope decisions before design")

These bind the spec, the plan and the implementation. A reviewer checks against them and does not re-open them.

CL-1 - Additive through new operations only; no new version; four gaps deferred. The slice adds new operations and the new schemas used only by them. Existing schemas (WorkPackage, WorkPackageSummary, MissionHead, ReviewResult) and existing operations are unchanged. The new reads are: a work-package detail sub-resource (subtask titles, dependency titles and lanes, review cycles, workspace, owned files, artifact references), an artifact listing, and an artifact content read. The four v1 gaps (per-Mission staleness, actor on WorkPackageSummary, project branch, lane weights) are out of this slice, deferred to the next major version, and the module CHANGELOG records that. There is no new contract version: the operator ruling of 2026-10-05 (CL-2) puts the additions into the same unreleased 1.0.0-SNAPSHOT release as the previous API changes.

CL-2 - Baseline and sequencing (amended by the operator ruling of 2026-10-05). The work branches from main. The reality-check extension builds on #5581, which has since merged to main (AD-15). The Mission Status contract is still unreleased on main (info.version: 1.0.0-SNAPSHOT, never tagged), so the additions of this Mission go into the SAME release as the previous API changes, with no new version number: info.version stays 1.0.0-SNAPSHOT, the CHANGELOG additions merge into the existing 1.0.0-SNAPSHOT section, and no release tag is needed. "1.1" in the Mission slug and the issue title is a name, not a version. The additive proof compares the candidate with the contract tree on main (the --baseline-root option of contracts/tools/breaking_check.py, over the module and _shared extracted from main; commit aef7cc967 or the current origin/main); the tag-based baseline (contract-<module>-v<semver>) does not apply. With no tag, breaking_check.py accepts only 1.0.0 or 1.0.0-SNAPSHOT (first_release), which is what the tree carries, so the breaking-change job of the Contracts workflow does not fail for this slice.

CL-3 - Artifact content is a JSON envelope, bounded, redacted, refused loudly. The envelope is {path, kind, mediaType, encoding, sizeBytes, redacted, content}. The cap is 256 KiB. A file over the cap is a 413 Problem, and the listing marks that file readable: false. A file that is not UTF-8, or contains a NUL byte, is a 415 Problem. Content is always redacted ([path], [email]). sizeBytes is the on-disk size. (The treatment of credentials, which CL-3 does not address, is AD-17.)

CL-4 - Owned files carry a change state. Each owned file is {pattern, isGlob, changeState} with changeState one of changed, unchanged, unknown. It is derived with git against the lane branch. It is unknown whenever no worktree or branch is available, which includes every corpus run (the corpus run passes a host capability without worktrees, FR-016). The git path is proven by a synthetic-repository test. (The exact inputs and the glob semantics, which CL-4 leaves open, are FR-007 and AD-20.)

Removed-dashboard routes covered (the artifact reads of the replaced dashboard; #5533 stays the route-mapping job): /api/artifact/, /api/research/, /api/contracts/, /api/checklists/, and the artifact part of /api/dossier/*. The dossier overview and the snapshot export are not part of this slice.

Rulings recorded 2026-10-04 (binding)

CL-5 - Acknowledgement of AD-5 to AD-22, and three open questions closed (2026-10-04). (1) The operator acknowledged AD-5 through AD-22 as written; their content is unchanged and each entry says so. (2) The operator resolved OQ-2: AD-18 is kept (review cycles are read from the primary planning surface only, no change under src/). (3) The orchestrator resolved OQ-5: all three new operations carry a refusal code on their 404; the detail 404 gets its code through a module refusal schema built like ArtifactRefusal (WorkPackageDetailRefusal, FR-001, FR-013); v1's getWorkPackage is unchanged. (4) The orchestrator resolved OQ-6: source_unreadable, and the other detail refusal code (not_found), are pinned by that module refusal schema through the titled enum WorkPackageDetailRefusalCode, pinned in contracts/tools/enum_pins.json like the other new enums (FR-015 (d), FR-019). OQ-1 and OQ-4 stay open for the plan phase. Also in this round (author, no operator wording involved): a WP*.md file whose frontmatter does not parse or has no valid work_package_id is skipped, as bootstrap does (Terminology, FR-021, FR-022; orchestrator ruling R6-NEW-1), and AD-21 lists the detail's 500 source_unreadable (R6-NEW-2). Operator rulings of 2026-10-04 (binding): (5) the glob rule of changeState is exactly spec-kitty's existing owned-files matching, _matches_any_glob (src/specify_cli/policy/commit_guard.py, lines 141 to 149) equal to _mt_matches_owned_file (src/specify_cli/cli/commands/agent/tasks_move_task.py, lines 838 to 847): normalise \ and a leading ./, then fnmatch.fnmatch or the directory-prefix rule, plain directory entries included; FR-007 rule 3 states it and has no algorithm of its own (AD-20); (6) the FR-015 extension of the contract tools is acknowledged; (7) AD-15 and AD-16 are acknowledged, within the range AD-5 to AD-22.

Author decisions (need squad scrutiny)

These are decisions this spec makes where the operator's decisions leave a choice. They are reviewable; if one is overturned, the FRs that cite it change with it. A decision marked acknowledged by the operator 2026-10-04 extended or interpreted a binding decision (CL-1 to CL-4); it now binds as written, stands as the most conservative reading, and the plan does not build beyond it.

  • AD-1 Operations and paths. Three new operations: getWorkPackageDetail at GET /missions/{missionId}/work-packages/{wpId}/detail, listArtifacts at GET /missions/{missionId}/artifacts, and getArtifactContent at GET /missions/{missionId}/artifacts/content with a required query parameter path. The artifact path is a query parameter so that no path segment has to carry / or percent-encoded separators. Final path-file names follow the README naming rule and are the plan's to fix; the three-operation shape and the query-parameter form are the decision.
  • AD-2 The Mission directory read is the primary planning surface. Artifacts are read from the Mission directory on the primary planning surface, as the replaced dashboard did (it resolved the Mission directory through a planning-surface resolver and checked containment against both the Mission directory and the artifact directory), and not from the coordination status surface. Ledger entry SK-310 records that the two surfaces differ on a coordination-topology Mission and that reading a planning artifact from the status surface makes it look missing. Which resolver the plan uses is the plan's job; the surface is the decision.
  • AD-3 The detail schema is separate and self-contained. WorkPackageDetail does not embed or extend WorkPackage (closed schemas cannot be extended without changing the existing one, and CL-1 forbids that). It carries missionId, wpId and the new groups only; a screen that needs both calls both reads.
  • AD-4 Unknown stays unknown. Where a value cannot be determined, the field is null (or unknown for changeState) and the read still succeeds, because the Mission and work package exist. The read fails (a Problem) only when the thing asked for does not exist, is refused by a rule, or cannot be read at all. A field is never filled with 0, an empty string or a guessed value to look complete.
  • AD-5 Status records are not artifacts (acknowledged by the operator 2026-10-04). This is an author narrowing of the operator's "artifact listing", grounded in v1's decision D-8 that raw event-log rows are never forwarded; the operator did not exclude status records. status.events.jsonl and status.json are excluded from the listing and cannot be read through the artifact content read (404, as for any non-eligible path). The exclusion is by exact root-level file name only: those two names directly in the Mission directory. A file of the same name at any other depth (for example tasks/WP01/status.json) is an ordinary artifact, listed and readable. The typed reads and the change stream are the access path to status (free-text notes and host paths live in the event log). Nine of the eleven files over 256 KiB at measurement are event logs, so this decision also removes most oversize cases from the listing; the remaining oversize cases (two evidence text files at measurement) still exercise the 413 path, and a synthetic boundary test covers it exactly. Which behaviour of the replaced dashboard's artifact routes this changes, if any, is for #5533 to record; this Mission only records the narrowing in the CHANGELOG entry.
  • AD-6 Symlinks are never followed (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). A symlink inside a Mission directory is neither listed nor readable (404). This is stricter than "resolve and check containment" (the corpus has none, so nothing is lost) and removes the whole symlink-escape class instead of policing it.
  • AD-7 readable covers every refusal, and the listing reads whole files (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). readable is false exactly when a content read of that file would be refused with 413, 415, 422 (credential, AD-17) or 500 (artifact_unreadable). The listing therefore decides readable by running the very same checks as the content read (FR-013), in the same order and with the same text-check, over the whole file for every returned entry whose st_size is at or below 262144 bytes; a file over the cap is decided from st_size alone and is not opened. A NUL byte or an invalid sequence at the last byte of a file of exactly 262144 bytes must therefore give readable: false. Consequence: readable: true implies the content read succeeds (barring a concurrent change), a checkable invariant. The cost is stated, not hidden: the worst case of one listing is 1000 entries times 262144 bytes, 262,144,000 bytes read (the largest Mission tree at measurement has 119 files). The alternatives (size-only readable, or a prefix check) are cheaper but let the listing say readable: true for a file that always fails; this spec rejects both.
  • AD-8 Listing shape (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). A flat array of files only (no directory entries; the tree is implied by the paths), sorted by path in ascending byte order, capped at 1000 entries (the largest Mission tree at measurement has 119 files). Over the cap the response carries truncated: true; it never silently drops entries. No pagination. The cap bounds the listing only: the content read and artifactReferences use eligibility (see Terminology), which is a rule over one path and does not depend on the cap, so a file past the 1000th is not listed but is readable by path and is still named by artifactReferences when it is the work package's prompt or the spec. The listing must visit every name under the Mission directory (to sort and to set truncated) but opens only the files of the entries it returns.
  • AD-9 Malformed path is 400, an absent path is 404 (acknowledged by the operator 2026-10-04, the 400 status included; CL-3 fixes 413 and 415 only). A path that is malformed (the single predicate of Terminology: empty, over 512 characters, absolute including ~/ and a drive letter, backslash, NUL, empty segment or trailing /, . or .. segment) is a 400 Problem (invalid_artifact_path). A well-formed path that names no eligible file is 404 (not_found). The same 404 covers a directory, a symlink (including a path through a symlinked directory), an excluded status record and a vanished file. Percent-decoding happens once, at the HTTP layer; the reader never decodes again. A path that is missing or given more than once is a 400 invalid_artifact_path too. An ill-formed missionId or wpId names no Mission or work package and is a 404 (v1 declares only 404 and default for those parameters; on all three new reads the 404 carries not_found). The 400 status is stable once released: removing it later is a major version (AD-21).
  • AD-10 redacted means a replacement was made (acknowledged by the operator 2026-10-04; CL-3 names the member but not its meaning). redacted is true when at least one [path] or [email] substitution changed the content, false when the text passed through unchanged. Redaction itself is always applied (CL-3). The operator's wording ("content is always redacted") can also be read as redacted being constantly true; this spec does not rely on that reading because it tells a consumer nothing, and the operator acknowledged the reading written here on 2026-10-04. The member nevertheless stays x-provisional (AD-14): it carries an open_decision text stating both readings and is named in the CHANGELOG Provisional section, so a later change of meaning is not a major version.
  • AD-11 mediaType comes from a fixed extension table (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). md is text/markdown, json is application/json, yaml and yml are application/yaml, jsonl is application/x-ndjson, csv is text/csv, everything else readable is text/plain. It is never sniffed from content. The response's own HTTP media type is application/json (the envelope).
  • AD-12 worktreePresent is true only for a lane worktree (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). resolve_workspace_for_wp resolves a planning-artifact work package to the repository root checkout, which always exists. Reporting that as a present worktree would mislead, so worktreePresent is true only when the resolved workspace (ResolvedWorkspace in src/specify_cli/workspace/context.py) has runs_in_checkout_root false and exists true on the serving host. The reader uses runs_in_checkout_root and does not re-derive the test. The resolver raises for cases this spec treats as normal; FR-006 maps each exception.
  • AD-13 A review cycle is a review-cycle file (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). The reviewCycles array has one entry per review-cycle artifact of the work package (file review-cycle-<N>.md, N a positive integer without leading zero, under the work package's task directory on the primary planning surface, AD-18). Reviews that left no cycle file (many approvals have none) are not cycles; they remain visible in v1's history through reviewVerdict. The verdict of a cycle comes from the status events, matched to the cycle by the full review-cycle pointer (FR-005), and is null when no event matches; it is never read from the file or guessed. A cycle file that cannot be parsed is still listed (cycle number from its file name, reviewedAt and reviewer null; verdict, feedbackReference and artifactPath follow their own rules, FR-005) so the failure is visible; it is not dropped. The reviewedAt of a parseable cycle is the stored value only when it is a valid RFC 3339 date-time with offset, otherwise null (FR-005).
  • AD-14 What is provisional (acknowledged by the operator 2026-10-04). The operator's decisions fix the content envelope, the owned-file object, the 256 KiB cap and the 413/415 codes. They do not fix the member shapes of reviewCycles and workspace, the listing entry, the ArtifactKind vocabulary, the listing's truncated member, the meaning of redacted (AD-10), the refusal codes other than 413 and 415, or any HTTP status other than 413 and 415. Those are author choices. Because every response is closed, a later member added to a stable schema forces a major version. The conservative choice is therefore x-provisional (with its open_decision text and its name in the CHANGELOG Provisional section) on exactly these elements: the reviewCycles and workspace properties of WorkPackageDetail and the ReviewCycle and Workspace schemas, the ArtifactKind schema, the truncated property of ArtifactListing, the redacted property of ArtifactContent, and the code property of ArtifactRefusal and of WorkPackageDetailRefusal. Everything else is stable, the declared HTTP statuses included: only the code values are provisional, not the statuses they are pinned to, so adding or removing a status (400, 404, 422 or 500, AD-21) on a released operation is breaking (C-001) and costs a major version. The rejected alternative (nothing provisional, a major bump for any later member of an author-chosen shape) is recorded here; the cost of the choice is that a consumer of the provisional elements must tolerate their change within 1.x. The environment-dependent fields say so in their descriptions as well.
  • AD-15 Two deliverables, one order of work (acknowledged by the operator 2026-10-04). #5581 has merged to main; tests/contract/_mission_status_payloads.py and tests/contract/test_mission_status_reality.py exist there. This checkout predates the merge, so before the plan or any implementation the Mission branch must be brought level with main; that is an orchestrator step, not done in the spec phase, and the branch is not rebased by the spec. The contract slice (schemas, paths, examples, CHANGELOG, version, breaking-check proof) does not depend on the projector and can be reviewed first. The reference reader (FR-021) and the synthetic-repository tests (FR-022, which exercise the reference reader) both live in the test tree beside that projector and follow the same order; neither is gated differently from the other. The Mission is not complete until both exist. The corpus floors and counts quoted in this spec were measured before the merge and are re-measured on the level branch (A-5).
  • AD-16 The charter rule "no version numbers in scope" is not violated (acknowledged by the operator 2026-10-04; amended by the operator ruling of 2026-10-05). There is no new version: info.version stays 1.0.0-SNAPSHOT, the contract module's own property (a property of the deliverable), not a release-scope assignment for the CLI product.
  • AD-17 Credentials in served content (acknowledged by the operator 2026-10-04; CL-3 fixes only [path] and [email], and only 413 and 415 as statuses). contracts/tools/leak_patterns.py (leak_codes) returns a third code, SECRET, for the credential kinds of the Terminology entry "Credential pattern" (that entry states the rule and the authority; this decision only says what is done with a match), and lists forbidden property names (FORBIDDEN_PROPERTY_NAMES). The most conservative treatment: (1) the content read refuses a file whose decoded text holds a credential pattern: 422, code artifact_secret, no content in the body; the listing marks that file readable: false through the same check (AD-7); the refusal is decided after 413 and 415. The content is never served with the credential masked, because a masked credential is still evidence of a leaked one. The 422 status is an author addition and is stable once released (AD-21). (2) In human text fields (subtask and dependency title) a title that holds a credential pattern is withheld, not masked, so that no token beyond the operator's [path] and [email] is introduced: a subtask title is null (FR-003) and a dependency title is the work package id (FR-004). Path and e-mail substrings of such fields are still replaced by [path] and [email]. The alternative, a [secret] token for titles, would be a new token and is not adopted without the operator. (3) In strict fields a SECRET match is handled like any other leak code in a strict field (null where nullable, else a projection error). (4) Forbidden property names are checked as keys of the payloads the readers build, as in v1. Inside served JSON, YAML or JSONL content, a forbidden name is only text: it is not refused and not parsed; its value is redacted when it is a host path or e-mail address (the path and e-mail patterns apply to all text) and is otherwise the accepted residual. The leak scan of the reality check runs all three codes (HOST_PATH, EMAIL, SECRET) and the forbidden-name key check over every payload.
  • AD-18 Review cycles are read from the primary planning surface only (acknowledged by the operator 2026-10-04; narrows the product's own resolver). No public function enumerates a work package's review-cycle files; the only enumerator, _review_cycle_wp_dir in src/specify_cli/review/cycle.py, is private and reads the coordination worktree when one is materialized on the host, which would make reviewCycles host-dependent and send the reference reader through a private seam, against the projector's public-readers-only rule (load_source) and NFR-005. So the reader enumerates tasks/<wp-slug>/review-cycle-<N>.md under the Mission's own primary directory only, where <wp-slug> is the stem of the work package's prompt file (Terminology, which also fixes the choice when several qualify); a work package with no prompt file has no cycles ([]). A cycle that exists only on a coordination surface is not shown: the reviewCycles description says so, and v1's history still carries the verdicts. Consequences, all stated: artifactPath is consistent with the artifact reads (both use the primary surface, AD-2, and the same eligibility rule, AD-8); the answer is deterministic across hosts; a stranded primary-checkout record (ledger SK-348) is found, because it sits in the primary directory. The alternative (one public listing seam in src/, with a justification against NFR-007 and SC-008) was OQ-2; the operator resolved it on 2026-10-04 by keeping this decision (primary planning surface only, no src/ change), so no seam is added.
  • AD-19 The artifact path is its own leak class (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). An artifact path is a repository file name, public content already, not a host location. The strict class's home-segment and e-mail patterns would reject a legitimate relative name (for example data/home/ann/notes.md, or a name containing an at sign); one such file would fail the whole listing or silently vanish from it. The decision: such a file is listed with its real path and readable by that path, and the path field (listing entries, content envelope, artifactPath, artifactReferences[].path) is checked by the artifact-path rule instead (Terminology: the negation of the one malformed-path predicate that AD-9 also uses). Credentials and e-mail addresses inside a file name are not detected (accepted residual: the name is repository content). A file whose own path is malformed under the artifact-path rule (a backslash in the name, over 512 characters, a first-level ~ directory, a leading drive letter and colon) is not an eligible file (Terminology): the walk does not list it and the content read cannot name it, so no emitted path is ever malformed and listing, content read and links agree. The shared scanners choose a field class by key name, so this carve-out needs a change to them (FR-015).
  • AD-20 changeState inputs and glob semantics (acknowledged by the operator 2026-10-04; interprets CL-4). The inputs, the base, the committed-only rule, deletion and rename handling, the detached-HEAD rule and the glob matcher are those of FR-007. The matcher is exactly the repository's owned-files matching (operator ruling 2026-10-04, CL-5): _matches_any_glob in src/specify_cli/policy/commit_guard.py lines 141 to 149 and its near copy _mt_matches_owned_file in src/specify_cli/cli/commands/agent/tasks_move_task.py lines 838 to 847 (fnmatch on the normalised path, or the directory-prefix rule, plain directory entries included). The two agree on every path git produces. They diverge on two inputs only, a leading ./ and a backslash (the second copy normalises both, the first does not); git yields neither in a change set, and the parity test bounds both divergences. Brace patterns are unknown: is_glob_pattern treats { as a glob character but the repository's matcher has no brace expansion, and guessing would risk a silent wrong unchanged.
  • AD-21 Failure statuses beyond 413 and 415 (acknowledged by the operator 2026-10-04; CL-3 fixes 413 and 415 only). The operator decided two statuses. Every other status the three new operations declare is an author addition: 400 (AD-9), 404 (FR-013 and FR-001; on all three reads it carries the code not_found, the detail through WorkPackageDetailRefusal), 422 (AD-17) and 500. Three states had no status or code: a file that cannot be opened or read for another reason (permission, I/O), a Mission directory or an entry stat that cannot be read in the listing, and a Mission or work package source that the detail read cannot read (FR-001, FR-020). They are 500 Problems with the stable codes artifact_unreadable, artifact_listing_unreadable and, for the detail, source_unreadable (in WorkPackageDetailRefusal). The statuses are stable once released (AD-14 makes only the code values provisional): a status added to or removed from a released operation is breaking (RESPONSE_ADDITION_IDS, C-001), so reversing 400, 404, 422 or 500 once released costs a major version. The alternative for 422 is to fold it into a status the operator fixed (415, which would stretch the meaning of the operator's code); that choice is the operator's. Size is decided before content: 413 outranks 415, which outranks 422 (FR-013).
  • AD-22 modifiedAt format (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). RFC 3339 UTC with a Z suffix and whole seconds, the file's modification time truncated (floored), never rounded: YYYY-MM-DDTHH:MM:SSZ. It is the modification time on the serving host: not authorship, not commit time, and in a clone it is the checkout time; a consumer must not use it for ordering or freshness across hosts.

User Scenarios & Testing (mandatory)

User Story 1 - A work-package detail screen renders everything about one work package (Priority: P1)

A screen developer builds the work-package detail screen against the contract. With the v1 reads alone the screen shows ids where titles belong (subtasks, dependencies), shows only the latest review verdict, and cannot say where the work happens or whether the owned files were touched. With the detail read the screen shows subtask titles with their states, each dependency by title and current status lane, every review cycle in order, the lane and branch, whether the worktree exists on the serving host, each owned file with its change state, and links to the prompt and the spec.

Why this priority: it is the first consumer of the slice and defines the shape.

Independent Test: validate the detail example payloads against the schema, and build the detail for every work package of the repository's own Missions with the reference reader (after the branch is level with main, AD-15) with the reality-check assertions of AC-DETAIL.

Acceptance Scenarios: 1. Given a work package whose tasks.md lists T001 in a table row and T002 as a checkbox row, When the detail is read, Then both subtasks carry their titles. 2. Given a work package with no cycle file, When the detail is read, Then reviewCycles is an empty array (not null, not absent). 3. Given a Mission with no worktree on the serving host, When the detail is read, Then worktreePresent is false and every owned file's changeState is unknown.

User Story 2 - An artifacts screen shows what a Mission contains and what can be opened (Priority: P1)

The artifacts screen lists a Mission's spec, plan, tasks, research, data model, contracts, checklists, quickstart and work-package prompts, greys out what cannot be opened, and opens the rest as text.

Independent Test: AC-LIST and AC-CONTENT, including the invariant that readable: true entries open and readable: false entries are refused with the stated Problem.

Acceptance Scenarios: 1. Given a Mission with a file over 256 KiB, When the listing is read, Then that entry has readable: false, and reading its content is a 413 Problem. 2. Given a file with a NUL byte, When its content is read, Then the answer is a 415 Problem, never an empty or truncated 200. 3. Given a markdown artifact quoting a host path and an e-mail address, When it is read, Then the envelope content holds [path] and [email] and redacted is true. 4. Given a file containing a credential of a kind the leak patterns recognise, When its content is read, Then the answer is a 422 Problem with no content in the body, and the listing shows that file readable: false (AD-17).

User Story 3 - A consumer pins 1.x and is not broken (Priority: P1)

A consumer of the contract as it stands on main adopts the additions without touching its client: every existing response is byte-identical and every addition is a new operation.

Independent Test: the breaking-change check, run with the contract tree on main as baseline (--baseline-root, scratch copy with a lower version), exits successfully with breaking=0; a scripted comparison against that tree shows the existing files unchanged and only new operations and schemas added; the tree's info.version is still 1.0.0-SNAPSHOT; and planted violations make each of those checks fail (FR-017).

User Story 4 - A maintainer sees the deferred decisions (Priority: P2)

A maintainer or consumer reading the CHANGELOG finds the four deferred v1 gaps stated as deferred to the next major version, not silently dropped.

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

The reality check builds the three new reads for every Mission of this repository and validates them, with floors, controls and the leak scan, so a contract that does not fit the ledger fails before a service is built on it.

Edge Cases

  • A Mission with no tasks.md, or a tasks.md with no row for a subtask id, or a row whose title cell is empty, whitespace or holds a credential (AD-17): the subtask title is null (9 work packages at measurement have no subtasks at all: subtasks empty array). A tasks.md that exists but cannot be decoded is not null: the read fails (FR-020).
  • A work package whose frontmatter has no subtasks key: subtasks is [], as in v1 (FR-003).
  • A dependency that names no work package of the Mission: listed with its id as the title and a null status lane (AD-4); never dropped.
  • A Mission with no lanes.json (77 of 544 at measurement) or a work package in no lane: laneId and laneBranch are null. A lanes.json that exists but is malformed: the read fails, not null (FR-006). A work package in the planning lane (lane-planning): laneId is lane-planning, laneBranch is null (FR-006).
  • A review-cycle file that sits only in the coordination partition: not shown (AD-18). A stranded record in the primary checkout (ledger SK-348): shown. Zero found is [].
  • A review-cycle directory whose Mission or work-package segment fails pointer validation: the cycle is still an entry with feedbackReference null (FR-005).
  • A Mission directory containing a symlink, a hidden file, a file with a space or non-ASCII name, a file whose name holds home/<x>/ or an at sign: the symlink is skipped (AD-6); the others are listed with their real relative paths (AD-19). A regular file whose path is malformed (a backslash in the name, over 512 characters) is not eligible and is omitted from the listing (Terminology).
  • A nested status.json beside the root one: the nested file is listed, the root one is not (AD-5).
  • A file changed or removed between the listing and the content read: the content read answers from the file as it is then (404 if gone); the listing is not a lock. A file that vanishes between the directory walk and its stat is omitted from the listing.
  • Two artifacts differing only by case on a case-insensitive host: both are listed with their real names; no normalisation.
  • A Mission with more than 1000 files: truncated: true, entries sorted, first 1000 returned (AD-8); a file past the 1000th is not listed but is readable by path and is still named by artifactReferences if it is the prompt or the spec.
  • A zero-byte file: listed readable: true, content read is 200 with empty content and sizeBytes 0 (a real file state, not an undetermined one). A file with a leading byte order mark: the mark is preserved in content (strict UTF-8 decode, no signature stripping).
  • A file over the cap that is also not UTF-8: 413 (size is decided first).
  • An owned-file pattern that is a glob matching nothing: unchanged when the change set is known, unknown when it is not. A pattern with a brace: unknown (AD-20).
  • A Mission that exists but whose planning directory cannot be resolved or read: a Problem, not an empty listing (FR-020).

Reflexivity: Missions in flight

kitty-specs/ on main is archive-frozen: record commits are add-only, and no kitty-specs file already on main is modified by this Mission. This Mission's own directory is part of the corpus the reality check reads once merged; the reference reader must pass over it too.

Requirements (mandatory)

Functional Requirements

IDTitleUser StoryPriorityStatusDelivery
FR-001Work-package detail operationUS1HighOpen[build]
FR-002WorkPackageDetail identity and closed shapeUS1HighOpen[build]
FR-003Subtasks with titlesUS1HighOpen[build]
FR-004Dependencies with titles and status lanesUS1HighOpen[build]
FR-005Review cyclesUS1HighOpen[build]
FR-006WorkspaceUS1HighOpen[build]
FR-007Owned files with change stateUS1HighOpen[build]
FR-008Artifact referencesUS1, US2HighOpen[build]
FR-009Artifact listing operationUS2HighOpen[build]
FR-010Listing entries and kindsUS2HighOpen[build]
FR-011Listing scope, exclusions and boundsUS2HighOpen[build]
FR-012Artifact content operation and envelopeUS2HighOpen[build]
FR-013Content refusalsUS2HighOpen[build]
FR-014Containment and read-onlyUS2HighOpen[build]
FR-015Redaction and leak rulesUS1, US2HighOpen[build]
FR-016Environment-dependent fields and their verificationUS1, US5HighOpen[build]
FR-017Additive-only proof against the contract tree on mainUS3HighOpen[build]
FR-018Version unchanged, CHANGELOG and the four deferred gapsUS3, US4HighOpen[build]
FR-019Citations, examples, closed schemas, Problem responsesUS1, US2HighOpen[build]
FR-020Loud failure: unknown is explicit, never null or zero as successUS1, US2HighOpen[build]
FR-021Reality-check extension and reference readerUS5HighOpen[build] (after the branch is level with main, AD-15)
FR-022Synthetic-repository testsUS1, US2HighOpen[build] (after the branch is level with main, AD-15)
FR-023Planted negative per endpoint and CI gatesUS5HighOpen[build]
FR-024Removed-dashboard route coverage: cross-reference to #5533US2MediumOpen[reference]
FR-001 Work-package detail operation

The module gains getWorkPackageDetail: GET /missions/{missionId}/work-packages/{wpId}/detail (AD-1), a read-only sub-resource of the existing work-package path. It reuses the existing path parameters MissionId and WpId unchanged. It returns 200 with WorkPackageDetail (application/json). An unknown Mission, an ill-formed missionId or wpId, or a work package that is not a work package of the Mission (Terminology: not selected by wp_task_files, including an id present only in the status log) is a 404 application/problem+json response carrying the code not_found, and a source that exists but cannot be read is a 500 carrying source_unreadable (FR-020); both are a WorkPackageDetailRefusal (a new module refusal schema built like ArtifactRefusal, FR-013, resolved OQ-5 and OQ-6). default is the shared Problem. The existing operation getWorkPackage is not edited and its 404 stays the codeless shared Problem (C-001).

FR-002 WorkPackageDetail identity and closed shape

WorkPackageDetail is closed (additionalProperties: false; D-2 of v1) and has these required properties: missionId (the ULID, MissionId), wpId (WpId), subtasks, dependencies, reviewCycles, workspace, ownedFiles, artifactReferences. Every array is present and may be empty (never null or absent). It does not embed WorkPackage (AD-3). No property of any new schema carries an absolute path, a worktree path, an e-mail address or a credential (FR-015). reviewCycles and workspace are provisional (AD-14).

FR-003 Subtasks with titles

subtasks is an array, in the authored roster order, of closed objects {id, title, statusLane}:

  • id: the subtask identifier, RelativeReference-shaped, from the authored roster as v1 reads it: AuthoredGroup.subtasks in src/specify_cli/status/wp_view.py, which is built from normalize_authored_subtask_roster (src/specify_cli/core/subtask_rows.py) and never raises: a work package whose frontmatter has no subtasks key, or with no locatable file, has an empty roster. authored_subtask_roster in the same module is not the source: it raises SubtaskRosterResolutionError for a missing key or a missing or ambiguous work-package file, and could disagree with v1. The ids equal WorkPackage.authored.subtasks for the same work package (a control, AC-DETAIL); x-source.
  • title: the subtask's title as written in the Mission's tasks.md, or null. null means "no title was authored" and covers exactly: tasks.md absent, no row for the id, a row whose title is empty or whitespace-only (a title is never an empty string), or a title withheld because it holds a credential (AD-17). The contract does not distinguish those four causes. A tasks.md that exists but cannot be decoded as strict UTF-8 is not null: the detail read fails (FR-020), so a decode failure is never mistaken for "no title". Human text (FR-015). tasks.md carries a subtask in two row formats (a table row | T001 | title | WP01 | [P] | and a checkbox row - [ ] T001 title; about 12.7 thousand and 8.7 thousand rows at measurement); both are understood. The extraction rule, stated in the x-derived rule: for a table row the title is the second cell, trimmed; for a checkbox row it is the remainder of the line after the id, trimmed; in both a leading [P] parallel tag is not part of the title and a title wrapped in one pair of ** markers loses them; the result is trimmed again and empty becomes null. No canonical reader returns titles (iter_wp_section_subtask_rows in src/specify_cli/core/subtask_rows.py understands the checkbox form and yields no title), so title is x-derived with this rule and inputs naming those readers and tasks.md. The same id appearing twice in one work package's rows resolves by the first row, and the rule says so.
  • statusLane: the resolved state of that subtask from the status snapshot (the same state WorkPackage.subtasks holds for it), or null when the snapshot records none. A roster id with no recorded state is null, not planned.

An empty roster is [] (9 work packages at measurement have subtasks: []).

FR-004 Dependencies with titles and status lanes

dependencies is an array, in authored dependency order, of closed objects {wpId, title, statusLane}:

  • wpId: the dependency identifier as authored (AuthoredGroup.dependencies in src/specify_cli/status/wp_view.py).
  • title: the dependency's title. WPMetadata.display_title (src/specify_cli/status/wp_metadata.py) returns the work package id only when the title is None; for any set title it returns the stripped title, so a title of only whitespace (or an empty string) gives an empty string. The reader therefore applies one rule on top of it, x-derived from display_title and the work package id: the stripped title when it is non-empty, otherwise the work package id; and the work package id also when the title holds a credential (AD-17). A title is never an empty string. Human text.
  • statusLane: the dependency's current status lane in the status snapshot, StatusLane, or null when the dependency has no events (v1's Readiness treats that as planned for readiness; this field does not invent a lane).

A dependency that names no work package of the Mission is listed with its id as title and statusLane: null (AD-4); a dependency whose own title is empty or whitespace-only has its id as title as well. The set of ids equals the ids dependency_readiness_for_wp (src/specify_cli/core/dependency_graph.py) is given. v1's dependencies and readiness.unsatisfied id lists are unchanged.

FR-005 Review cycles

reviewCycles is an array ordered by cycleNumber ascending of closed objects {cycleNumber, reviewedAt, reviewer, verdict, feedbackReference, artifactPath}. The array is provisional (AD-14) and its description states that a cycle held only on a coordination surface is not shown (AD-18). A cycle is a file tasks/<wp-slug>/review-cycle-<N>.md under the Mission's own primary directory, <wp-slug> being the stem of the work package's prompt file (Terminology); N matches [1-9][0-9]* (the product's own filename rule, _REVIEW_CYCLE_FILE_RE in src/specify_cli/review/cycle.py); a file of another name in that directory is not a cycle.

  • cycleNumber: the positive integer of the cycle, from the file name (ReviewCycleArtifact.cycle_number in src/specify_cli/review/artifacts.py is the parsed value; the two agree for a parseable file).
  • reviewedAt: the cycle file's reviewed_at string when it is a valid RFC 3339 date-time with an offset (Z or +HH:MM, fractional seconds allowed; the test is the contract tooling's own date-time check, is_rfc3339_date_time in contracts/tools/schema_formats.py), otherwise null. The product accepts any non-empty string (ReviewCycleArtifact.from_file in src/specify_cli/review/artifacts.py only checks that reviewed_at is a non-empty string, its comment says ISO 8601 UTC), and the corpus holds values that are not date-times with offset: on this checkout the review-cycle files carry about 1.14 thousand YYYY-MM-DDTHH:MM:SSZ values, about 36 with an offset (3 of them with fractional seconds) and 3 date-only YYYY-MM-DD values, re-counted at plan time (A-5). So null has three causes the contract does not distinguish: the file cannot be parsed, or the stored value is not a valid date-time with offset (a date-only value included). The reader never coerces (a date-only value is not read as midnight) and never rewrites a value. The corpus floors count the entries whose reviewedAt is null for the invalid-value reason by a scan independent of the reader.
  • reviewer: ActorHandle or null (reviewer_agent; a stored value that is not a valid handle is null, the v1 rule; null as well when the file cannot be parsed).
  • verdict: ReviewVerdict or null, taken from the status events (HistoryEntry.reviewVerdict in v1, from the event's review_result). Match key: the status events of the same work package whose review_result.reference, trimmed, equals the cycle's full pointer string (the feedbackReference below, mission segment included). A pointer-shaped reference of another Mission segment (a renamed Mission) does not match and is never guessed; a reference that is not a review-cycle pointer (an approval reference, a legacy feedback:// reference) matches nothing. When several events carry the same pointer (a work package moved again), the last of them in the status event log's order decides. null when none matches or when feedbackReference is null. Never read from the cycle file and never inferred (AD-13).
  • feedbackReference: the pointer review-cycle://<mission-slug>/<wp-slug>/review-cycle-<N>.md, built by build_review_cycle_pointer (src/specify_cli/review/cycle.py) from the Mission directory name, the work-package directory name and the file name. It is never taken from an event's feedback_path, which holds absolute host paths in hundreds of events. Because the builder raises ReviewCycleError when a segment is unsafe, the member is a string matching the pointer pattern or null when the builder refuses a segment; the entry is kept either way (AD-13), the reader never fabricates a pointer, and the schema makes the member nullable.
  • artifactPath: the artifact path (FR-010) of the cycle file, decided by eligibility as FR-008 decides its references: non-null exactly when the cycle file is an eligible file (Terminology: no symlink at any component, regular, not malformed), and null only when it is absent or not eligible. It does not depend on the 1000-entry cap or on readable: a cycle file past the cap is still non-null and readable by path. It is never decided by building the listing: the reader lstats every component of that one path. It links the cycle to the artifact read. A symlink is never followed (AD-6): a tasks/<wp-slug> directory that is a symlink is not entered (reviewCycles is []), and a cycle file that is itself a symlink is still an entry whose parsed members are null and whose artifactPath is null.

Zero cycle files is []. A cycle file that cannot be parsed is still an entry (AD-13). The override actor and reason of a cycle file are not projected (v1's Review.override already carries the override). Reviews that left no cycle file stay visible in v1's history only.

FR-006 Workspace

workspace is a closed object {laneId, laneBranch, planningBranch, worktreePresent}, provisional (AD-14). Inputs: the Mission's lanes.json through read_lanes_json (src/specify_cli/lanes/persistence.py; LanesManifest in src/specify_cli/lanes/models.py with lane_for_wp and target_branch) and the resolver resolve_workspace_for_wp (src/specify_cli/workspace/context.py).

  • laneId: the code lane that holds the work package (LanesManifest.lane_for_wp), or null when the Mission has no lanes.json (read_lanes_json returns None) or no lane holds the work package. For a planning-artifact or single_branch work package the lane is the canonical lane-planning and laneId is lane-planning.
  • laneBranch: for a lane other than lane-planning, the lane branch name lane_branch_name(mission_slug, lane_id, target_branch=<the manifest's target_branch>) (src/specify_cli/lanes/branch_naming.py; target_branch is a required keyword and the manifest's field is its source). For lane-planning it is null: lane_branch_name returns the target branch itself for that lane, which is not a lane branch and must not be reported as one. null as well when laneId is null. A value failing the strict branch pattern projects to null (D-14 of v1).
  • planningBranch: the work package's planning_base_branch frontmatter value, or null when absent.
  • worktreePresent: boolean; true only when the resolved workspace (ResolvedWorkspace) has runs_in_checkout_root false and exists true (AD-12). It describes the serving host at read time; it is not a property of the Mission. It is false in CI and in every corpus run (FR-016).

Resolver and reader exceptions have stated outcomes, none silent: MissingLanesError (no lanes.json for a work package that needs one) and the ValueError for a work package in no lane give worktreePresent: false with laneId and laneBranch as above (the documented absence). CorruptLanesError (a lanes.json that exists but is malformed, which read_lanes_json raises on purpose to prevent a silent fallback) and MissionSelectorAmbiguous fail the whole read with the 500 Problem of FR-020; they are never turned into null or false.

No worktree path, repository path or other location is a member of this object. The heavy topology materialisation is not used (it raises for a work package without a lane). Leak-check lanes.json write_scope and collapse_report.evidence before any value derived from them is exposed; this slice exposes neither.

FR-007 Owned files with change state

ownedFiles is an array, in authored order, of closed objects {pattern, isGlob, changeState}:

1. Determined only when all of these hold: the work package has a code lane (laneId is non-null and not lane-planning); worktreePresent is true; the HEAD of that resolved worktree is on the recorded lane branch (laneBranch; a detached HEAD, or a HEAD on another branch, is not determined); the base ref, which is the manifest's target_branch, resolves in that worktree; and merge_base_changed_files_checked(worktree, base_ref) (src/specify_cli/core/vcs/git.py) returns a tuple. Anything else, a failing git call included, is unknown; the helper's None is unknown, never unchanged. 2. The change set is the files that differ between the merge base of the worktree's HEAD with the base ref, and that HEAD. Only committed changes count: uncommitted, staged and untracked files are ignored and never make a state changed. A deleted path counts as changed. A renamed path counts as changed under both its old and its new name; the helper runs its diff with rename detection, so the reference reader must obtain both names (a no-renames diff such as changed_paths_in_range in src/specify_cli/consolidation/git_probes.py does), and the plan names the seam; if no existing public seam gives both names the plan escalates it as an NFR-007 decision instead of reporting only one. 3. Matching is exactly the repository's owned-files matching, and this rule has no algorithm of its own (operator ruling 2026-10-04, CL-5). It is the behaviour of _matches_any_glob in src/specify_cli/policy/commit_guard.py (lines 141 to 149) and of _mt_matches_owned_file in src/specify_cli/cli/commands/agent/tasks_move_task.py (lines 838 to 847); the two copies agree. A changed path matches a pattern when, after normalising the path (backslashes turned into /, a leading ./ removed; _mt_matches_owned_file does this inside, _matches_any_glob receives git-relative paths that need none, so the parity test normalises first; the pattern is used as authored), either fnmatch.fnmatch(path, pattern) is true (, ? and [seq] match any characters including /; is only two stars; case-sensitive on POSIX, the repository's own behaviour) or the directory-prefix rule holds: the pattern with every / and then every / removed and any trailing / stripped is a non-empty prefix P and the path starts with P followed by /. The directory-prefix rule applies to plain directory entries too: a pattern that is not a glob, such as src/foo, covers src/foo/x.py. A pattern with a literal glob character in its directory, such as [x]/, is matched by the same prefix step ([x]/ covers [x]/y, which fnmatch alone does not match, because [x] is a character class). Consequences the tests pin (FR-022): .py matches src/x.py; src/.py matches src/a/x.py; src/foo/ and src/foo/ match src/foo/a/b.py; src/foo/ does not match src/foobar/x.py; a//c matches a/c/z (the prefix step also strips a / in the middle); src//x.py does not match src/x.py (no zero-directory ) and matches src/a/b/x.py; /x.py does not match x.py and matches a/x.py. The reference reader reproduces this behaviour in a function inside the slice file set (never an edit of src/), and a parity test runs both repository copies and the reader's function over every FR-022 glob case and requires equal results. A pattern containing { is not matchable*: is_glob_pattern treats { as a glob character but the repository's matcher has no brace expansion, and guessing would risk a silent wrong unchanged; its state is unknown, whatever the change set. 4. changed: determined and at least one changed path matches. unchanged: determined and none does. A glob matching nothing is unchanged when determined.

  • pattern: the owned-file entry as authored (AuthoredGroup.owned_files), a RelativeReference (never absolute; none of the 14.6 thousand entries at measurement is).
  • isGlob: boolean, from the repository's own glob test (is_glob_pattern in src/specify_cli/ownership/validation.py: any of *, ?, [, {); about 7 percent of entries at measurement.
  • changeState: changed, unchanged or unknown (closed enum), derived per work package, once, from this rule (AD-20):
  • Invariants: changeState is changed or unchanged only when worktreePresent is true; and a work package whose entries are determined has no unknown entry other than brace patterns.

changeState is x-derived with the rule and inputs stated. v1's WorkPackage.ownedFiles is unchanged. The value depends on the serving host (FR-016).

FR-008 Artifact references

artifactReferences is a closed object {prompt, spec}, each member null or a closed object {path, kind} naming an eligible file of the same Mission (FR-010): prompt is the prompt file of the work package being read (Terminology: directly under tasks/, name starting with that work package's wpId and a hyphen, kind work_package_prompt; the first in byte order when several qualify, and never another work package's prompt), spec is the Mission's spec.md (kind spec). A member is non-null exactly when that file is eligible, so a link never points at a file the content read refuses as absent. It is derived from eligibility, not from the listing: it reads one directory (tasks/) and lstats every component of one path, never the file contents and never the whole tree, and it does not depend on the 1000-entry cap: on a truncated listing the member is still non-null for an eligible prompt or spec that fell past the cap (AD-8). When the file is absent (or not eligible), the member is null.

FR-009 Artifact listing operation

The module gains listArtifacts: GET /missions/{missionId}/artifacts (AD-1), returning 200 with ArtifactListing (application/json): a closed object {missionId, entries, truncated} (truncated is provisional, AD-14). An unknown Mission is a 404 Problem with code not_found. A Mission that exists but whose directory cannot be read, or an entry whose stat fails other than by vanishing, is a 500 Problem with code artifact_listing_unreadable (AD-21, FR-020), never entries: [] and never a silently shortened list. The entries of a Mission directory with no artifact is [] only when the directory was read and holds none.

FR-010 Listing entries and kinds

Each entries item is a closed object {path, kind, sizeBytes, modifiedAt, readable}:

1. review_cycle: tasks/<dir>/review-cycle-<N>.md (one directory level below tasks/; N a positive integer without leading zero); 2. work_package_prompt: a prompt file (Terminology: tasks/WP[0-9]{2,}-.md) directly under tasks/ (no further separator, so it never matches a file inside a work-package directory); tasks/WP-notes.md and any other tasks/.md that is not a prompt file fall through to other. The classifier is the same pattern as the reference, with the work package id left free: every file artifactReferences.prompt can name is classed work_package_prompt; 3. spec (spec.md), plan (plan.md), tasks (tasks.md), data_model (data-model.md), quickstart (quickstart.md), analysis_report (analysis-report.md): each only at the Mission root; 4. research: research.md at the Mission root, and every file under research/; 5. contract: every file under contracts/; 6. checklist: every file under checklists/; 7. other: every other listed file (decisions, traces, reviews, ad hoc). The classifier is total and ordered: every listed file has exactly one kind, and a file matching several rules takes the first.

  • path: the artifact path, under the artifact-path rule (AD-19, Terminology): not malformed.
  • kind: a closed enum (the ArtifactKind schema, provisional, AD-14), assigned by one ordered classifier shared with the content read, first match wins:
  • sizeBytes: the on-disk size in bytes of the file (st_size), a non-negative integer. It is the size before decoding and before redaction.
  • modifiedAt: the file's modification time in the format of AD-22 (YYYY-MM-DDTHH:MM:SSZ, truncated). It is not authorship and not commit time; the description says so and says not to use it for ordering (FR-016).
  • readable: boolean, false exactly when a content read of that file would be refused with 413, 415, 422 or 500 (AD-7).

Entries are sorted by path ascending by byte order (AD-8).

FR-011 Listing scope, exclusions and bounds

The listing covers eligible files (Terminology) under the Mission directory on the primary planning surface (AD-2), recursively, and nothing else. Excluded: a file whose path is malformed (not eligible), the root-level status.events.jsonl and status.json by exact name (AD-5; the same names at another depth are listed), symlinks (AD-6; a symlinked directory is not entered), directories, and anything outside the Mission directory. The listing holds at most 1000 entries; beyond that truncated is true and the first 1000 by sort order are returned (AD-8). truncated is false otherwise. The read is bounded and says exactly what it reads: the walk lstat-s every name under the Mission directory (to exclude symlinks and directories, to sort and to set truncated) and stats the entries it returns for size and time; for each returned entry of at most 262144 bytes it reads the whole file once and runs the content read's checks (AD-7); a file over the cap is never opened. No file outside the returned entries is opened.

FR-012 Artifact content operation and envelope

The module gains getArtifactContent: GET /missions/{missionId}/artifacts/content with the required query parameter path (an artifact path, at most 512 characters). It returns 200 with ArtifactContent (application/json), a closed object with all members required:

  • path: the requested artifact path, as listed.
  • kind: the kind the listing gives that path (same classifier).
  • mediaType: from the fixed extension table (AD-11).
  • encoding: the constant utf-8.
  • sizeBytes: the on-disk size of the opened file (CL-3), equal to the listing's sizeBytes (st_size) for that path barring a concurrent change; never derived from the content read (FR-013).
  • redacted: boolean (AD-10); provisional (AD-14).
  • content: the file's text after redaction (FR-015).

The content read serves exactly the eligible files whose listing entry says readable: true (barring concurrent change), and a file past the listing cap by the same checks. It refuses the rest as FR-013 says. Eligibility, not membership of the listing, is the access rule (AD-8): the read neither enumerates nor sorts the tree; it validates the path, lstats its components and opens one file.

FR-013 Content refusals

Every refusal is application/problem+json and an ArtifactRefusal (a new module schema: the shared Problem plus a required code whose values are the titled enum schema ArtifactRefusalCode (so the pin check can read it) and the status pinned by the code, built like StreamRefusal and PageCursorRefusal; the shared Problem file is not edited, FR-017), served through new responses/ files. The new operations reference it for the statuses below and keep default as the shared Problem. Every refusal has a stable code, and no refusal body contains file content, a host path or the Mission directory's location:

StatuscodeWhen
400invalid_artifact_paththe path is malformed, missing or repeated (AD-9)
404not_foundunknown or ill-formed Mission id, or a well-formed path naming no eligible file (nonexistent, directory, symlink or through a symlinked directory, root status record, vanished)
413artifact_too_largesize greater than 262144 bytes (256 KiB); a file of exactly 262144 bytes is readable
415artifact_not_textthe bytes are not valid UTF-8, or contain a NUL byte
422artifact_secretthe decoded text holds a credential pattern (Terminology, AD-17)
500artifact_unreadablethe file exists and is eligible but cannot be opened or read, or the Mission directory cannot be resolved or read, on the content read (permission, I/O error)
500artifact_listing_unreadablelisting only: the Mission directory cannot be read, or an entry's stat fails other than by vanishing (FR-009)

Order of decision, so that two correct-looking answers never exist: 400, then 404, then the size of the opened file handle against the cap (413), then the read (500 on failure), then strict UTF-8 and NUL (415), then credentials (422). Size is decided before any content is read, so 413 wins over 415 and 422 (an oversize non-UTF-8 file is 413). The read takes at most 262144 bytes plus one; if that one byte arrives the answer is 413, whatever the earlier stat said (a file that grows during the read is 413, never a truncated 200). sizeBytes is the on-disk size (CL-3): the size of the opened file handle (fstat), with no second definition. If the bytes actually read differ from that size while staying within the cap (a concurrent change between fstat and read), the envelope still reports the handle size and the text read; the equality of sizeBytes with the byte length of the content is not asserted for a file that changed during the read, and no content-derived size is ever substituted (a change that carries the read past the cap is 413, above). A zero-byte file is a 200 with empty content, sizeBytes 0 and readable: true. The decode is strict UTF-8 with no signature stripping: a leading byte order mark stays in content. A refusal never degrades to a truncated or empty 200, and a read never decodes with replacement characters. The listing's readable is false for exactly the files whose read is refused with 413, 415, 422 or artifact_unreadable (AD-7).

All three new operations carry a refusal code on their 404 (resolved OQ-5, 2026-10-04): listArtifacts and getArtifactContent carry not_found in an ArtifactRefusal, and getWorkPackageDetail carries not_found in a WorkPackageDetailRefusal, a separate module refusal schema: the shared Problem plus a required code that references the titled enum schema WorkPackageDetailRefusalCode (values not_found and source_unreadable, so the pin check can read it) and the status pinned by the code (404 for not_found, 500 for source_unreadable). The 404 of v1's getWorkPackage is not edited and stays codeless; default stays the shared Problem on every new operation.

FR-014 Containment and read-only

Every artifact the listing or the content read touches resolves under the one Mission directory that missionId selects: the requested path is validated (AD-9), joined to that directory, every component of it is lstat-ed (a symlink at any component is a 404, AD-6), and it is refused if its resolution leaves the directory (the repository's existing containment helper ensure_within_directory in src/specify_cli/core/utils.py, which also rejects symlink loops, is the reference rule). A path can never name a file of another Mission, by a sibling component or by any other means; the Mission directory is chosen by missionId alone. The reads open files read-only and write nothing: no file is created, changed, renamed or removed under the repository by any of the three reads, and no file's content hash and no directory listing differs before and after.

FR-015 Redaction and leak rules

Field classes follow v1 D-14, with these assignments for the new fields. The single authority for patterns is contracts/tools/leak_patterns.py; this slice adds none and weakens none (the credential kinds are stated in Terminology). Its leak_codes reports three codes, HOST_PATH, EMAIL and SECRET, and FORBIDDEN_PROPERTY_NAMES lists keys that may not appear in a payload; the leak scans of this Mission run all three codes and the key check.

  • Strict fields (a value failing the strict patterns projects to null where nullable, otherwise is a projection error that fails the check; never redacted): missionId, wpId, id of a subtask, dependency wpId, laneId, laneBranch, planningBranch, owned-file pattern, feedbackReference, reviewer, every kind, mediaType, changeState.
  • Artifact path fields (path of an entry, of the envelope, artifactPath, artifactReferences[].path): the artifact-path rule of AD-19, not the strict host-path and e-mail patterns.
  • Human text fields (a real host path and any e-mail address in the value are replaced, matching substring only, by [path] and [email]; the field stays non-null): subtask title, dependency title. A title that holds a credential is not masked but withheld: a subtask title is null, a dependency title is the work package id (AD-17).
  • Artifact content: a text that holds a credential pattern is refused (422, AD-17) and never served, masked or not. Otherwise it is always redacted with [path] and [email] and the same patterns, applied to the decoded text; the result is redacted (AD-10). The cap is measured on raw bytes before redaction. Forbidden property names inside served content are text (AD-17); their values are redacted by the path and e-mail patterns like any text.

Field-class assignment needs a tooling change. Both scanners choose the class by key name from one global list: contracts/tools/leak_scan.py (_Scanner.walk, str(key) in STRICT_FIELDS, the list being STRICT_FIELDS in contracts/tools/fixture_builder.py) and the reference reader's payload_leaks (tests/contract/_mission_status_payloads.py on main, the same test on fixtures.STRICT_FIELDS). That list holds none of id, laneId, laneBranch, planningBranch, pattern, feedbackReference, reviewer, kind, mediaType, changeState, and every other string, an artifact path included, is scanned as human text. Left as is, the strict fields above would be checked by the weaker human patterns and the AD-19 artifact-path rule would be implemented by no scanner, which would weaken FR-015. This Mission therefore edits, and the plan names: (a) STRICT_FIELDS in contracts/tools/fixture_builder.py, adding the strict names above (title stays out: it is human text); (b) an artifact-path class in _Scanner.walk of leak_scan.py and in payload_leaks, applying the AD-19 rule to the path of entries and envelopes, artifactPath and artifactReferences[].path; (c) a planted negative and a clean control for each strict name and for the artifact-path rule (each case of the malformed-path predicate) registered in the negative-case registry contracts/tools/negative_cases.json (run by contracts/tools/run_negative_cases.py, the negative-tests job of the Contracts workflow, using its runtime-assembled build plants so no leaking text is committed), and in the existing contract-tools tests (tests/contract/test_leak_scan.py, tests/contract/test_fixture_builder.py) and the reality check, so that CI runs them; (d) pins for the new enums (ChangeState, ArtifactKind, ArtifactRefusalCode and WorkPackageDetailRefusalCode) added to contracts/tools/enum_pins.json under mission-status, because enum_pin_check checks only the enums that file lists, so without the edit the enum-pin acceptance criteria would be vacuous. enum_pin_check reads a pin by the title of a schema that carries an enum list, so each pinned value set lives in its own titled enum schema: ChangeState, ArtifactKind, a new ArtifactRefusalCode (the code values of ArtifactRefusal, which ArtifactRefusal references instead of listing them inline) and a new WorkPackageDetailRefusalCode (the code values of WorkPackageDetailRefusal: not_found and source_unreadable, referenced the same way) The encoding member is a constant (const), not an enum, and is not pinned: its guard is schema validation of the examples (AC-CONTENT). Because leak_scan.py runs over the whole contracts/ root, the change is accepted only if every existing contract file (v1 module, _shared, fixtures) still passes it; a name that would flag existing legitimate content is scoped by path instead of added to the global list. Scope: the files of the slice file set (Terminology), that is leak_scan.py, fixture_builder.py, negative_cases.json and enum_pins.json under contracts/tools/, and the test tree, and no file under src/. It extends the operator's scope ("new operations and new schemas", CL-1) to the shared leak scanner that guards every module; that is an author scope extension, recorded in the acknowledgement table and qualified in Out of Scope and SC-008, and it stays within NFR-007. FR-017's additive-only sentence governs the module's published files, not the tools.

No new field ever carries an event's feedback_path, a record_path, a worktree location or an e-mail address. Host paths and e-mail addresses are present in the corpus (hundreds of files at measurement), so the redaction is exercised on real data, not only on planted values.

FR-016 Environment-dependent fields and how they are verified

Three values depend on the serving host, not on the Mission: workspace.worktreePresent, ownedFiles[].changeState and entries[].modifiedAt. Their schema descriptions say so.

Host capability is an explicit input. The reference reader takes the host capability (git and lane worktree lookups available or not) as a parameter and never discovers it. The corpus run passes a host without worktrees, so every changeState is unknown and worktreePresent is false by construction, in CI and in a maintainer checkout that holds live lane worktrees alike; the invariant is deterministic anywhere and is not an environment fact. Verification in the corpus is therefore: every changeState is unknown, worktreePresent is false, the number of owned-file entries examined equals the number of WorkPackage.ownedFiles entries counted independently and is at least the floor, and modifiedAt matches the AD-22 format. A planted case runs the same fixture Mission with a real lane worktree and a capable host and asserts the states are not unknown; the corpus assertion is not weakened to accommodate it.

The invariants asserted on every detail built, corpus and synthetic: changeState is changed or unchanged only when worktreePresent is true; and worktreePresent is true only when the workspace does not run in the repository root checkout (AD-12). Git-derived values (changed, unchanged, the unknown fallbacks), worktreePresent in each case and modifiedAt equality with the file's set modification time are proven by synthetic-repository tests (FR-022), where the repository, the worktrees and the times are constructed by the test.

FR-017 Additive-only proof against the contract tree on main

Every change to the module's published files is within the slice file set (Terminology): new files, new _index.yaml entries, the openapi.yaml version and path map, and the CHANGELOG entry. No existing path file, no existing schema file (in particular WorkPackage, WorkPackageSummary, MissionHead, ReviewResult, and the shared Problem), and no existing parameter or response file changes. New schemas are referenced only by new operations; the new operations reference existing schemas (ActorHandle, StatusLane, ReviewVerdict, RelativeReference, WpId, MissionId, the shared Problem) without changing them. No response addition is made to any existing operation (the response additions listed in RESPONSE_ADDITION_IDS of contracts/tools/breaking_check.py stay at zero for existing operations). The sentence is about the published files of the module under contracts/mission-status/ and the shared files it references; the tooling edit of FR-015 (contracts/tools/ files of the slice file set, and the test tree) is outside it and outside the baseline comparison of observable 3 (an acknowledged scope extension, see the acknowledgement table). The observables run at 1.0.0-SNAPSHOT, the version the tree keeps, against the contract tree on main as baseline.

breaking_check alone does not prove that the change is additive: its only failures are BREAKING_WITHOUT_MAJOR, BUNDLE_CHANGED_VERSION_SAME, VERSION_DECREASED and NO_BASELINE_NOT_INITIAL, it prints no new-operation figure, and against a baseline that carries the same info.version it fails any bundle change with BUNDLE_CHANGED_VERSION_SAME (compare in contracts/tools/breaking_check.py), so the real main tree cannot be used unchanged as its baseline once the unreleased version stays the same (the scratch baseline of observable 4 therefore carries a lower version). Provisional elements are also removed before it compares. The proof is therefore the set of these observables, each scripted, each with a planted negative that makes it fail, and the plan names the test or job that runs them (in the contract-tools tests that the router runs):

1. No new version: info.version stays 1.0.0-SNAPSHOT, exactly as on main, and the CHANGELOG additions sit in the existing 1.0.0-SNAPSHOT section (FR-018). There is no version-bump assertion and no 1.1.0, 1.0.1 or 2.0.0 scratch test. A permanent contract-tools test asserts that the tree's info.version equals the one on the baseline tree, so a stray bump is caught. 2. Breaking check: python contracts/tools/breaking_check.py --root contracts --baseline-root <dir> over the module and _shared extracted from main with the baseline copy's info.version rewritten to a lower value in the scratch directory only (observable 4) exits 0, and its last line counts: shows baselines=1 and breaking=0; the assertion reads the counts: line, because at a higher major the check counts breaking changes without failing. A planted response property added to WorkPackage, and a planted new status code on an existing operation, each make breaking non-zero. 3. Diff against the contract tree on main (run at 1.0.0-SNAPSHOT): with the baseline taken from main (aef7cc967 or the current origin/main), (a) every existing path, schema, parameter and response file is byte-identical (git diff --exit-code <main> -- <those files>), the only edits to existing files of the module being those of item (2) of the slice file set; and (b) the bundle of the candidate, with the three new operations and the new schemas, parameters and responses removed, equals the baseline bundle, so the existing operations' response parts are unchanged and the additions are new operations and schemas only. A planted byte change in an existing schema file makes (a) fail; a planted change to an existing operation's response makes (b) fail. 4. Baseline run: extract the module and _shared from main (aef7cc967 or the current origin/main) into a scratch directory laid out as <dir>/mission-status and <dir>/_shared, rewrite the scratch baseline's info.version to a lower value (scratch only, so that the same-version rule BUNDLE_CHANGED_VERSION_SAME does not apply), and run breaking_check.py --root contracts --baseline-root <dir> (the option exists; find_baseline then compares against the module under that directory instead of a tag). The command, its output and the observables above (run against that scratch baseline) are recorded as evidence in the plan or the PR. 5. What CI does: the breaking-change job of the Contracts workflow (pull requests and pushes to main only) finds no contract-mission-status-v* tag and accepts 1.0.0-SNAPSHOT as the first release, as it does on main today. It therefore proves nothing about additivity for this slice; the observables above do. No release tag is needed and the pull request is not held for one. The branch is rebased onto upstream main before the pull request, without a tag.

FR-018 Version unchanged, CHANGELOG and the four deferred gaps

info.version stays 1.0.0-SNAPSHOT (no new version, CL-2). The module CHANGELOG.md additions merge into the existing 1.0.0-SNAPSHOT section under the existing headings, with no new version heading (Added lists the three operations and the new schemas, parameters and responses; Changed and Removed keep their existing text; Provisional names every x-provisional element of AD-14 as a whole word). The same section records the four v1 gaps (per-Mission staleness on MissionOverview, an actor on WorkPackageSummary, the project branch, the lane weights behind weightedPercentage) as decided out of this slice and deferred to the next major version, each with one line of reason (each would add a property to an existing response, which the breaking-change check treats as breaking). It also records the two behaviours of the artifact reads a reader of the replaced dashboard's routes should know: invalid UTF-8 is a 415 (not a replacement-character 200), and the root-level status records are not artifacts (AD-5). The section heading text stays the existing one, as the structure check requires. Where the record sits (a fifth heading such as "Deferred" inside the section, or prose under an existing heading) is the plan's call; the plan verifies the structure check accepts it. Cutting the release is not a task of this Mission.

FR-019 Citations, examples, closed schemas, Problem responses

Every property of every new schema carries x-source (repository-relative path plus symbol) or x-derived ({rule, inputs}), resolved by the citation check; symbols are cited by name, not line number. Every new schema has examples that validate and that cover the empty and unknown shapes: detail with and without cycles, with changeState values of each kind, with all null workspace values; a cycle with a null feedbackReference and a cycle with a null reviewedAt; a listing with a readable: false entry and with truncated: true; a content envelope with redacted true and false; and ArtifactRefusal examples, inside that schema's examples, for each of its codes (invalid_artifact_path, not_found, artifact_too_large, artifact_not_text, artifact_secret, artifact_unreadable, artifact_listing_unreadable), and WorkPackageDetailRefusal examples for each of its codes (not_found, source_unreadable), so the code-to-status pairing is stated by the contract and its examples have a home. Response object schemas are closed. The artifact operations declare ArtifactRefusal responses for the statuses of FR-013, getWorkPackageDetail declares WorkPackageDetailRefusal responses for its 404 and 500, and every new operation keeps the shared Problem for default. The lint, structure, citation, enum-pin, example, layout and provisional checks of v1 pass over the new files, with the new enums (the titled schemas ChangeState, ArtifactKind, ArtifactRefusalCode and WorkPackageDetailRefusalCode) pinned in contracts/tools/enum_pins.json (FR-015 (d)) and every x-provisional element named in the CHANGELOG. The descriptions of the 422 response and of readable list the credential kinds of the Terminology entry "Credential pattern", and a committed test fails when a kind of SECRET_PATTERNS is not named in them. A scripted terminology scan (AC-VERSION) is a new check this Mission adds.

FR-020 Loud failure: unknown is explicit

Each read states what it does when it cannot do its job, and none reports null, 0, an empty array or an empty string as success when the truth is "could not determine" or "could not read":

  • Detail: null (or unknown for changeState) is permitted only for a field whose source is genuinely absent or genuinely undetermined by the stated rule (FR-003 to FR-007; a failing git call is unknown, a tri-state value, never unchanged). A source that exists but cannot be read or parsed (other than a WP*.md whose frontmatter does not parse, which is skipped, Terminology) fails the whole read: a 500 WorkPackageDetailRefusal with code source_unreadable: a tasks.md that cannot be decoded, a malformed lanes.json, an ambiguous Mission handle, a Mission or work package file that cannot be read. (source_unreadable is pinned by the module refusal schema WorkPackageDetailRefusal and its titled enum WorkPackageDetailRefusalCode, resolved OQ-6; that enum is pinned in contracts/tools/enum_pins.json like the others, FR-015 (d).) An unknown Mission or work package is a 404 WorkPackageDetailRefusal with code not_found.
  • A review-cycle file that cannot be parsed is a visible entry with reviewedAt and reviewer null (the other members follow their own rules, FR-005), not a failure (AD-13); a cycle whose pointer cannot be built keeps feedbackReference null (FR-005).
  • Listing: artifact_listing_unreadable (500) when the directory or an entry cannot be read; truncated and readable carry what was cut or refused.
  • Content: 400, 404, 413, 415, 422, 500 as FR-013 says, an unreadable Mission directory included (500 artifact_unreadable).
  • A genuine empty state is a success: a zero-byte file, an empty roster, a Mission with no artifacts after a successful read.
  • Every check this Mission adds fails when it cannot do its job (zero entries scanned, zero Missions, a missing baseline tree, an injected fault that did not fire) and has a floor and a planted negative (FR-021, FR-023).
FR-021 Reality-check extension and reference reader

PR #5581 has merged to main; the reality check and its projector are already there (AD-15). This Mission edits those files and follows their conventions, verified against upstream main: tests/contract/_mission_status_payloads.py (public readers only, a pure function of the checked-out tree through load_source, MissionSource and resolve_read_dir; payload_leaks and its call ledger LEDGER; tree_fingerprint; work_package_trap; enumerate_missions and require_cases; the pinned FLOORS and floor_failures; write_fixture_mission for fixture Missions) and tests/contract/test_mission_status_reality.py (one parametrised case per Mission with a meta.json, a module-scoped fixture proving kitty-specs/ is untouched, and a last test asserting that every generated case executed). The projector already has build_detail, which builds MissionDetail; the new builders therefore take distinct names (for example build_work_package_detail, build_artifact_listing, build_artifact_content). Until the Mission branch is level with main none of these files exist in this checkout (AD-15); the plan and the implementation start only after that orchestrator step.

The extension covers every Mission directory of the checkout:

  • the detail for every work package of every Mission (validated against WorkPackageDetail), with the v1 snapshot-equality style controls for what v1 already carries: subtasks[].id equals WorkPackage.authored.subtasks, dependency ids equal WorkPackage.dependencies, owned-file patterns equal WorkPackage.ownedFiles, and subtask and dependency status lanes agree with the status snapshot read through the reducer v1 uses;
  • the listing for every Mission, with listing readable == (content read answers 200) asserted for every entry, and a refusal asserted to carry the code matching its cause;
  • the content for every listed file, with the leak scan (all three codes and the key check, FR-015) over every envelope's content and every string field, floors per field class and a planted control;
  • the reader is given the host capability explicitly (FR-016) and the corpus run passes a host without worktrees;
  • floors that cannot be met by skipping: the checker computes the discovered counts by a path independent of the reader under test (Missions: a separate scan of kitty-specs/*/meta.json; work packages: the number of distinct work_package_id values of the files wp_task_files selects (Terminology, work package of a Mission; a selected file whose frontmatter does not parse or has no valid work_package_id is skipped and counts for nothing); work packages present only in the status log are counted separately, answer 404 and are not examined; files: a separate walk) and asserts examined equals discovered, with no skip list and no per-Mission exception swallowed, but with a named refused list: the corpus holds Missions whose source the product refuses (today one: a legacy lanes.json with feature_slug and no mission_slug makes read_lanes_json raise CorruptLanesError, so by FR-006 its work packages answer 500 source_unreadable). Such work packages count as examined-as-refused, so examined + refused equals discovered. The refused list is derived by an independent scan (not by the reader under test); the check fails on any refusal outside the list and on a stale entry (a listed Mission that answers 200, or whose pinned count differs from its whole work package count). The numeric floors apply to the successful (200) count. Apart from that list: an exception in any case fails that case, it is never recorded and passed over. Numeric lower bounds, fixed at plan time from the re-measured corpus and pinned beside the existing FLOORS, remain as a second guard. Further floors: the number of readable: true and of readable: false entries compared, the files refused 413 and 415 by name (pinned in a fixture next to mission_status_expected.json), the files refused 422 (a planted fixture Mission, since the corpus is expected to hold none), and the cycles with a non-null verdict, compared with an independent count (the cycle files whose pointer equals the review_result.reference of an event of that work package, found by a direct scan of the logs). For orientation, at specify time and before the merge: about 546 Mission directories, about 16.5 thousand files, 0 symlinks, 2 files that are not UTF-8, 3 with NUL bytes, 2 over the cap once event logs are excluded, about 14.6 thousand owned-file entries, about 1300 review-cycle files across 225 Missions; all re-measured on the level branch (A-5);
  • read-only: a fingerprint of the corpus tree (tree_fingerprint: every file's hash and the directory listing) is equal before and after, and the new readers are covered by it; the work_package_trap, which guards the overview builder, is not applied to the new builders (they legitimately read work package files);
  • the job stays within the router corpus job's timeout, with margin (NFR-001).
FR-022 Synthetic-repository tests

Tests that construct a repository (with write_fixture_mission and real git where stated) and exercise the reference reader. The file-system operations of the reader (open, directory walk, stat) go through an injectable seam, so permission and I/O failures are induced by fault injection, never by chmod (which a root runner ignores); a test fails, not skips, when its injected fault did not fire.

  • Workspace matrix (kills the AD-12 bug): a work package in a real lane worktree (worktreePresent true); a lane worktree recorded in lanes.json but not created (false); a planning-artifact work package in the lane-planning lane, resolving to the repository root checkout (false, and the test fails if true; laneId lane-planning, laneBranch null); no lanes.json (laneId and laneBranch null, worktreePresent false); a work package in no lane (null, false); a malformed lanes.json (the read fails with source_unreadable, not null); planningBranch present and absent.
  • Change state (real git): a lane worktree on its lane branch with a base: an owned file edited and committed on the branch (changed), one untouched (unchanged); a glob matching a changed path (changed) and a glob matching none (unchanged); the glob and directory cases below, each checked by running _matches_any_glob and _mt_matches_owned_file (the reader's matcher must agree with both on every one, the path normalised first for _matches_any_glob): src/foo/* against src/foobar/x.py (unchanged) and against a changed src/foo/x.py (changed); a plain directory entry src/foo (not a glob) against a changed src/foo/x.py (changed) and against src/foobar/x.py (unchanged); a literal-glob-character directory [x]/ against a changed [x]/y (changed, by the directory-prefix rule alone: fnmatch does not match it); .py against a changed src/x.py (changed: the star crosses /); src/.py against a changed src/a/x.py (changed); src/foo/ against a changed src/foo/a/b.py (changed); a//c against a changed a/c/z (changed: the prefix step also strips a middle /); src//x.py against a changed src/x.py (unchanged: no zero-directory ) and against a changed src/a/b/x.py (changed); /x.py against a changed x.py (unchanged) and against a changed a/x.py (changed); a//b/*/c against a changed a/x/b/y/c (changed) and against a changed a/b/c (unchanged); a brace pattern src/{a,b}.py with BOTH a changed src/a.py (matching an expansion) and a changed src/{a,b}.py (the literal braces) in the change set (unknown); a deleted owned file (changed); a renamed owned file, matched by its old name and by its new name (changed each); an uncommitted edit, a staged edit and an untracked file (each unchanged); a detached HEAD, and a HEAD on another branch (unknown); a base ref that does not resolve (unknown); no worktree (unknown); a fault-injected git failure (unknown, asserted not unchanged); the invariant that no entry is changed or unchanged when worktreePresent is false.
  • Mutations the change-state tests must kill (each applied by a test, which must then fail): treating None from the git helper as unchanged; counting uncommitted or untracked files; ignoring deletions; ignoring the old side of a rename; a matcher in which (or ?, [seq]) does not cross / (killed by .py against src/x.py and src/.py against src/a/x.py); a matcher that adds zero-directory (killed by src//x.py against src/x.py, /x.py against x.py, and a//b//c against a/b/c, each unchanged); dropping the directory-prefix rule (killed by [x]/ against [x]/y and by the plain directory entry src/foo against src/foo/x.py); a directory-prefix match without the whole-segment boundary (a plain string prefix: killed by src/foo/* and src/foo against src/foobar/x.py); a directory-prefix rule applied to a trailing / only (killed by a//c against a/c/z, changed); a matcher that normalises nothing (killed by a changed path recorded as ./src/foo/x.py or src\foo\x.py, which must match src/foo/*); expanding or literally matching a brace (killed by the brace case, whose change set holds both a path matching an expansion and the literal-brace path, each of which would turn unknown into changed); accepting a detached HEAD; diffing against a base other than the manifest's target_branch.
  • Size boundary: 262144 bytes is readable, 262145 is 413, with readable agreeing; a file that grows past the cap between stat and read (fault-injected opener) is 413, never a truncated 200.
  • Encoding and credentials: invalid UTF-8, a NUL byte, each also as the last byte of a 262144-byte file (readable: false in the listing and 415 on read); an oversize non-UTF-8 file (413, not 415); a valid multi-byte UTF-8 file (200, content exact); a zero-byte file (200, empty, readable true); a file with a leading byte order mark (200, the mark preserved); a file holding each credential pattern kind (422, readable: false, no content in the body) and an oversize file holding one (413).
  • Mutations the content tests must kill: readable decided by size only; readable decided from a prefix of the file; 415 decided before 413; a signature-stripping decode; a replacement-character decode; a credential masked instead of refused.
  • Containment and eligibility: each case of the malformed-path predicate (.., ., absolute, ~/x, C:/x, backslash, NUL, empty, over 512 characters, an empty segment a//b, a trailing /; each 400), a doubly-percent-encoded traversal (not decoded twice), a symlink to a file outside the Mission directory, a symlink to a directory outside (and a path through it), a symlink inside, a path naming another Mission's file through a sibling component or by any other form, a path to a directory, a file under a symlinked tasks/ or tasks/<wp-slug>/ directory (404, and artifactPath and artifactReferences null for it), a missing and a repeated path parameter (400), an ill-formed missionId and wpId (404) (all refused as AD-6 and AD-9 say, none leaking the target's content). In a glob, src/**/x.py against src/x.py and src/a/b/x.py is covered under change state.
  • Redaction: host paths and e-mail addresses of each pattern kind in a title and in content, with redacted true and false cases; a credential in a subtask title (null) and in a dependency title (the work package id); a JSON content with a feedback_path key holding a host path (the value redacted).
  • Listing: sort order; the classifier for every kind in the vocabulary, including a review-cycle file (not work_package_prompt) and a prompt (not review_cycle); a root status.events.jsonl and status.json excluded beside a nested status.json listed; a file named with a home/<x>/ segment and one with an at sign, listed with their real paths and readable (AD-19); a regular file with a backslash in its name and one whose path exceeds 512 characters, a first-level directory named ~ and a name beginning with a drive letter and colon, each omitted from the listing and answered 400 on a request for that path (the readable invariant still holds); tasks/WP1-x.md classed other; truncation at 1001 files; a 1001-file Mission whose prompt and spec.md sort past the cap, still named by artifactReferences and readable by path; modifiedAt equal to the file's set modification time in the AD-22 format, with a sub-second mtime truncated not rounded; a file that vanishes between walk and stat (omitted); a fault-injected directory read and a fault-injected stat failure (artifact_listing_unreadable, not an empty listing); a fault-injected open failure (artifact_unreadable, readable: false).
  • Topology and edge fixtures: a coordination-topology Mission (plan.md and the other planning artifacts found on the primary surface, ledger SK-310); a review-cycle file only in the coordination partition (not shown, reviewCycles []) and a stranded record in the primary directory (shown, ledger SK-348); a Mission with no tasks.md; a dependency naming no work package (id as title, statusLane null); a dependency whose title is whitespace-only (the id as title, never an empty string) and one holding a credential (the id); a work package with no subtasks key ([]); a work package present only in the status log (the detail is 404); a stray tasks/WP-notes.md whose frontmatter has no valid work_package_id, and a WP*.md file whose frontmatter does not parse (each: not listed as a work package, the detail for its stem WP-notes is 404, the independent count of distinct work_package_id values is unchanged, the reader raises no exception, and never answers 500 for it) and two mutations the tests must kill: the reader lists such a file as a work package with a null id (killed by the WP-notes case), and the reader turns the unparseable-frontmatter file into a 500 source_unreadable or lets the exception escape (killed by the unparseable-frontmatter case, which asserts the other work packages of the Mission still answer 200); a work package file with no status events (200, status lanes null), and a work package whose file is no Prompt file, namely a copy of the corpus file sync-diagnose-canonical-allowlist-01KS4F8H/tasks/WP01.md (200, artifactReferences.prompt null, listing kind other); a symlinked tasks/<wp-slug>/ directory (reviewCycles []) and a cycle file that is itself a symlink (an entry with null members and a null artifactPath).
  • Subtask titles: a table row, a checkbox row, a checkbox row with a [P] tag and bold, a table row with an empty title cell (null), an id with no row (null), a duplicate id (first row), an undecodable tasks.md (the read fails with source_unreadable), a subtask with no recorded state (statusLane null, not planned); a subtask title holding a credential (null).
  • Review cycles: two cycle files out of order (ascending by cycleNumber); an event whose reference equals the pointer (verdict set), one for another cycle (not matched), one whose mission segment differs (not matched), two events for one pointer (the last wins), no event (null), an approval reference that is no pointer (matches nothing); an unparseable cycle file (an entry with reviewedAt and reviewer null, the other members by their own rules, and the number from its file name); a cycle directory whose segment fails pointer validation (an entry with feedbackReference null); a file review-cycle-0.md or review-cycle-01.md (not a cycle); a Mission with two prompt files for one work package (the first in byte order is the prompt and decides the cycle directory) and a stray tasks/WP-notes.md (not a prompt, kind other, never named by artifactReferences); a cycle file with a date-only reviewed_at (reviewedAt null, no midnight), one with an offset and fractional seconds (the stored string unchanged) and one with a Z value.
FR-023 Planted negative per endpoint and CI gates

Each new endpoint has a planted-negative pair under the leak rules (a deliberately leaking input and a clean control on the same fixture): detail (a title with a host path, an e-mail address and a credential, and an event whose feedback_path is absolute: the host path and e-mail address come out as [path] and [email], the credential title is withheld (null for a subtask, the id for a dependency), and the pointer is present, never the path); listing (a Mission directory whose symlink points outside: absent from the listing; a planted listing example whose path is absolute fails the artifact-path scan); content (a file full of host paths and e-mail addresses: redacted; a file with a credential: refused; a planted unredacted content in an example: the example leak scan fails). The Contracts workflow jobs (lint, structure, citation, enum-pin, example, layout, provisional, breaking, verify-pins, bundle, the release dry run, the negative-tests job that runs the negative-case registry, and the router's contract-tools tests) and the corpus reality job pass; the .github/ci-module-registry.yml and ci-gate-mechanics.md mention of the job set is updated only if the plan adds a job. No full architectural, end-to-end or performance sweep is run in mission work (charter): the plan names the targeted files.

FR-024 Removed-dashboard route coverage: cross-reference

The route-by-route mapping from the removed dashboard to the new reads is #5533's job, not this Mission's; this Mission records no such map. The one statement kept from the operator's scope decision (#5625 comment "Scope decisions before design"): this slice covers the artifact reads of the removed dashboard, namely /api/artifact/, /api/research/, /api/contracts/, /api/checklists/ and the artifact part of /api/dossier/*; the dossier overview and the snapshot export are not part of it. The two behaviours this Mission itself introduces for those reads (a 415 for invalid UTF-8, and the exclusion of the root status records, AD-5) are stated in the CHANGELOG entry (FR-018). The Mission edits no README.

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001Corpus runtimeThe corpus reality job (10-minute timeout, 3m43s before this slice) stays at or under 8 minutes with the extension, measured on the CI runner and recorded in the plan; no single new test case exceeds 60 seconds. The worst case of one listing, 1000 entries times 262144 bytes (262,144,000 bytes read, AD-7), is stated and is covered by one timed synthetic case.PerformanceHighOpen
NFR-002Bounded readsA work-package detail read never builds the listing and opens no artifact content for its artifactReferences or artifactPath (a directory read and lstats only); it opens only the sources of its own fields: meta.json and the status log or snapshot (Mission resolution, status lanes, verdicts), tasks.md, lanes.json, the work package files of the Mission (this one, and the others named in dependencies for their titles) and the review-cycle files, never an artifact for artifactReferences or artifactPath, and none read for more than its own parse needs (the counting-opener assertion of AC-DETAIL is scoped to the artifact links), so the 262,144,000-byte worst case of the listing is not part of a detail read. A content read touches at most 262144 bytes plus one byte of one file, plus the lstat of the components of one path, and neither enumerates nor sorts the tree. A listing lstat-s every name under one Mission directory, opens only the entries it returns, reads each of those of at most 262144 bytes in full exactly once, and opens no file over the cap (AD-7); at most 1000 entries are opened. No read walks outside one Mission directory.Performance, SecurityHighOpen
NFR-003Read-onlyZero writes under the repository by any new read, proven by the corpus fingerprint (FR-014, FR-021).SafetyHighOpen
NFR-004Leak-free artefactsZero host paths, zero e-mail addresses and zero credentials in the new contract files, in the new examples (including envelope content), and in every corpus-built payload content and string field (FR-015).SecurityHighOpen
NFR-005DeterministicListing order is a pure function of the file names; two reads of an unchanged Mission give byte-identical listings, details (given an unchanged host capability) and contents; modifiedAt is a function of the file's modification time and is not used for ordering; reviewCycles does not depend on whether a coordination worktree is materialized (AD-18); two bundle builds are byte-identical (v1 NFR-002).ReliabilityMediumOpen
NFR-006Test disciplineNew Python code passes ruff check and ruff format (hard-enforced; mypy does not run). Every new assertion is non-vacuous: it compares against an independently discovered count (FR-021) and asserts a minimum input count.QualityHighOpen
NFR-007No product changeNo file under src/ changes (x-source inputs only); the only edits outside contracts/ and the test tree are those the plan justifies. A seam for FR-007 rule 2 would be the only conceivable src/ change and needs its own justification (OQ-2 was resolved on 2026-10-04: no seam for the review cycles).LocalityMediumOpen

Constraints

  • C-001 The four operator decisions (CL-1 to CL-4) bind; nothing may add a response property, status code, media type, header, default or range response to an existing operation (RESPONSE_ADDITION_IDS, RESPONSE_KEY_ADDED_ID), because contracts/tools/breaking_check.py treats each as breaking.
  • C-002 New schemas are used only by new operations; request-side additions and x-provisional elements are the only other additive changes available, and none is needed.
  • C-003 kitty-specs/ on main is archive-frozen: record commits are add-only; this Mission modifies no existing kitty-specs file and the reality check reads them read-only.
  • C-004 Public repository: no absolute home path, user name, e-mail address or private reference in any artifact this Mission writes (spec, plan, contract, examples, tests, CHANGELOG).
  • C-005 The contract tooling stays Node-free as in v1; no new third-party dependency is introduced.

Key Entities

  • WorkPackageDetail: the detail response; groups subtasks, dependencies, review cycles, workspace, owned files, artifact references.
  • Subtask, DependencyRef, ReviewCycle, Workspace, OwnedFile, ArtifactReference: the closed object schemas inside it.
  • ChangeState: enum changed | unchanged | unknown.
  • ArtifactListing, ArtifactEntry, ArtifactKind: the listing response, its item and the closed kind vocabulary.
  • ArtifactContent: the content envelope.
  • ArtifactRefusal: the module refusal schema (shared Problem plus a required code that references ArtifactRefusalCode, and the pinned status).
  • ArtifactRefusalCode: the titled enum schema of the refusal codes, so that enum_pin_check can pin it (FR-015 (d)).
  • WorkPackageDetailRefusal, WorkPackageDetailRefusalCode: the module refusal schema of getWorkPackageDetail (shared Problem plus a required code referencing the titled enum WorkPackageDetailRefusalCode, values not_found and source_unreadable, each with its pinned status; resolved OQ-5 and OQ-6). The plan may choose other names; the roles are the requirement.
  • Problem codes: invalid_artifact_path, not_found, artifact_too_large, artifact_not_text, artifact_secret, artifact_unreadable, artifact_listing_unreadable and, for the detail, not_found and source_unreadable (WorkPackageDetailRefusalCode).

Where each field comes from (field catalogue)

FieldSourceKind of citation
subtasks[].idAuthoredGroup.subtasks (src/specify_cli/status/wp_view.py), from normalize_authored_subtask_roster (src/specify_cli/core/subtask_rows.py)x-source
subtasks[].titlerows of tasks.md, two formats; no existing title readerx-derived (inputs: iter_wp_section_subtask_rows, normalize_authored_subtask_roster)
subtasks[].statusLaneResolvedGroup.subtasks (src/specify_cli/status/wp_view.py)x-source
dependencies[].wpIdAuthoredGroup.dependenciesx-source
dependencies[].titleWPMetadata.display_title (src/specify_cli/status/wp_metadata.py) with the empty-title and credential rule of FR-004x-derived (inputs: display_title, the work package id)
dependencies[].statusLanestatus snapshot work-package state (src/specify_cli/status/reducer.py)x-source
reviewCycles[].cycleNumber, reviewerReviewCycleArtifact (src/specify_cli/review/artifacts.py)x-source
reviewCycles[].reviewedAtReviewCycleArtifact.reviewed_at, kept only when it is a valid RFC 3339 date-time with offset (is_rfc3339_date_time, contracts/tools/schema_formats.py), else nullx-derived
reviewCycles[].verdictstatus events, ReviewResult (src/specify_cli/status/models.py) matched by the full pointerx-derived
reviewCycles[].feedbackReferencebuild_review_cycle_pointer (src/specify_cli/review/cycle.py)x-source
workspace.laneIdread_lanes_json (src/specify_cli/lanes/persistence.py), LanesManifest.lane_for_wpx-source
workspace.laneBranchlane_branch_name (src/specify_cli/lanes/branch_naming.py), input target_branch from the manifest; null for lane-planningx-source
workspace.planningBranchWPMetadata frontmatter planning_base_branchx-source
workspace.worktreePresentResolvedWorkspace (src/specify_cli/workspace/context.py): runs_in_checkout_root, exists; AD-12x-derived
ownedFiles[].patternAuthoredGroup.owned_filesx-source
ownedFiles[].isGlobis_glob_pattern (src/specify_cli/ownership/validation.py)x-source
ownedFiles[].changeStatemerge_base_changed_files_checked (src/specify_cli/core/vcs/git.py) plus the matching rule of FR-007x-derived
artifactReferenceseligibility of the work package's prompt file (Terminology) and spec.mdx-derived (inputs: the Mission directory)
entries[], content envelopefiles of the Mission directory; no product reader exists (the dashboard that read them was removed)x-derived, rule stated; sizes and times from the file system

Symbols were verified on this checkout when the spec was written; line numbers are deliberately not cited. The plan re-verifies each symbol before it writes the citation.

Acceptance Criteria

Each criterion says how it fails. A check that passes because it examined nothing is itself a defect (charter Standing Order 5; v1 D-15).

AC-DETAIL (FR-001 to FR-008, FR-015, FR-016, FR-020, NFR-005)

  • Every detail example validates against WorkPackageDetail; fails on a planted example with an extra property, a missing required property or an absolute feedbackReference.
  • For every work package of the corpus (Terminology: work package of a Mission), the built detail validates, and examined + refused equals discovered (FR-021: the independent count of work packages, with no skip list and a named refused list of Missions whose source the product refuses, derived by an independent scan, failing on a refusal outside it and on a stale entry; the numeric floors apply to the 200 count); numeric lower bounds recorded at plan time are the second guard; fails on any shortfall, on any per-Mission exception, and when zero work packages were examined.
  • Controls against v1, in order, for every work package: subtasks[].id equals WorkPackage.authored.subtasks, dependencies[].wpId equals WorkPackage.dependencies, ownedFiles[].pattern equals WorkPackage.ownedFiles; fails on a planted mismatch of each (the planted case is detected by the same assertion).
  • Subtask titles: for a fixture with a table row, a checkbox row, a checkbox row with a [P] tag and bold, an empty-title row, an id with no row and a duplicate id, the titles are the stated ones, null for the empty and the missing, and the first row for the duplicate; fails when a title is an empty string, when the checkbox-only reader's behaviour (no table titles) is reproduced, or when an undecodable tasks.md yields null titles instead of failing the read. Subtask statusLane is null (not planned) for an id with no recorded state; fails otherwise.
  • Dependencies: each title and statusLane equals the dependency's own work package title (with the display_title fallback) and status-snapshot lane; a dependency naming no work package is listed with its id as title and statusLane null; a dependency with no events has statusLane null, not planned; a dependency whose title is empty or whitespace-only has its id as title (never an empty string, whatever display_title alone returns); fails on a planted wrong title or lane, on a dropped dangling dependency and on title equal to an empty string.
  • artifactReferences: prompt and spec are non-null exactly when the file is eligible and null otherwise, including on a Mission whose listing is truncated before that file; with two prompt files for one work package the first in byte order is named, and a stray tasks/WP-notes.md or another work package's prompt is never named; fails on a non-null member for an absent file, a null member for an eligible file past the cap, and a member naming a path the content read answers 404 for.
  • artifactPath and artifactReferences need no listing: on a Mission with more than 1000 files whose cycle file (and prompt) sorts past the cap, artifactPath is non-null and the content read answers 200 for it; a cycle file that is a symlink, or a cycle file under a symlinked tasks/<wp-slug>/ directory, gives null; fails on a null for an eligible cycle file past the cap, on a non-null for a symlink, a path through a symlinked directory or an absent file, and on a detail read that builds the listing or opens an artifact for its links (counting opener).
  • reviewCycles: ordered by cycleNumber ascending; the verdict equals the status event matched by the full pointer (the last of several), is null when none matches, and is null for a planted event of another cycle or another Mission segment; a work package with no cycle file yields []; an unparseable cycle file is an entry with reviewedAt and reviewer null, the other members by their own rules, and the cycle number from its file name; a cycle whose pointer cannot be built keeps feedbackReference null (never dropped, never fabricated); reviewedAt is the stored string when it is a valid RFC 3339 date-time with offset (the Z, offset and fractional-second forms) and null for a date-only or otherwise invalid stored value, never midnight; the corpus floor counts the cycles with a null reviewedAt for that reason against an independent scan and the corpus holds at least one; fails on a date-only value passed through or coerced, and on each planted violation. The corpus contains both a work package with no cycle and one with cycles (both floors), and the number of cycles with a non-null verdict equals the independent count and meets its floor (FR-021).
  • feedbackReference check: every non-null value passes validate_review_cycle_pointer (src/specify_cli/review/cycle.py: exactly three segments, a review-cycle-<N>.md file name), not a prefix test, and no value in the payload equals or contains an event's feedback_path; fails on a planted review-cycle:///abs/path, a planted two-segment pointer, and a planted detail built with an absolute feedback_path (negative pair: the control with the pointer passes).
  • workspace and ownedFiles over the corpus (host without worktrees, FR-016): every changeState is unknown, worktreePresent is false, the owned-file entries examined equal the independent count and meet the floor; fails on any other value or on zero entries. On the synthetic matrix (FR-022): worktreePresent is true only for a real lane worktree and fails if true for a planning-artifact work package resolving to the repository root checkout; laneId/laneBranch are as FR-006 states for no lanes.json, a work package in no lane and lane-planning; a malformed lanes.json fails the read; changeState is never changed or unchanged when worktreePresent is false (asserted for every detail built).
  • changeState on the synthetic repository: each case and each mutation of FR-022 gives the stated state or makes the suite fail.
  • Leak scan over every string of every detail by field class (FR-015), all three codes; fails on any host path, e-mail address or credential, and on a planted title with each pattern kind (a path or e-mail title must come out as [path] / [email], a credential title must be withheld: null for a subtask, the work package id for a dependency); the two real titles of v1 (/tmp burn-down: sync, ~/.kittify Runtime Centralization) are positive-pass controls.
  • Determinism (NFR-005): two builds of the detail over an unchanged Mission with the same host capability are byte-identical, and reviewCycles is identical with and without a coordination worktree materialised; fails on a planted host-dependent member and on a cycle taken from a coordination surface.
  • Absence and unreadable sources: an unknown Mission, an unknown work package, a work package present only in the status log and an ill-formed missionId or wpId are 404 WorkPackageDetailRefusal responses with code not_found, and a work package file with no status events is a 200 with null status lanes; fails when any 404 case returns 200 with empty groups or without the code; a malformed lanes.json and an undecodable tasks.md are 500 WorkPackageDetailRefusal responses with code source_unreadable; fails when either returns 200 or another code; the enum-pin check fails on a changed WorkPackageDetailRefusalCode value set, and the schema fails validation of a status that does not match its code.

AC-LIST (FR-009 to FR-011, FR-014, FR-016, FR-020, NFR-005)

  • Every listing example validates; fails on an entry missing readable, an unknown kind, a path starting with / or holding a .. segment.
  • Over the corpus, the listing is sorted by path ascending, contains no root status.events.jsonl or status.json, no symlink, and every entry's sizeBytes equals the file's st_size; fails on a planted violation of each. Examined equals discovered for Missions and for files (an independent walk, an empty skip list); fails on any shortfall, on any per-Mission exception and when zero Missions or zero entries were examined.
  • Kinds: the ordered classifier assigns exactly one kind to every listed file and takes the first matching rule (a review-cycle file is review_cycle, a prompt is work_package_prompt); fails on a file left without a kind or with the wrong one, and across the corpus and the synthetic fixtures every kind of the vocabulary occurs at least once (a kind no input can produce is a defect in the vocabulary or the test).
  • readable invariant: for every entry, readable == (content read is 200); fails on a planted entry that disagrees (a NUL file marked readable, an oversize file marked readable, a credential file marked readable). Floors on the number of true and of false pairs compared (FR-021). Late defects: a 262144-byte file with the NUL byte, and one with the invalid sequence, as its last byte are readable: false; fails if either is true (a prefix check would pass them).
  • Bounds: a fixture of 1001 files returns 1000 entries with truncated: true; fails if it returns 1001, or 1000 with truncated: false. The files past the cap are readable by path (AC-CONTENT) and the listing opens no more than the 1000 returned entries.
  • Root-only exclusion: a root status.json is absent and a nested status.json is present; fails if the nested one is absent or the root one present.
  • Odd names: a file under a home/<x>/ segment and one with an at sign are listed with their real paths and readable: true; fails when either is dropped or fails the listing. A regular file whose path is malformed (backslash in the name, over 512 characters) is omitted from the listing and a request for that path is 400; fails when it is listed or when the listing fails.
  • Unreadable directory and entry: a Mission whose directory cannot be read, or an entry whose stat fails other than by vanishing, is a 500 artifact_listing_unreadable; fails when it returns entries: [] or a shortened list with 200, or when the injected fault did not fire. A vanished entry is omitted.
  • modifiedAt has the AD-22 format over the corpus; its equality with the file's modification time is asserted only on synthetic files, including a sub-second mtime that must be truncated; fails when a synthetic file's time differs by a second or has a sub-second part.
  • Unknown Mission: listArtifacts answers 404 with code not_found; fails when it answers 200 with entries: [] or another code. Determinism (NFR-005): two listings of an unchanged Mission are byte-identical, the order is a function of the names alone; fails on a planted ordering by modifiedAt.
  • Authorization: the contract declares no security scheme; fails if a security scheme is added to the new operations (they follow v1).

AC-CONTENT (FR-012 to FR-015, FR-020, NFR-002, NFR-003, NFR-005)

  • Every envelope example validates, with encoding exactly utf-8 and sizeBytes equal to the on-disk size; fails (schema validation of the const, not an enum pin) on a planted encoding of any other value. Over the corpus and on synthetic files, the envelope sizeBytes equals the file's st_size and the listing's sizeBytes for every file read; fails on a planted envelope whose sizeBytes is the redacted or decoded length (a multi-byte or redacted file makes the two differ).
  • Over the corpus, every readable: true file answers 200 whose content has zero host paths, e-mail addresses and credentials, and every readable: false file answers its Problem with the matching code and status; fails on any 200 with a leak, any 200 for a readable: false file, and any refusal with the wrong status or code. Floors: files read, files with at least one replacement (redacted: true), the files refused 415 and 413 named in the pinned fixture, and the 422 fixture, each at least its plan-time measurement (FR-021).
  • Boundary: 262144 bytes is 200; 262145 is 413; fails at either side. Precedence: an oversize non-UTF-8 file is 413; fails if 415. A file that grows past the cap during the read is 413; fails on a truncated 200.
  • Encoding: invalid UTF-8 and a NUL byte are 415; a valid multi-byte file is 200 with exact text; a zero-byte file is 200 with empty content; a file with a byte order mark keeps it; fails if any invalid file returns 200, any content contains a replacement character that was not in the file, the mark is stripped, or the zero-byte file is refused.
  • Credentials: a file holding a credential pattern (each kind of the Terminology entry) is 422 artifact_secret with no content in the body and readable: false; its clean twin is 200; fails if the credential is served, masked or not, or if the clean twin is refused.
  • Containment: every case in FR-022's containment list (a symlinked directory in the path, a missing or repeated path, an ill-formed id included) is refused with 400 or 404 as AD-9 says and the response never contains the target file's content or path; a path naming another Mission's file is refused; fails when any case returns 200 or leaks.
  • Eligibility past the cap: on a 1001-file Mission, a file past the cap answers 200 by path; fails if 404.
  • Refusal bodies: none contains file content, a host path or the Mission directory's location; fails on a planted Problem detail carrying a path (the leak scan runs over the ArtifactRefusal examples too). Each code appears with its pinned status in an example; the enum-pin check fails on a changed ArtifactRefusalCode value set and the schema fails validation of a status that does not match its code.
  • Open failure: an injected open failure, and an injected failure to read the Mission directory on the content read, are 500 artifact_unreadable; fails if 200 or 404, or if the injected fault did not fire.
  • Redaction negative pair: a file with a host path and an e-mail address answers [path]/[email] with redacted: true; its clean twin answers redacted: false; fails if the clean twin reports true or the dirty file reports false.
  • Unknown Mission: getArtifactContent answers 404 with code not_found; fails when it answers 200 or another code.
  • Bounded read (NFR-002): with a counting opener, a read of a file far over the cap reads at most 262145 bytes and opens one file; fails on a reader that reads the whole file or opens another. Determinism (NFR-005): two reads of an unchanged file are byte-identical.
  • Read-only: the corpus fingerprint before and after the whole run is equal; fails on a planted writing reader.

AC-VERSION (FR-017, FR-018, FR-019, FR-020 (last bullet), FR-023, FR-024, NFR-001, NFR-005, NFR-006, NFR-007)

  • No new version, permanent: the tree's info.version equals the baseline tree's (1.0.0-SNAPSHOT), and a contract-tools test fails (scripted) when it differs (FR-017 observable 1). There is no 1.1.0, no version-bump assertion and no 1.0.1 or 2.0.0 scratch test.
  • The breaking-change check, run with --baseline-root over the module extracted from main (aef7cc967 or the current origin/main), over a scratch baseline whose info.version is rewritten lower, exits 0 and its counts: line shows baselines=1 and breaking=0; the breaking count is non-zero on a planted response property added to WorkPackage and on a planted new status code on an existing operation (FR-017 observable 4). No release tag is involved.
  • The diff of existing path, schema, parameter and response files against the contract tree on main is empty (byte-identical, git diff --exit-code), and the candidate bundle minus the new operations and their schemas equals the baseline bundle; fails on any byte change of an existing schema and on a planted change to an existing operation's response.
  • The CHANGELOG 1.0.0-SNAPSHOT section (no new heading) names the four deferred gaps and every x-provisional element of AD-14, and records the 415 and root-status-record behaviours (FR-018); the structure check fails on a missing version heading, a review check greps the four gap names (per-Mission staleness, actor on WorkPackageSummary, project branch, lane weights) in the section and fails when any is missing; the provisional check fails when a provisional element is not named.
  • Citation, enum-pin, example, layout, lint and provisional checks pass with the new files; the citation check fails on a planted new property without x-source/x-derived, the enum-pin check fails on a changed ChangeState, ArtifactKind, ArtifactRefusalCode or WorkPackageDetailRefusalCode value set.
  • Terminology (a new check this Mission adds: no existing guard scans contract files; tests/architectural/test_no_legacy_terminology.py gates two other retired terms and not Mission versus feature): a scripted, case-insensitive scan, a test in tests/contract/ run with the contract-tools tests, over the new files under contracts/mission-status/, the new examples and the 1.1.0 CHANGELOG entry, finds no "feature"; the values of x-source and x-derived are skipped, so a cited symbol name such as feature_dir is allowed. Fails on a planted new description containing "feature", and passes on a planted x-source citing feature_dir.
  • Scope (NFR-007, SC-008): a scripted git diff --name-only between the merge base with main and the Mission branch lists only paths in the slice file set (Terminology) plus the Mission's own kitty-specs/<mission>/ directory (excluded from this comparison and checked add-only against C-003), none under src/, and for existing files under contracts/ only the edits of items (2) and (3) of that set, and outside contracts/ and tests/contract/ only the two OD-1 X-pure registration paths of item (5); fails on a planted touched file under src/ or elsewhere outside the set (run against a scratch copy).
  • Determinism of the bundle (NFR-005): two bundle builds are byte-identical; fails on a planted nondeterministic member.
  • Runtime and quality (NFR-001, NFR-006): the corpus job stays at or under 8 minutes with no new case over 60 seconds, and ruff check and ruff format --check pass over every new or changed Python file; fails on an overrun and on a planted unformatted file.
  • Checks that cannot do their job fail (FR-020): zero entries scanned, zero Missions, a missing baseline tree and an injected fault that did not fire each exit non-zero; fails if any of the four passes.
  • Route coverage (FR-024): the plan and the CHANGELOG contain a reference to #5533 and no route-by-route map; a review check greps for the five route families of the operator's scope comment in the one-line statement and fails if a mapping table is present or the dossier overview and snapshot export are not named as not covered.

Out of Scope

  • The read service implementation (the epic's Java service) and the screens themselves.
  • Authentication and authorization: the serving layer's responsibility; the contract declares no security scheme, as in v1.
  • Write routes.
  • The event stream: no change to the provisional stream rules and no new event kind.
  • Governance reads (charter, glossary); #5533 decides where those go.
  • The route-by-route mapping from the removed dashboard to the new reads (#5533, FR-024); the dossier overview and the snapshot export of the removed dashboard.
  • The four v1 gaps (per-Mission staleness, actor on WorkPackageSummary, project branch, lane weights): decided out, deferred to the next major version (CL-1).
  • Cutting the 1.0.0 contract release (operator/maintainer step); there is no 1.1.0 release. Bringing the Mission branch level with main (an orchestrator step, AD-15).
  • Any change outside the slice file set (Terminology) and the Mission's own kitty-specs/<mission>/ directory (C-003): in particular anything under src/, any existing file under contracts/mission-status/ or contracts/_shared/ other than the edits that set lists, and any other file under contracts/tools/. The one acknowledged extension is FR-015.
  • Reading from the coordination status surface, the event log or status.json through the artifact reads (AD-5); review cycles held only on a coordination surface (AD-18).
  • Pagination, search, diff or history of artifacts; binary or image artifacts; serving anything over 256 KiB.

Assumptions

  • A-1: #5581 has landed on main (PR merged 2026-10-04): the projector tests/contract/_mission_status_payloads.py and the corpus test tests/contract/test_mission_status_reality.py are the base the extension edits (FR-021). This checkout predates the merge; the Mission branch is brought level with main by the orchestrator before the plan (AD-15). If upstream has moved those files again by then, the plan adapts FR-021 and records it.
  • A-2: #5582 (documentation) lands independently; if it has landed when this Mission edits the module CHANGELOG.md, the edit merges with it. This Mission edits no README (FR-024).
  • A-3: #5326 (touches src/specify_cli/status/{__init__,reducer,store}.py) changes no cited symbol's meaning; the plan re-verifies the status citations after it.
  • A-4: The contract is unreleased and stays so: this Mission works at 1.0.0-SNAPSHOT, no release tag is needed, and the additive proof is demonstrated with --baseline-root over the contract tree on main (FR-017). The branch is rebased onto upstream main before the pull request, without a tag.
  • A-5: The corpus facts quoted here (counts, sizes, encoding anomalies) were measured on main before the #5581 merge and drift as Missions are added; floors are re-measured on the level branch at plan time and pinned below the measurement, and examined-equals-discovered is the first guard (FR-021).
  • A-6: The reference reader may call existing public readers in src/ (they are inputs). The review-cycle seam is decided (AD-18: the primary directory only, no private resolver). Adding a public seam in src/ (for FR-007 rule 2; OQ-2 was resolved on 2026-10-04 against a review-cycle seam) is the only conceivable src/ change and would need its own justification against NFR-007.
  • A-7: The serving host in production has the repository and git available for changeState; where it does not, unknown is the documented answer, not an error.

Risks and Dependencies

ItemKindEffectHandling
#5581 reality check and projectorDependency (merged)The files exist on main (build_detail already builds MissionDetail, so the new builders need other names); this checkout predates the mergeAD-15, A-1: bring the branch level before plan; FR-021 follows the projector's conventions
#5582 documentationDependencyedits the same CHANGELOGA-2: merge order, no content conflict by design
#5326 status modulesDependencycitations to status/ symbols may moveA-3: re-verify at plan
1.0.0 contract releaseOperator/maintainer step, not a preconditionthe tag-based baseline does not exist and is not needed; the contract stays at 1.0.0-SNAPSHOTCL-2, A-4: the additive proof is the --baseline-root run over the contract tree on main (FR-017); the PR is not held for a tag
Review-cycle files in two places (ledger SK-348: a stranded primary-checkout record on a coordination-topology Mission)Riska cycle on a coordination surface is not shownAD-18 (primary only, stated in the schema description); FR-022 fixtures with the record in each place; [] only when none is on the primary surface
Planning vs coordination surface (ledger SK-310)Riskreading the wrong surface makes plan.md look missingAD-2; FR-022 fixture with a coordination-topology Mission
tasks.md title parsing is new logic with no canonical readerRisktwo row formats, driftx-derived with the extraction rule of FR-003, both formats and the edge rows in fixtures, corpus floors (about 21 thousand rows)
Redaction can mangle legitimate text (a title or document that names /tmp)Riskcontent differs from diskaccepted as in v1 (same patterns, matching substring only); redacted and the always-redact rule (CL-3) make it visible
A credential in a served fileRiska leaked secret reaches a screenAD-17: refused (422), never masked; planted per endpoint (FR-023)
Listing reads every returned file in fullRiskslow listing on huge Mission trees (worst case 1000 times 262144 bytes)stated in NFR-001/NFR-002; the one timed synthetic case; the corpus Missions are far below the cap
Author-chosen shapes in closed responsesRiska later member forces a major versionAD-14: those elements are x-provisional
Environment-dependent fields differ per hostRiskflaky corpus assertionsFR-016: host capability is an explicit input; the corpus passes a host without worktrees
kitty-specs/ archive freezeConstraintreality check must never write thereC-003, read-only fingerprint
Ledger accountingNotethe ledger entries touching this behaviour are SK-310 (surface), SK-348 (review-cycle placement) and SK-309 (an empty status.json from ignored events: the detail must not trust a snapshot that disagrees with the log; the plan reads the snapshot through the same reducer v1 uses)each is handled in the rows above or FR text; no ledger entry blocks the slice

Open Questions (plan phase; none block the spec)

Open for the plan phase: OQ-1 and OQ-4. Resolved on 2026-10-04 (CL-5): OQ-2, OQ-5 and OQ-6. OQ-3 is withdrawn.

  • OQ-1 (open): Does the structure check accept a fifth CHANGELOG heading for the deferred record, or must it sit under an existing heading (FR-018)?
  • OQ-2 (resolved 2026-10-04, operator): keep AD-18 (primary directory only, no src/ change); no public seam in src/ that lists a work package's review-cycle files across surfaces is added.
  • OQ-3: withdrawn during the specify phase (its question became AD-8, the listing cap); the number is kept so the other question ids stay stable.
  • OQ-4 (open): The exact 1000-entry cap and 256 KiB cap are decided here; the plan may move the entry cap only with a measurement and a squad note.
  • OQ-5 (resolved 2026-10-04, orchestrator): all three new operations carry a refusal code on their 404. The detail 404 gets a code through a module refusal schema like ArtifactRefusal (WorkPackageDetailRefusal); v1's getWorkPackage is unchanged.
  • OQ-6 (resolved 2026-10-04, orchestrator): source_unreadable (and the detail's other refusal code, not_found) is pinned by that module refusal schema through the titled enum WorkPackageDetailRefusalCode, which enum_pins.json pins.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: The three operations and every new schema validate, bundle, lint and cite their sources; the example check passes with examples for every new schema including every code of ArtifactRefusal and of WorkPackageDetailRefusal (AC-VERSION).
  • SC-002: The breaking-change check run with the contract tree on main as baseline (--baseline-root, scratch baseline with a lower rewritten version) exits 0 with breaking=0, info.version is still 1.0.0-SNAPSHOT, the diff of the existing files against that tree is empty, the candidate bundle minus the new operations equals the baseline bundle, and planted violations make each of those fail (FR-017).
  • SC-003: Over every Mission of this repository, the reality check builds and validates a detail for every work package, a listing for every Mission and the content of every listed file, with examined equal to discovered, zero leaks, readable consistent with the content read for 100 percent of entries, and every refusal carrying its stated code (AC-DETAIL, AC-LIST, AC-CONTENT; after the branch is level with main).
  • SC-004: Every changeState in the corpus run is unknown because the run passes a host without worktrees, and the synthetic-repository tests show changed, unchanged and unknown each produced by the constructed cause, with the listed mutations killed (FR-016, FR-022).
  • SC-005: Each of the three endpoints has a planted negative that fails and a clean control that passes (FR-023).
  • SC-006: The corpus fingerprint is equal before and after (read-only), and the corpus job stays within NFR-001.
  • SC-007: The CHANGELOG 1.0.0-SNAPSHOT section records the four deferred gaps as deferred to the next major version and names every provisional element (FR-018).
  • SC-008: No file outside the slice file set (Terminology) and the Mission's own kitty-specs/<mission>/ directory (C-003) changed: nothing under src/, no existing contract file except the edits that set lists (version, path map, index entries, CHANGELOG entry, the four contracts/tools/ files of FR-015), an acknowledged scope extension (NFR-007, C-001); plus the two OD-1 X-pure registration paths (.github/workflows/packs.yml, tests/architectural/test_ci_corpus_trigger_completeness.py), operator-decided exceptions (OD-1).

Traceability

RequirementDecisionAcceptanceSuccess
FR-001, FR-002CL-1, AD-1, AD-3, AD-4, work package of a MissionAC-DETAIL (examples, corpus coverage, absence)SC-001, SC-003
FR-003, FR-004CL-1, AD-4, AD-17AC-DETAIL (v1 controls, subtask titles, dependencies, leak scan)SC-001, SC-003
FR-005CL-1, AD-2, AD-8, AD-13, AD-14, AD-18AC-DETAIL (reviewCycles, pointer check, reviewedAt)SC-001, SC-003
FR-006CL-1, AD-4, AD-12, AD-14AC-DETAIL (workspace)SC-003, SC-004
FR-007CL-4, AD-20AC-DETAIL (changeState, glob cases)SC-004
FR-008CL-1, AD-2, AD-8, AD-18, eligible fileAC-DETAIL (artifactReferences)SC-003
FR-009CL-1, AD-1, AD-2, AD-8, AD-14, AD-21AC-LIST (bounds, unreadable directory, unknown Mission)SC-001, SC-003
FR-010CL-1, AD-14, AD-19, AD-22AC-LIST (kinds, odd names, modifiedAt)SC-001, SC-003
FR-011CL-1, AD-2, AD-5, AD-6, AD-7, AD-8AC-LIST (root-only exclusion, bounds)SC-003
FR-012CL-3, AD-1, AD-2, AD-5, AD-8, AD-10, AD-11, AD-14AC-CONTENT (envelope, redaction pair, unknown Mission)SC-003
FR-013CL-3, AD-5, AD-6, AD-7, AD-9, AD-17, AD-21AC-CONTENT (boundary, encoding, credentials, refusal bodies, containment, open failure)SC-003
FR-014CL-3, AD-2, AD-5, AD-6, AD-8, AD-9, AD-19AC-LIST (listing containment, root-only exclusion), AC-CONTENT (containment, eligibility past the cap, read-only)SC-003, SC-006
FR-015CL-1, CL-3, AD-10, AD-17, AD-19, slice file setAC-DETAIL, AC-LIST, AC-CONTENT (leak scans and planted negatives)SC-003, SC-005
FR-016CL-4, AD-12, AD-22AC-DETAIL, AC-LISTSC-004
FR-017, FR-018CL-1, CL-2, AD-14, AD-16, slice file setAC-VERSION (version, breaking check, baseline diff, scope)SC-002, SC-007, SC-008
FR-019CL-1, AD-4, AD-14, AD-17, AD-21AC-VERSION (citation, enum-pin, terminology scan)SC-001
FR-020AD-4, AD-7, AD-21AC-DETAIL (absence), AC-LIST, AC-CONTENT, AC-VERSION (checks that cannot do their job fail)SC-001, SC-003
FR-021 to FR-023CL-2, CL-4, AD-15, AD-17, AD-18, work package of a MissionAC-DETAIL, AC-LIST, AC-CONTENT, AC-VERSIONSC-003 to SC-006
FR-024removed-dashboard routes (cross-reference to #5533)AC-VERSION route-coverage bullet (a grep, not a map)SC-001
NFR-001AD-7AC-VERSION (runtime and quality)SC-006
NFR-002AD-7, AD-8AC-DETAIL (artifact links, counting opener), AC-LIST (bounds), AC-CONTENT (bounded read)SC-003
NFR-003AD-6AC-CONTENT (read-only)SC-006
NFR-004AD-17, AD-19AC-DETAIL, AC-LIST, AC-CONTENT (leak scans)SC-003, SC-005
NFR-005AD-18, AD-22AC-DETAIL, AC-LIST, AC-CONTENT (determinism), AC-VERSION (bundle)SC-003
NFR-006allAC-VERSION (runtime and quality)SC-003
NFR-007AD-18, AD-20AC-VERSION (scope)SC-008