Expand task update lane coverage across every direct and failure-isolated producer.
- Inventory direct and safe task:updated producer routes.
- Exercise warm and cold lane metadata through public producer operations.
- Cover lifecycle, workflow integrity, and completion update paths.
Files changed:
.../task-updated-lanes-emit-surfaces.test.ts | 308 +++++++++++++++++----
1 file changed, 256 insertions(+), 52 deletions(-)
Fusion-Task-Id: FN-8658
Fusion-Task-Lineage: 7d93593c-6f5a-4299-96f3-f4ee8f260e41
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Refresh canonical live-task claims at serialized scheduler, planning, and merge admission boundaries, including workflow-step leases and reservation handoffs. Back off capacity-denied merges safely across abort and restart lifecycles. Render queued planning cards in the header badge family with compact reason-specific icons.
Route durable planning and review continuations through shared project admission, retain reservations through execution, and prevent duplicate continuations from releasing another run's slot.
Quarantine the timing-sensitive Kimi K3 SDK catalog test without changing timeout budgets.
- Reuse the native model registry once per test file.
- Add the observed CI timeout to the dashboard quarantine ledger and config.
- Document validation and timeout-budget preservation requirements.
Files changed:
docs/testing.md | 8 ++++++++
...ister-model-routes-kimi-k3-supplemental.test.ts | 23 ++++++++++++++++++++--
packages/dashboard/vitest.config.ts | 8 ++++++++
scripts/lib/test-quarantine.json | 5 +++++
4 files changed, 42 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-8647
Fusion-Task-Lineage: 31e79677-d923-4003-a8e8-082159334e65
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Retained directories on queued, paused, blocked, or terminal tasks no longer
consume scheduler slots. Agent concurrency and worktree capacity now count the
same canonical live-task population through one project admission ceiling
(resolveActiveTaskCapacityLimit) with an atomic reserveIfAvailable claim, so
planning, execute, and merge lanes cannot each observe and claim the final
worktree slot independently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The field was defined in persistence and serialization, the executor's Plan
Review replan-cap park wrote it, the triage manual gate null-cleared it, and
the dashboard special-cases it (isReviewBudgetExhaustedApproval badge + detail
explanation) — but updateTask's field-by-field merge never applied the key, so
every writer silently dropped it. FN-8647's 15-cycle non-converging Plan Review
loop therefore parked with a generic 'needs approval' and no hint it was a cap
escalation.
Merge contract, pinned by tests with a measured revert proof (3/4 fail
pre-fix): set persists, explicit null clears, a status write that leaves
awaiting-approval without addressing the reason auto-clears it so an approved
or replanned card cannot carry a stale escalation reason into its next park,
and unrelated updates leave it untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The right-padding bug recurred three times because 'Task Detail modal' names ONE
surface with THREE shells and fixes kept landing in the wrong file. Disambiguate:
- Rename the just-introduced .floating-window--tablet marker to
.floating-window--tablet-viewport and document the naming contract next to it:
--tablet-viewport = viewport MODE classifies tablet (touch or not, styling
surface); --touch-geometry = tablet AND touch (enlarged 44px targets only).
- Add SHELL NAMING MAP breadcrumbs at the top of TaskDetailModal.css and
TerminalModal.css pointing inset/padding fixes at the FloatingWindow shell
that tablet popups and floating terminals actually render through.
Comment wording deliberately avoids dot-prefixed class tokens because
FloatingWindow.test.tsx scans raw CSS (comments included) with selector regexes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow queued tasks to reuse worktrees they already hold when the durable worktree ledger is at or above capacity. Preserve independent agent and semaphore limits, and avoid releasing a worktree slot that a rejected transfer never acquired.
Third recurrence of the Task Detail right-padding bug (FN-8630/FN-8634): those
fixes only covered the .modal-overlay shells, while every tablet task popup and
floating terminal renders through FloatingWindow, whose shared body carries
FN-8015's margin-inline-end scrollbar gutter — a 16px right border with a 0px
left one. Tablet-mode windows (.floating-window--tablet, keyed on viewport MODE
so non-touch tablet widths match too) now zero the gutter; touch never grabs
scrollbar thumbs, so the desktop hot-zone conflict FN-8015 solves cannot occur.
GitHub-import's detail panel, which used the gutter as its right inset,
compensates locally. Desktop keeps the FN-8015 contract.
Also per operator request: the floating terminal is draggable from the empty
strip space behind the tabs and anywhere in the top toolbar, not only the
FN-8633 grip. Tab presses keep stopPropagation (scoped to .terminal-tab) so
they never start a window drag; the tablet floating header supersedes the
pan-x contract with touch-action: none (an overflowing strip is replaced by
the mobile-tabs dropdown, so no visible strip pans horizontally).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Census
**Before: `COLUMN guards (the backlog): 2`, `--strict` RED. After:
`BACKLOG ZERO`, all five gates green.**
Two commits from last night's `maxWorktrees` rollout copied the same
holder ledger, both with literals:
| commit | file | gate |
|---|---|---|
| `374956ef23` | `triage.ts` | planning admission |
| `6c7467a78d` | `executor.ts` | `fn_spawn_agent` |
```ts
t.column !== "done" && t.column !== "archived"
```
## What it costs
Both exclude terminal lanes because a finished card's worktree is
**cleanup-owned, not capacity**. On a renamed board neither literal
matches, so every finished card keeps counting as a live holder. The
count only grows, the gate reaches zero room on a board with free slots,
and planning admission is withheld forever / every spawn is refused.
That is the **mirror** of the breach these commits fixed, and strictly
worse: 8 planners on a 4-slot board is visible; a permanent stall is
silent. The recorded reason even names the worktree budget, which the
operator then checks and finds has room.
## The conversion
`resolveProjectColumnsForRoles(store, ["complete", "archived"])` —
project-level, because the ledger spans the whole board with no single
task to resolve against. Matches triage's existing use in
`sweepStalePlanningStatuses` and executor's at the wip gates.
Legacy-seeded, so a default board still excludes exactly `done` and
`archived` — byte-identical there.
## Both conversions were UNCOVERED when written
Measured with #3214's blinding procedure **before** writing tests:
reverting either to the literals left **all 19 tests in the capacity
suites green**. Nothing in the tree could tell the conversion from what
it replaced — which is how the literals got there in the first place.
Each now has a renamed-board case that fails when blinded:
```
triage converted 2 passed | BLINDED 1 failed | 1 passed | restored 2 passed
executor converted 8 passed | BLINDED 1 failed | 7 passed | restored 8 passed
```
## The pairing earned itself immediately
Both new cases assert an **absence** (no throttle / no refusal), so each
is paired with a positive proving the gate still fires on the same
renamed board when a card genuinely holds the last worktree.
That caught a real defect in my own fixture: the candidate scan resolves
each task's **own workflow selection**, not `listWorkflowDefinitions`,
so my first version fell back to the default board where `drafting`
isn't a hold lane. No card was eligible, nothing throttled, and the
absence assertion **passed for the wrong reason**. The positive failed
and exposed it. Recorded at the fixture so the next reader doesn't
reintroduce it.
## Verification
```
42 tests across 6 capacity suites pass
check-fnxc-future-dates green
check-inert-sync-lane-conversions green
check-lane-wiring green
check-sql-column-literals green
census --strict green (BACKLOG ZERO restored)
```
No changeset: internal engine fix, no published-package surface change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Last loose thread from the 2026-07-31 stamp-repointing wave. Two comment
lines.
```
line 1027 ConcurrencyAdmission 2026-07-31-09:00 -> 2026-07-21-22:30 (eef5eb751e)
line 1471 WorkflowLifecycleColumns 2026-07-31-05:00 -> 2026-07-30-20:55 (109204c590)
```
## Not a revert — neither value was ever right
| stamp | originally | after #3280 | authoring commit (UTC) |
|---|---|---|---|
| `ConcurrencyAdmission` | `2026-08-06-09:00` (16 days ahead) |
`2026-07-31-09:00` (10 days late) | **2026-07-21 22:30** |
| `WorkflowLifecycleColumns` | `2026-08-01-05:00` (~1.5 days ahead) |
`2026-07-31-05:00` (~1 day late) | **2026-07-30 20:55** |
Both were written **ahead of their own commits** to begin with. Three
lanes then repointed stamps to turn `main` green (#3261, #3269, #3280),
moving the **date** back a day while keeping the clock time — which
converts an hours-off stamp into a days-off one in the opposite
direction. #3282 reverted the batch it owned; these two were outside its
scope.
So restoring the originals would be wrong too. The defensible values are
the authoring commits' UTC timestamps, per the `date -u` rule #3281
settled.
## Why now
`scheduler.ts` is a hot file. This survived two successive claimants — I
flagged it on #3262 and again on #3288 rather than opening a conflicting
PR, and said I'd take it once the file was unclaimed.
`check-file-claimed` now reports UNCLAIMED, so here it is.
## Scope
The gate is **green either way** — #3277 fixed the comparison, so
nothing is blocked by this. It is purely about the FNXC trail recording
when the work actually happened, which is the only reason the trail
exists. A stamp that satisfies a check while misstating the date by ten
days is worse than no stamp.
## Verification
```
check-fnxc-future-dates green
check-inert-sync-lane-conversions green
check-lane-wiring green
check-sql-column-literals green
census --strict green
```
Diff is two comment lines — no executable change. No changeset.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What
Pins the **worktree-capacity arithmetic** — the gap #3262 measured,
named, and explicitly left for someone to claim. Two commits: a
behaviour-preserving seam extraction, then the test.
#3262's own scope note:
> blinding this predicate to `false` leaves all 22 scheduler suites
green (365 tests). The capacity logic it feeds has no behavioural
coverage at all.
## Two live defects, opposite directions
Both came out of these few lines:
- **UNDER-COUNT admits work over the cap.** `maxWorktrees=4`, four
planning sessions each holding a worktree, and a replan dispatch
admitted as the **fifth** — the ledger counted WIP cards only and never
learned to count planners.
- **OVER-COUNT self-deadlocks.** A planned Ready card *retains* its
planning worktree for execution reuse, so counting it as a holder blocks
its own release: `2 wip + 3 idle-held = 5/4`, and the first unpause
released 2 of 4 slots' worth of work.
**Both are pinned, and the asymmetry is why.** Under-counting breaks the
cap and lets real work over it; over-counting only starves dispatch. A
test covering the "safe" direction alone would leave the expensive one
open.
## Mutation-tested — all four caught
| mutation | result |
|---|---|
| drop the terminal exclusion | 1 failed / 7 passed |
| count WIP cards twice | 2 failed / 6 passed |
| count cards holding no worktree | 1 failed / 7 passed |
| drop the self-slot subtraction | 1 failed / 7 passed |
```
clean: 8 passed (8)
scheduler suite: 16 files / 151 tests passed (behaviour preserved by the extraction)
typecheck, lint: clean
```
## Scope, stated rather than implied
The terminal predicate is **injected**, not resolved here. Which lanes
are terminal is #3262's test; resolving it in this file would make it
fail for that reason instead of this one. This pins the **set
arithmetic** — who is excluded, and how the total is formed.
Still not covered, and I am not claiming otherwise: the *stateful* half
of the ledger — the `+= 1` on dispatch and the `Math.max(0, … - 1)` on
failure inside `schedule()`'s loop. Extracting that would mean
restructuring dispatch itself, which is a different change from this
one.
## Process note
I claimed this on #3262 **before** starting rather than after, because
`scheduler.ts` is the hottest file in the tree and I produced three
duplicate PRs earlier tonight by picking up small shared-surface work
someone else already had in flight. Announcing first cost one comment;
the duplicates cost three PRs and two closes.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved scheduler capacity calculations for tasks that retain
existing worktrees.
* Prevented WIP tasks from being counted twice.
* Excluded completed and worktree-less tasks from reserved capacity.
* Corrected candidate capacity calculations when no worktree capacity is
reserved.
* **Tests**
* Added coverage for worktree reservation totals and candidate reuse
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
#3286 fixed a real user-facing regression and **shipped no test**, so
nothing stops it returning. The regression was mine.
## The bug
#3215 (mine) added `isArchivedColumn` to an effect's dependency list to
keep the task fetch honest. That effect **also owned four `setState`
calls**, and `isArchivedColumn` is a `useMemo` over
`useBoardWorkflows()` — which revalidates asynchronously.
Every revalidation re-ran the reset over whatever the operator had
typed. A title entered before the workflows settled silently reverted to
`Research: <heading>`, and the task was created with a title nobody
wrote.
## Why my own four tests could not see it
Every existing case in this file asserts the **filtered task list** —
render, await `fetchTasks`, read the datalist. **None types into the
form.**
I tested what I added and not what I touched. That is why the regression
belongs in this file rather than a new one: the gap is this file's.
## The case
Renders with `boardWorkflows: null` — the state when an operator opens
the modal and starts typing — types a title with per-character
`userEvent`, then rerenders with a resolved workflow set (a **new object
identity**, which is the entire mechanism) and asserts the typed text
survived.
Two details that each cost a cycle, recorded at the site:
- **`fetchTasks` is not awaited.** It runs only in enrich mode, while
the title field exists only in create mode — so the reset effect, not
the fetch, is under test. My first version waited on it and failed for
the wrong reason.
- **`userEvent.type`, not `fireEvent.change`.** The documented failure
is state overwritten between renders; a single synthetic change event
can land after the reset and mask it.
## Measured both directions, on main `6834ba35bd`
| state | result |
|---|---|
| fixed main | **5 passed** |
| dependency re-added to the reset effect (my bug) | **1 failed / 4
passed** — and only that case |
The second row is the point: it fails on precisely the mutation that
recreates the defect, and leaves the four archived-lane cases green — so
it pins the regression without duplicating what is already covered.
`eslint` clean, `check-fnxc-future-dates` 0. Test-only.
## Note on provenance
Getting this measurement took three attempts: a `git checkout` of the PR
branch silently failed (stderr suppressed), so I twice ran against the
wrong tree and nearly concluded the fix did not work. HEAD and
dirty-count are printed beside every number above for that reason.
Found by chasing the deterministic half of #3264 (dashboard red on
`main`). **The tests were right; the product is broken.**
## The bug
Open **Create Task** from a research finding, type a title before the
board workflows settle, and the field silently reverts to the derived
default `Research: <heading>`. The task is then created with a title the
operator did not write. `description`, `priority` and `taskId` reset the
same way.
`ResearchTaskActionModal` reset those four fields in the same effect
that fetched the task list, and that effect's dependency list carried
`isArchivedColumn`:
```ts
const isArchivedColumn = useMemo(() => { … }, [boardWorkflows]); // useBoardWorkflows() — async
useEffect(() => {
setTitle(`Research: ${finding.heading || run.title}`); // ← re-runs on every revalidation
…
}, [open, mode, projectId, finding.heading, preview, run.title, isArchivedColumn]);
```
`useBoardWorkflows` resolves and revalidates asynchronously, so the
memo's identity changes and the reset re-runs over whatever the operator
has typed.
Introduced by #3215, which correctly added the archived-column filter
but hung its dependency on an effect that also owns form state. Same
class as the documented
`docs/solutions/ui-bugs/skill-autocomplete-highlight-reset-on-swr-revalidation.md`.
## The fix
Split into two effects: the reset depends only on what it derives from;
the fetch keeps `isArchivedColumn`. No behaviour change to the archived
filter — #3215's guard is untouched.
## Verification, both directions
The three standing `ResearchView` tests fail without this and pass with
it:
```
isArchivedColumn back on the reset effect: 3 failed | 24 passed (27)
as committed: 27 passed (27)
```
## What I tried and removed, because it matters
I wrote a dedicated invariant test (per "fix the invariant, not the
repro") asserting that *all* typed fields survive a revalidation. **I
deleted it, because it did not work.**
- First draft used `mockImplementationOnce` to defer
`fetchBoardWorkflows`. `ResearchView` resolves board workflows on mount,
so that once-implementation was consumed before the modal opened.
Reverting the product fix left the test **green** — it proved nothing.
- Second draft deferred *every* call. `beforeEach` uses
`vi.clearAllMocks()`, which clears calls but **not implementations**, so
the deferral leaked into later tests and left `fetchBoardWorkflows`
permanently pending — masking two of the three genuine failures. The
revert then showed `1 failed` instead of `3`, i.e. my test was hiding
real bugs.
Rather than ship a regression test that cannot regress, I removed it.
The three existing tests already fail without the fix, which is real
coverage; a broader invariant test needs a modal-level harness that
resets implementations between cases, and that is worth doing properly
rather than badly here.
## Scope
Also in #3264: `TaskCard.badge-wrap` (1 deterministic failure, unrelated
— CSS/layout), and `useChat` / `WorkflowNodeEditor` /
`PlanningModeModal`, which pass standalone and are cross-file
contamination, not product bugs. Untouched here; the issue has the
per-file matrix.
No changeset — `@fusion/dashboard` is private.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Preserved form entries during workflow revalidation in the research
task modal.
* Limited task selection to active workflow columns when enriching
findings.
* Prevented outdated task results from replacing newer selections.
* Improved loading and task-list behavior when source findings or modal
state changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>