- workflow-graph-executor: wrap each post-merge walk() in try/catch so a malformed
post-merge IR / traversal error is logged and skipped, never flipping an already-
merged task to failed (non-blocking post-merge contract) [T9, real bug].
- Refresh stale FNXC comments now that graphNativePostMerge is default-ON and the
legacy merger post-merge path was removed (experimental-features, workflow-graph-
executor, workflow-graph-post-merge.test) [T6/T7/T8].
- Normalize FNXC timestamps to yyyy-MM-dd-hh:mm (TaskCard.test, taskProgress.test) [T2/T3].
- Changeset: category fix → feature to match the minor bump [T0].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a focused engine regression guard for user-configured command paths.
- Add a static Vitest registry that slices protected configured-command helpers and fails on execSync usage.
- Assert each protected command path keeps bounded async safeguards such as timeout, maxBuffer, or maxLifetimeMs.
- Document the guard and its deliberate git-plumbing exclusions in the testing guide.
Files changed:
docs/testing.md | 5 +
.../user-configured-command-no-execsync.test.ts | 288 +++++++++++++++++++++
2 files changed, 293 insertions(+)
Fusion-Task-Id: FN-7056
Fusion-Task-Lineage: b92f19cf-526e-4345-b18e-869a278e0e10
Final cutover — nothing reads workflow_steps at runtime, so migration 131 drops it.
- Removed the merger post-merge execution path entirely (runPostMergeWorkflowSteps,
hasEnabledPostMergeWorkflowSteps, executePostMerge{Prompt,Script}Step, post-merge
worktree helpers + call site). Graph owns post-merge.
- Executor recovery no longer reads getWorkflowStep().gateMode; gate-ness comes from the
recorded WorkflowStepResult.status.
- Removed store CRUD (create/update/deleteWorkflowStep), materializeWorkflowSteps, and
migrateLegacyWorkflowSteps; selectTaskWorkflow now seeds default-on optional-group node
ids (consistent with create-time). KEPT the plugin step-template palette (getWorkflowStep
plugin-only resolver / listWorkflowSteps plugin-only) — never touches the table.
Removed the dashboard migrate-legacy-steps route + editor migration UI.
- SCHEMA_VERSION 130→131; migration 131 DROP TABLE IF EXISTS workflow_steps; SCHEMA_SQL
table def removed; historical migrations 77/105/109/130 guarded with tableExists().
Proof nothing stranded: the graph executes IR nodes resolved from workflowId (never
stepIds/compiled rows) — materializeWorkflowSteps writes were vestigial. Full @fusion/core
suite (6290), reliability backstop (154), boot smoke, and a seed-at-130 drop test all pass
with the table gone.
Plan U7c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes graph-native post-merge the default and cuts the merger's legacy post-merge
path over so post-merge runs exactly once (via the graph), with migration data prep.
- experimentalFeatures.graphNativePostMerge → default ON; merger
hasEnabledPostMergeWorkflowSteps/runPostMergeWorkflowSteps become inert when on
(no double-run; proven by a new no-double-run test). Merger code kept (U7c removes it).
- Migration 130 (SCHEMA_VERSION 129→130) rewrites each task's enabledWorkflowSteps
entries that are legacy built-in pre-merge workflow_steps row ids → the optional-group
node id (browser-verification/code-review); dedupes; idempotent; identity-stable;
leaves node-ids/compiled/custom entries untouched. Table KEPT (U7c drops it).
- Investigation (real DBs): NO custom/plugin post-merge steps exist; the only post-merge
step is compound-engineering's 'document' graph node — so the merger no-op strands
nothing. Custom-step re-pointing was verified unnecessary and skipped.
Safe-to-drop in U7c still blocked by live readers: merger post-merge fns, store CRUD,
migrateLegacyWorkflowSteps/readConfig materialization (executor recovery reader is
already null-safe→advisory).
Plan U7b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore mobile horizontal panning from direct touches on Agent Detail tab buttons.
- Apply pan-x touch-action to Agent Detail tab buttons alongside the tab strip.
- Cover both base and mobile tab styles so the global mobile pan-y lock does not block button-origin swipes.
- Extend the mobile scroll regression test to assert tab buttons expose horizontal touch panning.
- Add a patch changeset for the published CLI package.
Files changed:
.changeset/fn-7052-agent-detail-mobile-tab-touch-action.md | 7 +++++++
packages/dashboard/app/components/AgentDetailView.css | 5 +++++
.../components/__tests__/AgentDetailView.mobile-scroll.test.tsx | 8 ++++++--
3 files changed, 18 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7052
Fusion-Task-Lineage: 42921948-f940-46d6-8c86-c159e94603b5
Keep workflow container fallback layouts connected in the editor.
- Space fallback and auto-layout columns by each node's rendered width so wide containers do not overlap adjacent handles.
- Treat incomplete top-level saved layouts as stale and fall back to a coherent graph layout.
- Cover optional-group, foreach, and mobile graph mapping with regression tests.
Files changed:
.changeset/fn-7016-optional-group-edges.md | 7 +
.../__tests__/workflow-auto-layout.test.ts | 18 +++
.../__tests__/workflow-flow-mapping.test.ts | 154 ++++++++++++++++++++-
.../__tests__/workflow-mobile-graph.test.ts | 37 ++++-
.../app/components/workflow-auto-layout.ts | 28 +++-
.../app/components/workflow-flow-mapping.ts | 40 +++++-
6 files changed, 271 insertions(+), 13 deletions(-)
Fusion-Task-Id: FN-7016
Fusion-Task-Lineage: a009fc2d-05cb-41cc-8be1-c5522f7398ff
Adds graph-native post-merge step execution behind experimentalFeatures.graphNativePostMerge
(default OFF — byte-identical behavior until enabled). After a successful merge-attempt
(the merge seam awaits the merge Promise), the graph runs post-merge optional-group nodes
and records phase:"post-merge" results, non-blocking. The merge-region traversal hop is
inert when the flag is off, and empty for builtin:coding even flag-on (its merge exits only
reach merge-region nodes or end), so the parity oracle holds. Optional-group recording now
derives phase + log prefix from config.phase (defaults pre-merge). Adds postMergeOptionalGroupNode
factory for migration/custom workflows. Legacy merger post-merge path untouched (U7b cutover).
Plan U7a.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the Global Models pricing editor into a compact summary with a modal table for detailed edits.
- Add a View pricing table modal with escape, overlay, and close-button dismissal.
- Keep add, edit, delete, and LiteLLM fetch workflows available while showing override counts in the collapsed settings view.
- Update model pricing tests, styling, and documentation for the collapsed summary and modal editor.
Files changed:
docs/dashboard-guide.md | 2 +-
docs/settings-reference.md | 4 +-
.../settings/sections/ModelPricingSection.css | 36 +++++-
.../settings/sections/ModelPricingSection.test.tsx | 62 ++++++++-
.../settings/sections/ModelPricingSection.tsx | 144 +++++++++++++++------
5 files changed, 198 insertions(+), 50 deletions(-)
Fusion-Task-Id: FN-7049
Fusion-Task-Lineage: 9ba63354-28e7-4717-81e6-805f1f05fd97
Render browser-native previews for right-dock file selections while preserving text editing behavior.
- Add image, video, audio, and PDF preview rendering through the workspace download URL.
- Keep previewable and known binary files out of the text editor, with a read-only fallback for unsupported binaries.
- Cover compact and two-pane dock preview behavior with tests, docs, and a patch changeset.
Files changed:
.changeset/fn-7031-dock-files-binary-preview.md | 7 +
docs/dashboard-guide.md | 2 +-
.../dashboard/app/components/DockFilesView.css | 45 ++++++
.../dashboard/app/components/DockFilesView.tsx | 85 ++++++++++-
.../components/__tests__/DockFilesView.test.tsx | 170 ++++++++++++++++++++-
5 files changed, 303 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-7031
Fusion-Task-Lineage: a52bdd57-bb3e-494e-85a8-81326a1548e2
Render the Automations modal in the shared floating-window shell while preserving embedded and mobile behavior.
- Wrap the modal presentation in FloatingWindow with persisted geometry, drag handle, and resize sizing.
- Update Automations modal CSS so desktop fills the floating shell and mobile remains full-screen with resize handles hidden.
- Cover floating-window rendering, dragging, resizing, mobile CSS contract, and embedded mode in ScheduledTasksModal tests.
- Add a minor changeset for the published Fusion package.
Files changed:
.changeset/fn-7036-automation-modal-floating.md | 7 ++
.../app/components/ScheduledTasksModal.tsx | 43 +++++----
packages/dashboard/app/components/ScriptsModal.css | 78 +++++++++-------
.../__tests__/ScheduledTasksModal.test.tsx | 103 +++++++++++++++++++--
4 files changed, 172 insertions(+), 59 deletions(-)
Fusion-Task-Id: FN-7036
Fusion-Task-Lineage: 3d6926b7-ec6d-4179-ad17-573bcfef297f
From the U1-U6 code review (correctness/adversarial/reliability/maintainability):
- Delete dead code left by the runWorkflowSteps removal: parkTaskAfterWorkflowStepPause,
handleWorkflowRevisionRequest (+ createWorkflowRevisionFollowUpTask,
injectWorkflowRevisionInstructions), handleWorkflowStepFailure, the dead
partitionWorkflowRevisionFeedback export + its test, and 2 orphaned jsdocs;
reword 2 stale comments (executor.ts FN-6722, self-healing.ts jsdoc).
- Record step output/notes on graph workflow-step results: runGraphCustomNode now
emits contextPatch:{output,notes} so the Workflow tab shows real review feedback
and [pre-merge] revision logs carry detail (was always the fallback before).
- Sync the workflow-step seam contract: core (workflow-compiler SEAM_NAMES/order,
workflow-ir column map, builtin-workflow-prompts) now rejects the workflow-step
seam to match the engine's resolveSeamName, preventing a latent run-time crash on
a persisted/cloned def that core would otherwise parse.
Residual (tracked in the PR): re-introduce the FN-4343 per-step scope gate on the
graph path; the parked-failed recovery log wording; malformed-advisory->passed edge
case; recording for non-optional-group/split-branch step realizations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
U6: delete the built-in WORKFLOW_STEP_TEMPLATES catalog + its materializer
(getBuiltInWorkflowTemplate/ensureWorkflowStepForTemplate/toBuiltInWorkflowStep);
inline the browser-verification + code-review name/prompt/toolMode/gateMode into
their optional-group IR builders (node bytes unchanged); simplify
resolveEnabledWorkflowSteps to an identity-stable pass-through (no materialization,
so the optionalGroupIdSet collision guard is no longer needed). Plugin-contributed
step templates are kept as the editor palette.
U5: remove the legacy /api/workflow-steps REST surface (GET/POST/PATCH/DELETE +
/refine + /workflow-step-templates/:id/create), the dead client fns, and the
Settings management UI; GET /api/workflow-step-templates now serves plugin
templates only. The create-time optional-step toggles remain.
Scope: the workflow_steps store CRUD + table are intentionally KEPT — still consumed
by the engine (merger/recovery) and needed by U7's migration; their removal + the
table drop land in U7.
Plan U5 + U6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the oversized Command Center area test suite into focused files while preserving shared fixtures and coverage.
- Extract shared date ranges, fixtures, and helpers into areas.test-harness.tsx.
- Move GitHub and Signals area assertions into a dedicated sibling suite.
- Keep the remaining areas.test.tsx below the 2000-line cap with existing coverage intact.
Files changed:
.../areas/__tests__/areas.github-signals.test.tsx | 406 +++++++++++++
.../areas/__tests__/areas.test-harness.tsx | 302 ++++++++++
.../command-center/areas/__tests__/areas.test.tsx | 648 +--------------------
3 files changed, 730 insertions(+), 626 deletions(-)
Fusion-Task-Id: FN-7044
Fusion-Task-Lineage: 63886ec9-e9c3-410e-bbc6-c5116cddb168
Fix the workflow results settings display for configured optional steps.
- Treat workflow step definitions as missing only when the lookup has no entry.
- Preserve empty descriptions for found optional-group steps such as Code Review.
- Add regression coverage for configured, checkbox, and ordering displays.
- Add a patch changeset for the published Fusion package.
Files changed:
.../fn-7041-workflow-step-definition-not-found.md | 7 +++++
.../app/components/WorkflowResultsTab.tsx | 9 +++++-
.../__tests__/WorkflowResultsTab.test.tsx | 36 ++++++++++++++++++++++
3 files changed, 51 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7041
Fusion-Task-Lineage: 84698437-d4d3-4b68-9c5f-c8b15f513230
## Problem
Operators intermittently saw **"all my global settings reset"** —
including the global concurrency cap surfacing as `gate=semaphore` with
a low value in scheduler queue logs.
## Root cause
`CentralCore` is supposed to live at `~/.fusion/fusion-central.db`, but
`resolveGlobalDir(dir)` returns an explicit dir **verbatim**, and
several production call sites passed the **project** `.fusion/` dir:
- `store.getSecretsStore()` → `new CentralCore(store.getFusionDir())`
- dashboard secrets/proxy/node/secrets-sync/settings-sync routes → `new
CentralCore(store.getFusionDir())`
Each spawned a **stray per-project central DB**
(`<project>/.fusion/fusion-central.db`) seeded with **default** global
state (`globalMaxConcurrent=4`, empty secrets, default
`centralSettings`) that shadowed the real global DB whenever a
read/write hit one of those paths. Confirmed on disk: 14+ stray DBs at
default `4` vs the real `~/.fusion` at the operator's actual value.
## Fix
- Add `TaskStore.getGlobalSettingsDir()` returning the **resolved global
dir** (`string`); route the secrets store + all the affected dashboard
routes through it instead of `getFusionDir()`.
- `getSecretsStore()` also passes the global dir to `MasterKeyManager`
(co-locates the master key; makes the path test-exercisable).
- Add a `resolveGlobalDir()` **guard** that throws on a project-local
`.fusion/` dir (basename `.fusion` with a `.git` parent), with an
explicit `FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR` opt-out for
legitimately version-controlled custom global dirs. Inert under VITEST.
## Tests
- `global-settings-guard.test.ts` — guard rejects project/worktree
`.fusion` dirs, allows home + custom non-repo dirs.
- `store-secrets-store-global-dir.test.ts` — **symptom-based**:
`getSecretsStore()` creates the central DB in the global dir and
**never** spawns a stray project-local `fusion-central.db`.
- Added `getGlobalSettingsDir()` to route-test mock stores (the new
getter is now called by the routes).
## Operator note
Existing stray project-local `fusion-central.db` files (all
default/empty in practice) should be removed; on the affected machine
they were quarantined to `.legacy-central-db-backup-*` folders.
## Verification
- `pnpm --filter @fusion/core run typecheck` / `@fusion/dashboard`
typecheck — clean
- core guard + regression tests pass; previously-affected route suites
(proxy, nodes-sync, browse-directory, secrets-sync) pass (313/313)
- `pnpm check:changesets` — clean; eslint on changed files — clean
Companion to #1786 (global concurrency slider UI), which is independent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1787">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
</picture>
</a>
<!-- stage-review-badge-end -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed intermittent resets of global settings (including the global
concurrency cap).
* Ensured global settings/secrets are read from and written to the
correct global storage location rather than a project-local one.
* Added a safeguard to prevent using a project-local settings directory
when running inside a repository.
* **Tests**
* Added regression coverage for global directory resolution/guard
behavior.
* Updated route and secrets sync tests to validate the global directory
selection used for proxying and sync.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## What
Adds a **Global Max Concurrent** slider to two operator surfaces and
groups the Scheduling settings by scope.
- **Footer engine menu** (`EngineControlMenu`) and **Command Center →
Concurrency card** (`CommandCenterControls`) each get a global-cap
slider.
- **Settings → Scheduling** (`SchedulingSection`) now groups fields
under labeled **"Global — applies to all projects"** and **"This
project"** subheadings with scope badges (clearer on mobile, and
surfaces the existing `globalMaxConcurrent` field that was easy to
miss).
## How
Both sliders are backed by a single shared `useGlobalConcurrency` hook
(module-level store + subscribers), so they read/write **one source of
truth** and **revalidate after every `PUT /api/global-concurrency`**.
This avoids the last-writer-wins / stale-clobber races that two
independent per-component caches would cause (which would ironically
re-create a "global cap resets" symptom). The hook also:
- treats a fetch error as **non-interactive** (slider disabled, not
stuck showing `1` and persistable),
- surfaces a **save-state** indicator (Saving…/Saved/Save failed)
mirroring the project sliders,
- **flushes a pending edit on menu close / unmount** so a quick
drag-then-dismiss isn't silently dropped.
## Review
These race/feedback behaviors were hardened in response to a multi-agent
`/code-review` (correctness, adversarial, reliability, frontend-races
all flagged the per-component-cache approach). Independent of the
companion fix PR (global-settings reset).
## Verification
- `pnpm --filter @fusion/dashboard run typecheck` — clean
- eslint on changed files — clean
- Not yet browser-verified (next step).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1786">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
</picture>
</a>
<!-- stage-review-badge-end -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a global concurrency slider that applies across all projects.
* Global and per-project scheduling controls are now clearly separated
with scope labels and badges.
* The concurrency setting now stays in sync across different dashboard
views and saves automatically after brief pauses.
* **Bug Fixes**
* Improved feedback for loading, saving, and error states.
* Prevented confusion between global settings and project-specific
settings, especially on smaller screens.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Removes the legacy workflow-step EXECUTION path now that the graph records results
(U2): delete runWorkflowSteps(), the workflow-step seam + runWorkflowStep primitive
(runtime-primitives, workflow-node-handlers, authoritative-driver), and the legacy
execute() step blocks. Keeps task.workflowStepResults + its store write path (the
graph's sink) and executeWorkflowStep/executeScriptWorkflowStep (reused by the graph).
- Watchdog recoverCompletedTask now re-enters via maybeExecuteWorkflowGraph so the
graph re-runs pending gates, records results, and owns the in-review/back-for-fix
transition (KTD-2).
- maybeExecuteWorkflowGraph fails CLOSED (parks) when a store lacks
getTaskWorkflowSelection AND the task has enabled pre-merge steps — closing the
FN-7039 silent-skip class without changing minimal-store implementation runs (KTD-5).
KNOWN GAP (follow-up): the FN-4343 per-step workflowStepScopeEnforcement leak check
lived only in runWorkflowSteps and is NOT yet replicated on the graph path. Merge-time
File Scope enforcement (FileScopeViolationError, squash overlap) is unaffected.
Plan U4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Avoid the legacy-migration side effect on the hot path: run the cheap,
read-only checks (basename === ".fusion" && parent has .git) FIRST, and
only call resolveGlobalDirForHome() (which can perform a one-time rename)
when a dir actually looks project-local.
- Normalize paths before the home-dir comparison (realpathSync when present,
else resolve) so a trailing slash, doubled separator, or symlinked home
doesn't make the legitimate home global dir trip the guard.
- Harden the browse-directory regression test: the mock returns a global dir
DISTINCT from getFusionDir() and asserts CentralCore is constructed with the
global dir, so a revert to getFusionDir() now fails the test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Double-commit race: null pendingValueRef synchronously at commit() start
so the close-flush + unmount-cleanup guards can't both fire and send a
duplicate PUT for the same value.
- Clamp ceiling: clamp setValue against max(SLIDER_BASE_MAX, currentValue)
instead of Number(raw) (which had no effective upper bound for
programmatic callers).
- Out-of-band staleness: force-revalidate on each activate so a Settings-modal
save (which writes globalMaxConcurrent directly, bypassing this store) is
reflected when a slider surface reopens.
- Load-error label: add an explicit error branch in both EngineControlMenu
and CommandCenterControls so a failed initial load shows "Load failed"
instead of "Ready" while the slider is disabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getUnifiedTaskProgress reads task.workflowStepResults (graph-written in U2) and
no longer needs workflowStepNameLookup — names come from result.workflowStepName.
Removed the lookup prop + fetchWorkflowSteps-name plumbing end-to-end (App,
MainContent, RightDock, Board, Column, Lane, WorktreeGroup, TaskCard). Added the
running state (pending + startedAt) and a distinct advisory_failure (amber, non-
blocking) vs failed (red) treatment; advisory_failure counts as complete.
Plan U3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prior WIP added a default-on code-review optional-group to builtin:coding on the
success path (execute -> browser-verification -> code-review -> review). 9 exact
visitedNodeIds expectations across 3 suites predated that node and were failing.
Updated expectations to match the real traversal (insert code-review in position).
No product code touched; no assertions weakened.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes real wall-clock waits and per-test rebuilds from the slowest
test files, replacing them with deterministic seams. **No assertions
weakened, no timeouts widened, no retries added** — this is anti-pattern
removal per FN-5048, verified by re-running each file.
## Changes
| File | What | Result |
|---|---|---|
| `dashboard/.../insights-routes.test.ts` | Boot server+store **once**
in `beforeAll` (was `createServer` + `TaskStore.init` per test ×24);
reset insight tables per test for isolation; drive sweeper via fake
timers | test-exec **~3.7s → ~0.8s** |
| `core/.../db.test.ts` | Fixed 150ms write-lock hold → manual stdin
signal-release (keeps the real OS-lock contention under test); fixed a
real EPIPE on redundant release | 152 pass, non-flaky / 8 runs; −300ms
dead wait |
| `core/.../mission-store.test.ts` | 4 real `setTimeout` sleeps (only
there to force distinct timestamps) → `vi.setSystemTime` controlled
clock | anti-pattern removed |
| `core/.../agent-store.test.ts` | 1 real ordering-sleep → injected
`renewedAt` clock; **assertions strengthened** to pin exact timestamp
values | anti-pattern removed |
| `engine/.../in-process-runtime.test.ts` | Fake the one real 25ms
sleep; drop its inflated 30s per-test timeout | anti-pattern removed |
## Honest accounting
- The **real wins** are `insights-routes` (per-test server boot
eliminated, ~75% execution-time cut) and `db` (dead lock-hold removed).
- The **timestamp-sleep removals** (mission-store, agent-store,
in-process-runtime) are small absolute wins — the headline per-file
durations (16–25s) were **full-suite shard contention, not in-file dead
time** (each runs in 3–10s isolated). But they eliminate the FN-5048
real-wait anti-pattern, so a hub edit no longer drags real sleeps into
every `--changed` selection.
- **`workflow-routes.test.ts` was evaluated for splitting and
deliberately NOT split.** A measured A/B showed the 4-way split
*regressed* wall-clock (6s → 11s): the file is import/transform-bound
(per-file esbuild + `@fusion/core`/express import ≈ 5s > the ~4.3s test
runtime), and per-test store migration was already amortized by
`installInMemoryDbSnapshot`. Splitting only multiplies the dominant
fixed cost. Left intact.
## Verification
- `core` 612/612, `dashboard` 24/24, `engine` 78/78 (file-scoped).
- `tsc --noEmit` clean on all 3 packages; eslint clean.
Follow-up (not in this PR): `scripts/test-timings.json` is stale (its
former #1 file no longer exists) — refresh via `pnpm test:velocity --
--measure --write-report` so the watchdog budgets and velocity report
reflect reality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1784">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
</picture>
</a>
<!-- stage-review-badge-end -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Made several test suites more deterministic by replacing real-time
delays with controlled timers and fixed timestamps.
* Improved lock and task checkout tests to use manual release signals,
reducing timing-related flakiness.
* Streamlined route test setup/teardown for faster, more reliable runs.
* Added safer cleanup around timer-based tests to avoid intermittent
failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Enabled optional-group nodes now upsert their outcome into the existing
task.workflowStepResults field keyed by node id, and emit [pre-merge] logs at
parity with the legacy runWorkflowSteps path. Disabled groups stay byte-inert.
Reuses the existing WorkflowStepResult type + store.updateTask path (no new
table/type/store method). Unblocks the unified progress bar for graph-run steps.
Plan U2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Why
Diagnosing "tasks take too long" showed the dominant end-to-end
wall-clock is **waiting**, not work: tasks sit in `todo` (queue) and
`in-review` (merge wait) far longer than the agent actually runs. Today
only `cumulativeActiveMs` exists, which measures **in-progress time
only** — every other stage's dwell had to be reconstructed by hand from
agent logs.
## What
Adds `columnDwellMs?: Record<string, number>` to `Task` — a per-column
accumulator (column name → cumulative ms), recorded at the **same store
column-transition seam** as `cumulativeActiveMs` (`moveTaskInternal`).
On each move it adds `columnMovedAt(new) − columnMovedAt(prev)` to the
bucket for the column being left:
- clamped `>= 0` (clock skew safe);
- unparseable/missing prior timestamp and zero-dwell moves are skipped
(no spurious buckets);
- second visits **add** to the existing bucket (multi-visit churn is
captured);
- flag-independent — keys off the generic `columnMovedAt` delta, so it
runs for both the workflow-hook and legacy-inline move paths.
This makes per-stage dwell directly queryable, the same way
`productivity-analytics.ts` already consumes `cumulativeActiveMs`.
## Persistence
JSON-text task column, following the v129 `workspaceWorktrees` precedent
exactly: `SCHEMA_SQL` column + `SCHEMA_VERSION` 129→130 + a versioned
`addColumnIfMissing` migration. Additive and behavior-preserving —
pre-existing rows start NULL and accumulate from their next transition.
Survives archive/restore (added to the archive-entry mapping).
## Tests
`src/__tests__/store-execution-timing.test.ts` — new regression asserts
dwell across
`todo→in-progress→in-review→done→todo→in-progress→in-review` accumulates
the right per-column ms (second visits add) and survives a `getTask` DB
round-trip.
```
pnpm --filter @fusion/core exec vitest run src/__tests__/store-execution-timing.test.ts ... --reporter=dot
→ store-execution-timing 5/5, schema suites (goals/secrets) green, 17/17 total
```
Migration chain verified end-to-end (secrets-schema test climbs v11/v82
→ v130). No hardcoded literal version assertions in the suite; schema
tests assert against the `SCHEMA_VERSION` constant.
`@fusion/core` is private — no changeset.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1781">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
</picture>
</a>
<!-- stage-review-badge-end -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added per-column dwell timing to tasks, showing how long work spent in
each column across multiple visits.
* Preserved this timing data when tasks are archived and restored.
* **Bug Fixes**
* Task timing now updates correctly during column moves, including
repeated returns to the same column.
* Existing data can be upgraded to the new timing format without
breaking stored tasks.
* **Tests**
* Added coverage for multi-step task movement and data reloading to
verify timing totals stay accurate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Add a "Global Max Concurrent" slider to the footer engine menu and the
Command Center Concurrency card, and group the Scheduling settings by
Global vs Project scope so the global cap isn't mistaken for a per-project
setting (clearer on mobile).
Both sliders are backed by a single shared `useGlobalConcurrency` hook
(module-level store) so they read/write one source of truth and revalidate
after every PUT /api/global-concurrency — fixing the last-writer-wins and
stale-clobber races a per-component cache would cause. The hook treats a
fetch error as non-interactive (slider disabled, not stuck at 1), surfaces
a save-state indicator, and flushes a pending edit on menu close / unmount
so a quick drag-then-dismiss is never silently dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses code-review findings on the global-settings reset fix:
- getGlobalSettingsDir() now returns a resolved `string` (was `string |
undefined`), so the project-local `.fusion` guard fires at the getter
call site instead of leaking CentralCore's undefined-default semantics.
- getSecretsStore() passes the resolved global dir to MasterKeyManager so
the master key co-locates with the global central DB and the path is
exercisable under tests (a bare new MasterKeyManager() throws in VITEST).
- resolveGlobalDir() guard gains an explicit FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR
opt-out so a legitimately version-controlled custom global dir (dotfiles
repo with a .git parent) is not hard-rejected.
- Add a symptom-based regression test (store-secrets-store-global-dir) proving
the secrets central DB lands in the global dir and never spawns a stray
project-local fusion-central.db.
- Add getGlobalSettingsDir() to route-test mock stores (CentralCore is mocked,
so it mirrors getFusionDir()) and FNXC comments to the remaining route sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace real wall-clock waits and per-test rebuilds in the slowest test files
with deterministic seams. No assertions weakened, no timeouts widened, no
retries added — anti-pattern removal only.
- insights-routes.test.ts: boot the server + store ONCE in beforeAll (was a full
createServer + TaskStore.init per test x24), reset insight tables per test for
isolation, drive the sweeper via fake timers. Test-execution time ~3.7s -> ~0.8s.
- db.test.ts: convert the fixed 150ms write-lock hold to manual stdin signal-
release; keeps the real OS-lock contention under test, removes 2x150ms dead
wait. Fixed a real EPIPE on redundant release. 152 pass, non-flaky over 8 runs.
- mission-store.test.ts / agent-store.test.ts: replace real setTimeout sleeps
used only to force distinct timestamps with a controlled clock (vi.setSystemTime
/ injected renewedAt). agent-store assertions strengthened to pin exact values.
- in-process-runtime.test.ts: fake the one real 25ms sleep, drop its inflated
30s per-test timeout.
Honest note: the timestamp-sleep removals are small absolute wins (the headline
per-file durations were full-suite shard contention, not in-file dead time) but
eliminate the FN-5048 real-wait anti-pattern. workflow-routes.test.ts was
evaluated for splitting and deliberately NOT split — measured A/B showed the
split regressed wall-clock (the file is import/transform-bound, already amortized
by installInMemoryDbSnapshot), so splitting only multiplies fixed import cost.
Verified: core 612/612, dashboard 24/24, engine 78/78; typecheck + eslint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Production code constructed `new CentralCore(store.getFusionDir())`, pointing
the central/global DB at the project's `.fusion/` instead of `~/.fusion/`.
`resolveGlobalDir()` returns an explicit dir verbatim, so this spawned stray
per-project `fusion-central.db` files seeded with default global settings
(globalMaxConcurrent=4, empty secrets) that shadowed the real global DB
whenever a read/write hit one of those paths — surfacing as intermittent
"all my global settings reset".
- Add TaskStore.getGlobalSettingsDir() (resolved global dir; undefined→~/.fusion)
- Route the secrets store + secrets/proxy/node/secrets-sync/settings-sync
dashboard routes through it instead of getFusionDir()
- Add a resolveGlobalDir() guard that throws on a project-local `.fusion/`
dir (basename `.fusion` with a `.git` parent); inert under VITEST
- Regression tests in global-settings-guard.test.ts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
optionalGroupIdSet falls back to builtin:coding (mirroring the executor's
unselected-task resolution) so a toggled built-in group id like
browser-verification is no longer downgraded to a legacy WS-xxx step row the
graph executor never matches. Create-time optional-step controls resolve
builtin:coding when no project default workflow is set so the toggles render.
First unit of the graph-native workflow-step refactor (see
docs/plans/2026-06-25-001-refactor-workflow-steps-graph-native-plan.md).
Fusion-Task-Id: FN-7039
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>