Phase 1 Data Model — Mission Completion Terminal State

No new persisted store; this mission adds fields/derivations over the existing append-only status event log. No schema migration (NFR-002).

Entities

1. Cancellation Provenance (on the canceled status event)

(status/models.py, round-tripped in to_dict/from_dict), set at the emit site (tasks_move_task.py). operator iff the reason came from a non-empty --note. None for non-cancel events (backward-compatible default). Not policy_metadata; not a reduce-time template match.

non-empty operator note). The CLI's auto-synthesized "Force move to canceled" / "move-task: …" default is synthetic and never accept-eligible.

known synthetic templates is treated as operator (NFR-002).

  • Home: an event in status.events.jsonl with to_lane == "canceled".
  • Fields used/added:
  • reason: str | None — existing; the human note.
  • reason_source: "operator" | "synthetic" | Nonenew first-class field on StatusEvent
  • actor, at, force — existing; carried into reporting.
  • Invariant: a cancellation is accept-eligible iff reason_source == "operator" (a
  • Legacy rule: for events predating reason_source, a reason that does not match the

2. Reduced snapshot — cancellation slot

when lane == "canceled". All other lanes unchanged (NFR-002).

stays deterministic and its golden tests are updated in lockstep.

  • Home: per-WP dict from reduce() (status/reducer.py:166-177).
  • Field added: cancellation_reason: str | None (+ its reason_source), projected only
  • Invariant: the projection is derived purely from the event log (C-002); the reducer

3. Acceptable-Ending Decision (predicate)

  • Home: is_acceptable_ending(lane, *, has_provenance) -> bool in status_lanes.py.
  • Truth table:
lanehas_provenanceacceptable ending
approved
done
canceledtrue
canceledfalse❌ (structured blocker)
any other

merge (executor.py + policy/merge_gates.py), dependency gate (dependency_graph.py). Single source of truth.

status_lanes.py) — the one reader consumers call so reason_source == "operator" is never inlined at 3 sites (avoids a whack-a-field regression).

  • Consumers: accept (acceptance/__init__.py, replacing _ACCEPTED_READY_LANES),
  • Companion accessor: has_operator_provenance(wp_snapshot) -> bool (co-located in

4. canceled_wps report entry (accept --json)

appears in blockers, not here.

  • Shape (pinned, NFR-003): { "wp_id": str, "reason": str, "actor": str, "at": str (ISO-8601) }.
  • Rule: appears only for accept-eligible cancellations; a non-provenance cancellation

5. Post-Integration Trigger (FR-007 detector)

branch the forge will run", "consecutive runs", "merge-blocked-when-absent"), validated against a fixed labeled corpus. Advisory only — produces a warning record, never a block.

  • Home: authoring-time check over a work package's acceptance-criteria / subtask text.
  • Fields: wp_id, matched_phrase, criterion_excerpt.
  • Signal: membership in an enumerable trigger-phrase set (e.g. "after merge", "on a

State transition (unchanged matrix, C-001)

The nine-lane matrix is untouched. This mission changes only how downstream consumers interpret the existing terminal canceled lane:

canceled (terminal)  ──has operator provenance──▶  acceptable ending (accept ✅, merge skips WP, dep resolved)
canceled (terminal)  ──synthetic reason only──▶    structured blocker (accept ❌)