Phase 0 Research — Mission Completion Terminal State

All "unknowns" here are mechanism decisions the post-spec squad deferred to plan; the product decisions (D1–D3) are settled in spec.md. Evidence anchors: research/post-spec-squad-findings.md.

R1 — Provenance mechanism (operator-authored vs synthetic)

a reason via --note. The mechanism is a first-class reason_source: "operator" | "synthetic" field on StatusEvent (status/models.py), set at the emit site (tasks_move_task.py, not the pure-planning build_transition_plan in tasks_transition_core.py) and projected into the reduced snapshot. Not overloading policy_metadata, and not a reduce-time template match (post-tasks reviewer: a template match is fakeable and reintroduces F1 one layer up).

(tasks_transition_core.py:307-317) is a pure planning function that never emits; owned_files corrected to include status/models.py, status/emit.py, and tasks_move_task.py. The provenance must be asserted on the raw event (read_events), not only via the reducer.

that "non-empty reason" is auto-forged — --force with no note yields reason="Force move to canceled". Only an explicit operator signal is non-forgeable and makes FR-003 reachable through the canonical command (SC-002).

forced this" audit that force=true + actor already give; and empty strings invite whitespace fake-greens (validate.py:126-128 is truthiness-only).

change; deferred as unnecessary for this slice.

  • Decision: A canceled work package is accept-eligible only when the operator supplied
  • Post-tasks correction (pedro BLOCKER): the original WP01 anchor
  • Rationale: The squad proved (F1, reviewer+debugger, tasks_transition_core.py:307-317)
  • Alternatives considered:
  • Force-cancel-without-note records an empty sentinel — reachable, but loses the "who
  • Dedicated cancellation-reason event type — cleaner long-term but a larger surface

R2 — Provenance read seam

per-WP reduced snapshot (reducer.py:166-177) when lane==canceled.

(acceptance/__init__.py:997,1021), which today drops reason. Projecting keeps accept a single read and stays within C-002 (still event-sourced) and C-001 (a projection, not a state-machine change).

a second event read into the acceptance layer and duplicates event-parsing logic.

  • Decision: Project a cancellation_reason (and its provenance marker) into the
  • Rationale: F3 (architect+debugger) — accept reads the snapshot
  • Alternatives: log lookup by last_event_id inside acceptance/ — rejected; scatters

R3 — Acceptable-ending authority

src/specify_cli/status_lanes.py; consumed by accept, merge, and the dependency gate. Admits {approved, done} unconditionally; admits canceled only with provenance; references TERMINAL_LANES only to classify canceled. Deletes the three _ACCEPTED_READY_LANES copies.

acceptability ({approved,done}), and provenance are three separable decisions. Consuming is_terminal wholesale would reject approved and accept canceled blindly. One predicate satisfies directives 043/044.

  • Decision: is_acceptable_ending(lane, *, has_provenance) -> bool in
  • Rationale: F2 (architect, supersedes planner) — terminality ({done,canceled}),
  • Alternatives: reuse is_terminal() directly — rejected (category error, proven).

R4 — Merge exclusion granularity

all-canceled lane guard in _phase_merge_lanes. Retain the cancellation audit record.

merge asserts per-WP done/review (done_bookkeeping.py). Lane-level "skip" is the wrong unit and would drop surviving approved work.

(would fail to integrate surviving approved WPs in the same lane).

  • Decision: Filter canceled WPs from all_wp_ids at merge/executor.py:1660; add an
  • Rationale: F4 (architect+debugger) — a lane holds many WPs (lanes/models.py:84);
  • Alternatives: skip the whole lane whenever it contains a canceled WP — rejected

R5 — Dependency-on-canceled closure (FR-009)

is treated as resolved/removed via the same acceptable-ending authority.

depended-upon WP (reachable from in_progress without --force) strands the dependent → the mission's own trap, and a third disagreeing consumer.

move-task — rejected as more invasive and it forbids a legitimate replan; resolving the gate is the smaller, more honest change.

lane-only; the claim gate consumes it in workflow_executor.py:634 (which already reduces the snapshot). WP04 owns workflow_executor.py and adds an optional provenance param (default preserves the 5 read-only callers). It also replaces _SATISFYING_DEPENDENCY_LANES with the predicate (identical truth table — directive 044), rather than special-casing. The FR-009 face splits: claim gate = WP04 (dependency_graph.py + workflow_executor.py); merge gate = WP03 (policy/merge_gates.py).

