Resolve conflicts from main's analytics schema additions (plugin
activations, per-model token buckets) against the PR's contract-assertion
type column:
- db.ts: renumber behavioral-verification migration 124 -> 126, bump
SCHEMA_VERSION to 126 so it follows main's migrations 124/125
- core/roadmap tests: adopt main's SCHEMA_VERSION-constant assertions
instead of stale literal 124
- test-quarantine.json: keep all four quarantine entries from both sides
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add required `---` frontmatter fences to the changeset so the
changesets CLI picks it up during release (greptile P1).
- Mark the remote version as reloaded only when a reload actually
proceeds, so a pre-existing RELOAD_FLAG no longer permanently
suppresses future reloads for that version (greptile P2).
- Bump service-worker cache to v5 after merging main's v4 and update
the pwa test assertions accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align dashboard navigation styling and tests with defined spacing and status tokens.
- Replace undefined sidebar CSS tokens with defined dashboard values while preserving circular and todo-active styling.
- Update the mobile nav touch-target test to assert the tokenized 36px minimum height.
- Add FNXC comments documenting the token requirements behind the fixes.
Files changed:
packages/dashboard/app/__tests__/mobile-nav-bar-css.test.ts | 3 ++-
packages/dashboard/app/components/LeftSidebarNav.css | 12 ++++++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-6809
Fusion-Task-Lineage: 43ddb9cb-5ead-4289-9254-6c0fa5c3af40
- Reformat shard-floor justification as an FNXC:TestInfrastructure comment
(project-standards: AGENTS.md FNXC_LOG convention).
- Clarify that the shard and dashboard-lane 15min floors are not coupled and
may diverge (maintainability: avoid implying an unenforced contract).
- Add a regression-guard test pinning shard.floor=15min and asserting a 525s
derived budget clamps up to the floor, so an accidental revert to the old
5min floor fails loudly (correctness + testing + project-standards).
The Full Suite (non-blocking) workflow has been red for 30+ runs on main.
Diagnosis: the @fusion/engine [1/2], [2/2] and @fusion/core [2/2] shard
slices were SIGKILLed at their watchdog budgets (405s/405s/338s), not because
they hang but because those budgets are too tight for current wall-clock.
Local baselines (this machine, all pass, exit 0):
- engine [2/2]: 145s wall / 309 files
- core [2/2]: 283s wall / 172 files (old budget was only 338s!)
deriveBudgetMs tightens the budget to expected*3.5 whenever the committed
scripts/test-timings.json is <30d old. The snapshot (2026-06-03) undercounts
the import- and real-git-subprocess overhead of these heavy slices, so the
'fresh' snapshot produced a too-tight, false-kill budget on slower CI runners
-- the exact failure mode the floor/ceiling band exists to prevent.
Fix (plan KTD-2): raise the shard band floor 5min -> 15min so the heaviest
slices can't be tightened into a false-kill, while a true hang is still bounded
far under the job's 60min ceiling. Mirrors the dashboard-lane heavy-lane floor.
Follow-up: refresh scripts/test-timings.json from a default-branch CI run.
Keep the experimental left sidebar Settings control clear of the fixed executor footer while preserving readable project selector fallbacks.
- Share the executor footer visibility state with the left sidebar and project content.
- Reserve executor-footer height on the sidebar when the status footer is visible so Settings stays clickable in expanded and collapsed modes.
- Add project-selector translation fallbacks for incomplete locale fixtures.
- Cover footer-clearance behavior in left sidebar tests and add a patch changeset.
Files changed:
.changeset/fn-6819-sidebar-footer-clearance.md | 5 +++
packages/dashboard/app/App.tsx | 8 +++--
.../dashboard/app/components/LeftSidebarNav.css | 9 ++++++
.../dashboard/app/components/LeftSidebarNav.tsx | 4 ++-
.../dashboard/app/components/ProjectSelector.css | 2 +-
.../dashboard/app/components/ProjectSelector.tsx | 11 +++++--
.../components/__tests__/LeftSidebarNav.test.tsx | 36 ++++++++++++++++++++++
7 files changed, 67 insertions(+), 8 deletions(-)
Fusion-Task-Id: FN-6819
Fusion-Task-Lineage: 950da852-8dcf-4379-a91b-0188b12d1a6c
Align workflow switcher status count badges with their board column colors.
- Map Todo, In Progress, and Done workflow count badge text to the matching board column CSS tokens.
- Cover the badge color contract with a CSS fixture regression test.
- Document the color-token behavior for board and list workflow dropdown counts.
Files changed:
docs/dashboard-guide.md | 2 +-
.../dashboard/app/components/WorkflowSwitcher.css | 10 +++++++---
.../components/__tests__/WorkflowSwitcher.test.tsx | 22 ++++++++++++++++++++++
3 files changed, 30 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-6805
Fusion-Task-Lineage: aab7140c-ad37-4bd8-b73d-199534285b37
- TaskChatTab: add FNXC:TaskDetailChat prefix/date to the ephemeral
active-session comment, matching the file's comment convention.
- useModalManager: clear stale planning/subtask resume-session id (and
initial plan) when opening a fresh planning/subtask flow, so the modal
no longer reopens into a prior session.
- boardWorkflowsCache: validate cached workflow item shape (id/name/
columns) and taskWorkflowIds value types so a malformed cache entry
can't pass and later crash Board on render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reliability (P3): emit an `Auto-recovered:`-prefixed log on the
benign-todo stale-failure clear path so NotificationService
proactively cancels the pending failure timer (recoveredStatus path)
instead of relying only on the fire-time re-check, which is
race-contingent when failureNotificationDelayMs is near 0. Scoped to
the actual-clear path so the common no-failure re-queue isn't
mislabeled as a recovery.
- Project-standards (P3): add the required yyyy-MM-dd-hh:mm stamp to the
new FNXC comments (AGENTS.md FNXC_LOG convention).
- Maintainability (P3): extract the scheduler "queued" waiting marker to
a named SCHEDULER_WAITING_STATUS constant.
- Testing: pin the guard's skip on a clean todo row, assert the
Auto-recovered log fires on the stale-failure path, and add a
paused+unassigned in-progress idle case (paused early-return wins over
the ephemeral active-session path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the in-progress fix for in-review: a reviewer/merger runs
ephemerally with no assignedAgentId/checkedOutBy, so an in-review task
in an active review/merge status (reviewing, merging, merging-fix,
fixing) now reads as a live session without an assignment. A null-status
in-review row is awaiting human review, not actively worked, so it stays
assignment-gated and idle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isActiveAgentSession required a persistent assignedAgentId/checkedOutBy,
but the scheduler only sets those when ephemeralAgentsEnabled === false
(scheduler.ts:1857). The default is ephemeralAgentsEnabled: true
(settings-schema.ts:351), so an actively-executing in-progress task
never has either field and always read as idle, showing the
"No agent is working on this task right now" hint while an agent was
running. sessionLive only covers live CLI sessions, not ephemeral runs.
Treat assignment as sufficient-but-not-necessary: a non-blocked,
non-"queued" in-progress task is a live agent session on its own.
"queued" (the waiting marker) stays assignment-gated and in-review is
unchanged, so existing FN-6314 idle/active behavior is preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A pause/resume abort parked status:"failed" on an earlier non-todo
observation stays dispatchable (scheduler filters on column+paused, not
status, scheduler.ts:1288) and re-enters the FN-6782 benign-todo branch.
That branch logged "benign" but left status:"failed"/error on the row,
so the board kept showing it failed and the deferred failure
notification fired (notification-service fire-time check sees
status==="failed") — contradicting the benign log. recoverPausedAbortFailures
that would clear it is suppressed during global/engine pause
(self-healing.ts:8125), so the failure survived the pause window.
Reconcile the row with the benign reclassification: clear status/error
when the live row carries them, so the board agrees it's benign and the
pending notification is suppressed at dispatch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the sidebar brand-row collapse control with a floating border toggle.\n\n- Remove the empty sidebar brand shell and keep the collapse control reachable in both expanded and rail modes.\n- Style the collapse button as a floating affordance on the sidebar edge.\n- Cover the missing brand shell, accessible toggle labels, pressed state, and persisted collapsed state in sidebar tests.\n- Update dashboard documentation to describe the border-mounted toggle.\n\nFiles changed:\n docs/dashboard-guide.md | 2 +-\n .../dashboard/app/components/LeftSidebarNav.css | 30 +++++++++++-----------\n .../dashboard/app/components/LeftSidebarNav.tsx | 28 ++++++++++----------\n .../components/__tests__/LeftSidebarNav.test.tsx | 27 +++++++++++++++++++\n 4 files changed, 56 insertions(+), 31 deletions(-)
Fusion-Task-Id: FN-6801
Fusion-Task-Lineage: e1384565-c0cc-4564-b556-f5b74a501c41
Add the Shadcn color family to dashboard theme selection and persisted theme validation.
- Register blue, green, red, purple, pink, orange, yellow, mono, and black Shadcn theme IDs across core and dashboard selectors.
- Add swatches and CSS token blocks for light and dark variants, including bootstrap loading coverage.
- Document the expanded theme count and include a minor changeset for the published CLI package.
Files changed:
.changeset/shadcn-color-variants.md | 5 +
docs/dashboard-guide.md | 2 +-
packages/core/src/types.ts | 10 +
.../dashboard/app/components/ThemeSelector.css | 126 +++
packages/dashboard/app/components/themeOptions.ts | 9 +
.../dashboard/app/hooks/__tests__/useTheme.test.ts | 41 +
packages/dashboard/app/index.html | 2 +-
packages/dashboard/app/public/theme-data.css | 1177 ++++++++++++++++++++
8 files changed, 1370 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-6756
Fusion-Task-Lineage: 143e717a-9718-419f-a6c5-34305e53c65d
Normalize mobile bottom navigation icon slots so unread indicators no longer skew perceived spacing.
- Wrap every mobile nav icon in a consistent token-sized slot.
- Keep chat unread dots inside the icon slot instead of protruding past it.
- Extend MobileNavBar tests to assert icon-slot structure and spacing CSS.
- Add a patch changeset for the published Fusion package.
Files changed:
.changeset/fn-6800-mobile-nav-spacing.md | 5 ++++
packages/dashboard/app/components/MobileNavBar.css | 22 ++++++++++++-----
packages/dashboard/app/components/MobileNavBar.tsx | 28 ++++++++++++++++------
.../app/components/__tests__/MobileNavBar.test.tsx | 24 ++++++++++++++++++-
4 files changed, 65 insertions(+), 14 deletions(-)
Fusion-Task-Id: FN-6800
Fusion-Task-Lineage: 66ac21cf-6761-4b2c-b2ae-723998ae2632
Addresses review feedback (Greptile P1/P2, CodeRabbit) on the async
background integrity check. All three issues shared one root cause:
state read/cleared from a pre-await snapshot across the async boundary.
- Closed-instance race (P1): runBackgroundIntegrityCheck guards `this.closed`
before every in-process integrityCheck() call (both before the offload and
after the await). A connection closing mid-offload no longer throws out of
the scheduler.
- Stuck-pending on throw + late-subscriber race (P1/P2): the scheduler now
clears integrityCheckPending UNCONDITIONALLY in a finally, iterating the
CURRENT shared.subscribers set rather than a pre-await snapshot. A thrown
check, or a Database that subscribed during the seconds-long await window,
can no longer be left pinned at integrityCheckPending=true forever.
- No spawn timeout (P2): integrityCheckSqliteFileAsync now passes
AbortSignal.timeout(5m); a disk-stalled/hung sqlite3 child always settles
the promise (→ verified:false fallback) instead of leaking and stranding
the shared entry. AbortSignal.timeout's timer is unref'd, so it never keeps
the process alive at shutdown.
- FNXC markers added to the two new symbols (per AGENTS.md convention).
Rejected: the suggestion to remove the `// FNXC:Database` marker — FNXC is a
mandatory project convention (AGENTS.md), and a prior review flagged the
absence of these markers as a defect.
Tests: regression test that pending clears for all participants when the
check throws; test that runBackgroundIntegrityCheck returns ok without
throwing on a closed instance; strengthened the healthy-DB assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The boot-time integrity check (scheduleBackgroundIntegrityCheck, ~60s
after init) ran PRAGMA integrity_check on the live connection, walking
every page and freezing the event loop for several seconds — the largest
single read-contention stall in normal operation.
Offload it to the sqlite3 CLI in a child process (async spawn), matching
the existing out-of-process pattern used by quickCheckSqliteFile and
.recover. The CLI connection is opened -readonly so it can never
checkpoint or write the live WAL; this works because the live process
holds the DB open (so the -shm exists). When the CLI is unavailable or
can't open read-only, fall back to the in-process check (verified=false),
preserving today's behavior on those environments.
- New integrityCheckSqliteFileAsync(dbPath, limit) module helper.
- New private runBackgroundIntegrityCheck() seam (offload + fallback) so
the scheduler has one testable, deterministic policy point.
- Background scheduler callback is now async (IIFE + finally) with errors
swallowed so a background timer can't crash the process.
VACUUM is intentionally NOT offloaded: the call graph shows it is invoked
only by the `fn db vacuum` CLI command and tests, never from the periodic
maintenance loop, so it is not a background event-loop stall — and an
out-of-process VACUUM on a live WAL DB would add corruption surface for no
hot-path benefit.
Tests updated to the async/offloaded seam (deterministic regardless of
whether the sqlite3 CLI exists in the environment), plus coverage for the
new helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigation of reported DB read contention found two cross-process
contention sources in the SQLite layer (single synchronous node:sqlite
connection per process, WAL mode):
- Unbounded WAL on central-db and archive-db. Neither set
journal_size_limit, so their WAL never truncated back down after a
checkpoint and every reader paid an ever-growing WAL-index scan. Add
journal_size_limit=4MB (matching db.ts) plus explicit
synchronous=FULL/wal_autocheckpoint=1000 for intent. central-db is the
most cross-process-shared DB; archive-db had the same latent gap.
- vacuum() held the EXCLUSIVE lock past its own runtime. Resetting
locking_mode to NORMAL does not drop the WAL exclusive lock until the
connection next touches the DB, so other processes stayed locked out of
reads (SQLITE_BUSY) until some unrelated query ran. A plain read does
NOT release it in WAL mode (verified); a PASSIVE checkpoint does. Run
one in the finally, guard the locking_mode reset so it can't mask the
original error or skip the release, and log swallowed failures.
Tests: assert the new PRAGMAs on central-db and archive-db, and that a
second connection can read immediately after vacuum() returns.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses findings from a multi-agent review of the two prior fixes.
P0 (executor.ts): the stale-conflict recovery force-removed worktreePath with
no bounds check; that path can come from a git admin entry resolving outside
.worktrees/. Now refuses unless the path is inside the worktrees dir, not a
symlink (realpathSync), not a registered worktree, and not actively owned, and
re-verifies liveness in the catch instead of trusting the error string. Also
excludes spawn failures (spawn git ENOENT) from the stale-path classification.
worktree-pool.ts: resolveGitdirPointer -> dotGitPointerIsDangling. Reaps only
when a .git link's gitdir target is confirmed missing; a real .git dir,
unparseable pointer, or any read/stat failure is treated as NOT dangling
(conservative) so a transient read error on a live worktree can't trigger rm.
Drops the string|"directory"|null sentinel union.
core store.ts: bypass the reconcile recency window when the live task table is
empty (corruption/restore: surviving task.json keep old mtimes) and when
fusion.db was auto-recovered on startup, so .recover row loss isn't stranded.
Adds an ignoreRecencyWindow option.
Tests: executor recovery + out-of-bounds refusal, unparseable .git skip,
recency boundary, empty-DB/forced bypass. engine 135 + core 12 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On restart, reconcileOrphanedTaskDirs re-imported ancient .fusion/tasks/<id>/
directories that had no DB row, surfacing old low-numbered tasks (FN-001, ...)
onto the live board — looking like "all task IDs reset / starting over".
The sweep is meant to recover dirs that appear after store init (heartbeat
races) or rows lost to recent DB corruption. Modern deletes leave a soft-delete
tombstone (caught by taskIdExistsAnywhere), but legacy hard-deletes left none,
so a months-old task.json with no DB row was silently re-imported.
Gate recovery on a 7-day recency window (task.json mtime). Older orphans are
skipped (reason: stale-orphan-dir-beyond-recency-window) and left for explicit
recovery or directory cleanup; heartbeat-race and recent-corruption recovery
still work. Adds a regression test for the stale-dir skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directories under .worktrees/ that survive with a dangling .git pointer
(present on disk, but their .git/worktrees/<name> admin entry is gone) are
invisible to `git worktree list`/`prune` yet collide with freshly generated
worktree names. The executor's conflict cleanup then fails with
"is not a working tree", failing the workflow graph at node 'execute' after
3 attempts.
- executor.ts: extend FN-4813 stale-conflict recovery to also treat
"is not a working tree" and ENOENT (not just "validation failed, cannot
remove working tree") as "no live worktree here" — prune the admin entry,
force-remove the leftover dir, and proceed with fresh creation.
- worktree-pool.ts: reapOrphanWorktrees skipped any dir on mere .git-file
presence, contradicting its own documented invariant. Resolve the .git
pointer and only skip when the gitdir target exists; reap dangling
pointers like any other orphan so they stop accumulating across runs.
- Tests for both the dangling (reaped) and valid (skipped) .git cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a selectable business workflow for managing lead-generation tasks.
- Register a built-in lead-generation workflow with custom business columns, lead fields, and stage prompts.
- Cover workflow registration, column traits, field metadata, compilation, and IR round-tripping with core tests.
- Document the workflow catalog entry and add a minor changeset for the published CLI package.
Files changed:
.changeset/fn-6759-lead-generation-workflow.md | 5 +
docs/workflow-steps.md | 4 +
.../builtin-lead-generation-workflow-ir.test.ts | 123 ++++++++++++++++++
.../core/src/__tests__/builtin-workflows.test.ts | 10 ++
.../src/builtin-lead-generation-workflow-ir.ts | 141 +++++++++++++++++++++
packages/core/src/builtin-workflows.ts | 20 +++
packages/core/src/index.ts | 1 +
7 files changed, 304 insertions(+)
Fusion-Task-Id: FN-6759
Fusion-Task-Lineage: 6c724f84-a6a6-4b01-93ec-cabb9387be2f