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
StatusLanevocabulary. 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'slanes.json) is named in the new fieldslaneIdandlaneBranch; 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
.mdfile directly undertasks/whose name is a work package id followed by a hyphen (WP[0-9]{2,}-<anything>.md: theWpIdpattern ofcontracts/mission-status/schemas/WpId.yaml, at least two digits, then-; thewpIdof a prompt file is its name up to the first hyphen). A file such astasks/WP1-x.mdhas one digit, is no prompt file and is classifiedother(FR-010). The prompt files of one work package are those whose name starts with that work package'swpIdand 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 astasks/WP-notes.md, or one of another work package's id, is not a prompt file of this work package;tasks/WP-notes.mdis no prompt file at all and is classifiedother(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
workPackageslist andgetWorkPackageserve, selected the way the product selects work package files: the single selectorwp_task_files(src/specify_cli/status/wp_metadata.py: the entries oftasks/whose name starts withWPand ends with.md, case-sensitive), each identified by thework_package_idof 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 exampletasks/WP01.mdofsync-diagnose-canonical-allowlist-01KS4F8H) is a work package here, answers 200, and hasartifactReferences.promptnull(a link is made only to a Prompt file) and kindotherin 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, withnullstatus lanes. AWP*.mdfile thatwp_task_filesselects but whose frontmatter does not parse (a frontmatter or validation error) or has no validwork_package_id(for example a straytasks/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 distinctwork_package_idvalues of the fileswp_task_filesselects, 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 onlyartifactReferences.promptand thework_package_promptkind (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 malformedpathin a request is a 400 (AD-9); an emittedpath(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.pythat #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,
artifactPathandartifactReferencesagree. 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
ghtoken with prefixghp,gho,ghu,ghsorghr, or a fine-grained token with prefixgithub_pat), an AWS access key id (prefixAKIAorASIA) and the header line of a PEM private key. The authority for the exact expressions isSECRET_PATTERNSincontracts/tools/leak_patterns.py; this spec adds none and weakens none. The descriptions of the 422 response and ofreadablelist 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 withSECRET_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 ofcontracts/mission-status/: onlyopenapi.yaml(the version and the path map), the_index.yamlfiles (new entries) andCHANGELOG.md(the 1.1.0 entry). (3) Incontracts/tools/: onlyleak_scan.py,fixture_builder.py(STRICT_FIELDS),negative_cases.jsonandenum_pins.json(pins for the new enums). (4) The test tree undertests/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--deselectentries in.github/workflows/packs.ymland the registry rows intests/architectural/test_ci_corpus_trigger_completeness.py, are part of the slice file set; the scope check allows exactly these two paths. Nothing undersrc/and nothing else undercontracts/,contracts/_shared/included. The Mission's own directorykitty-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 onmain), 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.
| Decision | What the operator fixed | What this spec adds or reads | Conservative reading kept | Cost 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 readable | excluded by exact root-level name only | a 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 containment | never followed, no resolution | a 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 read | readable 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 listing | cap 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 400 | 400 declared on getArtifactContent | a 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 content | that reading, with redacted marked x-provisional and named in the CHANGELOG Provisional section | none 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 sniffed | the table of AD-11 | a 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 false | true only when runs_in_checkout_root is false and exists is true | workspace 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 pointer | file-based cycles, no guessed verdict | reviewCycles 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 415 | member shapes of reviewCycles and workspace, ArtifactKind, truncated, redacted, the refusal code values | those elements are x-provisional | none 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 order | the order stated in AD-15 | process 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 product | the reading in AD-16 | none, 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 fields | a 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 readable | the artifact-path rule of Terminology | a 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 introduced | no new token; credentials are refused or withheld, never masked | the 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 shown | primary 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 time | that format | a 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 rule | edited 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-015 | tooling 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 unknown | the rules of FR-007 | changeState 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-21 | the 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:
getWorkPackageDetailatGET /missions/{missionId}/work-packages/{wpId}/detail,listArtifactsatGET /missions/{missionId}/artifacts, andgetArtifactContentatGET /missions/{missionId}/artifacts/contentwith a required query parameterpath. 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.
WorkPackageDetaildoes not embed or extendWorkPackage(closed schemas cannot be extended without changing the existing one, and CL-1 forbids that). It carriesmissionId,wpIdand 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(orunknownforchangeState) 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 with0, 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.jsonlandstatus.jsonare 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 exampletasks/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
readablecovers every refusal, and the listing reads whole files (acknowledged by the operator 2026-10-04; interprets CL wording, see the table).readableisfalseexactly when a content read of that file would be refused with 413, 415, 422 (credential, AD-17) or 500 (artifact_unreadable). The listing therefore decidesreadableby 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 whosest_sizeis at or below 262144 bytes; a file over the cap is decided fromst_sizealone and is not opened. A NUL byte or an invalid sequence at the last byte of a file of exactly 262144 bytes must therefore givereadable: false. Consequence:readable: trueimplies 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-onlyreadable, or a prefix check) are cheaper but let the listing sayreadable: truefor 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
pathin ascending byte order, capped at 1000 entries (the largest Mission tree at measurement has 119 files). Over the cap the response carriestruncated: true; it never silently drops entries. No pagination. The cap bounds the listing only: the content read andartifactReferencesuse 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 byartifactReferenceswhen it is the work package's prompt or the spec. The listing must visit every name under the Mission directory (to sort and to settruncated) 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
paththat 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. Apaththat is missing or given more than once is a 400invalid_artifact_pathtoo. An ill-formedmissionIdorwpIdnames no Mission or work package and is a 404 (v1 declares only 404 anddefaultfor those parameters; on all three new reads the 404 carriesnot_found). The 400 status is stable once released: removing it later is a major version (AD-21). - AD-10
redactedmeans a replacement was made (acknowledged by the operator 2026-10-04; CL-3 names the member but not its meaning).redactedistruewhen at least one[path]or[email]substitution changed the content,falsewhen the text passed through unchanged. Redaction itself is always applied (CL-3). The operator's wording ("content is always redacted") can also be read asredactedbeing constantlytrue; 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 staysx-provisional(AD-14): it carries anopen_decisiontext stating both readings and is named in the CHANGELOGProvisionalsection, so a later change of meaning is not a major version. - AD-11
mediaTypecomes from a fixed extension table (acknowledged by the operator 2026-10-04; interprets CL wording, see the table).mdistext/markdown,jsonisapplication/json,yamlandymlareapplication/yaml,jsonlisapplication/x-ndjson,csvistext/csv, everything else readable istext/plain. It is never sniffed from content. The response's own HTTP media type isapplication/json(the envelope). - AD-12
worktreePresentis true only for a lane worktree (acknowledged by the operator 2026-10-04; interprets CL wording, see the table).resolve_workspace_for_wpresolves a planning-artifact work package to the repository root checkout, which always exists. Reporting that as a present worktree would mislead, soworktreePresentistrueonly when the resolved workspace (ResolvedWorkspaceinsrc/specify_cli/workspace/context.py) hasruns_in_checkout_rootfalse andexiststrue on the serving host. The reader usesruns_in_checkout_rootand 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
reviewCyclesarray has one entry per review-cycle artifact of the work package (filereview-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'shistorythroughreviewVerdict. The verdict of a cycle comes from the status events, matched to the cycle by the full review-cycle pointer (FR-005), and isnullwhen 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,reviewedAtandreviewernull;verdict,feedbackReferenceandartifactPathfollow their own rules, FR-005) so the failure is visible; it is not dropped. ThereviewedAtof a parseable cycle is the stored value only when it is a valid RFC 3339 date-time with offset, otherwisenull(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
reviewCyclesandworkspace, the listing entry, theArtifactKindvocabulary, the listing'struncatedmember, the meaning ofredacted(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 thereforex-provisional(with itsopen_decisiontext and its name in the CHANGELOGProvisionalsection) on exactly these elements: thereviewCyclesandworkspaceproperties ofWorkPackageDetailand theReviewCycleandWorkspaceschemas, theArtifactKindschema, thetruncatedproperty ofArtifactListing, theredactedproperty ofArtifactContent, and thecodeproperty ofArtifactRefusaland ofWorkPackageDetailRefusal. Everything else is stable, the declared HTTP statuses included: only thecodevalues 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.pyandtests/contract/test_mission_status_reality.pyexist there. This checkout predates the merge, so before the plan or any implementation the Mission branch must be brought level withmain; 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
charterrule "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.versionstays1.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, codeartifact_secret, no content in the body; the listing marks that filereadable: falsethrough 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 dependencytitle) 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 subtasktitleisnull(FR-003) and a dependencytitleis 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 aSECRETmatch 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_dirinsrc/specify_cli/review/cycle.py, is private and reads the coordination worktree when one is materialized on the host, which would makereviewCycleshost-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 enumeratestasks/<wp-slug>/review-cycle-<N>.mdunder 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: thereviewCyclesdescription says so, and v1'shistorystill carries the verdicts. Consequences, all stated:artifactPathis 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 insrc/, 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, nosrc/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
pathis 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 exampledata/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 thepathfield (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
changeStateinputs 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_globinsrc/specify_cli/policy/commit_guard.pylines 141 to 149 and its near copy_mt_matches_owned_fileinsrc/specify_cli/cli/commands/agent/tasks_move_task.pylines 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 areunknown:is_glob_patterntreats{as a glob character but the repository's matcher has no brace expansion, and guessing would risk a silent wrongunchanged. - 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 throughWorkPackageDetailRefusal), 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 codesartifact_unreadable,artifact_listing_unreadableand, for the detail,source_unreadable(inWorkPackageDetailRefusal). The statuses are stable once released (AD-14 makes only thecodevalues 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
modifiedAtformat (acknowledged by the operator 2026-10-04; interprets CL wording, see the table). RFC 3339 UTC with aZsuffix 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 atasks.mdwith no row for a subtask id, or a row whose title cell is empty, whitespace or holds a credential (AD-17): the subtasktitleisnull(9 work packages at measurement have no subtasks at all:subtasksempty array). Atasks.mdthat exists but cannot be decoded is notnull: the read fails (FR-020). - A work package whose frontmatter has no
subtaskskey:subtasksis[], as in v1 (FR-003). - A dependency that names no work package of the Mission: listed with its id as the title and a
nullstatus lane (AD-4); never dropped. - A Mission with no
lanes.json(77 of 544 at measurement) or a work package in no lane:laneIdandlaneBrancharenull. Alanes.jsonthat exists but is malformed: the read fails, notnull(FR-006). A work package in the planning lane (lane-planning):laneIdislane-planning,laneBranchisnull(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
feedbackReferencenull(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.jsonbeside 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 byartifactReferencesif it is the prompt or the spec. - A zero-byte file: listed
readable: true, content read is 200 with emptycontentandsizeBytes0 (a real file state, not an undetermined one). A file with a leading byte order mark: the mark is preserved incontent(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:
unchangedwhen the change set is known,unknownwhen 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
| ID | Title | User Story | Priority | Status | Delivery |
|---|---|---|---|---|---|
| FR-001 | Work-package detail operation | US1 | High | Open | [build] |
| FR-002 | WorkPackageDetail identity and closed shape | US1 | High | Open | [build] |
| FR-003 | Subtasks with titles | US1 | High | Open | [build] |
| FR-004 | Dependencies with titles and status lanes | US1 | High | Open | [build] |
| FR-005 | Review cycles | US1 | High | Open | [build] |
| FR-006 | Workspace | US1 | High | Open | [build] |
| FR-007 | Owned files with change state | US1 | High | Open | [build] |
| FR-008 | Artifact references | US1, US2 | High | Open | [build] |
| FR-009 | Artifact listing operation | US2 | High | Open | [build] |
| FR-010 | Listing entries and kinds | US2 | High | Open | [build] |
| FR-011 | Listing scope, exclusions and bounds | US2 | High | Open | [build] |
| FR-012 | Artifact content operation and envelope | US2 | High | Open | [build] |
| FR-013 | Content refusals | US2 | High | Open | [build] |
| FR-014 | Containment and read-only | US2 | High | Open | [build] |
| FR-015 | Redaction and leak rules | US1, US2 | High | Open | [build] |
| FR-016 | Environment-dependent fields and their verification | US1, US5 | High | Open | [build] |
| FR-017 | Additive-only proof against the contract tree on main | US3 | High | Open | [build] |
| FR-018 | Version unchanged, CHANGELOG and the four deferred gaps | US3, US4 | High | Open | [build] |
| FR-019 | Citations, examples, closed schemas, Problem responses | US1, US2 | High | Open | [build] |
| FR-020 | Loud failure: unknown is explicit, never null or zero as success | US1, US2 | High | Open | [build] |
| FR-021 | Reality-check extension and reference reader | US5 | High | Open | [build] (after the branch is level with main, AD-15) |
| FR-022 | Synthetic-repository tests | US1, US2 | High | Open | [build] (after the branch is level with main, AD-15) |
| FR-023 | Planted negative per endpoint and CI gates | US5 | High | Open | [build] |
| FR-024 | Removed-dashboard route coverage: cross-reference to #5533 | US2 | Medium | Open | [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.subtasksinsrc/specify_cli/status/wp_view.py, which is built fromnormalize_authored_subtask_roster(src/specify_cli/core/subtask_rows.py) and never raises: a work package whose frontmatter has nosubtaskskey, or with no locatable file, has an empty roster.authored_subtask_rosterin the same module is not the source: it raisesSubtaskRosterResolutionErrorfor a missing key or a missing or ambiguous work-package file, and could disagree with v1. The ids equalWorkPackage.authored.subtasksfor the same work package (a control, AC-DETAIL); x-source.title: the subtask's title as written in the Mission'stasks.md, ornull.nullmeans "no title was authored" and covers exactly:tasks.mdabsent, 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. Atasks.mdthat exists but cannot be decoded as strict UTF-8 is notnull: the detail read fails (FR-020), so a decode failure is never mistaken for "no title". Human text (FR-015).tasks.mdcarries 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 thex-derivedrule: 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 becomesnull. No canonical reader returns titles (iter_wp_section_subtask_rowsinsrc/specify_cli/core/subtask_rows.pyunderstands the checkbox form and yields no title), sotitleisx-derivedwith this rule and inputs naming those readers andtasks.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 stateWorkPackage.subtasksholds for it), ornullwhen the snapshot records none. A roster id with no recorded state isnull, notplanned.
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.dependenciesinsrc/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 isNone; 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-derivedfromdisplay_titleand 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). Atitleis never an empty string. Human text.statusLane: the dependency's current status lane in the status snapshot,StatusLane, ornullwhen the dependency has no events (v1'sReadinesstreats 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_numberinsrc/specify_cli/review/artifacts.pyis the parsed value; the two agree for a parseable file).reviewedAt: the cycle file'sreviewed_atstring when it is a valid RFC 3339 date-time with an offset (Zor+HH:MM, fractional seconds allowed; the test is the contract tooling's own date-time check,is_rfc3339_date_timeincontracts/tools/schema_formats.py), otherwisenull. The product accepts any non-empty string (ReviewCycleArtifact.from_fileinsrc/specify_cli/review/artifacts.pyonly checks thatreviewed_atis 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 thousandYYYY-MM-DDTHH:MM:SSZvalues, about 36 with an offset (3 of them with fractional seconds) and 3 date-onlyYYYY-MM-DDvalues, re-counted at plan time (A-5). Sonullhas 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 whosereviewedAtisnullfor the invalid-value reason by a scan independent of the reader.reviewer:ActorHandleornull(reviewer_agent; a stored value that is not a valid handle isnull, the v1 rule;nullas well when the file cannot be parsed).verdict:ReviewVerdictornull, taken from the status events (HistoryEntry.reviewVerdictin v1, from the event'sreview_result). Match key: the status events of the same work package whosereview_result.reference, trimmed, equals the cycle's full pointer string (thefeedbackReferencebelow, 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 legacyfeedback://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.nullwhen none matches or whenfeedbackReferenceisnull. Never read from the cycle file and never inferred (AD-13).feedbackReference: the pointerreview-cycle://<mission-slug>/<wp-slug>/review-cycle-<N>.md, built bybuild_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'sfeedback_path, which holds absolute host paths in hundreds of events. Because the builder raisesReviewCycleErrorwhen a segment is unsafe, the member is a string matching the pointer pattern ornullwhen 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), andnullonly when it is absent or not eligible. It does not depend on the 1000-entry cap or onreadable: 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): atasks/<wp-slug>directory that is a symlink is not entered (reviewCyclesis[]), and a cycle file that is itself a symlink is still an entry whose parsed members arenulland whoseartifactPathisnull.
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), ornullwhen the Mission has nolanes.json(read_lanes_jsonreturnsNone) or no lane holds the work package. For a planning-artifact orsingle_branchwork package the lane is the canonicallane-planningandlaneIdislane-planning.laneBranch: for a lane other thanlane-planning, the lane branch namelane_branch_name(mission_slug, lane_id, target_branch=<the manifest's target_branch>)(src/specify_cli/lanes/branch_naming.py;target_branchis a required keyword and the manifest's field is its source). Forlane-planningit isnull:lane_branch_namereturns the target branch itself for that lane, which is not a lane branch and must not be reported as one.nullas well whenlaneIdisnull. A value failing the strict branch pattern projects tonull(D-14 of v1).planningBranch: the work package'splanning_base_branchfrontmatter value, ornullwhen absent.worktreePresent: boolean;trueonly when the resolved workspace (ResolvedWorkspace) hasruns_in_checkout_rootfalse andexiststrue (AD-12). It describes the serving host at read time; it is not a property of the Mission. It isfalsein 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), aRelativeReference(never absolute; none of the 14.6 thousand entries at measurement is).isGlob: boolean, from the repository's own glob test (is_glob_patterninsrc/specify_cli/ownership/validation.py: any of*,?,[,{); about 7 percent of entries at measurement.changeState:changed,unchangedorunknown(closed enum), derived per work package, once, from this rule (AD-20):- Invariants:
changeStateischangedorunchangedonly whenworktreePresentistrue; and a work package whose entries are determined has nounknownentry 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 (theArtifactKindschema, 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,falseexactly 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 constantutf-8.sizeBytes: the on-disk size of the opened file (CL-3), equal to the listing'ssizeBytes(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:
| Status | code | When |
|---|---|---|
| 400 | invalid_artifact_path | the path is malformed, missing or repeated (AD-9) |
| 404 | not_found | unknown 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) |
| 413 | artifact_too_large | size greater than 262144 bytes (256 KiB); a file of exactly 262144 bytes is readable |
| 415 | artifact_not_text | the bytes are not valid UTF-8, or contain a NUL byte |
| 422 | artifact_secret | the decoded text holds a credential pattern (Terminology, AD-17) |
| 500 | artifact_unreadable | the 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) |
| 500 | artifact_listing_unreadable | listing 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
nullwhere nullable, otherwise is a projection error that fails the check; never redacted):missionId,wpId,idof a subtask, dependencywpId,laneId,laneBranch,planningBranch, owned-filepattern,feedbackReference,reviewer, everykind,mediaType,changeState. - Artifact path fields (
pathof 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): subtasktitle, dependencytitle. A title that holds a credential is not masked but withheld: a subtasktitleisnull, a dependencytitleis 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 isredacted(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(orunknownforchangeState) 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 isunknown, a tri-state value, neverunchanged). A source that exists but cannot be read or parsed (other than aWP*.mdwhose frontmatter does not parse, which is skipped, Terminology) fails the whole read: a500WorkPackageDetailRefusalwith codesource_unreadable: atasks.mdthat cannot be decoded, a malformedlanes.json, an ambiguous Mission handle, a Mission or work package file that cannot be read. (source_unreadableis pinned by the module refusal schemaWorkPackageDetailRefusaland its titled enumWorkPackageDetailRefusalCode, resolved OQ-6; that enum is pinned incontracts/tools/enum_pins.jsonlike the others, FR-015 (d).) An unknown Mission or work package is a 404WorkPackageDetailRefusalwith codenot_found. - A review-cycle file that cannot be parsed is a visible entry with
reviewedAtandreviewernull(the other members follow their own rules, FR-005), not a failure (AD-13); a cycle whose pointer cannot be built keepsfeedbackReferencenull(FR-005). - Listing:
artifact_listing_unreadable(500) when the directory or an entry cannot be read;truncatedandreadablecarry 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[].idequalsWorkPackage.authored.subtasks, dependency ids equalWorkPackage.dependencies, owned-file patterns equalWorkPackage.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
contentand 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 distinctwork_package_idvalues of the fileswp_task_filesselects (Terminology, work package of a Mission; a selected file whose frontmatter does not parse or has no validwork_package_idis 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 legacylanes.jsonwithfeature_slugand nomission_slugmakesread_lanes_jsonraiseCorruptLanesError, so by FR-006 its work packages answer 500source_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 existingFLOORS, remain as a second guard. Further floors: the number ofreadable: trueand ofreadable: falseentries compared, the files refused 413 and 415 by name (pinned in a fixture next tomission_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-nullverdict, compared with an independent count (the cycle files whose pointer equals thereview_result.referenceof 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; thework_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 (
worktreePresenttrue); a lane worktree recorded inlanes.jsonbut not created (false); a planning-artifact work package in thelane-planninglane, resolving to the repository root checkout (false, and the test fails if true;laneIdlane-planning,laneBranchnull); nolanes.json(laneIdandlaneBranchnull,worktreePresentfalse); a work package in no lane (null, false); a malformedlanes.json(the read fails withsource_unreadable, not null);planningBranchpresent 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_globand_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/*againstsrc/foobar/x.py(unchanged) and against a changedsrc/foo/x.py(changed); a plain directory entrysrc/foo(not a glob) against a changedsrc/foo/x.py(changed) and againstsrc/foobar/x.py(unchanged); a literal-glob-character directory[x]/against a changed[x]/y(changed, by the directory-prefix rule alone:fnmatchdoes not match it);.pyagainst a changedsrc/x.py(changed: the star crosses/);src/.pyagainst a changedsrc/a/x.py(changed);src/foo/against a changedsrc/foo/a/b.py(changed);a//cagainst a changeda/c/z(changed: the prefix step also strips a middle/);src//x.pyagainst a changedsrc/x.py(unchanged: no zero-directory) and against a changedsrc/a/b/x.py(changed);/x.pyagainst a changedx.py(unchanged) and against a changeda/x.py(changed);a//b/*/cagainst a changeda/x/b/y/c(changed) and against a changeda/b/c(unchanged); a brace patternsrc/{a,b}.pywith BOTH a changedsrc/a.py(matching an expansion) and a changedsrc/{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 (changedeach); an uncommitted edit, a staged edit and an untracked file (eachunchanged); 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 notunchanged); the invariant that no entry ischangedorunchangedwhenworktreePresentis false. - Mutations the change-state tests must kill (each applied by a test, which must then fail): treating
Nonefrom the git helper asunchanged; 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.pyagainstsrc/x.pyandsrc/.pyagainstsrc/a/x.py); a matcher that adds zero-directory(killed bysrc//x.pyagainstsrc/x.py,/x.pyagainstx.py, anda//b//cagainsta/b/c, eachunchanged); dropping the directory-prefix rule (killed by[x]/against[x]/yand by the plain directory entrysrc/fooagainstsrc/foo/x.py); a directory-prefix match without the whole-segment boundary (a plain string prefix: killed bysrc/foo/*andsrc/fooagainstsrc/foobar/x.py); a directory-prefix rule applied to a trailing/only (killed bya//cagainsta/c/z,changed); a matcher that normalises nothing (killed by a changed path recorded as./src/foo/x.pyorsrc\foo\x.py, which must matchsrc/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 turnunknownintochanged); accepting a detached HEAD; diffing against a base other than the manifest'starget_branch. - Size boundary: 262144 bytes is readable, 262145 is 413, with
readableagreeing; 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: falsein 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,readabletrue); 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:
readabledecided by size only;readabledecided 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 segmenta//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 symlinkedtasks/ortasks/<wp-slug>/directory (404, andartifactPathandartifactReferencesnull for it), a missing and a repeatedpathparameter (400), an ill-formedmissionIdandwpId(404) (all refused as AD-6 and AD-9 say, none leaking the target's content). In a glob,src/**/x.pyagainstsrc/x.pyandsrc/a/b/x.pyis covered under change state. - Redaction: host paths and e-mail addresses of each pattern kind in a title and in content, with
redactedtrue and false cases; a credential in a subtask title (null) and in a dependency title (the work package id); a JSON content with afeedback_pathkey 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 (notreview_cycle); a rootstatus.events.jsonlandstatus.jsonexcluded beside a nestedstatus.jsonlisted; a file named with ahome/<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.mdclassedother; truncation at 1001 files; a 1001-file Mission whose prompt andspec.mdsort past the cap, still named byartifactReferencesand readable by path;modifiedAtequal 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.mdand 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 notasks.md; a dependency naming no work package (id as title,statusLanenull); 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 nosubtaskskey ([]); a work package present only in the status log (the detail is 404); a straytasks/WP-notes.mdwhose frontmatter has no validwork_package_id, and aWP*.mdfile whose frontmatter does not parse (each: not listed as a work package, the detail for its stemWP-notesis 404, the independent count of distinctwork_package_idvalues 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 500source_unreadableor 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 lanesnull), and a work package whose file is no Prompt file, namely a copy of the corpus filesync-diagnose-canonical-allowlist-01KS4F8H/tasks/WP01.md(200,artifactReferences.promptnull, listing kindother); a symlinkedtasks/<wp-slug>/directory (reviewCycles[]) and a cycle file that is itself a symlink (an entry with null members and a nullartifactPath). - 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 undecodabletasks.md(the read fails withsource_unreadable), a subtask with no recorded state (statusLanenull, notplanned); 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 withreviewedAtandreviewernull, the other members by their own rules, and the number from its file name); a cycle directory whose segment fails pointer validation (an entry withfeedbackReferencenull); a filereview-cycle-0.mdorreview-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 straytasks/WP-notes.md(not a prompt, kindother, never named byartifactReferences); a cycle file with a date-onlyreviewed_at(reviewedAtnull, no midnight), one with an offset and fractional seconds (the stored string unchanged) and one with aZvalue.
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
| ID | Title | Requirement | Category | Priority | Status |
|---|---|---|---|---|---|
| NFR-001 | Corpus runtime | The 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. | Performance | High | Open |
| NFR-002 | Bounded reads | A 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, Security | High | Open |
| NFR-003 | Read-only | Zero writes under the repository by any new read, proven by the corpus fingerprint (FR-014, FR-021). | Safety | High | Open |
| NFR-004 | Leak-free artefacts | Zero 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). | Security | High | Open |
| NFR-005 | Deterministic | Listing 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). | Reliability | Medium | Open |
| NFR-006 | Test discipline | New 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. | Quality | High | Open |
| NFR-007 | No product change | No 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). | Locality | Medium | Open |
Constraints
- C-001 The four operator decisions (CL-1 to CL-4) bind; nothing may add a response property, status code, media type, header,
defaultor range response to an existing operation (RESPONSE_ADDITION_IDS,RESPONSE_KEY_ADDED_ID), becausecontracts/tools/breaking_check.pytreats each as breaking. - C-002 New schemas are used only by new operations; request-side additions and
x-provisionalelements are the only other additive changes available, and none is needed. - C-003
kitty-specs/onmainis 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
Problemplus a requiredcodethat referencesArtifactRefusalCode, and the pinned status). - ArtifactRefusalCode: the titled enum schema of the refusal codes, so that
enum_pin_checkcan pin it (FR-015 (d)). - WorkPackageDetailRefusal, WorkPackageDetailRefusalCode: the module refusal schema of
getWorkPackageDetail(sharedProblemplus a requiredcodereferencing the titled enumWorkPackageDetailRefusalCode, valuesnot_foundandsource_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_unreadableand, for the detail,not_foundandsource_unreadable(WorkPackageDetailRefusalCode).
Where each field comes from (field catalogue)
| Field | Source | Kind of citation |
|---|---|---|
subtasks[].id | AuthoredGroup.subtasks (src/specify_cli/status/wp_view.py), from normalize_authored_subtask_roster (src/specify_cli/core/subtask_rows.py) | x-source |
subtasks[].title | rows of tasks.md, two formats; no existing title reader | x-derived (inputs: iter_wp_section_subtask_rows, normalize_authored_subtask_roster) |
subtasks[].statusLane | ResolvedGroup.subtasks (src/specify_cli/status/wp_view.py) | x-source |
dependencies[].wpId | AuthoredGroup.dependencies | x-source |
dependencies[].title | WPMetadata.display_title (src/specify_cli/status/wp_metadata.py) with the empty-title and credential rule of FR-004 | x-derived (inputs: display_title, the work package id) |
dependencies[].statusLane | status snapshot work-package state (src/specify_cli/status/reducer.py) | x-source |
reviewCycles[].cycleNumber, reviewer | ReviewCycleArtifact (src/specify_cli/review/artifacts.py) | x-source |
reviewCycles[].reviewedAt | ReviewCycleArtifact.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 null | x-derived |
reviewCycles[].verdict | status events, ReviewResult (src/specify_cli/status/models.py) matched by the full pointer | x-derived |
reviewCycles[].feedbackReference | build_review_cycle_pointer (src/specify_cli/review/cycle.py) | x-source |
workspace.laneId | read_lanes_json (src/specify_cli/lanes/persistence.py), LanesManifest.lane_for_wp | x-source |
workspace.laneBranch | lane_branch_name (src/specify_cli/lanes/branch_naming.py), input target_branch from the manifest; null for lane-planning | x-source |
workspace.planningBranch | WPMetadata frontmatter planning_base_branch | x-source |
workspace.worktreePresent | ResolvedWorkspace (src/specify_cli/workspace/context.py): runs_in_checkout_root, exists; AD-12 | x-derived |
ownedFiles[].pattern | AuthoredGroup.owned_files | x-source |
ownedFiles[].isGlob | is_glob_pattern (src/specify_cli/ownership/validation.py) | x-source |
ownedFiles[].changeState | merge_base_changed_files_checked (src/specify_cli/core/vcs/git.py) plus the matching rule of FR-007 | x-derived |
artifactReferences | eligibility of the work package's prompt file (Terminology) and spec.md | x-derived (inputs: the Mission directory) |
entries[], content envelope | files 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 absolutefeedbackReference. - 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[].idequalsWorkPackage.authored.subtasks,dependencies[].wpIdequalsWorkPackage.dependencies,ownedFiles[].patternequalsWorkPackage.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,nullfor 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 undecodabletasks.mdyieldsnulltitles instead of failing the read. SubtaskstatusLaneisnull(notplanned) for an id with no recorded state; fails otherwise. - Dependencies: each
titleandstatusLaneequals the dependency's own work package title (with thedisplay_titlefallback) and status-snapshot lane; a dependency naming no work package is listed with its id astitleandstatusLanenull; a dependency with no events hasstatusLanenull, notplanned; a dependency whose title is empty or whitespace-only has its id astitle(never an empty string, whateverdisplay_titlealone returns); fails on a planted wrong title or lane, on a dropped dangling dependency and ontitleequal to an empty string. artifactReferences:promptandspecare non-null exactly when the file is eligible andnullotherwise, 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 straytasks/WP-notes.mdor another work package's prompt is never named; fails on a non-null member for an absent file, anullmember for an eligible file past the cap, and a member naming a path the content read answers 404 for.artifactPathandartifactReferencesneed no listing: on a Mission with more than 1000 files whose cycle file (and prompt) sorts past the cap,artifactPathis non-null and the content read answers 200 for it; a cycle file that is a symlink, or a cycle file under a symlinkedtasks/<wp-slug>/directory, givesnull; fails on anullfor 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 bycycleNumberascending; theverdictequals the status event matched by the full pointer (the last of several), isnullwhen none matches, and isnullfor 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 withreviewedAtandreviewernull, the other members by their own rules, and the cycle number from its file name; a cycle whose pointer cannot be built keepsfeedbackReferencenull(never dropped, never fabricated);reviewedAtis the stored string when it is a valid RFC 3339 date-time with offset (theZ, offset and fractional-second forms) andnullfor a date-only or otherwise invalid stored value, never midnight; the corpus floor counts the cycles with anullreviewedAtfor 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-nullverdictequals the independent count and meets its floor (FR-021).feedbackReferencecheck: every non-null value passesvalidate_review_cycle_pointer(src/specify_cli/review/cycle.py: exactly three segments, areview-cycle-<N>.mdfile name), not a prefix test, and no value in the payload equals or contains an event'sfeedback_path; fails on a plantedreview-cycle:///abs/path, a planted two-segment pointer, and a planted detail built with an absolutefeedback_path(negative pair: the control with the pointer passes).workspaceandownedFilesover the corpus (host without worktrees, FR-016): everychangeStateisunknown,worktreePresentisfalse, 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):worktreePresentistrueonly for a real lane worktree and fails iftruefor a planning-artifact work package resolving to the repository root checkout;laneId/laneBranchare as FR-006 states for nolanes.json, a work package in no lane andlane-planning; a malformedlanes.jsonfails the read;changeStateis neverchangedorunchangedwhenworktreePresentisfalse(asserted for every detail built).changeStateon 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:nullfor 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
reviewCyclesis 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
missionIdorwpIdare 404WorkPackageDetailRefusalresponses with codenot_found, and a work package file with no status events is a 200 withnullstatus lanes; fails when any 404 case returns 200 with empty groups or without the code; a malformedlanes.jsonand an undecodabletasks.mdare 500WorkPackageDetailRefusalresponses with codesource_unreadable; fails when either returns 200 or another code; the enum-pin check fails on a changedWorkPackageDetailRefusalCodevalue 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 unknownkind, apathstarting with/or holding a..segment. - Over the corpus, the listing is sorted by path ascending, contains no root
status.events.jsonlorstatus.json, no symlink, and every entry'ssizeBytesequals the file'sst_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 iswork_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). readableinvariant: 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 oftrueand offalsepairs compared (FR-021). Late defects: a 262144-byte file with the NUL byte, and one with the invalid sequence, as its last byte arereadable: false; fails if either istrue(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 withtruncated: 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.jsonis absent and a nestedstatus.jsonis 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 andreadable: 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 returnsentries: []or a shortened list with 200, or when the injected fault did not fire. A vanished entry is omitted. modifiedAthas 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:
listArtifactsanswers 404 with codenot_found; fails when it answers 200 withentries: []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 bymodifiedAt. - 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
encodingexactlyutf-8andsizeBytesequal to the on-disk size; fails (schema validation of theconst, not an enum pin) on a plantedencodingof any other value. Over the corpus and on synthetic files, the envelopesizeBytesequals the file'sst_sizeand the listing'ssizeBytesfor every file read; fails on a planted envelope whosesizeBytesis the redacted or decoded length (a multi-byte or redacted file makes the two differ). - Over the corpus, every
readable: truefile answers 200 whosecontenthas zero host paths, e-mail addresses and credentials, and everyreadable: falsefile answers its Problem with the matchingcodeand status; fails on any 200 with a leak, any 200 for areadable: falsefile, 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_secretwith no content in the body andreadable: 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
detailcarrying a path (the leak scan runs over theArtifactRefusalexamples too). Each code appears with its pinned status in an example; the enum-pin check fails on a changedArtifactRefusalCodevalue 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]withredacted: true; its clean twin answersredacted: false; fails if the clean twin reportstrueor the dirty file reportsfalse. - Unknown Mission:
getArtifactContentanswers 404 with codenot_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.versionequals 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-rootover the module extracted frommain(aef7cc967or the currentorigin/main), over a scratch baseline whoseinfo.versionis rewritten lower, exits 0 and itscounts:line showsbaselines=1andbreaking=0; thebreakingcount is non-zero on a planted response property added toWorkPackageand 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
mainis 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-SNAPSHOTsection (no new heading) names the four deferred gaps and everyx-provisionalelement 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 onWorkPackageSummary, 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 changedChangeState,ArtifactKind,ArtifactRefusalCodeorWorkPackageDetailRefusalCodevalue set. - Terminology (a new check this Mission adds: no existing guard scans contract files;
tests/architectural/test_no_legacy_terminology.pygates two other retired terms and not Mission versus feature): a scripted, case-insensitive scan, a test intests/contract/run with the contract-tools tests, over the new files undercontracts/mission-status/, the new examples and the 1.1.0 CHANGELOG entry, finds no "feature"; the values ofx-sourceandx-derivedare skipped, so a cited symbol name such asfeature_diris allowed. Fails on a planted new description containing "feature", and passes on a plantedx-sourcecitingfeature_dir. - Scope (NFR-007, SC-008): a scripted
git diff --name-onlybetween the merge base withmainand the Mission branch lists only paths in the slice file set (Terminology) plus the Mission's ownkitty-specs/<mission>/directory (excluded from this comparison and checked add-only against C-003), none undersrc/, and for existing files undercontracts/only the edits of items (2) and (3) of that set, and outsidecontracts/andtests/contract/only the two OD-1 X-pure registration paths of item (5); fails on a planted touched file undersrc/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 checkandruff format --checkpass 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 undersrc/, any existing file undercontracts/mission-status/orcontracts/_shared/other than the edits that set lists, and any other file undercontracts/tools/. The one acknowledged extension is FR-015. - Reading from the coordination status surface, the event log or
status.jsonthrough 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 projectortests/contract/_mission_status_payloads.pyand the corpus testtests/contract/test_mission_status_reality.pyare the base the extension edits (FR-021). This checkout predates the merge; the Mission branch is brought level withmainby 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-rootover the contract tree onmain(FR-017). The branch is rebased onto upstreammainbefore the pull request, without a tag. - A-5: The corpus facts quoted here (counts, sizes, encoding anomalies) were measured on
mainbefore 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 insrc/(for FR-007 rule 2; OQ-2 was resolved on 2026-10-04 against a review-cycle seam) is the only conceivablesrc/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,unknownis the documented answer, not an error.
Risks and Dependencies
| Item | Kind | Effect | Handling |
|---|---|---|---|
| #5581 reality check and projector | Dependency (merged) | The files exist on main (build_detail already builds MissionDetail, so the new builders need other names); this checkout predates the merge | AD-15, A-1: bring the branch level before plan; FR-021 follows the projector's conventions |
| #5582 documentation | Dependency | edits the same CHANGELOG | A-2: merge order, no content conflict by design |
| #5326 status modules | Dependency | citations to status/ symbols may move | A-3: re-verify at plan |
| 1.0.0 contract release | Operator/maintainer step, not a precondition | the tag-based baseline does not exist and is not needed; the contract stays at 1.0.0-SNAPSHOT | CL-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) | Risk | a cycle on a coordination surface is not shown | AD-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) | Risk | reading the wrong surface makes plan.md look missing | AD-2; FR-022 fixture with a coordination-topology Mission |
tasks.md title parsing is new logic with no canonical reader | Risk | two row formats, drift | x-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) | Risk | content differs from disk | accepted as in v1 (same patterns, matching substring only); redacted and the always-redact rule (CL-3) make it visible |
| A credential in a served file | Risk | a leaked secret reaches a screen | AD-17: refused (422), never masked; planted per endpoint (FR-023) |
| Listing reads every returned file in full | Risk | slow 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 responses | Risk | a later member forces a major version | AD-14: those elements are x-provisional |
| Environment-dependent fields differ per host | Risk | flaky corpus assertions | FR-016: host capability is an explicit input; the corpus passes a host without worktrees |
kitty-specs/ archive freeze | Constraint | reality check must never write there | C-003, read-only fingerprint |
| Ledger accounting | Note | the 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 insrc/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'sgetWorkPackageis 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 enumWorkPackageDetailRefusalCode, whichenum_pins.jsonpins.
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
ArtifactRefusaland ofWorkPackageDetailRefusal(AC-VERSION). - SC-002: The breaking-change check run with the contract tree on
mainas baseline (--baseline-root, scratch baseline with a lower rewritten version) exits 0 withbreaking=0,info.versionis still1.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,
readableconsistent 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 withmain). - SC-004: Every
changeStatein the corpus run isunknownbecause the run passes a host without worktrees, and the synthetic-repository tests showchanged,unchangedandunknowneach 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-SNAPSHOTsection 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 undersrc/, no existing contract file except the edits that set lists (version, path map, index entries, CHANGELOG entry, the fourcontracts/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
| Requirement | Decision | Acceptance | Success |
|---|---|---|---|
| FR-001, FR-002 | CL-1, AD-1, AD-3, AD-4, work package of a Mission | AC-DETAIL (examples, corpus coverage, absence) | SC-001, SC-003 |
| FR-003, FR-004 | CL-1, AD-4, AD-17 | AC-DETAIL (v1 controls, subtask titles, dependencies, leak scan) | SC-001, SC-003 |
| FR-005 | CL-1, AD-2, AD-8, AD-13, AD-14, AD-18 | AC-DETAIL (reviewCycles, pointer check, reviewedAt) | SC-001, SC-003 |
| FR-006 | CL-1, AD-4, AD-12, AD-14 | AC-DETAIL (workspace) | SC-003, SC-004 |
| FR-007 | CL-4, AD-20 | AC-DETAIL (changeState, glob cases) | SC-004 |
| FR-008 | CL-1, AD-2, AD-8, AD-18, eligible file | AC-DETAIL (artifactReferences) | SC-003 |
| FR-009 | CL-1, AD-1, AD-2, AD-8, AD-14, AD-21 | AC-LIST (bounds, unreadable directory, unknown Mission) | SC-001, SC-003 |
| FR-010 | CL-1, AD-14, AD-19, AD-22 | AC-LIST (kinds, odd names, modifiedAt) | SC-001, SC-003 |
| FR-011 | CL-1, AD-2, AD-5, AD-6, AD-7, AD-8 | AC-LIST (root-only exclusion, bounds) | SC-003 |
| FR-012 | CL-3, AD-1, AD-2, AD-5, AD-8, AD-10, AD-11, AD-14 | AC-CONTENT (envelope, redaction pair, unknown Mission) | SC-003 |
| FR-013 | CL-3, AD-5, AD-6, AD-7, AD-9, AD-17, AD-21 | AC-CONTENT (boundary, encoding, credentials, refusal bodies, containment, open failure) | SC-003 |
| FR-014 | CL-3, AD-2, AD-5, AD-6, AD-8, AD-9, AD-19 | AC-LIST (listing containment, root-only exclusion), AC-CONTENT (containment, eligibility past the cap, read-only) | SC-003, SC-006 |
| FR-015 | CL-1, CL-3, AD-10, AD-17, AD-19, slice file set | AC-DETAIL, AC-LIST, AC-CONTENT (leak scans and planted negatives) | SC-003, SC-005 |
| FR-016 | CL-4, AD-12, AD-22 | AC-DETAIL, AC-LIST | SC-004 |
| FR-017, FR-018 | CL-1, CL-2, AD-14, AD-16, slice file set | AC-VERSION (version, breaking check, baseline diff, scope) | SC-002, SC-007, SC-008 |
| FR-019 | CL-1, AD-4, AD-14, AD-17, AD-21 | AC-VERSION (citation, enum-pin, terminology scan) | SC-001 |
| FR-020 | AD-4, AD-7, AD-21 | AC-DETAIL (absence), AC-LIST, AC-CONTENT, AC-VERSION (checks that cannot do their job fail) | SC-001, SC-003 |
| FR-021 to FR-023 | CL-2, CL-4, AD-15, AD-17, AD-18, work package of a Mission | AC-DETAIL, AC-LIST, AC-CONTENT, AC-VERSION | SC-003 to SC-006 |
| FR-024 | removed-dashboard routes (cross-reference to #5533) | AC-VERSION route-coverage bullet (a grep, not a map) | SC-001 |
| NFR-001 | AD-7 | AC-VERSION (runtime and quality) | SC-006 |
| NFR-002 | AD-7, AD-8 | AC-DETAIL (artifact links, counting opener), AC-LIST (bounds), AC-CONTENT (bounded read) | SC-003 |
| NFR-003 | AD-6 | AC-CONTENT (read-only) | SC-006 |
| NFR-004 | AD-17, AD-19 | AC-DETAIL, AC-LIST, AC-CONTENT (leak scans) | SC-003, SC-005 |
| NFR-005 | AD-18, AD-22 | AC-DETAIL, AC-LIST, AC-CONTENT (determinism), AC-VERSION (bundle) | SC-003 |
| NFR-006 | all | AC-VERSION (runtime and quality) | SC-003 |
| NFR-007 | AD-18, AD-20 | AC-VERSION (scope) | SC-008 |