Feature Specification: Evict runtime-mutable WP state into the event log
Feature Branch: mission-prep/2684-wp-runtime-state-eviction
Created: 2026-07-19
Status: Draft (spec + ADR authored; no cluster-touching work pending — the adjacent PRs are pr:deferred and yield to this mission)
Input: #2684 (P0) — "Evict runtime-mutable WP state (shell_pid, history, subtask-checkbox, review-cycle, activity-log) from tasks/WP##.md into the event log", the execution vehicle for the #2093 authority ruling.
Grounding: docs/plans/investigations/2684-task-move-cluster-scoping.md (§0 = the seven resolved decisions). Design of record: docs/adr/3.x/2026-07-19-1-wp-runtime-state-event-log-eviction-via-innerstatechanged.md.
This spec encodes decisions already ratified by HiC (brief §0 / the ADR), including that this mission owns the
implement.py:1730shell_pid-writer restructuring (the #2160 co-sequence is resolved — see FR-014). No cluster-touching work is pending; the adjacent PRs arepr:deferred.
User Scenarios & Testing (mandatory)
User Story 1 - Hand a completed WP to review without hand-ticking checkboxes (Priority: P1)
An operator/agent finishes a WP's work and moves it to for_review. Completion recorded through the
canonical path must be honored by the review gate — no manual mark-status per subtask.
Why this priority: This is the P0 operator friction (mission #2736 catfooding) and the invariant the already-merged red test pins. It is the visible payoff of the eviction.
Independent Test: On a single_branch mission, complete WP01's subtasks via the canonical path
and run move-task WP01 --to for_review — it succeeds without touching tasks.md checkboxes.
(tests/regression/test_issue_2684_subtask_completion_event_sourced.py flips green.)
Acceptance Scenarios:
- Given WP01
in_progresswith its subtasks recorded complete in the reduced snapshot andtasks.mdcheckboxes not hand-edited, Whenmove-task WP01 --to for_review, Then it succeeds and the WP isfor_review. - Given WP01 with genuinely-incomplete subtasks (no completion recorded anywhere), When
move-task WP01 --to for_review, Then it is correctly refused (the gate honors the log, it does not "always allow").
User Story 2 - No false drift across a WP lifecycle (Priority: P1)
The dossier/sync content hash of tasks/WP##.md and tasks.md stays stable across
claim → subtask-done → review → history, because nothing writes runtime state into those files.
Why this priority: AC-5 is the headline proof of the mission and the long-homeless dossier churn fix.
Independent Test: Run a WP through a full lifecycle; assert the raw-byte content_hash_sha256 of
tasks/WP##.md and the WP's tasks.md section is unchanged from claimed to done.
Acceptance Scenarios:
- Given a WP taken through claim, subtask completion, review, and history, When the dossier parity hash is computed at each step, Then the WP's file hashes are identical throughout.
User Story 3 - Claim-liveness and resume resolve from the log, not frontmatter (Priority: P2)
Claim-liveness reads the reduced snapshot. A claimed WP whose frontmatter carries no shell_pid is
still detected live; a resumed in_progress WP refreshes its PID via an off-axis event and is not
falsely flagged stale.
Why this priority: AC-2; and it closes the Option-B false-stale window by construction (Option A carries the resume refresh).
Independent Test: Claim a WP (empty frontmatter), assert liveness = live from the snapshot; resume
it, assert the PID refresh is recorded as an InnerStateChanged event and liveness stays live.
Acceptance Scenarios:
- Given a claimed WP with no frontmatter
shell_pid, When liveness is evaluated, Then it resolves live from the reduced snapshot. - Given a resumed
in_progressWP, When its shell PID refreshes, Then anInnerStateChangeddelta records it with a truthfulat, no lane transition, and noforce_countincrement.
User Story 4 - Activity Log / History / review render from events (Priority: P2)
The ## Activity Log, ## History, and review sections render from the event log with no content
loss, now that they are no longer written into the WP file.
Why this priority: AC-4; guards against the M7 data-loss risk during eviction.
Independent Test: Drive a WP through notes + a review cycle; assert the rendered Activity Log/History/review content matches, sourced from events.
User Story 5 - Existing missions migrate idempotently (Priority: P3)
A migration backfills existing missions' frontmatter/checkbox runtime state into seed events, and is safe to re-run.
Why this priority: AC-6; makes the cutover safe for the live corpus.
Independent Test: Run the migration twice on a corpus; assert the reduced snapshot equals pre-migration state by count+value and that re-running seeds nothing new.
Edge Cases
- Subtask checkboxes carry no timestamp → the backfilled mark is clamped to the WP's
claimedtimestamp; "no data loss" is asserted against count+value parity, not literal temporal fidelity. - A new off-axis emit site that resolves its write target from
Path.cwd()silently reopens #2647 — targets MUST come from stored topology/target branch (guard:test_transaction_legacy_topology_routing). history[]frontmatter is dead and mis-filed inSTATIC_FIELDS— delete outright, no migration.progressis dead — retire explicitly, don't silently drop.- The external writer
orchestrator_api/commands.py:1563(Activity Log) must migrate too, or it keeps writing the retired surface from outside the host CLI.
Requirements (mandatory)
Functional Requirements
- FR-001: Introduce a single generic non-transition event,
InnerStateChanged, carrying a typed partial delta (WPInnerStateDelta— optional typed fields, not a freedict). It has nofrom_lane/to_lane, bypassesvalidate_transition, and is folded by the reducer. - FR-002: The reducer folds
InnerStateChangeddeltas onto the reduced snapshot with per-field merge rules — replace forshell_pid/shell_pid_created_atand per-subtask status, union fortracker_refs, append fornotes— after transition folds, last-writer-wins, and never incrementsforce_count. The snapshot gains typed slots:shell_pid,shell_pid_created_at,subtasks: Mapping[str, Status],notes: list,tracker_refs. - FR-003: Subtask completion is recorded as an
InnerStateChangedsubtasksdelta (single or batch). The review gate_guard_subtasks(tasks_transition_core.py:384viatasks_shared.py:412) and done-inference_infer_subtasks_complete(status/emit.py:279) resolve unchecked/done==totalfrom the reduced snapshot, nottasks.mdbytes. - FR-004: The claim
(shell_pid, shell_pid_created_at)rides the realplanned→claimedtransition via the existingpolicy_metadatasidecar (no wire-schema change); a resume refresh of an already-in_progressWP is recorded as anInnerStateChangeddelta. - FR-005: Claim-liveness (
stale_detection.py:402-403) and the model readersWorkPackage.{shell_pid,agent,assignee}(task_utils/support.py:287-296) andWPMetadatacoercion resolve from the reduced snapshot; the frontmatter fallback is retained behind a flag until backfill is verified, then removed. - FR-006:
tracker_refsis runtime, event-sourced:map-requirements(tasks_map_requirements.py:428) andmove-task(tasks_move_task.py:1721) emit anInnerStateChangedtracker_refsunion delta; FR-011 runtime append is preserved;tracker_refsis removed as a WP-file write. - FR-007:
## Activity Lognotes are recorded asInnerStateChangednoteappends from all six writers, including the externalorchestrator_api/commands.py:1563; the section renders from events. - FR-008: No
implement/mark-status/move-task/review action writestasks/WP##.mdor thetasks.mdsubtask surface. The fourshell_pidwriters (implement.py:1730,workflow_executor.py:695&:1370,tasks_move_task.py:1770), theagent/assigneewriters, the subtask checkbox write/uncheck (tasks_mark_status.py:266,tasks_move_task.py:1806), and the move-task god-write (_mt_persist_wp_file) are cut off the file. - FR-009: Evict all review-cycle state in this mission: delete the dead verdict-field read
fallbacks
workflow_cores.py:340-341anddone_bookkeeping.py:104-105(guaranteeing canonical done-evidence), and evict the actively-writtenreview_artifact_override_*(tasks_materialization.py:58-61,125-128) to the event log. - FR-010: Extend the canonical
migration/strip_frontmatter.py:MUTABLE_FIELDS(do not fork) to addshell_pid_created_at,review_artifact_override_*,reviewer_shell_pid, and movehistoryout ofSTATIC_FIELDS; retireprogress(remove from the set/schema; document the removal). Deletehistory[]outright (dead, no migration). - FR-011: Migration order is backfill → verify → reader cutover → writer cutover → delete
fallbacks + land the AC-5 hash guard. Backfill seeds transition +
InnerStateChangedevents with deterministic namespaced ULID ids (frommission_id + wp_id + field; a content hash is not a valid ULID); it is idempotent; verify confirms snapshot == pre-migration state by count+value. - FR-012: Every new off-axis emit site resolves its write target from stored topology/target
branch, never
Path.cwd()(do not reopen #2647). - FR-013: A refactor-stable architectural test asserts no consumer reads a dynamic frontmatter field as authority (the #2093 invariant).
- FR-014: This mission owns the restructuring of the
implement.py:1730shell_pid claim writer required by the eviction. The prior "co-sequence with #2160" constraint is retired: #2160's writer work ispr:deferredand yields to this mission, so the writer cutover proceeds within this mission without an externalblocks/blocked_bygate. The #2647 invariant (FR-012) still binds every emit site touched by that restructuring.
Key Entities
InnerStateChanged— the generic off-axis event; no lane transition; typedWPInnerStateDelta.WPInnerStateDelta— typed partial:shell_pid?,shell_pid_created_at?,subtasks?: Mapping[str, Status],note?: str,tracker_refs?: list.- Reduced snapshot (per WP) — gains typed runtime slots; the single read path for runtime state.
Assumptions
- This mission owns the
implement.py:1730restructuring (FR-014). The former #2160 hard co-sequence is resolved: #2160's writer work ispr:deferredand yields to this mission, so the writer cutover is not gated on external #2160 work and this branch does not wait on it. - #2684 is authoritative over the WP-metadata surface; deferred PRs (#2641, #2766, #2612, and #2160's writer work) yield to the mission and rebase onto it, not the reverse.
- The seven design decisions (brief §0 / ADR 2026-07-19-1) are settled and not re-litigated here.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001 (AC-1): A full WP lifecycle leaves
tasks/WP##.mdbytes and mtime unchanged (no runtime write). - SC-002 (AC-2): A claimed WP with empty frontmatter resolves live from the snapshot; resume does not falsely flag stale.
- SC-003 (AC-3):
move-task --to for_reviewsucceeds on log-recorded subtask completion with uncheckedtasks.md— the merged red test is green; genuinely-incomplete WPs are still refused. - SC-004 (AC-4): Activity Log / History / review render from events with no content loss.
- SC-005 (AC-5): The dossier content hash of the WP files is stable across the full lifecycle.
- SC-006 (AC-6): Migration is idempotent on the live corpus; snapshot == pre-migration by count+value; re-runs seed nothing.
Out of scope
- The final static-model authority election (enrich
WPMetadatavs electWorkPackageEntry) — deferred to a follow-up, gated on this landing (B4), coordinated with #1619. - The 3.3.x YAML-authoritative / markdown-derived WP-prompt flip (requires #2400-owner re-ratification).
- The semantic-only content-hash slice (#2686) — shrinks to a small follow-up once this lands.