Mission Specification: Mission Repository Encapsulation

Feature Branch: feature/agent-profile-implementation Created: 2026-03-27 Status: Specifying Mission: software-dev Target: Spec Kitty 2.x (2.x line only)

Problem Statement

Mission asset access in Spec Kitty is fragmented across 14 production files using at least 4 different patterns:

1. MissionRepository instance methods -- only 2 files actually call these (dossier/manifest.py, next/runtime_bridge.py), and 5 of 7 query methods have zero callers 2. 5-tier resolver -- reimplements get_command_template and get_template logic via direct path construction in runtime/resolver.py 3. Direct path construction -- 10+ files build mission asset paths manually (missions_root / mission / "actions" / ..., missions_root / mission / "command-templates" / ...) 4. Multiple default_missions_root() implementations -- MissionRepository.default_missions_root(), kernel.paths.get_package_asset_root(), and importlib.resources.files("doctrine") / "missions" all do the same thing

This fragmentation leads to:

  • Path knowledge leaking into every consumer
  • No single authority for how mission assets are accessed
  • Consumers building filesystem paths directly, making it impossible to swap the storage strategy
  • The 5-tier override chain only accessible through a low-level resolver module, not through the domain API

Goal

Rename MissionRepository to MissionTemplateRepository, expand it to be the single authoritative API for all mission asset access, and encapsulate filesystem paths behind a content-returning public interface. Consumers call public methods that return content (str) or structured data. Private _path() variants exist only for the few internal callers that genuinely need filesystem access (init template copying, packaging, bootstrap).

User Scenarios & Testing

User Story 1 - Read a Command Template by Content (Priority: P1)

A prompt builder or CLI command needs the text content of a mission command template (e.g., the "implement" prompt for the software-dev mission). It calls MissionTemplateRepository.get_command_template("software-dev", "implement") and receives the template content as a string, without knowing or caring where the file lives on disk.

Why this priority: This is the most common access pattern. The prompt builder, runtime bridge, and show-origin all need template content.

Acceptance Scenarios:

1. Given the software-dev mission with a bundled implement template, When calling get_command_template("software-dev", "implement"), Then it returns a non-empty str containing the template content. 2. Given a nonexistent mission, When calling get_command_template("nonexistent", "implement"), Then it returns None. 3. Given a valid mission but nonexistent template name, When calling get_command_template("software-dev", "nonexistent"), Then it returns None. 4. Given multiple callers reading the same template, When each calls get_command_template(), Then all receive identical content.


User Story 2 - Read a Content Template by Content (Priority: P1)

A planning or task-generation tool needs the text content of a mission content template (e.g., spec-template.md, plan-template.md). It calls get_content_template("software-dev", "spec-template.md") and receives the string content.

Acceptance Scenarios:

1. Given a valid mission and template name, When calling get_content_template("software-dev", "spec-template.md"), Then it returns a non-empty str. 2. Given a nonexistent template, When calling get_content_template("software-dev", "nonexistent.md"), Then it returns None.


User Story 3 - Resolve a Template Through the Override Chain (Priority: P1)

A runtime CLI command needs a template that respects user/project overrides. It calls resolve_command_template("software-dev", "implement", project_dir=some_dir) and receives the content from the highest-priority tier that has the template (OVERRIDE > LEGACY > GLOBAL_MISSION > GLOBAL > PACKAGE_DEFAULT).

Acceptance Scenarios:

1. Given a project with no overrides, When calling resolve_command_template(...), Then it returns the doctrine default content (tier 5). 2. Given a project with an override file at .kittify/override/software-dev/command-templates/implement.md, When calling resolve_command_template(...), Then it returns the override file's content (tier 1). 3. Given no project_dir argument, When calling resolve_command_template("software-dev", "implement"), Then it falls back to doctrine defaults. 4. Given a template not found at any tier, When calling resolve_command_template(...), Then it raises FileNotFoundError.


User Story 4 - Enumerate Available Templates (Priority: P2)

A CLI tool or validator needs to list all command or content templates for a given mission to display options or validate inputs.

Acceptance Scenarios:

1. Given the software-dev mission, When calling list_command_templates("software-dev"), Then it returns a sorted list including at least ["implement", "plan", "specify", "tasks"]. 2. Given the software-dev mission, When calling list_content_templates("software-dev"), Then it returns a sorted list including at least ["plan-template.md", "spec-template.md", "task-prompt-template.md"]. 3. Given a nonexistent mission, When calling list_command_templates("nonexistent"), Then it returns an empty list.


User Story 5 - Read Action Assets (Priority: P2)

The constitution context builder needs action index data and guidelines text for a mission action. It calls get_action_index("software-dev", "implement") and receives parsed YAML data, or get_action_guidelines("software-dev", "implement") and receives markdown content as a string.

