MissionStatus Write-Path Completion & Profile-Load Surface Remediation

Mission ID: 01KTB6AN8XJWN4ZVMHK4YBAYBY Slug: status-writepath-profile-surface-remediation-01KTB6AN Mission type: software-dev Target branch: feature/status-writepath-profile-surface-remediation

Purpose

Close two distinct but independently-shippable remediation gaps that a prior mission and a doctrine-skill drift left behind:

1. #1667 residual — MissionStatus write path. (Scope materially reduced after dialectic review — see dialectic-review.md.) The aggregate's read path shipped in 01KT6HVH (WP04); its write-method test coverage and _read_meta fail-closed guard then shipped in PR #1682 (cdc258002) — which the original spec missed because it relied on the pre-#1682 review report. The genuine residual is: (a) a mission_slug validation guard at load() (FR-007), and (b) wiring the live status-write surface through the aggregateagent status emit currently calls emit_status_transition_transactional directly, leaving #1667's single-domain status-write ownership intent unmet. Operator decision (2026-06-05): fork Y — route agent status emit through MissionStatus.transition()/.save() (FR-004) so the aggregate is the sole status-write entry point, accepting overlap with #1673. #1667 stays open and is delivered here.

2. #1636 — profile-load command surfaces. The ad-hoc-profile-load doctrine skill documents four CLI commands (agent profile show / hierarchy / init / create) that do not exist; only agent profile list does, and it is activation-blind (returns every profile on disk regardless of charter activation). This mission delivers an activation-aware profile list and a new profile show, routes them through the existing charter activation chokepoint, and reconciles the skill doc so the documented workflow stops failing.

The two workstreams share no code and may land as independent lanes; they are bundled here because both are small, contained surface-remediations discovered in the same investigation.

Source Issues

IssueTitleRelationship
#1667Introduce MissionStatus aggregate (Mission Management domain)Residual hardening — aggregate exists and is tested (coverage shipped in PR #1682); this closes the remaining unwired write path (RISK-001) by routing the live surface through it
#1636Missing agent profile show <id> CLI command — documented by skill, never implementedPrimary — implement + activation-aware listing/show; reconcile skill drift
#1672Strangler step 1: e2e parity ratchet (CWD-invariance gate)Consumed gate, narrow slice only — the ratchet exists but covers only the status read; this mission extends it over the status write path it touches. Full #1672 stays owned by its assignee
#1619Execution-state CWD-derivation root cause (Strangler Fig)Parent of #1667; out of scope here except as the governing domain model

Governing ADRs (already on main): architecture/3.x/adr/2026-06-03-1-execution-state-domain-model.md (Status owned by Mission Management), …-2-executioncontext-owner-and-committarget.md. No new ADR is required for either workstream (the domain model is already ratified); the profile-activation gating decision (lineage Option A) is recorded in data-model.md and plan.md (D-2).

User Scenarios & Testing

Scenario A — Status write path is exercised and verified

A workflow surface applies a lane transition through MissionStatus.transition(request) and persists it through MissionStatus.save(operation=...). The transition is validated by the aggregate (domain invariant), BookkeepingTransaction is called internally, and a CommitReceipt is returned. Both paths have unit coverage for the happy path and the rejection path.

Scenario B — Status write path rejects an illegal transition

MissionStatus.transition() is called with an illegal (from_lane, to_lane) pair and no force. It raises before any event is appended or any commit is made (fail-closed, no partial state).

Scenario C — profile list reflects charter activation

In a project whose charter has explicitly activated a subset of agent profiles, spec-kitty agent profile list shows only the activated profiles. --all shows every available profile across built-in/org/project layers, annotated by source and activated | available state. In a project with no explicit activation (the common case), the list is unchanged from today (all built-ins).

Scenario D — profile show is activation-gated

spec-kitty agent profile show <id> prints the full resolved profile definition for an activated profile. For a non-activated id it fails closed with a structured profile_not_activated error listing the activated candidates. --all bypasses the gate for inspection.

Scenario E — Abstract parent profile (lineage gate, Option A)

