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:

  1. Tracing every symbol imported from doctrine.missions.* to its implementation, and every caller of MissionTemplateRepository.
  2. 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 bare Path(__file__).parent / "missions"-shaped constructions.
  3. A deliberate sweep of src/specify_cli/upgrade/migrations/*.py (25 files inspected).
  4. Reading the actual WP04 lane branch (kitty/mission-doctrine-consumer-surface-missions-extraction-01KZ6G6H-lane-b, commit c681a8910) 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 from src/kernel/paths.py looking for the glob shape src/*/missions. At the repo-root ancestor this matches src/doctrine/missions (still a directory, now holding only .py files + __pycache__) and src/specify_cli/missions (a distinct, still-data-bearing legacy tree — see "Two distinct specify_cli/missions trees" below). _first_match sorts candidates and returns the first by directory listing order with no content-sniff at all — doctrine sorts before specify_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 bare missions directory. One ancestor level up (src/doctrine), the pattern missions matches the repository module's own containing directory — i.e., the resolver finds itself, one level up, before ever reaching packs/built-in/missions. This is the exact trap the WP04 review flagged: the fail-closed MissionsRootNotFound guard added by WP04 never fires, because the ancestor-walk does find a directory named missions — 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): runtime denotes files under src/specify_cli/runtime/* (e.g. agent_commands.py, bootstrap.py, home.py, resolver.py). Cycle 1 labeled some of those rows specify_cli by 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.

  1. src/kernel/paths.py — R-01
  2. src/doctrine/missions/repository.py — A/row 1 (default_missions_root)
  3. src/doctrine/missions/mission_type_repository.py — R-03
  4. src/doctrine/missions/mission_step_repository.py — R-04
  5. src/doctrine/missions/step_contracts.py — R-05
  6. src/doctrine/drg/migration/extractor.py — A/row 2 (_missions_root)
  7. src/charter/pack_manager.py — R-07
  8. src/charter/catalog.py — R-08 (coordinate with #1)
  9. src/specify_cli/runtime/home.py — R-09
  10. src/specify_cli/skills/command_installer.py — R-11
  11. src/specify_cli/template/manager.py — R-12, R-12b, R-13 (three sites, one file)
  12. src/specify_cli/cli/commands/init.py — R-14 (lines 320/321 and 361; coordinate with #9)
  13. src/specify_cli/upgrade/migrations/m_2_1_4_enforce_command_file_state.py — M-01
  14. src/specify_cli/upgrade/migrations/m_2_1_3_restore_prompt_commands.py — M-02
  15. src/specify_cli/runtime/agent_commands.py — N-01 (cycle 2)
  16. src/specify_cli/migration/rewrite_shims.py — N-02 (cycle 2)
  17. src/specify_cli/runtime/bootstrap.py — N-03 (cycle 2)
  18. 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_match has 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 default spec-kitty init path.
  • 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, then shutil.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 scanning src/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) names src/doctrine/missions/documentation/ as its source, but the actual code reads src/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-move doctrine/missions/{mission}/{templates,command-templates}/ shape; the code itself is correctly parameterized (stay), only the comment is stale.