Acceptance Scenarios:

1. Given a valid mission and action with an index.yaml, When calling get_action_index("software-dev", "implement"), Then it returns a dict with the parsed YAML content. 2. Given a valid mission and action with a guidelines.md, When calling get_action_guidelines("software-dev", "implement"), Then it returns a non-empty str. 3. Given a nonexistent action, When calling either method, Then it returns None.


User Story 6 - Read Mission Configuration (Priority: P2)

A mission loader needs the configuration for a specific mission. It calls get_mission_config("software-dev") and receives parsed YAML data.

Acceptance Scenarios:

1. Given the software-dev mission, When calling get_mission_config("software-dev"), Then it returns a dict with the parsed mission.yaml content. 2. Given a nonexistent mission, When calling get_mission_config("nonexistent"), Then it returns None.


User Story 7 - Read Expected Artifacts Manifest (Priority: P2)

The dossier ManifestRegistry needs the expected-artifacts manifest for a mission type. It calls get_expected_artifacts("software-dev") and receives parsed YAML data.

Acceptance Scenarios:

1. Given the software-dev mission, When calling get_expected_artifacts("software-dev"), Then it returns a dict (or list) with the parsed manifest content. 2. Given a mission without an expected-artifacts file, When calling get_expected_artifacts("plan"), Then it returns None.


User Story 8 - Backward Compatibility During Transition (Priority: P1)

Existing code that imports MissionRepository must continue to work without immediate changes. A compatibility alias ensures the old import path resolves to the renamed class.

Acceptance Scenarios:

1. Given existing code with from doctrine.missions import MissionRepository, When importing after the rename, Then the import succeeds (alias to MissionTemplateRepository). 2. Given existing code calling MissionRepository(root).get_command_template(m, c), When the rename is applied, Then the call still returns the same Path as before (old path-returning methods remain accessible but private/deprecated). 3. Given shipped migrations referencing MissionRepository, When the rename is applied, Then those migrations are untouched and the alias keeps them functional.


User Story 9 - Eliminate Direct Path Construction in Consumers (Priority: P1)

After the refactor, no production code outside MissionTemplateRepository itself constructs mission asset paths directly. The 10+ files currently building paths like missions_root / mission / "command-templates" / name are rerouted to use the repository API.

Acceptance Scenarios:

1. Given the refactor is complete, When searching production code for direct mission path construction, Then no matches are found outside the repository class, shipped migrations, and test_package_bundling.py. 2. Given constitution/context.py which currently builds missions_root / mission / "actions" / action / "guidelines.md", When the refactor is applied, Then it calls MissionTemplateRepository.get_action_guidelines() instead. 3. Given runtime/resolver.py which currently builds pkg_missions / mission / subdir / name, When the refactor is applied, Then it uses MissionTemplateRepository._command_template_path() or equivalent private method.

Functional Requirements

