Second FN-8596 strand, found after the first fix shipped. Clearing the
stale `planning` status moved the card into a state owned by NOBODY:
- planning excluded it: stale `firstExecutionAt` from its first pass made
hasAdvancedPastPlanning true, and the previous fix only rescued cards
that still carried a planning-stage status;
- recoverAdvancedTriageTasks — the designated owner of that
"stranded-advanced" class — also excluded it, because it bails on
`workflowIrPinColumnId === "triage"`: it cannot resume a card into the
column it already occupies (the pin was plan-replan, which lives in
triage).
So the card sat indefinitely with no sweep, log, or audit event naming it.
hasAdvancedPastPlanning now decides on arrival order alone for any card in
the planner column: a stamp written BEFORE the card reached triage belongs
to a previous pass, whatever the status is now. A card that genuinely
advanced is still caught by the column check at the top, and one claimed by
execution AFTER landing here has a stamp newer than its arrival, so it
still reads advanced and stays with advanced-recovery. This flips one case
I added in the previous commit — production proved that classification
stranded the card.
Hardening, so this class cannot hide again:
- detectStalledCards: a detect-only watchdog emitting
`task:stall-watchdog-detected` for any non-terminal, unpaused card idle
past 30m with no live session and no queued continuation. Deduped per
shape. It deliberately does NOT mutate — a generic mutator racing the
specialized sweeps is the bug class this file keeps re-fixing, so
recovery stays with the sweep that owns each shape and this guarantees
visibility.
- The silent skips are now loud: runIfStillPlanningUnderTaskLock (all
four callers inherit it), the planning handoff moveTaskIf, and the four
requestPreMergeOptionalStepFix refusals now log why nothing was
scheduled and that the card was left parked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make planning-guard and remediation no-ops emit warnings, and detect idle non-terminal cards with no session or continuation so FN-8596-class strands show up in logs and run-audit.
Root cause of the FN-8596 strand (card sat in Planning, doing nothing,
until an engine restart).
Plan Review returned REVISE, the graph rebounded the card to `triage` with
`needs-replan`, and triage claimed it — overwriting the status with the
TRANSIENT `planning`. `needs-replan` is a durable park that outranks the
execution timestamps, but `planning` is deliberately excluded from
REPLAN_PARK_STATUSES, so the card fell through to the stamp check. Those
stamps were written when it entered `in-progress` on its FIRST pass and are
never cleared, so the replanning card read as "advanced past planning" for
the rest of the session.
From there everything was a silent no-op:
updatePlanningStateIfStillCurrent returned false and its callers returned
with no log, no audit and no requeue. The revision session wrote the
revised PROMPT.md (via the store tool, which bypasses the guard) and the
finalize refused to hand the card off — "prompt written, then total
silence", status frozen at `planning`.
Stale stamps are now discriminated from a live claim by arrival order: a
stamp written BEFORE the card arrived in the planner column belongs to a
previous pass, while one written after arrival means execution genuinely
won the FN-8361 race and recovery must not clear the status out from under
it. A missing/unparseable columnMovedAt keeps the prior answer, so this can
only narrow the strand, never widen the race. The PR #2360
stranded-advanced class (stamps with no planning status) is untouched — all
30 pre-existing guard cases still pass.
Also warns when a planning finalize declines to hand off. That path was
completely silent, which is why this strand left nothing in any log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Observed on FN-8596: a plan-review REVISE routed to `plan-replan`, triage
claimed the card with `status:"planning"` and ran the revision session, and
the session wrote the revised PROMPT.md then died without finalizing. The
card sat in `triage` with `status:"planning"`, no live planner, and no
workflow continuation.
That status makes the card invisible to triage rediscovery (it looks
claimed), and the only sweep that cleared it ran at STARTUP — so the card
was unrecoverable short of an engine restart. The leaked-slot reaper then
reclaimed its concurrency slot, which made it look idle without making it
runnable.
Adds a periodic counterpart in the poll loop. Clearing the status is the
whole repair: the card is back in triage with a real spec, so ordinary
rediscovery re-picks it. It does not move, pause, or fail the card.
Guards against racing a healthy planner: the in-process `processing` set,
plus a 20-minute staleness floor that also covers a planner owned by
another node this process cannot see. Operator parks are never touched.
This fixes the recovery gap, not the trigger — why that session failed to
finalize is still under investigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The `.floating-window__body` resize-handle clearance gutter (FN-8015) was
carved out only for 769-1024px, so landscape iPads (1180-1366 CSS px) fell
through to the desktop contract and kept it — content stopped ~17px short of
the right edge while the left edge stayed flush.
Gate the carve-out on the input device instead of viewport width: the gutter
only protects resize hot zones a pointer can actually grab. `(pointer: coarse)`
is primary-input only, so a touchscreen laptop on a trackpad still reports
`fine` and keeps desktop clearance.
Measured at 1180px: header inset went from 1px/17px to 1px/1px.
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>
Review of 2dbfe3d31 + 05b704dc6 surfaced real defects in both fixes:
- The unusable-worktree probe composed two helpers across an unnecessary
self-healing -> step-runner import edge, and the directory check added no
discriminating power over the `.git` probe. Replaced with one canonical
hasUsableWorktreeShape beside classifyTaskWorktree, which also applies the
repo-root gate (FN-6861) when a rootDir is available; both call sites pass one.
Its narrower guarantee vs the canonical classifier is now documented and
pinned by tests, including the de-registered shape it cannot see.
- REPLAN_PARK_STATUSES is derived from PLANNING_STAGE_STATUSES instead of
re-listed, so a new durable park status cannot be added to one set only.
- The preserve/clear decision no longer pretends to steer `worktree`: the rebound
is a reopen move, which clears it regardless. Documented, and the test now
asserts the durable row rather than only the updateTask argument.
- `branch` is cleared only when it is the re-derivable canonical fusion/<id>;
a non-canonical branch survives so a card's only commit pointer is not dropped.
- The recovery log named the recorded worktree even when the session had targeted
an AI-merge clean room. It now names the refused path and says whether the
recorded worktree was gone too.
- Added task:auto-recover-worktree-session-metadata so the decision is legible to
agents, not only in human log prose.
- isTaskStillInPlanningStage's parameter type now includes the execution stamps
its implementation reads.
- Test hygiene: real-fs fixtures wrapped in try/finally; changeset dev note
corrected; FN-8361 asserted at the discovery surface, not only in the guard
table.
Also captures the shared bug class in docs/solutions.
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>
Unusable-worktree recovery preserved task.worktree whenever the failing path
differed from it, treating the difference as proof the recorded worktree was
live. The reported strand had both gone: an AI-merge clean room refused as an
"incomplete worktree" while the task worktree had already been removed, so
every requeue re-dispatched into a missing directory ("Working directory does
not exist … Cannot execute bash commands") until the retry budget burned out
and the card parked failed in review. Preserve the recorded worktree only when
it is still a usable checkout; otherwise clear it so the next dispatch builds a
fresh one from the branch.
Also splits the planning-stage guard: only the DURABLE replan parks
(needs-replan, plan-review-unavailable) outrank sticky execution stamps.
"planning" is the transient planner claim, so a stamp landing on it still means
execution won the FN-8361 race.
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>
Plan Review REVISE rebounds a card to a planner lane with status
needs-replan, but hasAdvancedPastPlanning read the sticky
firstExecutionAt/executionStartedAt stamps as proof the card had left
planning. Triage discovery filters on that guard, so a rebounded card was
never re-admitted and sat in triage/needs-replan forever ("stuck in
planning"). An explicit planning-stage status now outranks the stamps in
both planner lanes; a triage card stamped with no planning status is still
excluded so self-healing's advanced recovery keeps owning it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group mobile project switcher favorites ahead of other projects.
- Reuse the shared bookmark store for mobile project rows and toggles.
- Add responsive favorite-control styling and coverage for grouping, empty states, and selection behavior.
- Add a release changeset for the mobile favorite-project experience.
Files changed:
.changeset/mobile-project-favorites.md | 7 ++
packages/dashboard/app/components/Header.tsx | 120 +++++++++++++++------
.../dashboard/app/components/ProjectSelector.css | 40 +++++++
.../Header.mobile-project-favorites.test.tsx | 116 ++++++++++++++++++++
4 files changed, 253 insertions(+), 30 deletions(-)
Fusion-Task-Id: FN-8595
Fusion-Task-Lineage: 1f739ee9-1ff5-4b47-ac9e-7e4a5857f1a5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Extract shared fixed-menu positioning so deps/agent/node/priority pickers
clamp max-height without detaching from the trigger when free space is
shorter than the preferred dropdown height.
Re-pin the execSync allowlist after self-healing/executor line shifts, capture
implementation-session tools under the graph-owned pause harness, treat
worktree alone as not past planning for replan targets, and age starved-
refinement fixtures past the post-escalation cooldown window.
Planning sessions run in the task's own worktree with the coding tool
surface, but the spec path handed to the planner is relative
(.fusion/tasks/<id>/PROMPT.md) while finalization reads it against
rootDir. A planner using the generic write tool instead of
fn_task_prompt_write stranded the spec inside the worktree, where
finalization could not see it and worktree disposal destroyed it.
project.tasks also has no `prompt` column, so PROMPT.md was
filesystem-only and the project checkout was the sole durable copy of
every plan.
Add plan-artifact-writeback.ts:
- reconcileWorktreePlanArtifact copies a worktree-stranded PROMPT.md
back into the main project .fusion folder through
store.updateTask({ prompt }), keeping File Scope validation, the root
write, and the task.json sync atomic. Empty, absent, and identical
worktree copies are no-ops so a correct spec is never clobbered.
- mirrorPlanToProjectDb mirrors the authoritative plan into the `plan`
task document, which triage already reads as a planning-draft
fallback, making that recovery path DB-backed. Identical content is
skipped so revisions do not churn.
Both are best-effort: a failure never turns a good planning pass into an
error. Wired at the reconcile-before-finalize-read seam, at finalization
with the post-hygiene accepted content, and inside fn_task_prompt_write.
Covers the invariant rather than the repro: tests assert worktree-
stranded, root-only, empty worktree file, absent file, identical
content, persistence failure, redundant-mirror skip, and mirror failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The footer rail already had overflow-x: auto, so it scrolled with a mouse,
but the global mobile `* { touch-action: pan-y }` lock intersected horizontal
pans away from every element. Opt the rail and its inner touch targets (the
buttons receive the touchstart; touch-action is not inherited) back into
pan-x, contain overscroll so a fling does not chain out to the document, and
free the button groups from the mobile max-width: 100% reset that squeezed
them into overlap instead of widening the scrollable content.
Also align the footer's media query with MOBILE_MEDIA_QUERY (max-width 768px
OR max-height 480px), which SettingsModal.tsx uses to pick the mobile footer
markup: landscape phones were rendering mobile markup under desktop CSS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Multi-column fling reach came from release velocity alone, so a quick short
thumb flick (~30px, several px/ms) bought 2-3 extra columns and the board flew
past the intended column. Extra pages now also have to be earned with travel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fn_task_promote can now pass force:true to start execution when a task is still
waiting on planning or plan review, matching the dashboard's promote override.
The rejection message names the flag so a caller that hits the gate can decide,
and a forced release says the pending replan was cancelled rather than burying it.
Force stays opt-in per explicit promote request: the hold-release sweep and the
webhook event release have no force parameter, so FN-7648 still holds for every
automatic surface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reclaim sweep's tip-already-merged arm vetoed on the branch tip's foreign
Fusion-Task-Id trailer alone. A task branch cut from the base that never
committed anything (planning aborted, card moved back to todo) points at the
PREVIOUS task's landed commit, so the veto fired on inherited metadata: the card
kept stale worktree/branch/baseCommitSha and re-logged
"already-merged rejected ... reason=foreign-task-tip" every sweep.
Hoist the merge-base diff proof already used by already-merged and
branch-misbound recovery into a shared foreignTipRejection helper and route all
three callers through it. Rejection still fires when the branch carries unique
content or the base already has this task's own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Promote on a held card printed the raw i18n key `board.rejection.unplannedForExecution`:
FN-8471 added the server-side code without a client case or catalog entry, so
translateRejection fell through to `t(messageKey, messageKey)`.
- Add the explicit rejection case (both translate helpers) plus the en catalog
entry and secondary-locale stubs.
- promoteHeldTask(..., { force }) waives ONLY the unplanned-for-execution gate;
hold membership, capacity and slot reservation still arbitrate. It clears a
needs-replan/plan-review-unavailable status so triage rediscovery cannot pull
the card back into the waived replan, and emits task:promote-forced-unplanned.
- POST /tasks/:id/promote accepts { force: true }; the board asks for explicit
confirmation first and only offers the override for this rejection.
Force stays operator-only — the sweep, the webhook release and fn_task_promote
never set it, so FN-7648 still holds for every automatic surface.
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
Withdrawing a card from planning (todo -> Ideas) now stops the work:
- triage aborts and disposes the planning session through the same path
pause/delete already use, and clears status:"planning" so the planning badge
goes away and the card reads as a plain idea again;
- the executor aborts in-flight graph work on any backward move out of
todo/triage, so a Plan Review does not keep streaming against a card the
operator pulled back;
- moving it back to todo needs no new code: the existing column wake fires and,
with the status cleared, the card is an ordinary planning candidate again.
Pre-execution worktrees (planning acquires one now) are reclaimed two ways: an
immediate release on an explicit withdrawal, and a self-healing sweep
`reconcile-pre-execution-worktrees`. The sweep is deliberately timid — 30 days
of complete inactivity, and it skips anything active or waiting (todo,
executing, in-review, done, paused, carrying any status, blocked, or scheduled
for recovery). Every real safety condition lives in the executor: never
executed, no live session, clean branch, nothing uncommitted.
hasAdvancedPastPlanning no longer reads a worktree as execution evidence.
Planning owns a worktree now, so that signal would have made every planning
write skip; execution timestamps carry the meaning instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contention prevention (why tasks shared a path at all):
- Planning ran `tools: "coding"` at the repo root, so every planner had write
tools in the operator's checkout and all planners shared one path. Planning
now acquires the task's own worktree (TriageProcessor.acquirePlanningWorktree
-> TaskExecutor.ensureTaskWorktreeForPlanning).
- Graph nodes with no worktree acquired one instead of falling back to rootDir,
so Plan Review / Code Review / custom gates all run isolated. Plan Review
re-acquires when its recorded worktree is gone, replacing FN-7996's
run-from-the-repo-root degrade. Workspace projects are unchanged.
- Registration goes through acquireActiveSessionPath, which reclaims a leaked
entry whose holder is provably dead and aged past the FN-5256 floor. A live
holder still contends — real serialization is never clobbered.
Classification (the reported symptom):
- A lease held by another task is no longer a provider failure. It carries
SESSION_CONTENTION_HOLD_VALUE, classifies transient, is excluded from
isNonPlanDefectPlanReviewFailure, and stops burning the node's fast retries.
- The executor waits it out on a 10-attempt 5s->60s ladder and then leaves the
task cleanly queued. There is no terminal branch: contention always ends, so
parking would only ask a human to press Retry on a condition that fixed
itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plan Review needs no worktree, so it runs rooted at the project root. The
activeSessionRegistry key was the bare root path, so the second task to reach
Plan Review hit ActiveSessionPathHeldByForeignTaskError ("path ... is held by
task FN-1398; task FN-1403 may not overwrite it"). That surfaced as a Plan
Review provider failure, burned the in-place retry budget against a hold no
retry could clear, and left the task parked.
Task-scope the registry key for any session rooted at rootDir, in every project
mode — the workspace fix already did this for the shared browse-root. Root
exclusivity protects nothing here: write-capable nodes are refused at the root
outright, and every isPathActive consumer guards removable worktree paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mobile footer is a single nowrap scrolling rail, so an update banner
joining it clipped mid-sentence and pushed Import/Export/Reset/Close
off-screen. Render the update-check result in its own full-width row above
the rail on mobile; desktop/tablet keep it inline next to the version button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plan review pane rendered both the document-adjacent and the action-rail
"Add comment to selection" triggers, so operators saw duplicate buttons. Delete
the document variant and its --document/--mobile CSS pair; the rail button is
now the single control at every breakpoint.
Selection capture also ran on every mid-drag selectionchange, which mounted and
unmounted the trigger as the user dragged. Gate quote writes between pointerdown
and pointerup inside the plan document so the control appears once, on release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Closes the second half of the "started card does nothing" gap. Plan-in-place
workflows (Coding (Ideas)) finalize by clearing `status` in place — finalize
deliberately skips the triage->todo move — so a card that just became executable
emits only a bare task:updated. None of the scheduler's existing event wakes
(task:created, globalPause/enginePaused unpause, per-task unpause) fire for that
transition, so the operator paid one poll interval for planning to start and
another for execution to start.
Track ids seen with status "planning" and trigger a scheduling pass when they
return to a dispatchable state, mirroring the pausedTaskIds unpause tracker.
Guarded on !status, not paused/userPaused, and a schedulable column, so a
planning -> failed/awaiting-approval park does not trigger a pointless pass.
schedule()'s re-entrance guard drops the call if a pass is already in flight,
and the id is cleared on task:deleted alongside the other per-task sets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Start performs a bare column move, so "Started planning {id}" reported an
outcome the handler cannot observe: the engine still has to admit the card, and
a busy pool (per-project maxConcurrent or cross-project globalMaxConcurrent) can
defer that indefinitely. The wait was only visible in the engine log
("Plan throttled by running-agent cap|global semaphore"), so a throttled card
looked like a bug.
- Add a "Queued to plan" badge: the exact complement of "Ready" (same idle-Todo
conditions, but no steps yet, so it waits for a PLANNING slot rather than a WIP
slot). Three Todo states are now distinguishable: planning in flight, queued to
plan, and ready. Pause suppression matches Ready; the badge reuses the existing
status-badge primitives with a color-mix tint, no new tokens.
- Retitle the Start toast to "Queued {id} for planning" in both call sites
(TaskCard Start and QuickEntryBox quick-add Start).
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>
The "Open" button on a possible-duplicate warning did nothing — the modal
closed and no task appeared. The app has two task deep-link shapes and only
one had a consumer: `?task=<id>` was handled by useDeepLink, while
`#/tasks/<id>` had no `hashchange` listener anywhere in the dashboard.
Five surfaces write the hash form. InlineCreateCard and NewTaskModal write it
unconditionally, so their Open was always dead. QuickEntryBox, Column, and
ListView try an in-memory board lookup first and fall through to the dead hash,
which is why it looked intermittent: duplicate matches come from a
project-wide searchTasks, so a match that is `done` or outside the loaded
board slice misses the lookup and lands on the no-op.
Handle the hash form inside useDeepLink so the app keeps one deep-link
authority owning both shapes, rather than forking a parallel hook. The id is
resolved by fetch instead of an in-memory lookup (that lookup is the dead end
being removed), the hash is cleared via replaceState so re-Opening the same
task fires again, and an unresolvable id toasts instead of failing silently.
Regression tests assert the invariant shared by all five surfaces rather than
the single reported repro: a written hash always resolves to an open or a
toast, never a no-op. Verified against the pre-fix code — 5 of the 6 new tests
fail without this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /system/agents/restart-all constructed its AgentStore from rootDir alone,
with no AsyncDataLayer, so it fell through to the sync SQLite Database path
deleted under VAL-REMOVAL-005 and threw instead of bouncing agents. It now
builds the store against the scoped project's PostgreSQL layer via
requireAsyncLayer, failing loudly when project wiring is incomplete rather
than reading a SQLite shadow. This was the last unmigrated AgentStore call
site; the route test harness lacked getAsyncLayer, which is why nothing
caught it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLI JSON/create success lines write via result() (raw stdout) so quiet mode
cannot drop machine-readable output; capture that seam in research/update/task
tests instead of console.log. Allowlist nested voiceInput settings for the
FN-7505 default-description guard and ship locale keys for Voice Input UI.
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>