Context
The retrospective learning loop (FR-011 through FR-016) requires a lifecycle gate
that decides, for a given mission, whether the transition to done is permitted
given the mission's current retrospective state and resolved governance mode
(autonomous vs. human_in_command).
The gate must be consulted from two independent surfaces:
specify_cli.next— the canonical control loop. Every mission completion path runs through this surface. When the last domain step succeeds,nextmust check the gate before signaling mission completion.- Any future status-transition surface that refuses mission-level completion
(e.g., a direct API caller that asks to move the mission to
done). These surfaces need to apply the same mission-level mode policy without knowing the internals of thenextruntime.
Three placement options were evaluated:
- (A) Place the gate inside
specify_cli.status.transitions, alongside the existing per-WP transition matrix. - (B) Bury the gate inside
specify_cli.next(the control-loop internals). - (C) Place the gate in a new dedicated module
specify_cli.retrospective.gateand have both callers invoke it through a published typed API.
Decision
The lifecycle gate lives in src/specify_cli/retrospective/gate.py as a
single source of truth (option C). It exposes one public function:
def is_completion_allowed(
mission_id: MissionId,
*,
feature_dir: Path,
repo_root: Path,
mode_override: Mode | None = None,
) -> GateDecision: ...
Both callers stay thin:
specify_cli.next._internal_runtime.retrospective_hookcallsgate.is_completion_allowed(...)immediately before signaling mission completion; it does not re-implement any gate logic.- Any status-transition surface that intends to complete a mission calls the same
function;
specify_cli.status.transitionsis not modified.
The gate itself resolves mode through specify_cli.retrospective.mode.detect()
(charter override > explicit flag > environment > parent process; FR-016, C-013)
and consults the mission event log to find the latest retrospective event.
The decision is deterministic: the same event log and mode signals always produce
the same GateDecision (NFR-008).
Consequences
- No logic duplication: both
nextand any status-transition surface call one function; changes to gate logic land in one file and are immediately visible to all callers. - Isolated testability:
retrospective.gatehas no dependency onnextinternals or onstatus.transitions. It can be unit-tested by replaying event log fixtures and charter fixtures without spinning up a full runtime. - Explicit cross-package dependency:
specify_cli.nextand any status-transition surface depend on theretrospectivepackage. This is an intentional layering: the retrospective package owns mission-level governance policy;nextandstatusare callers of that policy, not its authors. - WP-level transitions are unchanged: per-WP transition guards in
specify_cli.status.transitionsremain governed by the existing per-WP transition matrix. Mission-level mode policy does not bleed into that module. - Performance: when
retrospective.completedis already present, the gate reads at mostmeta.json, the mission event log filtered to retrospective events, and (optionally) the retrospective YAML for hash verification — all constant-bounded reads. NFR-007 (< 500 ms on completion) is achievable.
Alternatives considered
- Option A — gate in
specify_cli.status.transitions: rejected. Thestatus.transitionsmodule encodes per-WP lifecycle guards; inserting mission-mode policy there conflates two orthogonal concerns (WP-level state machine vs. mission-level governance). The module would need to load charter overrides and parse retrospective events, which are not its responsibilities. Future readers would be surprised to find retrospective mode-detection logic inside the WP transition matrix. - Option B — gate buried in
specify_cli.next: rejected.nextis the canonical control loop, but it is not the only path from which a missiondonetransition can be initiated. A status-transition surface that needs to refuse completion would have to reach intonextinternals — violating the architectural boundary that_internal_runtimeis next-private. The gate would then be duplicated, or the status surface would take on a hard dependency onnext's internal module, which is worse.
References
- AD-001 in mission plan:
kitty-specs/mission-retrospective-learning-loop-01KQ6YEG/plan.md(§Architecture Decisions, AD-001) - Gate API contract:
kitty-specs/mission-retrospective-learning-loop-01KQ6YEG/contracts/gate_api.md - Mission spec FR-011–FR-016, NFR-007, NFR-008, C-013:
kitty-specs/mission-retrospective-learning-loop-01KQ6YEG/spec.md