IDRequirementPriorityStatus
FR-001Rename MissionRepository to MissionTemplateRepository in src/doctrine/missions/repository.pyP1Proposed
FR-002Public get_command_template(mission, name) returns TemplateResult (content + origin + tier) or NoneP1Proposed
FR-003Public get_content_template(mission, name) returns TemplateResult or NoneP1Proposed
FR-004resolve_command_template(mission, name, project_dir?) on ConstitutionTemplateResolver (in src/constitution/) returns TemplateResult through 5-tier chain, raises FileNotFoundError if not found. Composes MissionTemplateRepository (tier 5) + resolver.P1Proposed
FR-005resolve_content_template(mission, name, project_dir?) on ConstitutionTemplateResolver returns TemplateResult through 5-tier chain, raises FileNotFoundError if not foundP1Proposed
FR-006Public list_command_templates(mission) returns sorted list[str] of template names (without .md extension)P2Proposed
FR-007Public list_content_templates(mission) returns sorted list[str] of template filenamesP2Proposed
FR-008Public list_missions() returns sorted list[str] of mission namesP2Proposed
FR-009Public get_action_index(mission, action) returns ConfigResult (content + origin + parsed dict) or NoneP2Proposed
FR-010Public get_action_guidelines(mission, action) returns TemplateResult or NoneP2Proposed
FR-011Public get_mission_config(mission) returns ConfigResult or NoneP2Proposed
FR-012Public get_expected_artifacts(mission) returns ConfigResult or NoneP2Proposed
FR-013Private _command_template_path(mission, name) returns Path or None for internal callersP1Proposed
FR-014Private _content_template_path(mission, name) returns Path or None for internal callersP1Proposed
FR-015Private _missions_root() returns Path for internal callers needing directory accessP1Proposed
FR-016Backward-compatible MissionRepository alias exported from doctrine.missionsP1Proposed
FR-017Reroute doctrine-asset consumers (11 files: context.py, both catalog.py copies, show_origin.py, resolver.py, bootstrap.py, migrate.py, manager.py, both compiler.py copies, feature.py stale path) to use MissionTemplateRepository public APIP1Proposed
FR-018ConstitutionTemplateResolver in src/constitution/ composes MissionTemplateRepository (doctrine) with the 5-tier resolver. MissionTemplateRepository in doctrine depends only on kernel (the true zero-dependency root) — no imports from specify_cli or constitution. constitution depends only on doctrine and kernel — no imports from specify_cli.P1Proposed
FR-019Reroute project-local mission path construction in manifest.py, mission.py, config.py through a constitution-module indirection (ProjectMissionPaths) to prepare for future constitution-aware resolutionP2Proposed
FR-020Add IN_REVIEW lane to the status model (Lane enum) with transitions: for_review → in_review, in_review → approved, in_review → done, in_review → planned, in_review → in_progress, in_review → blocked, in_review → canceledP1Proposed
FR-021Record reviewer role (e.g. architect, implementer) in WP frontmatter when review startsP1Proposed
FR-022Populate approved_by frontmatter field with the approver's agent-profile when a WP moves to approvedP1Proposed
FR-023agent metadata field stores the LLM tool identifier (e.g. claude-opus-4-6), semantically distinct from agent_profile / roleP1Proposed
FR-024Workflow review command (spec-kitty agent workflow review) moves WP to in_review lane (not in_progress) and populates role, agent_profile, and agent in frontmatterP1Proposed
FR-025Dashboard renders in_review lane column, role badge on kanban cards, and approved_by in WP detail pane. WPs without these fields render gracefully.P2Proposed

Non-Functional Requirements

IDRequirementThresholdStatus
NFR-001No circular imports between doctrine and specify_cli at module load timeZero ImportError on import doctrine.missionsProposed
NFR-002Existing test suite passes after Phase 1 complete (WP04). Temporary breakage between WP01-WP03 is expected during incremental refactor.0 regressions after WP04Proposed
NFR-003Content-returning methods must not cache file reads (templates may change during runtime via overrides)Each call reads fresh from diskProposed
NFR-004YAML-returning methods must parse using ruamel.yaml with YAML(typ="safe") only (no unsafe deserialization, no stdlib yaml.load)Zero use of unsafe YAML loadingProposed

Constraints

IDConstraintStatus
C-001Shipped migrations must NOT be modified (frozen historical snapshots)Active
C-002test_package_bundling.py may retain repo-root paths (needs them for python -m build)Active
C-003The 5-tier resolver's internal logic must not change (only its callers are rerouted)Active
C-004CentralTemplateRepository (non-mission-scoped templates) is out of scopeActive
C-005No constitution intermediary added to the resolver chainActive

Key Entities

  • MissionTemplateRepository: The renamed + expanded class. Single authority for all mission asset access.
  • 5-tier resolver: Internal implementation engine for project-aware resolution. Called by resolve_* methods via lazy import.
  • Mission asset types: command-templates (.md), content-templates (.md), action indexes (index.yaml), action guidelines (guidelines.md), mission configs (mission.yaml), expected-artifacts manifests (expected-artifacts.yaml).

Assumptions

1. The MissionRepository name is not referenced by external consumers outside this repository (it is an internal doctrine API). 2. All YAML files in mission assets are safe-loadable (no custom tags or Python objects). 3. Template content is always UTF-8 encoded text.

Dependencies

  • Depends on the completed doctrine migration (committed in PR #305 as 770bc0ed).
  • The 5-tier resolver in src/specify_cli/runtime/resolver.py must remain functional.

Out of Scope

  • Adding a constitution intermediary to the resolver chain
  • Changing the 5-tier resolver's internal resolution logic
  • Modifying shipped migrations
  • Changing CentralTemplateRepository (non-mission-scoped templates)
  • Adding caching or memoization to content reads

Success Criteria

1. MissionTemplateRepository is the sole public API for mission asset access, exported from doctrine.missions 2. Public methods return content (str) or parsed data (dict), never filesystem Path objects 3. All 14 consumer files are rerouted to use the public API (no direct path construction outside the repository). Exempt: shipped migrations (C-001, frozen snapshots), test_package_bundling.py (C-002, needs repo-root paths for python -m build), and kernel/paths.py (low-level primitive used by the repository itself). 4. MissionRepository alias exists for backward compatibility 5. Full test suite passes with zero regressions 6. New test module covers all public API methods with at least doctrine-level and project-aware scenarios