Cross-layer missions/ reader inventory
Mission: doctrine-consumer-surface-missions-extraction-01KZ6G6H (WP03, FR-003, SC-007)
Produced: 2026-08-04, against research/doctrine-wheel-mission-types-public-api (primary checkout).
Scope: every reader of src/doctrine/missions/ data content — across kernel, doctrine, charter, specify_cli, runtime, and upgrade migrations — with an explicit move/stay/repoint decision, per data-model.md's MissionsReaderRecord schema.
Method
Per research.md R6, a bare path-literal grep is a starting point only:
grep -rn "doctrine.missions\|doctrine/missions\|specify_cli.missions\|specify_cli/missions" src/ tests/ --include="*.py"
This sweep was run first (140 hits across src/), then narrowed and supplemented by:
- Tracing every symbol imported from
doctrine.missions.*to its implementation, and every caller ofMissionTemplateRepository. - Searching for resolution shapes rather than literal path substrings:
files("doctrine"),importlib.resources,get_package_asset_root,default_missions_root,built_in_root,resolve_pack_root,_missions_root,builtin_missions_root, and barePath(__file__).parent / "missions"-shaped constructions. - A deliberate sweep of
src/specify_cli/upgrade/migrations/*.py(25 files inspected). - Reading the actual WP04 lane branch (
kitty/mission-doctrine-consumer-surface-missions-extraction-01KZ6G6H-lane-b, commitc681a8910) to verify what the kernel primitive actually resolves post-convergence, rather than assuming convergence closes every gap.
Sites the naive grep missed, found only by (2)/(3)/(4):
| Site | Why the naive grep missed it |
|---|---|
doctrine/missions/mission_type_repository.py::MissionTypeRepository.default() |
Uses files("doctrine") / "missions" / "mission_types" — a chain of / operators, never the literal substring doctrine.missions or doctrine/missions. |
doctrine/missions/mission_step_repository.py::MissionStepRepository.default() |
Path(__file__).parent / "mission-steps" — same reason. |
doctrine/missions/step_contracts.py::MissionStepContractRepository._default_built_in_dir() |
files("doctrine.missions.built_in_step_contracts") — the literal string here is a dotted resource name, not a path; substring match on doctrine.missions actually would hit this one incidentally, but it was found by symbol-tracing MissionStepContractRepository's callers first, not by the grep line. |
specify_cli/skills/command_installer.py::_package_templates_dir() |
Path(doctrine.__file__).parent / "missions" / "mission-steps" / mission_type — multi-line /-operator chain. |
specify_cli/template/manager.py::copy_specify_base_from_local() / copy_specify_base_from_package() |
repo_root / "src" / "doctrine" / "missions" and doctrine_data_root.joinpath("missions") — same shape. |
specify_cli/upgrade/migrations/m_2_1_3_restore_prompt_commands.py::_get_runtime_command_templates_dir() |
Zero occurrences of the literal substrings doctrine.missions/doctrine/missions anywhere in this file — absent from the naive grep's output entirely. Found only via the get_package_asset_root/resolution-shape keyword sweep. This is the cleanest proof in this inventory that the naive grep is insufficient. |
specify_cli/upgrade/migrations/m_2_1_4_enforce_command_file_state.py::_get_runtime_command_templates_dir() |
The file does appear in the naive grep's output, but only because of a docstring two lines above the actual construction ( doctrine/missions/mission-steps/<mission_type>/<step_id>/prompt.md ) — the real code five lines later (Path(doctrine.__file__).parent / "missions" / "mission-steps" / _MISSION_NAME) is a multi-line /-chain the grep pattern does not match. This is the exact "found only via an unrelated comment nearby" phenomenon research.md R6 documents for repository.py's own files("doctrine") / "missions" call — reproduced independently in a second, unrelated file. |
charter/pack_manager.py::_scan_layout_for() |
Its return values are the literal strings "doctrine/missions/mission_types" / "doctrine/missions/built_in_step_contracts", so the naive grep does catch the string — but understanding that this is a load-bearing, silently-fails-closed-to-empty resolution path (not a docstring) required reading _scan_layer_dirs's consumption of base_dir at lines ~702-707. |
Live finding incorporated (per this WP's brief): the post-move self-match trap is NOT closed by WP04 alone
WP04's lane branch (commit c681a8910, not yet merged into this checkout) converges three call sites onto kernel.sibling_paths.resolve_installed_sibling(). I traced what each converged call actually resolves to after WP05 additionally moves the data subdirectories out, since src/doctrine/missions/ (the .py package) keeps existing:
kernel.paths.get_package_asset_root()(post-WP04) walks ancestors fromsrc/kernel/paths.pylooking for the glob shapesrc/*/missions. At the repo-root ancestor this matchessrc/doctrine/missions(still a directory, now holding only.pyfiles +__pycache__) andsrc/specify_cli/missions(a distinct, still-data-bearing legacy tree — see "Two distinctspecify_cli/missionstrees" below)._first_matchsorts candidates and returns the first by directory listing order with no content-sniff at all —doctrinesorts beforespecify_cli, so the data-less directory wins, silently.doctrine.missions.repository.MissionTemplateRepository.default_missions_root()(post-WP04) walks ancestors from its own file (src/doctrine/missions/repository.py) looking for a baremissionsdirectory. One ancestor level up (src/doctrine), the patternmissionsmatches the repository module's own containing directory — i.e., the resolver finds itself, one level up, before ever reachingpacks/built-in/missions. This is the exact trap the WP04 review flagged: the fail-closedMissionsRootNotFoundguard added by WP04 never fires, because the ancestor-walk does find a directory namedmissions— it just finds the wrong (data-less) one.
Conclusion: WP04's convergence is necessary but not sufficient. WP05 must additionally repoint the sibling-path pattern itself (e.g., search for packs/built-in/missions explicitly, or exclude an anchor's own directory from the ancestor-walk) for both of these converged call sites — simply routing through the shared primitive, unchanged in what it searches for, reproduces the exact silent-data-less-hit this mission exists to close. See rows R-01 and R-02 below.
Two distinct specify_cli/missions trees (do not conflate)
src/specify_cli/missions/ is not purely the backward-compat shim package it appears to be from its docstring. A directory listing shows it still contains live per-mission-type data (research/mission.yaml, research/templates/, documentation/mission.yaml, documentation/templates/, software-dev/mission.yaml, plan/mission.yaml, plan/templates/) alongside its .py coordination modules (_read_path_resolver.py, _substantive.py, _create.py, etc.). This is a separate, still-existing legacy data tree, distinct from src/doctrine/missions/, and its retirement is explicitly out of scope for this mission (spec.md C-002, issues #2468/#2652). Readers that resolve into this tree (rows in the "stay — different tree" group below) are unaffected by FR-005's move.
Reader inventory
Legend for layer: kernel | doctrine | charter | specify_cli | runtime | upgrade_migration.
Layer note (cycle 2):
runtimedenotes files undersrc/specify_cli/runtime/*(e.g.agent_commands.py,bootstrap.py,home.py,resolver.py). Cycle 1 labeled some of those rowsspecify_cliby package prefix; the two overlap and the file path in each row is authoritative. R-09/R-10 (runtime/home.py) and R-11/R-14's runtime consumption are runtime-layer in that sense — the labels were left as cycle-1 wrote them to avoid churning verified rows; do not read the label as contradicting the path.
A. Already-identified sites (confirmed, not rediscovered — per this WP's brief)
| file | line | layer | current_path_assumption | decision | rationale |
|---|---|---|---|---|---|
src/doctrine/missions/repository.py |
MissionTemplateRepository.default_missions_root(), ~97-115 (pre-WP04) / ~118-142 (WP04 lane-b) |
doctrine | importlib.resources.files("doctrine") / "missions", fallback Path(__file__).parent (pre-WP04); post-WP04, resolve_installed_sibling(anchor_file=__file__, sibling_relative_path=PurePosixPath("missions")) |
repoint | The already-promoted authority (tests/charter/test_missions_root_authority.py:19-22 defers full convergence to this issue). Even WP04's converged form is not sufficient — see "Live finding" above: the bare "missions" sibling pattern self-matches this module's own now-data-less containing directory via the ancestor-walk, one level before packs/built-in/missions would be considered. WP05 must repoint the pattern, not merely confirm the delegation exists. |
src/doctrine/drg/migration/extractor.py |
_missions_root(doctrine_root), ~103-120 |
doctrine | Docstring/implementation assert missions were "not relocated... still live inside the doctrine package"; resolves via files("doctrine"). |
repoint | This mission's move directly falsifies that assumption. Feeds packs/built-in/mission_type.graph.yaml / mission_step_contract.graph.yaml via extract_artifact_edges/generate_graph; both fragments must be regenerated and diffed byte-identical against committed state (tests/doctrine/drg/test_regen_roundtrip.py) in the same change, per SC-008. |
B. The .py-vs-data split within src/doctrine/missions/ itself
All 11 top-level .py modules stay (the package itself does not move — packs/built-in/ cannot host .py modules, per pack_paths.py's own docstring and the hyphenated-name argument in research.md R9). Three of them additionally contain their own internal repoint-needed resolver method — captured as separate rows in section C, not folded into the module-level stay verdict, per data-model.md's explicit allowance ("stay... unless a specific module's own internal path assumptions need repoint").
| file (module) | decision | rationale |
|---|---|---|
src/doctrine/missions/__init__.py |
stay | Pure re-export (MissionTemplateRepository, TemplateResult, ConfigResult); no path literal. |
src/doctrine/missions/repository.py |
stay (module) | Module stays; default_missions_root() classmethod is repoint — see row R-01. |
src/doctrine/missions/mission_type_repository.py |
stay (module) | Module stays; MissionTypeRepository.default() classmethod is repoint — see row R-03. |
src/doctrine/missions/mission_step_repository.py |
stay (module) | Module stays; MissionStepRepository.default() classmethod is repoint — see row R-04. |
src/doctrine/missions/step_projection.py |
stay | No missions_root/path construction; pure projection logic over already-loaded objects. |
src/doctrine/missions/models.py |
stay | Pydantic/dataclass models only; no filesystem access. |
src/doctrine/missions/action_index.py |
stay | load_action_index(missions_root: Path, ...) (line 61) takes the root as a caller-supplied parameter — no internal hardcode. Caller (charter/action_grain.py:204) correctly supplies builtin_missions_root(). |
src/doctrine/missions/primitives.py |
stay | No path construction (glossary/execution-context primitives only). |
src/doctrine/missions/step_contracts.py |
stay (module) | Module stays; MissionStepContractRepository._default_built_in_dir() staticmethod is repoint — see row R-05. |
src/doctrine/missions/step_offer_seam.py |
stay | No path construction (model-tier offer resolution only). |
src/doctrine/missions/glossary_hook.py |
stay | No path construction beyond a repo_root: Path parameter it receives, never derives. |
Data content that moves (unchanged from occurrence_map.yaml's moves: block, restated here for completeness): mission_types/, mission-steps/, built_in_step_contracts/, documentation/, plan/, research/, software-dev/, README.md.
C. Repoint — genuinely new findings (this WP's deliverable)
IDs R-02 and R-06 are intentionally absent from this table — those two sites are the already-identified default_missions_root() and _missions_root() rows, recorded once in Section A rather than duplicated here.
| # | file | line | layer | current_path_assumption | decision | rationale |
|---|---|---|---|---|---|---|
| R-01 | src/kernel/paths.py |
get_package_asset_root(), ~63-117 (pre-WP04); WP04 lane-b ~110-145 |
kernel | Pre-WP04: importlib.resources.files("doctrine") / "missions". Post-WP04: resolve_installed_sibling(..., sibling_relative_path=PurePosixPath("src/*/missions")). |
repoint | See "Live finding" above. WP04 removed the doctrine-specific vocabulary but kept the target shape generic-but-unchanged (any sibling's missions/ dir) — it still matches src/doctrine/missions after the move, now data-less, before packs/built-in/missions is ever considered. charter/catalog.py:185 is this function's only production consumer (R-08 below) — the two must be fixed together. |
| R-03 | src/doctrine/missions/mission_type_repository.py |
MissionTypeRepository.default(), ~69-77 |
doctrine | files("doctrine") / "missions" / "mission_types", fallback Path(__file__).parent / "mission_types" — no existence check at all on the primary branch (unlike default_missions_root(), which at least checks .is_dir()). |
repoint | Same entangled shape as the already-known site, one level deeper (targets mission_types/ specifically). Not named in spec.md/research.md — found only by tracing MissionTypeRepository's own constructor. After the move, mission_types/ itself relocates (it is in the moves: list), so this returns a Path to a directory that does not exist at all (not merely data-less) — failure is deferred to whatever the caller does with it (e.g. .iterdir() inside load_all()), not raised at resolution time. |
| R-04 | src/doctrine/missions/mission_step_repository.py |
MissionStepRepository.default(), ~217-219 |
doctrine | Path(__file__).parent / "mission-steps" — no importlib.resources branch at all, purely self-referential, no existence check. |
repoint | Same class of finding as R-03, for mission-steps/. This is the resolver MissionStepRepository.resolve() (the 3-tier project > org > built-in lookup engine mentioned in the module docstring) depends on for its builtin_steps_root — a load-bearing site for mission-step-contract resolution generally, not just template listing. |
| R-05 | src/doctrine/missions/step_contracts.py |
MissionStepContractRepository._default_built_in_dir(), ~189-198 |
doctrine | files("doctrine.missions.built_in_step_contracts") (dotted resource-package form), fallback Path(__file__).parent / "built_in_step_contracts". |
repoint | Same class of finding, for built_in_step_contracts/. Feeds get_by_action()'s step-contract lookups; also the artifact-kind MISSION_STEP_CONTRACT's flat built-in directory per charter/pack_manager.py (see R-07's interaction). |
| R-07 | src/charter/pack_manager.py |
_scan_layout_for() return values, lines 220 and 223; consumed at _scan_layer_dirs() line ~706 (candidate = root / base_dir for the flat built-in layer) |
charter | ("doctrine/missions/mission_types", "*.yaml", False) and ("doctrine/missions/built_in_step_contracts", kind.glob_pattern, False) — literal strings joined onto _SRC_ROOT. |
repoint | HIGH SEVERITY, silent failure mode. After the move, _SRC_ROOT / "doctrine/missions/mission_types" and .../built_in_step_contracts no longer exist at all (both are in the moves: list). _scan_layer_dirs()'s candidate.is_dir() guard (line ~708) then evaluates False and the built-in layer is silently dropped — no exception, no log — from charter pack's mission-type and mission-step-contract listings. This breaks spec-kitty charter pack list mission-type / mission-step-contract (and anything built on list_available_detailed) with zero error signal. Not named in spec.md/research.md; found by tracing _scan_layout_for's callers, not by the grep line alone (the grep line only shows the docstring/return-value text, not the consuming logic's fail-mode). |
| R-08 | src/charter/catalog.py |
resolve_doctrine_root(), step 3 fallback, line 185 (_get_package_asset_root().parent); import at line 16 |
charter | Assumes kernel.paths.get_package_asset_root()'s return value is a direct child of the doctrine package root (i.e. <doctrine_root>/missions), so .parent recovers <doctrine_root>. This is kernel.paths.get_package_asset_root()'s only production consumer. |
repoint (coordinate with R-01) | Currently true by coincidence (parent of src/doctrine/missions is src/doctrine). If R-01 is fixed by repointing the sibling pattern to packs/built-in/missions (the natural fix), .parent here would then yield packs/built-in, not src/doctrine — breaking this fallback in a new way. WP05 must resolve R-01 and R-08 as one coordinated change, not independently — fixing one without the other reproduces a different silent-wrong-root bug. |
| R-09 | src/specify_cli/runtime/home.py |
get_package_asset_root(), primary for package in ("doctrine", "specify_cli") loop, ~93-103 |
specify_cli | importlib.resources.files(package) / "missions"; loop checks only missions_dir.is_dir(), no content-sniff. |
repoint | This is the single clearest instance of the "resolves to an existing-but-data-less directory" hazard named in this WP's brief. This is a separate, distinct implementation from kernel.paths.get_package_asset_root() (same name, do not conflate — confirmed by reading both bodies side by side) and is not one of the three sites WP04 converges. After the move, the "doctrine" iteration matches src/doctrine/missions (still exists, .is_dir() True, zero data) and returns it immediately, before the loop even reaches the "specify_cli" case or the dev_roots fallback below. The sole-door mission's WP06 already retargeted the dev_roots fallback (R-10) onto default_missions_root() — but that fallback is unreachable in the doctrine case because this loop returns first. |
| R-10 | src/specify_cli/runtime/home.py |
_resolve_env_package_asset_root() candidates (~107-124) and get_package_asset_root()'s dev_roots fallback (~118-124) |
specify_cli | Env-override candidates are guarded by _looks_like_missions_root() (a real content-sniff: checks for actual template/command-template/step-prompt files) — correctly rejects a data-less candidate and keeps searching. dev_roots = (MissionTemplateRepository.default_missions_root(), Path(__file__).parent.parent / "missions") already delegates to the promoted authority per WP06. |
stay (currently unreachable, not itself broken) | Recorded as its own row, distinct from R-09, because it is a correctly-guarded code path that happens to be dead for the doctrine case today (R-09's loop returns before this is ever reached). Fixing R-09 does not require touching this row; this row's correctness should be re-verified once R-09 is fixed, since it would then actually execute for the first time in the doctrine case. |
| R-11 | src/specify_cli/skills/command_installer.py |
_package_templates_dir(), ~96-118 |
specify_cli | Path(doctrine.__file__).parent / "missions" / "mission-steps" / mission_type — no importlib.resources, no existence guard at the function itself. |
repoint | Feeds the Agent-Skills command-rendering pipeline (Codex/Vibe/Pi/Letta .agents/skills/spec-kitty.<command>/). After the move this resolves to a nonexistent directory (mission-steps/ relocates); the caller's own read will fail loudly rather than silently, but the site itself must be repointed so skill installation keeps working post-move. |
| R-12 | src/specify_cli/template/manager.py |
copy_specify_base_from_local(), line 45 (missions_src = repo_root / "src" / "doctrine" / "missions") |
specify_cli | .exists() check only, no content-sniff. |
repoint | HIGH SEVERITY, silent failure mode. Used by spec-kitty init --local <repo> (the dev-checkout bootstrap path). Called from src/specify_cli/cli/commands/init.py:790. After the move, missions_src.exists() remains True (directory still exists, .py-only) — this silently shutil.copytree()s the now-data-less directory into the new project's .kittify/missions/, producing a broken scaffold (a .kittify/missions/ containing .py files and __pycache__, no mission.yaml/templates) with zero error signal. |
| R-12b | src/specify_cli/template/manager.py |
_is_template_root() inner helper, ~122-127 (inside the template-root resolver) |
specify_cli | (path / "src" / "doctrine" / "templates" / "AGENTS.md").is_file() and (path / "src" / "doctrine" / "missions").is_dir() — the second conjunct is a bare .is_dir(), no content-sniff. |
repoint | Third site in this file (alongside R-12/R-13). Post-move the second conjunct still evaluates True against the data-less src/doctrine/missions, so template-root detection silently accepts a checkout whose missions data now lives under packs/built-in/ — masking a misdetection with no error. Added cycle 2. |
| R-13 | src/specify_cli/template/manager.py |
copy_specify_base_from_package(), missions_resource_candidates list and loop, ~99-107 |
specify_cli | First candidate: doctrine_data_root.joinpath("missions") where doctrine_data_root = files("doctrine"); loop uses _resource_exists() (.is_file() or .is_dir()), no content-sniff; breaks on first match. |
repoint | The single highest-severity finding in this inventory. Called from src/specify_cli/cli/commands/init.py:792 — this is the default spec-kitty init code path (no --local flag), i.e. every ordinary pip install spec-kitty-cli && spec-kitty init run. After the move, the first candidate resolves .is_dir() == True (data-less src/doctrine/missions) and the loop breaks immediately — the real packs/built-in/missions candidate (not even present in the candidate list today) is never reached. Every fresh spec-kitty init post-move would silently scaffold .kittify/missions/ with the wrong content (or fail inside copy_package_tree()'s resource.iterdir() on .py/__pycache__ entries) instead of the real mission-type data, with no error surfaced to the operator. |
| R-14 | src/specify_cli/cli/commands/init.py |
_get_package_templates_root(), line 320 (pkg_root = get_package_asset_root(), comment # .../doctrine/missions/) + line 321 (templates_dir = pkg_root.parent / "templates"); also line 361 (candidate_dirs.append(package_root / mission / "command-templates"), a second get_package_asset_root() consumption — in a different function, _resolve_mission_command_templates_dir, not _get_package_templates_root at 320) |
specify_cli | Same .parent-assumes-doctrine-root shape as R-08, but via the specify_cli.runtime.home implementation (R-09), not the kernel one. Line 361 additionally reads into the resolved root (/ mission / "command-templates"), not just its parent. |
repoint (coordinate with R-09) | Line 320/321 happens to still resolve correctly even when R-09's silent-data-less hit fires, because .parent of a still-existing-but-empty src/doctrine/missions is still src/doctrine. Line 361 does not get that coincidence — post-move it appends a nonexistent <data-less-root>/<mission>/command-templates, so command templates go silently missing. Must be re-verified once R-09 is repointed. (Cycle-1 named only line 320; line 361 added cycle 2.) |
| M-01 | src/specify_cli/upgrade/migrations/m_2_1_4_enforce_command_file_state.py |
_get_runtime_command_templates_dir(), ~118-127 |
upgrade_migration | import doctrine; Path(doctrine.__file__).parent / "missions" / "mission-steps" / _MISSION_NAME, guarded by .is_dir() before falling through to get_package_asset_root() (R-09/R-10) then ~/.kittify/. |
repoint | Confirms research.md R6's warning that upgrade migrations are historically the easiest layer to miss, in a second, independent file from the one R6 already names. Fails safe today (the joined path correctly evaluates False post-move since mission-steps/ itself relocates), but its fallback chain routes through R-09's bug — so fixing R-09 alone does not fully repair this migration's happy path; the primary doctrine.__file__-relative construction should also be repointed to avoid depending on a broken fallback. |
| M-02 | src/specify_cli/upgrade/migrations/m_2_1_3_restore_prompt_commands.py |
_get_runtime_command_templates_dir(), ~88-96 |
upgrade_migration | Identical shape to M-01. | repoint | Zero literal-substring hits for doctrine.missions/doctrine/missions anywhere in this file — the cleanest proof that symbol-tracing (searching for get_package_asset_root/resolution-shape keywords) finds sites the naive grep cannot, not merely "finds them faster." |
| N-01 | src/specify_cli/runtime/agent_commands.py |
_get_command_templates_dir(), ~95-98 and ~100-108 |
runtime | Two unguarded constructions, both worse than M-01/M-02 (which at least .is_dir()-gate before falling through). ~95-98: Path(loaded_file).parent / "missions" / "mission-steps" / DEFAULT_MISSION_KEY is returned directly, no existence check. ~100-108: the find_spec("doctrine") branch ends return doctrine_path / "missions" / "mission-steps" / DEFAULT_MISSION_KEY, also unchecked. It also consumes specify_cli.runtime.home.get_package_asset_root() (R-09) at ~90 for its legacy_command_templates probe. |
repoint | Missed by cycle 1. After the move mission-steps/ relocates, so both constructions return nonexistent paths, and the R-09 probe inherits R-09's silent-data-less hit. Verified in the working tree. |
| N-02 | src/specify_cli/migration/rewrite_shims.py |
_get_command_templates_dir(), ~45-57 |
specify_cli | Path(doctrine.__file__).parent / "missions" / "mission-steps" / _MISSION_NAME, guarded by .is_dir() (fails closed like M-01/M-02). |
repoint | Missed by cycle 1 because the migration sweep was directory-scoped to src/specify_cli/upgrade/migrations/*.py; this file lives under src/specify_cli/migration/ (singular) — a second, independent demonstration that a directory-scoped sweep has the same blind spot as a pattern-scoped one. Verified in the working tree. |
| N-03 | src/specify_cli/runtime/bootstrap.py |
populate_from_package(), ~97-114 |
runtime | asset_root = get_package_asset_root() (the runtime.home impl, R-09); then missions_src = asset_root; if missions_src.is_dir(): shutil.copytree(missions_src, missions_dst). .is_dir() only, no content-sniff. |
repoint | Third silent-wrong-content instance alongside R-12/R-13: post-move the data-less src/doctrine/missions is still .is_dir(), so it copytrees .py/__pycache__ into the staging area with zero error signal. Also a third must-fix-together pair with runtime/home.py, on two counts: missions_src, and asset_root.parent / "scripts" / asset_root.parent / "AGENTS.md" (~109-115), which carry the same .parent-recovers-doctrine-root assumption as R-08/R-14. The "two required-together pairs" summary line loses this — it is three. Verified in the working tree. |
| N-04 | src/charter/neutrality/lint.py |
_default_scan_roots(), ~336-338 |
charter | roots = [repo_root / "src" / "doctrine"]; roots.extend(_iter_charter_scan_roots(repo_root / "src" / "charter")); roots.extend(_iter_mission_scan_roots(repo_root / "src" / "specify_cli" / "missions")) — no root under packs/built-in/. |
repoint | Gate-coverage loss with no red test — a distinct failure class from the silent-wrong-content readers. Post-move the neutrality lint silently stops scanning the relocated mission prompts (it keeps scanning src/doctrine, now .py-only for the missions subtree) while staying green, covering strictly less. Verified in the working tree. |
D. Stay — confirmed correct or genuinely unaffected
| file | layer | current_path_assumption | decision | rationale |
|---|---|---|---|---|
src/doctrine/resolver.py (lines ~196, ~205, ~220, ~334-345) |
doctrine | Delegates to MissionTemplateRepository.default()._command_template_path() / ._content_template_path() / ._mission_config_path() / ._missions_root property. |
stay | Correctly parameterized through the one promoted authority; will resolve correctly automatically once R-02 (default_missions_root()) is repointed. Module docstring (line 8, PACKAGE -- doctrine/missions/{mission}/{templates,command-templates}/) names the pre-move shape cosmetically — found, not fixed (out of this WP's scope; a documentation-only staleness, not a code defect). |
src/specify_cli/runtime/resolver.py (641 lines; not a shim) |
runtime | Imports get_package_asset_root at line 49 and calls it at line 317 (pkg_missions = get_package_asset_root()), feeding missions_root=pkg_missions into the PACKAGE-tier factory (lines 253/319); a second consumption sits near line 631. It does re-export ResolutionResult/ResolutionTier from charter.resolution, but it is the 5-tier resolver, not a re-export shim. |
stay | Correction (cycle 2): the cycle-1 rationale called this "a re-export shim with no independent path literal" — false. stay is still correct, but because it is a consumer of R-09 (its get_package_asset_root() call resolves transitively once R-09 is repointed), not because it has no path literal. WP05 must treat it as an R-09 consumer to re-verify, not skip it as a shim. Layer is runtime, not specify_cli. |
src/charter/mission_type_profile_repository.py (builtin_missions_root() ~56-72, _default_built_in_dir() ~119-129) |
charter | Thin delegate onto MissionTemplateRepository.default_missions_root(), per the sole-door mission's WP06 promotion (confirmed in research.md R7). |
stay | Already correctly converges onto the one authority; will resolve correctly once R-02 is fixed. No independent hardcode to repoint. |
src/charter/action_grain.py (line 204 builtin_missions_root() call; line 212 MissionTypeRepository(root / "mission_types")) |
charter | Line 204: root = built_in_dir if built_in_dir is not None else builtin_missions_root() — consumes the promoted authority. Line 212: constructs root / "mission_types" and hands it to MissionTypeRepository. |
stay | Correction (cycle 2): the cycle-1 rationale said "no independent path construction elsewhere in the file" — line 212 does construct a path, but on the caller-supplied root (which is builtin_missions_root() when built_in_dir is None), so it stays correct once R-02 is repointed. stay holds; the reason is caller-parameterization, not the absence of construction. |
src/charter/compiler.py (line 1316) |
charter | _template_reference() fallback: mission_path = repo._mission_config_path(mission) or (doctrine_root / "missions" / mission / "mission.yaml") — an independent construction when _mission_config_path() returns None. |
stay | Correction (cycle 2): cycle-1 folded this into the "symbol imports only" charter row, which is wrong — it constructs a missions/-relative path. stay holds because doctrine_root is derived upstream through the resolver chain (fixed once R-02/R-08 land) and the primary arm is repo._mission_config_path(mission); but the fallback is a real path literal a WP05 implementer must re-verify, not a pure symbol import. |
src/specify_cli/mission_loader/command.py (lines 195-197) |
specify_cli | package_missions = Path(runtime_bridge.__file__).resolve().parent.parent / "missions" — an independent, self-referential construction of exactly the Path(__file__)... / "missions" shape the Method section says the naive grep misses. |
repoint (re-verify) | Correction (cycle 2): cycle-1 folded this into the "symbol imports only" specify_cli row denying any construction — false. Note the precise target: runtime_bridge lives at src/runtime/next/runtime_bridge.py, so .parent.parent / "missions" is src/runtime/missions — a directory that does not exist (neither the doctrine tree nor specify_cli/missions); this is a genuinely buggy mirror (its sibling producer runtime_bridge_io.py:237 computes specify_cli/missions). WP05 must re-verify and repoint if it feeds discovery of built-in mission data; if it is dead/buggy independent of the move, record it as a bug for a follow-up rather than repointing. |
src/specify_cli/cli/commands/charter/list_cmd.py (lines 66, 79) |
specify_cli | missions = project_root / "doctrine" / "missions" (line 66) and missions = org_root / "doctrine" / "missions" (line 79), each .is_dir()-guarded. |
stay | Correction (cycle 2): cycle-1 folded these into the "symbol imports only" row denying construction — false; there are two constructions. stay is nonetheless correct: these are the project/org override tiers (layer_roots["project"] / ["org"]), not the built-in tier this mission relocates — they resolve into a project/org .kittify doctrine layer, unaffected by the built-in move. The reason is "different tier," not "no construction." |
src/charter/{mission_type_profiles.py, resolver.py, primitives.py, pack_context.py, activations.py, drg.py, synthesizer/interview_mapping.py, context_renderers/bootstrap_text.py, context_renderers/template_include.py, template_resolver.py, mission_steps.py} (compiler.py extracted to its own row above — it has a path construction) |
charter | Symbol imports only: MissionTemplateRepository, MissionStepRepository, MissionTypeRepository, MissionStepContract, builtin_mission_type_id_set, MissionStep, execute_with_glossary, PrimitiveExecutionContext, ActionIndex/load_action_index. |
stay | No independent path/root construction in any of these files beyond importing classes/functions from the .py package, which stays and works correctly once its own internal resolvers (R-02 through R-05) are fixed. Consolidated into one row per this document's own judgment call on readability vs. exhaustiveness — each file was individually opened and confirmed symbol-import-only. |
src/specify_cli/{cli/commands/doctrine.py, cli/commands/mission_type.py, cli/commands/charter/mission_type.py, dossier/manifest.py, review/gate_bindings.py, doctrine/pack_validator.py, mission_loader/contract_synthesis.py, skills/command_renderer.py, core/constants.py, core/paths.py, model_task_routing/evaluator.py, doctrine/service.py} (cli/commands/charter/list_cmd.py and mission_loader/command.py extracted to their own rows above — both have path constructions) |
specify_cli / doctrine / doctrine | Symbol imports of MissionTypeRepository, MissionStepRepository, MissionStepContract, GateBinding, MissionTemplateRepository, resolve_model_tier_offer, MissionStepContractRepository — no independent path construction. pack_validator.py's except (PackRootNotFound, BuiltInContentDirNotAvailable) clause is the real consumer the WP04 exception-translation requirement protects (per data-model.md's SiblingPathResolutionPrimitive invariant), unrelated to missions data directly. |
stay | Each file individually opened; none constructs a missions/-relative path itself. |
src/specify_cli/ownership/audit_targets.py, line 24 |
specify_cli | "src/specify_cli/missions/*/command-templates/" |
stay | Names the separate, still-live specify_cli/missions/ legacy data tree (see "Two distinct trees" above), not doctrine/missions/. Out of scope per C-002. |
≈40 files under src/specify_cli/{cli/commands,coordination,status,merge,retrospective,dossier,lanes,migration,mission_runtime,orchestrator_api,widen,acceptance}/** importing specify_cli.missions._read_path_resolver / ._substantive / ._create / ._resolve_planning_branch / ._archive |
specify_cli | Import from the specify_cli.missions package's own internal coordination/status-resolution modules. |
stay | This is the dominant false-positive class in the naive grep's 140 hits (~40 of them). specify_cli.missions._read_path_resolver etc. are mission-selector/status-resolution helpers — a completely different concern from doctrine template data — and are entirely unaffected by src/doctrine/missions/'s data relocation. Excluding this class correctly required judgment, not just pattern-matching; recorded here as a single consolidated row rather than ~40 individual ones, since every one of them resolves identically (import-only, zero independent path logic). |
src/specify_cli/upgrade/migrations/{m_0_6_7_ensure_missions.py, m_0_9_2_research_mission_templates.py, m_0_10_14_update_implement_slash_command.py, m_0_11_1_update_implement_slash_command.py, m_0_12_0_documentation_mission.py, m_0_13_0_update_research_implement_templates.py, m_0_13_5_add_commit_workflow_to_templates.py, m_0_14_0_centralized_feature_detection.py} |
upgrade_migration | All resolve via files("specify_cli") / Path(specify_cli.__file__).parent, joined with "missions" — i.e. the separate, still-live specify_cli/missions/ legacy tree (confirmed present on disk today), not src/doctrine/missions/. |
stay | Unaffected by this mission's move. Found-but-not-fixed note: m_0_12_0_documentation_mission.py's own docstring (line 18) claims its source is src/doctrine/missions/documentation/, but its actual code (_find_source_mission()) reads src/specify_cli/missions/documentation — a pre-existing docstring/code mismatch, unrelated to this mission's scope, left unfixed per this WP's brief ("If you find a bug, record it... do not fix it"). |
src/specify_cli/upgrade/migrations/{m_2_1_2_install_git_workflow_skill.py, m_2_1_2_fix_runtime_next_skill.py, m_2_1_2_install_mission_system_skill.py, m_2_1_2_fix_glossary_context_skill.py, m_3_2_0rc30_fix_runtime_next_result_default.py, m_2_1_2_fix_orchestrator_api_skill.py, m_3_2_0rc35_fix_prompt_file_workaround.py} |
upgrade_migration | All resolve files("doctrine") then .joinpath("skills", <skill_name>, ...) — i.e. src/doctrine/skills/, not src/doctrine/missions/. |
stay / not applicable | Matched the resolution-shape keyword sweep (they use files("doctrine")) but read an entirely unrelated doctrine content kind. Recorded to show they were checked and explicitly ruled out, not silently skipped. |
src/specify_cli/upgrade/migrations/m_3_2_0rc35_activate_builtin_mission_types.py, line 112 |
upgrade_migration | from doctrine.missions.mission_type_repository import (builtin_mission_type_ids) — lazy, call-time symbol import. |
stay | No independent path construction; resolves correctly once the .py package's own internal resolvers (R-03) are fixed. |
Summary: the explicit repoint set (for WP05's owned_files widening)
The repoint set spans the files below (deduplicated; several files carry more than one
repoint-needing site). The row inventory above is the authority — do not treat the count as a
contract; cycle 2 added four files, and a future site would drift any hard number.
src/kernel/paths.py— R-01src/doctrine/missions/repository.py— A/row 1 (default_missions_root)src/doctrine/missions/mission_type_repository.py— R-03src/doctrine/missions/mission_step_repository.py— R-04src/doctrine/missions/step_contracts.py— R-05src/doctrine/drg/migration/extractor.py— A/row 2 (_missions_root)src/charter/pack_manager.py— R-07src/charter/catalog.py— R-08 (coordinate with #1)src/specify_cli/runtime/home.py— R-09src/specify_cli/skills/command_installer.py— R-11src/specify_cli/template/manager.py— R-12, R-12b, R-13 (three sites, one file)src/specify_cli/cli/commands/init.py— R-14 (lines 320/321 and 361; coordinate with #9)src/specify_cli/upgrade/migrations/m_2_1_4_enforce_command_file_state.py— M-01src/specify_cli/upgrade/migrations/m_2_1_3_restore_prompt_commands.py— M-02src/specify_cli/runtime/agent_commands.py— N-01 (cycle 2)src/specify_cli/migration/rewrite_shims.py— N-02 (cycle 2)src/specify_cli/runtime/bootstrap.py— N-03 (cycle 2)src/charter/neutrality/lint.py— N-04 (cycle 2)
Additionally re-verify (mis-described by cycle 1): src/specify_cli/runtime/resolver.py (stay — an
R-09 consumer, not a shim) and src/specify_cli/mission_loader/command.py (repoint (re-verify) — a
self-referential missions construction pointing at the nonexistent src/runtime/missions, a buggy
mirror; repoint only if it feeds built-in mission data, else record as a follow-up bug).
Required-together pairs (fixing one without the other reproduces a different silent-wrong-root
bug): (#1, #8) kernel/paths.py ↔ charter/catalog.py; (#9, #12) runtime/home.py ↔ cli/commands/init.py; and (#9, #17) runtime/home.py ↔ runtime/bootstrap.py — N-03's
missions_src and its asset_root.parent / "scripts"|"AGENTS.md" both share R-09's root. Three
pairs, not two (cycle-2 correction).
Readers that would silently resolve to the data-less src/doctrine/missions package directory after the move
This is the dangerous class NFR-001 needs enumerated (per this WP's brief: "the question is not merely 'does it name the old path' but 'after the move, does it silently resolve to a directory that exists but no longer holds the data?'"):
- R-01
kernel.paths.get_package_asset_root()(post-WP04) — ancestor-walk_first_matchhas no content-sniff. - A/row-1
doctrine.missions.repository.MissionTemplateRepository.default_missions_root()(post-WP04) — self-matches its own containing directory one ancestor level up. - R-09
specify_cli.runtime.home.get_package_asset_root()— primary loop checks only.is_dir(). - R-13
specify_cli.template.manager.copy_specify_base_from_package()—_resource_exists()has no content-sniff; this is the defaultspec-kitty initpath. - R-12
specify_cli.template.manager.copy_specify_base_from_local()—.exists()only, no content-sniff. - R-12b
specify_cli.template.manager._is_template_root()— second conjunct is a bare.is_dir(); silently accepts a data-less checkout as a valid template root. (cycle 2) - R-09/N-03
specify_cli.runtime.bootstrap.populate_from_package()—asset_root.is_dir()only, thenshutil.copytrees the data-less package dir into staging. (cycle 2) - N-04
charter.neutrality.lint._default_scan_roots()— a different class: not a wrong resolution but a silent gate-coverage loss; it keeps scanningsrc/doctrine(now.py-only for missions) and never scans the relocated prompts, staying green. (cycle 2)
Three further sites (R-03, R-04, R-07) resolve to a directory that does not exist at all post-move (rather than an existing-but-empty one), since the specific subdirectory they target (mission_types/, mission-steps/, built_in_step_contracts/) is itself in the moves: list — these fail at a different point (whatever the caller does with the returned/candidate path) rather than returning a plausible-looking empty directory, but still require repoint to keep working.
Bugs found, not fixed (per this WP's scope — recorded for a future WP)
m_0_12_0_documentation_mission.py's docstring (line 18) namessrc/doctrine/missions/documentation/as its source, but the actual code readssrc/specify_cli/missions/documentation— a pre-existing, harmless docstring/code mismatch unrelated to this mission.doctrine/resolver.py's module docstring (line 8) still names the pre-movedoctrine/missions/{mission}/{templates,command-templates}/shape; the code itself is correctly parameterized (stay), only the comment is stale.