Follow-ups to running the pre-merge review gates in `in-review`. Each was
verified against the code before being fixed; one reported issue was
refuted and is noted below.
1. Symbol locks (packages/core/src/task-store/moves.ts)
FN-8306 made the lifecycle transition the symbol-lock RELEASE authority
but wrote no counterpart. That was harmless while a task only left WIP
at handoff/terminal; the gate crossing now releases the task's declared
symbols and the remediation node re-enters `in-progress` to edit the
same files in the same live worktree with its locks gone. Neither
acquire site (scheduler dispatch, claimDueWorkflowWorkItem) is on the
graph re-entry path. Adds a symmetric re-acquire on `!wip -> wip`.
Best-effort by design: a contended symbol logs and proceeds, which is
exactly the pre-fix posture, rather than parking the remediation behind
another holder and re-creating the stranding this change set removed.
2. Premature merge (packages/engine/src/self-healing.ts)
`recoverMergeableReviewTasks` was the only in-review sweep with no
liveness gate. The graph commits the column crossing at node entry and
writes the gate's pending lease two DB round trips later, and
`getTaskMergeBlocker` has no notion of "enabled but resultless", so in
that window the sweep could enqueue a merge with Code Review never run.
Filters `executingIds`, matching recoverGhostReviewTasks.
3. Orphan sweep (packages/engine/src/self-healing.ts)
The reported restart hazard is REFUTED: nothing re-attaches an in-review
graph run, so those leases are genuinely dead and marking them failed is
correct FN-8492 behavior. But the sweep also runs from periodic
maintenance in the same live process, where a tick between the lease
write and session registration could fail a gate that just started.
Honors a within-floor `classifyReviewLease`, matching the semantics Plan
Review already had. Cleanup of dead leases is delayed by the staleness
floor, not defeated. The audit event gains `needsOperatorBypass` for
`autoMerge:false` rows, which self-healing deliberately skips and only
fn_task_bypass_review can clear — previously indistinguishable from an
auto-recoverable rewrite.
4. Stall detection (packages/engine/src/planner-overseer.ts)
The `reviewer` and `merger` stages had no time-based check at all and
returned `progressing` unconditionally, so a hung gate produced no
signal however long it sat. Adds gate-anchored detection on both (a
plain in-review card with no reviewState resolves to `merger`, not
`reviewer`), keyed on the pending lease's own `startedAt` rather than
`columnMovedAt` so it cannot fire during a legitimate human merge-wait.
`cumulativeActiveMs` is documented, not changed: it now excludes gate
runtime, but adding the `timing` trait to `in-review` would count arbitrary
human merge-wait as active work — a worse distortion than the omission.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review and Browser Verification now run with the card in `in-review`
instead of `in-progress`, so the board shows the card under review with the
running step as a badge (matching the Coding (Ideas) preset). Their paired
remediation nodes stay in `in-progress`, so a changes-requested verdict
visibly sends the card back to implementation.
The column move IS the badge switch: the dashboard badge was already
lane-gated on `column === "in-review"`. Applied to the shared stepwise
coding IR, so it is inherited by builtin:coding (the default),
builtin:stepwise-coding, builtin:brainstorming and builtin:coding-ideas;
builtin:legacy-coding keeps its historical placement.
Two consequences handled:
- Capacity: `in-review` has no `wip` trait, so the slot is released during
review and the remediation crossing back into `in-progress` can hit the
non-bypassable in-transaction capacity check. The column boundary now
PARKS the run on a `capacity-exhausted` rejection instead of failing it,
preserving the failed gate result and worktree so the next graph run
retries once a slot frees. Non-capacity rejections still propagate.
- Reopen clears: `applyReopenFieldClears` wiped `workflowStepResults` on
every in-review -> in-progress move, which the remediation crossing now
performs routinely. That destroyed the remediation input, made
`routeRetryableRemediationGraphFailureToPreMergeFix` and
`recoverFailedPreMergeWorkflowStep` silently no-op, and — worse — made
both `getTaskMergeBlocker` branches vacuously false, so a card could
return to `in-review` and be mergeable with its gate never re-run. Now
exempted for graph-owned in-review -> in-progress crossings only;
operator reopens, merge bounces and every -> todo/triage rebound still
clear, so the executor's documented bounce invariant is unchanged.
Adds regression coverage for both (there was previously none for the
reopen clear in either direction), and annotates the unreachable legacy
scheduler dispatch block rather than mirroring the fix into dead code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to 13a2b2a9d, from a multi-agent review of that commit. Three of its
claims did not hold.
1. fn_delegate_task bypassed the gate entirely (P0). It reaches the same
createAgentTask primitive, was registered unconditionally in both session
lanes, and validated only that the TARGET agent is non-ephemeral — never the
caller. Under Deny an ephemeral worker could enumerate agents and delegate
unlimited tasks. It is now withheld under Deny, and also under
upon_validation: delegation has no proposal channel, so leaving it available
would launder a create past the operator review that policy requires.
2. The widened dedupe window was capped at 5 minutes. The store query in
branch-and-pr-entities.ts carried its own independent `?? 60_000` /
`min(300_000, …)` pair, so widening only duplicate-guard.ts under-delivered
and made the new ceiling unreachable. Both sites now share
FINGERPRINT_WINDOW_DEFAULT_MS / FINGERPRINT_WINDOW_MAX_MS.
3. The pi-extension gate does not fire at all. pi's ExtensionContext carries no
agentId — the read is a speculative cast and only tests supply one, so every
real call short-circuits as a human caller. The fail-closed direction is kept
for the day an identity signal exists, but the limitation is now documented
instead of implied to be enforcement.
Also: the session prompt now states when creation is disabled and names
fn_task_log as the fallback (the base prompt still taught fn_task_create, which
is the same instruction/capability mismatch that fed the retry storm);
suppression emits an `agent:task-create-withheld` run-audit event; and the two
source-text ratchet tests are replaced with behavioral assertions on the tool
list the executor actually hands the model — verified to fail when the guard is
broken, which the string assertions did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator report: with project policy "Ephemeral agent follow-up tasks = Deny",
an executing agent filed ten follow-up tasks — five parallel fn_task_create
calls it reported as timed out, then five sequential retries.
Two defects:
1. Deny was advisory. fn_task_create was registered for every session and only
refused inside execute(), so the model still saw the tool, planned around it,
and retried it. The pi extension's isEphemeralCallerAgent also failed OPEN
whenever the caller id did not resolve to an agent row — which is the normal
shape of an ephemeral task-worker — so on that lane Deny was a no-op.
2. The deterministic content-fingerprint duplicate window was 60s, which only
covered concurrent in-flight creates. A retry two minutes later saw nothing
and filed a second task.
Fixes: isAgentTaskCreateToolAvailable() withholds the tool from ephemeral
sessions under Deny in both engine lanes (outer execution session, per-step
workflow session); isEphemeralCallerAgent fails closed on an unresolvable
caller id; the fingerprint window goes 60s -> 10m (clamp ceiling 5m -> 1h).
upon_validation keeps the tool, and permanent-agent and human/chat callers are
unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the `autoUpdateAndRestart` global setting (default off, Settings ->
General next to Release channel). When enabled, the dashboard host installs
available updates on the selected channel by itself and requests the
supervised in-place restart. Supervised hosts only: without a parent to
respawn, installing would leave a running process whose code no longer
matches its own install.
Fix two ways the restart affordance could silently do nothing:
- The supervisor now stamps FUSION_SUPERVISOR_PID and supervision is only
counted when that pid is the real parent. FUSION_RESTART_SUPERVISED is
inherited by every process Fusion spawns, so `fn dashboard` launched from
an agent terminal skipped its own supervisor while still advertising
restart support -- a restart request then killed it for good.
- Settings and the update banner probe /system/info on mount and treat
capability as advisory: the button always issues the request and shows the
server's actual refusal instead of sitting disabled after a failed probe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Route admin CREATE/DROP DATABASE through a short-lived postgres.js maintenance
connection instead of spawning psql via execSync per call, and remove the
redundant DROP-before-CREATE (db names are pid+random, never pre-exist).
Cuts ~2 of 3 subprocess forks per test across ~55 tests; the slowest core
test file drops from ~90s under full-suite contention (32.6s->27s standalone)
with all 75 tests still green.
Fusion-Task-Id: FN-SLOW-TEST
Quick-add "Start" collapses create+promote into one request: it submits the
workflow id AND the post-intake `todo` column together, so the card lands in
`todo` having never sat in the workflow's manual intake column. The intake test
in task-creation.ts only matched `triage` or the resolved intake column, so the
card got generateSpecifiedPrompt — whose hard-coded boilerplate steps
("Implement the required changes") no planner ever wrote.
That stranded the card permanently: triage's todo-discovery admits a card only
when its PROMPT.md reads as a seed, so the placeholder spec was classified
"already planned" and never planned, while nothing could execute it either
(steps: []). It sat in Todo forever with no log line in any lane. Observed on
FN-8587.
Creates into `todo` on a manual-intake workflow (resolved intake is not the
legacy `triage`) now get the bootstrap seed. The pinned contract for a plain
direct create into todo on the default workflow — which intentionally keeps
generateSpecifiedPrompt — is untouched, and both create sites are fixed in step.
Also instrument the hold/release sweep, which had reasons but no timings:
per-task held duration reported on release, a per-sweep summary breaking out the
prefetch cost (a sequential await per non-archived task, so it scales with board
size rather than with held cards), and a warn when a sweep exceeds 2s — so a
"ready card doesn't move" delay can be attributed between poll cadence, sweep
cost, and a card genuinely queued on capacity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressing Start on a Coding (Ideas) card only writes a column move — there is no
dispatch call in that path — so planning did not begin until the triage
processor's next timer tick, up to pollIntervalMs (15s default) later. The
"Started planning" toast was optimistic and the card just sat in Todo.
- Wake planning discovery on the store's task:updated/task:created event when a
task lands in todo/triage. Binding the wake to the store event rather than the
Start button covers every move surface (board drag, context menu, task detail,
List view, CLI, agent tools, POST /tasks/:id/move) by construction. The wake is
advisory: it only advances WHEN the poll runs, so every pause, seed-prompt,
dependency, and concurrency gate still applies.
- Admit a todo task whose PROMPT.md is missing instead of dropping it through a
silent `catch {}`. The scheduler KEEPS a candidate whose prompt it cannot read,
so such a card was invisible to planning while still visible to dispatch, with
no log line in either lane. Unreadable (non-ENOENT) prompts now log.
- Route the scheduler's dispatch filter through the shared isUnplannedSeedPrompt
predicate. Its open-coded strict bootstrap compare disagreed with triage on the
refinement-seed shape, leaving hold-release as the only thing between an
executor and a prompt containing just the operator's feedback text. The
predicate also normalizes line endings/trailing whitespace, so a CRLF or
trailing-newline round-trip no longer reclassifies an unplanned card as planned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A legacy source column (todo/in-progress/...) validated moves only against the
closed VALID_TRANSITIONS map, which cannot know about a workflow-declared
column, so Todo -> Ideas was rejected even though the board drag pre-check and
context menu both offered it. Legacy sources now union VALID_TRANSITIONS with
the task's workflow-resolved adjacency, resolved lazily only when the legacy
table alone would reject. builtin:coding adjacency is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An operator can hold a raw Anthropic API key and a Claude subscription OAuth
login at once, and the raw key always won silently. A stale or revoked saved
key therefore shadowed a working subscription and failed every direct Anthropic
call with 401 invalid x-api-key, while both Settings cards still read Active.
Add the global anthropicAuthPreference setting ("api-key" default, preserving
the historical precedence, or "subscription"), read in resolveAnthropicRuntimeApiKey
straight from ~/.fusion/settings.json so it applies without a restart and needs
no settings plumbing through createFusionAuthStorage. Neither value removes a
source: with one credential configured, resolution reaches it either way.
Settings -> Authentication now names the credential in use on the two Anthropic
cards and renders the control, but only when both are actually connected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The proactive status narration printed the internal 0-based step index, so the
final step of a 13-step task announced "Starting Step 12" next to a card
showing "12/13". Display now uses index + 1 in both the engine builders and
the store-side updateStep narration; the 0-based tool/PROMPT.md contract is
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Binary Release (v0.73.0-beta.5 was fully red):
- bun compile: mark chromium-bidi external — playwright-core@1.60 (feature-video)
optionally requires it and bun fails closed on unresolvable requires.
- Windows desktop EXE: quote -c.publish.channel=beta in release.yml; PowerShell
tokenizes the bare flag into `-c` + a path and electron-builder ENOENTs on it.
Full suite (all 4 shards red from stale-test drift, no product bugs found):
- engine: align mock stores/assertions with atomic store.moveTaskIf dispatch
(#2371), the fail-closed non-empty PROMPT.md artifact gate (#2390), oldest-
first admission (FN-8453), alreadyClaimed graph routing (#2393), startStep
step projection (#2403/FN-8464), structured retry presentation (FN-8503),
provider-lane pause reasons (#2339), typed column-boundary entry (#2378),
Type.Integer in CAS document schemas (#2375), bounded model-registry refresh.
- engine-no-blocking-shellout: re-pin 17 drifted allowlist line numbers and drop
the stale REBASE_HEAD entry whose execSync was removed.
- core: schema-applier expectations track migrations 0033-0035 (96 tables) and
the synthetic 0000 fixture gains workflow_work_items/mission_contract_assertions;
work-item terminal state is "succeeded" post-#2378.
Known follow-up (not addressed here): self-healing starved-refinement escalation
bumps task.priority, which FN-8453 oldest-first admission no longer consults.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reported bug (screenshot): deleting the task created from a plan left the
session permanently stuck on PLANNING_CREATED_TASK_MISSING — Retry create
replayed the same 409 forever. A linked task absent from the
include-archived scan (task-row authority; a successful scan proves
deletion, not a flaky read) now clears the stale linkage and creates a
fresh task, in both the create-task route and createTaskFromPlanSession;
a still-listed-but-unreadable task keeps failing closed.
Multi-agent review of fdd120232 (correctness/adversarial/reliability):
- P1: CLI planning sessions were memory-only — setAiSessionStore only ran
in the dashboard server, so --resume could never find a session across
invocations. New ensureDurablePlanningSessionStore wires the durable
AiSessionStore over the board store's public asyncLayer in runTaskPlan.
- P1: resume failures now THROW instead of process.exit (fn_task_plan
runs inside the pi host — an exit killed the whole agent session), and
a no-question resume requires an explicit refine focus (the provided
description) so merely resuming never rotates the epoch.
- P2: claim and finalize CAS gained the same expected-epoch WHERE guard
as reconcile, so a stale-epoch creator can no longer finalize an
old-epoch task onto a rotated session.
- Side-effect failures (documents, logEntry, validate, reconcile) are now
logged instead of swallowed; post-insert failures no longer mislabel
the just-created task alreadyCreated:true; the keep-refining readline
closes on thrown prompts and a failed refine after creation returns the
created task id with a resume hint; cross-process generating guard
added to createTaskFromPlanSession.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One planning session can now create multiple tasks. Task-creation claims
are epoch-scoped: proposalClaimId stays planning-session:{id} for epoch 0
and becomes planning-session:{id}#N after the plan is edited past a
created task (rotateTaskCreationEpochOnReopen archives createdTaskId into
createdTaskIds and resets claim state). Unedited Proceed replays stay
idempotent within an epoch; crash-after-insert dedup still reconciles via
the epoch-keyed task row. Complete sessions resume to an editable plan
review with a linked-task banner; the task-created handoff gains a
Continue planning action.
Hardening from the multi-agent code review (9 reviewers):
- Reopen + rotation run only AFTER turn admission, so a rejected request
never burns a phantom rotation (P1, 3 reviewers).
- Claim-lifecycle CAS writes are surgical jsonb merges and reconcile takes
an expected-epoch guard, so a concurrent rotation can never be reverted
or an archived task re-linked to a new epoch.
- create-task 409s while the session is still generating (turn-completion
persist could tear the fresh linkage).
- Durable-read fallback in create-task now logs before trusting the
in-memory epoch.
- linkedTaskId no longer leaks across session switches; the banner
resolves the just-created Task before the tasks prop refreshes and
falls back to the newest archived task after rotation; Continue
planning re-registers the active session.
- Shared applyCompletePlanningResume helper replaces triplicated resume
view-transitions; stale one-task-per-session comment corrected.
Tests: post-rotation replay idempotency and epoch-keyed crash reconcile
(e2e), rewind rotation + rejected-rewind non-rotation + payload
normalization round-trip (unit), Continue planning + banner-leak (UI),
create-task 409 (routes), and a new PG integration suite pinning the
surgical CAS merge and reconcile epoch guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- usage-limit-detector + provider-health-monitor: make three bare listTasks()
callers explicit with { slim: true }, restoring the architecture-hot-paths
contract (they only read scalar pause/column/model-provider fields).
- pg-test-harness beforeEach: wipe <rootDir>/.fusion/tasks after TRUNCATE ...
RESTART IDENTITY so filesystem isolation matches the id reset; stale task
dirs from prior tests no longer collide with reused IDs (fixes
store-reservation-atomicity rollback assertions).
After a hard host crash (SIGKILL, power loss), postmaster.pid survives with no
postmaster behind it. The optimistic join handed every subsequent boot a URL to
the dead port, so the dashboard could never start again without a manual pid
delete. Probe the recorded pid with signal 0: provably dead (ESRCH) rebuts the
live-lock presumption and the boot takes an owned start — PostgreSQL itself
re-validates and reclaims the stale lock file, so a recycled live pid keeps the
old join-then-fail behavior and a genuinely live postmaster still surfaces the
lock collision we already join on. EPERM counts as alive (fail-closed).
Verified end to end: real cluster started, postmaster SIGKILLed leaving the pid
file + interrupted WAL, fresh lifecycle detected the stale lock, ran an owned
start, and crash recovery preserved the marker row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
beta.4 follow-up from the issue thread: on an interrupted (not-cleanly-shut-down)
cluster, the elevated Windows launcher declared readiness on a bare TCP accept
while crash recovery still rejected every connection with 57P03, so
ensureDatabase failed and the start cleanup fast-shutdown the recovering
postmaster ~0.2s after launch; the retry then joined the instance it had just
told to stop, got ECONNREFUSED, and parked the dashboard in a dead shell. The
30s ".pgrunner sharing violation" stall was recovery's SyncDataDirectory fsync
walk hitting Fusion's own pgctl log inside the data dir.
- Move the pgctl runner dir to a sibling .pgrunner-<dataDirName> outside the
data dir (and sweep the legacy in-dataDir .pgrunner), so recovery's fsync
walk can never contend with the postmaster's inherited log handle.
- Ignore 57P03 recovery rejections in the elevated readiness fatal scan.
- Owned starts wait for the cluster to genuinely accept connections (retrying
57P03/socket errors, bounded by the start timeout) before ensureDatabase —
never stop a postmaster that is still in recovery.
- Join-path database verify retries the 57P03 recovery signal for up to 15s;
socket errors keep the instant optimistic-join contract for stale pids.
- startup-factory's joined-instance-unreachable retry backs off across ~15s
instead of a single 500ms attempt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
Wave 15 of package code organization.
### Peels
- `types/settings-scope.ts` — global/project settings (~2.2k lines)
- `types/archive-planning.ts` — archive, mesh/multi-project, planning
sessions
- `task-store/project-store-ops.ts` — rename of `remaining-ops-1` (last
numbered ops module)
### LOC
- `types.ts` ~5872 → ~3074
## Test plan
- [x] `@fusion/core` typecheck
- [ ] CI merge gate
**Stack:** this PR → #2397 → #2398
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Refactor**
* Reorganized and expanded the core public type surface into dedicated
modules for settings, archive/planning, board, tasks, todo lists, plugin
activation, and multi-project setup.
* Improved the browser-safe type exports to keep the public contracts
consistent.
* Updated internal project-level operation wiring to use the correct
project implementations.
* **Bug Fixes**
* Fixed a workflow creation test hook to inject the correct pre-insert
behavior for workflow-definition collision/allocator scenarios.
* **Chores**
* Refreshed internal headers and updated line-count baselines.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Two-part fix for #2411 — Windows embedded PostgreSQL backends dying with
exception `0xC0000142` and taking the whole dashboard down.
### 1. Crash hardening + recovery (FN-8522)
- Child-only native `PATH` hardening so forked backends can always
resolve their runtime DLLs.
- Non-blocking `.pgrunner` log monitoring (shared read), eliminating the
self-inflicted ~30s `sharing violation` retry window at boot.
- Detection of the ordered 0xC0000142 shutdown sequence with a single
automatic restart of owned clusters on their resolved port, plus
operator diagnostics.
### 2. Platform-aware `max_connections` default (follow-up from
[operator
report](https://github.com/Runfusion/Fusion/issues/2411#issuecomment-5054900702))
On Windows every PostgreSQL connection is a separate process; the
embedded cluster's unconfigured `max_connections=500` cap lets backend
spawn bursts exhaust the non-interactive desktop heap, which kills
forked backends with exactly `0xC0000142`. The reporter confirmed
stability after lowering the cap.
- `embeddedPostgresMaxConnections` is now schema-unset so the server can
distinguish "operator never set it" from an explicit choice
(`getSettings()` merges schema defaults, which previously pinned 500
unconditionally and made the runtime fallback dead code).
- New `resolveEmbeddedMaxConnections()` resolves the unset default
platform-aware: **150 on win32, 500 elsewhere**. Explicit settings are
honored on every platform, clamped to [32, 2000] as before.
- Settings UI renders the cap empty ("auto") with platform-aware help
copy across all six locales.
- Fixed a latent reset bug this exposed: global "Reset this menu" wrote
`undefined` for undefined-default keys, which JSON serialization drops —
the stored value silently survived reset. Now uses null-as-delete.
## Testing
- New unit tests for `resolveEmbeddedMaxConnections` (platform defaults,
clamping, non-integer handling).
- Updated settings-defaults, default-descriptions, and SettingsModal
tests; embedded lifecycle + recovery coverage from FN-8522.
- `@fusion/core` builds clean; changesets included for both parts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Code-review findings on 085f7b99c: a bare date like 2026-07-22 qualified
as a distinctive slug, so unrelated failure reports quoting the same date
silently converged (and the date outranked a real file-path anchor in the
sorted-first pick); reject slugs whose segments are all hex/numeric.
Also render non-http(s) sourceMetadata.issueUrl values as plain text to
block javascript:-scheme links from API-supplied metadata.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Widen computeCrossParentDiagnosticClaim so repair tasks phrased as
'exceeds limit / oversized / blocking X / so X passes' converge on a
file-path or distinctive-slug anchor at creation time (FN-8510/8511/
8513/8514 incident: four executors on unrelated parents filed the same
oversized-changeset follow-up and none deduped before triage).
Add a Provenance section to the Task Detail Stats tab showing source
type, parent task, creating agent, imported-issue link, and the triage
near-duplicate marker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The KTD-8 adoption sweep runs on every store open, and a DB with active tasks
never records the drained marker, so it re-runs constantly. Its resume-graph
mapping for 'planning' cleared FN-8504's freshly written live planner status
~100ms after triage claimed it (audit: task:reconcile-legacy-adoption,
priorStatus 'planning'), leaving a live replan planner rendered as an idle
READY card and invisible to every Running count.
Generalize the FN-8498 needs-replan fix: any status with a live post-cutover
writer is preserved — planning (triage's stale-planning sweep owns crash
recovery), queued (scheduler re-evaluates each poll), merging/merging-pr/
merging-fix (self-healing stale-merge recovery), stuck-killed (restart-
recovery coordinator). Only writer-less statuses (plan-review-unavailable,
triaged) keep resume-graph.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>