A profile child declares specializes_from a parent profile that is not itself activated (an "abstract base" holding shared elements). profile show child resolves successfully, composing inherited fields from the non-activated parent, and emits a user-facing warning that lineage traversed a non-activated parent. profile show <parent> (the abstract base, non-activated) fails the activation gate unless --all is passed.

Scenario F — Skill workflow no longer references phantom commands

After reconciliation, the ad-hoc-profile-load skill's Step 1 invokes a command that exists; no step references agent profile show/hierarchy/init/create as a working command unless that command is actually implemented.

Functional Requirements

Workstream A — MissionStatus write-path completion (#1667 residual)

> REVISED after dialectic review (dialectic-review.md); D-1 resolved → fork Y. Test coverage + _read_meta fail-closed were already delivered by PR #1682 (cdc258002) (struck rows below). Active Workstream A = FR-004 (wire the live write surface through the aggregate), FR-007 (slug guard), FR-008 (ratchet over the write path).

IDRequirementStatus
_(already delivered by PR #1682 — not re-scoped)_transition()/save() unit coverage, invariant placement, and _read_meta fail-closed all landed in cdc258002 (tests/unit/status/test_mission_status_aggregate.py:410-537, aggregate.py:244-278/355).Done upstream
FR-007MissionStatus.load() validates mission_slug against ^[A-Za-z0-9_-]+$ (.isascii()) at entry, typed error on mismatch, regression coverage incl. an accented-Latin case (DIRECTIVE_010/011). Genuinely new — no validation in load() today.Proposed
FR-004DECIDED → fork Y (operator, 2026-06-05). Route the live status-write surface agent status emit (cli/commands/agent/status.py:275) through MissionStatus.transition() + MissionStatus.save() instead of calling emit_status_transition_transactional directly, so #1667's single-domain status-write ownership is genuinely realized (the aggregate becomes the sole entry point for status writes). The existing TransitionRequest is passed through unchanged; behavior is preserved (the aggregate already delegates to the same transactional path). Overlap with #1673 residue routing is accepted.Proposed
FR-008Extend the #1672 parity ratchet (tests/architectural/test_execution_context_parity.py) to assert CWD-invariance of the status write transition driven via agent status emit (now routed through MissionStatus.transition()), across main-checkout and lane-worktree CWDs. Ratchet stays green (C-008). Narrow slice of #1672; full sequence stays owned by its assignee.Proposed

Workstream B — Profile-load command surfaces (#1636)

IDRequirementStatus
FR-010A shared factory build_activation_aware_doctrine_service(repo_root) constructs the inner doctrine.service.DoctrineService and wraps it in charter.resolver.DoctrineService(inner, pack_context=PackContext.from_config(repo_root)), so all profile surfaces resolve through one chokepointProposed
FR-011spec-kitty agent profile list defaults to activated-only. Corrected approach (per dialectic review): today the command builds its descriptor rows from ProfileRegistry.list_all() (profiles_cmd.py:30), not from doctrine.service. To preserve the existing descriptor schema and guarantee NFR-001 byte-identity, filter the existing ProfileRegistry row set by PackContext.from_config(repo_root).activated_agent_profiles (three-state: absent → all; empty → none; set → those) — do not swap the data source to the wrapper's .agent_profiles dict. The shared factory (FR-010) is used by show/--include, where no legacy schema is at stake.Proposed
FR-012profile list gains --all and --show-available flags (mirroring charter list) that drop to the unfiltered repository and annotate each row with source layer and `activatedavailable`
FR-013A new spec-kitty agent profile show <id> (alias get) prints a single profile's full resolved definition — initialization_declaration, specialization (primary/secondary/avoidance), collaboration (handoff_to/from, works_with), canonical_verbs, mode_defaults, directive/tactic references, source layer — with --jsonProposed
FR-014profile show is activation-gated on the requested (leaf) id: a non-activated id fails closed with a structured profile_not_activated error listing activated candidates; --all bypasses the gate for inspectionProposed
FR-015Lineage gate = Option A (gate leaf only). profile show resolution MAY traverse specializes_from parents that are not themselves activated, to support abstract base profiles (non-activated parents storing shared elements). When lineage traverses a non-activated parent, profile show emits a clearly-worded user warning naming the non-activated parent(s)Proposed
FR-016charter context --include agent-profile:<id> resolves through the activation-aware wrapper so the fetch path inherits the activation gate. Corrected (per dialectic review): _build_doctrine_service is at charter/context.py:1235, builds a plain DoctrineService(kwargs) with no PackContext, and has 6 callers (lines 333/352/863/1373/2620 + _maybe_build_doctrine_service@2887). To avoid changing the return type for all 6 sites, add a scoped wrapped variant (e.g. _build_activation_aware_doctrine_service) used only by the agent-profile include branch, constructing PackContext.from_config(repo_root) locally (the module already imports PackContext and constructs one for a different function near line 244). Do not** blanket-wrap the shared helper.Proposed
FR-019Glossary (DIRECTIVE_032): the new load-bearing user-facing terms — abstract base profile (and activated vs available profile, activation chokepoint) — are added to the canonical glossary before the profile show warning string ships. Vocabulary ratification precedes code; not deferred as "advisory".Proposed
FR-017The ad-hoc-profile-load skill source (src/doctrine/skills/ad-hoc-profile-load/SKILL.md) is reconciled: adopt/invoke steps point to spec-kitty ask / advise; profile-detail steps point to the new profile show; hierarchy / init / create references are either implemented or removed — no step references a non-existent commandProposed
FR-018A doc/CLI-parity guard (test) asserts every spec-kitty agent profile <subcommand> referenced in shipped skill docs corresponds to a registered Typer command, preventing future skill driftProposed

Non-Functional Requirements

IDRequirementThresholdStatus
NFR-001Backward compatibility of profile list: projects with no explicit activated_agent_profiles see identical output to pre-mission behaviorZero diff on unconfigured projectsProposed
NFR-002BookkeepingTransaction isolation: the write-path work calls it internally; no change to coordination/transaction.py internalsZero changes to coordination/transaction.pyProposed
NFR-003No activation regression for runtime: runtime/next profile resolution behavior is unchanged (it already uses the wrapper)Existing runtime tests greenProposed
NFR-004profile show --json output is machine-stable (sorted keys, documented schema) for scriptingSchema documented in contracts; snapshot-testedProposed

Constraints

IDConstraintStatus
C-001coordination/transaction.py internals must not be modified (NFR-002)Accepted
C-002The MissionStatus read path and its tests (shipped in 01KT6HVH) must remain green; this mission is additive to the write pathAccepted
C-003The activation wrapper charter.resolver.DoctrineService must not be duplicated; the shared factory wraps the existing classAccepted
C-004mission_number must not be used as identity or selector anywhere in new/modified code (ULID/slug only)Accepted
C-005Layer rule preserved: the activation-aware wrapper lives in charter. so it may import PackContext; profile CLI in specify_cli. constructs it with a real PackContext (DIRECTIVE_031 bounded-context boundary)Accepted
C-006Template/skill edits target the source (src/doctrine/skills/...), never the generated agent copies (per CLAUDE.md)Accepted
C-007Workstreams A and B are independently shippable; neither may introduce a hard dependency on the otherAccepted
C-008The e2e parity ratchet (tests/architectural/test_execution_context_parity.py, #1672) must remain green throughout; FR-008 extends it but must not weaken its existing assertionsAccepted

Key Entities

EntityDescription
MissionStatusExisting aggregate (src/specify_cli/status/aggregate.py) — read path shipped; write path (transition/save) completed + tested here
ActiveWPStatusRead projection from MissionStatus.claim() — unchanged
BookkeepingTransactionInfra coordinator (coordination/transaction.py) — called only internally; unchanged
CommitReceiptReturn type of save() (coordination/types.py) — unchanged
charter.resolver.DoctrineServiceActivation-aware wrapper (src/charter/resolver.py:56-129) — the activation chokepoint; reused, not duplicated
PackContextsrc/charter/pack_context.py — three-state activated_agent_profiles resolver
build_activation_aware_doctrine_serviceNew shared factory — single construction seam for all profile surfaces
profile showNew CLI command (profiles_cmd.py) — activation-gated single-profile inspector
Abstract base profileA profile referenced via specializes_from that is not itself activated; resolvable as lineage but gated for direct show

Success Criteria

#CriterionMeasurable threshold
1MissionStatus.transition() and .save() have unit coverage (happy + rejection)New tests in tests/.../test_mission_status_aggregate.py; both methods exercised; coverage no longer "MISSING"
2RISK-001 closedA named, tested wired path for the write methods exists (FR-004); review note resolvable
3profile list is activation-aware with non-breaking defaultUnconfigured project: identical output; configured project: filtered; --all shows annotated full catalog
4profile show <id> exists and is activation-gatedCommand registered; activated id prints full def; non-activated id → structured error; --all inspects
5Abstract-parent lineage works with warningprofile show child with non-activated parent resolves + warns; profile show parent gated
6Skill drift closedgrep "agent profile show/hierarchy/init/create" in src/doctrine/skills/ad-hoc-profile-load/SKILL.md references only implemented commands; parity guard test passes
7No regressionsFull existing status + charter + runtime test suites green; zero change to coordination/transaction.py

Assumptions

  • The MissionStatus aggregate, ActiveWPStatus, CoordAuthorityUnavailable, and the agent/status.py read-path migration are already shipped (verified on feature/... base, originating from 01KT6HVH); this mission does not rebuild them.
  • charter.resolver.DoctrineService, PackContext.from_config, and the construction pattern in charter/generate.py:46-74 are the canonical activation seam; the shared factory generalises that pattern.
  • activated_agent_profiles in .kittify/config.yaml is the authoritative activation key for agent profiles (confirmed via charter list showing agent-profile as a first-class activatable kind).
  • The four "phantom" skill commands were never implemented (git history confirms); reconciliation is doc-side plus the one genuinely-needed new command (show).

Open / Unresolved Decisions

Surfaced for plan to resolve — not hidden in plan detail (per spec-kitty specify guidance).

#DecisionStatusNotes
D-1#1667 disposition.RESOLVED → fork Y (operator, 2026-06-05)Wire agent status emit through MissionStatus.transition()/.save() (FR-004) for true single-domain status-write ownership; #1667 stays open and is delivered by this mission. #1673 overlap accepted. FR-008 (ratchet over the write path) is now active.
D-2Lineage activation gate (FR-015): leaf-only gate, abstract parents allowed.RESOLVED → Option A + warningOperator decision 2026-06-05: supports abstract base profiles (non-activated shared-element stores); inheritance must warn, never be silent.
D-3#1672 scope: narrow slice (extend ratchet over write path) vs. full e2e ratchet.RESOLVED → narrow slice (FR-008)Full #1672 remains owned by its assignee; bundling the P0 gate would muddy ownership.
D-4profile show not-found schema: exact JSON shape of profile_not_activated (field names, candidate list ordering).OPEN (low-risk)Align with existing selector-disambiguation error shape; finalize in plan/contracts.

Terminology & Governance Routing

  • Glossary (route to spk-doctrine-glossary during plan): new/load-bearing terms introduced here — activated vs available profile, abstract base profile, activation chokepoint, write-path / read-path of MissionStatus. Confirm canonical definitions before code (DIRECTIVE_032 conceptual alignment).
  • Charter (route to spk-doctrine-charter if scope shifts): Workstream B's semantics are governed by the charter activation model (activated_agent_profiles). No charter change is required; the mission consumes the existing activation contract. Flag if plan discovers a needed activation-vocabulary change.

Out of Scope

  • Broader ExecutionContext residue routing (#1673) and MissionRun → Mission back-reference (#1663).
  • status/ import-boundary enforcement test (#1664) — sibling follow-up, not bundled.
  • The full #1672 e2e parity ratchet (the complete next → implement → move-task → review → status sequence and its role as the universal CI gate) — only the write-path slice this mission touches is in scope (FR-008); #1672 itself stays owned by its assignee.
  • Implementing profile hierarchy, init, create as new commands (skill text is reconciled to not promise them; implementing them is a separate enhancement).
  • Any change to BookkeepingTransaction internals or the activation wrapper's filtering semantics.

Contracts & Design

Detailed contracts, seam maps, and the activation algorithm now live in the planning artifacts (authored post-spec): plan.md (architecture, data flow, test strategy) and data-model.md (factory signature, profile show resolution, profile_not_activated schema, invariants). Pre-spec evidence is in research.md; the scope correction is in dialectic-review.md.