BOTH workflow_executor.py:653 (agent action implement) AND implement.py:1317 (_ensure_wp_claim_preconditions — the primary spec-kitty implement WP## command, "the only supported way to prepare a workspace" per CLAUDE.md). The first WP04 review REJECTED because implement.py was left lane-only, leaving the #2945 strand trap open on the main claim path; it is now fixed and covered by an integration test (TestImplementClaimGateThreadsProvenance) that seeds a real event log and asserts the gate admits a canceled(operator) dependency and blocks a canceled(synthetic) one.

mission (they keep the backward-compatible lane-only default). All are fail-closed: a canceled-with-provenance dependency is treated as still-blocking (never wrongly admitted), so the worst case is an over-conservative refusal, not a silent skip. Correctly classified (post-merge cross-WP squad, HIGH finding):

(the external-API equivalent of implement.py's _ensure_wp_claim_preconditions), NOT a read-only caller. It reproduces the #2945 strand on the orchestrator-api claim path and the follow-up MUST thread provenance=…work_packages here, mirroring implement.py/workflow_executor.py.

spec-kitty next claimable-preview; collapses the snapshot to dict[str, Lane], so threading provenance needs a signature change. Governed Shared Package Boundary — warrants its own change. The follow-up must cover this next-loop parity.

read-only/advisory display surfaces; cosmetic divergence only. File a follow-up issue covering the two claim/preview surfaces so parity is not silently assumed.

  • Decision: In core/dependency_graph.py:59, a canceled-with-provenance dependency
  • Rationale: F5 (planner) — canceled is currently non-satisfying; canceling a
  • Alternatives: extend #3713's active-to-canceled rejection from finalize to runtime
  • Post-tasks correction (pedro HIGH / paula BLOCKER): dependency_readiness_for_wp is
  • Both CLI claim gates threaded (post-review REJECT fix): WP04 threads provenance through
  • Deferred callers of dependency_readiness_for_wp (tracked follow-up) — NOT updated in this
  • orchestrator_api/commands.py:1139 (start-implementation) — this is a mutating CLAIM gate
  • runtime/next/discovery.py:132 (_build_wp_lane_map/_preview_from_candidates) — the canonical
  • orchestrator_api/commands.py:817 (list-ready) and tasks_status_view.py:223 — genuinely

R8 — Other parallel {approved,done} authorities (unification disposition)

Post-spec F2 and post-tasks paula named additional lane-sets. Dispositions (directive 003/044):

collapsed onto is_acceptable_ending (WP02).

which files a completed WP touched for audit, not acceptance gating; different concern, out of scope.

state reconstruction; accepting canceled unconditionally is correct there (it is rebuilding history, not gating a live mission). Out of scope.

  • _ACCEPTED_READY_LANES ×3 (acceptance/__init__.py:145, gates_core.py:52, summary_core.py:173,202)
  • _SATISFYING_DEPENDENCY_LANES (dependency_graph.py:34) — collapsed (WP04).
  • policy/merge_gates.py (:155-164, :253) — routed through the predicate (WP03).
  • audit/classifiers/wp_files.py:16 {done,approved}knowingly separate: this classifies
  • migration/rebuild_state.py:140 {done,canceled,approved}knowingly separate: migration-only

R6 — Authoring-time detector (FR-007)

validated against a fixed labeled corpus (positive + adversarial-negative fixtures), advisory only. Precision/recall target: 100% recall on the positive fixtures, 0 false positives on the negative fixtures; the corpus is the oracle, not an open-world claim.

(ownership/models.py); an open-world "0% false positives" is unfalsifiable, so the claim is scoped to a fixed corpus. A structured completion_kind field is #3550 territory (C-003).

slice (scope; overlaps #3550's redesign).

  • Decision: Enumerable trigger-phrase detector over acceptance-criteria/subtask prose,
  • Rationale: F6 (debugger+reviewer+planner) — no structured signal exists
  • Alternatives: add completion_kind: post_integration WP field now — rejected for this

R7 — Regression baseline (NFR-001/SC-004)

mission start). "0 regressions" is measured against this commit for the suites named in NFR-001. Honor the repo's baseline-red gotcha (known-P0 reds are not this mission's).

  • Decision: Pin baseline commit a59460ec15 (branch base = upstream/main at
  • Rationale: F7 (reviewer) — unpinned baselines are subset-fakeable.

Adversarial evidence dispositions (per contracts/adversarial-evidence-contract.md)

FindingLens(es)Disposition
F1 provenance auto-synthesis (BLOCKER)reviewer, debuggerchanged — spec redefined provenance as operator-authored (D2); R1
F2 FR-005 mis-framedarchitect (vs planner)changed — acceptable-ending predicate; R3
F3 reducer drops reasonarchitect, debuggerchanged — reducer projection; R2 / C-002
F4 merge granularityarchitect, debuggerchanged — WP-granular exclusion; R4 / FR-004
F5 dependency strandplannerchanged — pulled into scope as FR-009/SC-005; R5
F6 FR-007 unmeasurabledebugger, reviewer, plannerchanged — fixed-corpus SC-003; R6
F7 unpinned baselinereviewerchanged — baseline a59460ec15 pinned; R7
C-002 get_lane_from_frontmatter misnomerarchitectdeferred_with_rationale — reader is correct today; rename out of scope unless free
#2745 shared merge surfaceplanneraccepted — noted as C-005 compose constraint; no code now

No contested finding was silently dropped.