- base-commit-capture.ts: shell-quote integration branch with a real
single-quoted POSIX literal instead of JSON.stringify (not shell-safe).
- TaskCard.tsx: memo compares full workspaceWorktrees values, not just key
sets, so a same-key worktreePath/branch change re-renders.
- TaskDetailModal.tsx: gate/render workspace summary off hydrated workingTask.
- worktree-acquisition.ts: null the singular worktree/branch columns in the
workspaceWorktrees write so isWorkspaceTask stays true; wrap non-fatal
post-acquire observability so logEntry/audit can't re-escalate to fatal.
- agent-tools.ts: register sub-repo worktree via onAcquired unconditionally
(idempotent) so a resumed/already-acquired path is tracked after restart.
- executor.ts: DB liveness fallback also checks task.workspaceWorktrees paths.
- executor-workspace.test.ts: root non-git assertion runs in fx.rootDir (".").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ce-code-review (5 personas) on Phase A. No P0; the workspace-root-removal path
was ruled out and the contract changes verified additive. Applied:
P1: fn_acquire_repo_worktree now catches WorkspaceRepoAcquireBusyError (and
generic failures) and returns a sanitized retryable isError instead of an
uncaught throw into the agent loop; runContext is forwarded so audit/log keep
attribution. Per-repo acquired worktree paths are now registered into the
executor's activeWorktrees Set (via an onAcquired callback) — previously the Set
only held the browse-only root, making the U1 per-repo liveness invariant hollow.
Post-acquire identity-guard install and base-SHA capture are now non-fatal
(log-and-continue): a hook/branch failure no longer strands the on-disk worktree
(the worktree is usable without the guard; an undefined baseCommitSha is already
an accepted state).
P2: the KTD3 settings-strip also strips settings.baseBranch (resolveFromSettings
falls back integrationBranch → baseBranch, so a shared baseBranch leaked); the
workspaceWorktrees write re-reads the task fresh before merging to avoid a
sibling-repo clobber on sequential acquires (store-level atomic merge deferred to
Phase B); the busy-path logging is wrapped so it can't mask the busy error; the
TaskCard memo compares key-sets not counts; the stuck-kill no-op for workspace
tasks is now logged; the exclusivity check-then-act synchrony is documented.
Residuals (Phase B): per-repo worktree teardown, orphan-scan coverage, reaper
dedup, store-level atomic merge.
Gate green: typecheck, lint, build, test:gate (649+58), affected (25 + TaskCard 251).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
acquireWorkspaceRepoWorktree now hardens each sub-repo worktree at acquisition:
(1) installs the identity guard with the executor's settings args
(commitMsgHookEnabled/taskPrefix/taskAttributionTrailerName) for single-repo
parity — it was installing no guard before; (2) captures a per-repo
baseCommitSha local-first against the repo's resolved integration branch via
resolveIntegrationBranch(repoAbsPath, {...settings, integrationBranch: undefined})
— stripping the shared override so each sub-repo falls through to its own
origin/HEAD, not a project-wide branch; (3) persists baseCommitSha into the
workspaceWorktrees[repo] entry (Task type extended); (4) registers same-sub-repo
exclusivity on the sub-repo path via activeSessionRegistry under a distinct
"workspace-repo-acquire" kind (released in finally), so two concurrent workspace
tasks contending for the same sub-repo are serialized (throws
WorkspaceRepoAcquireBusyError). Idempotent re-acquire short-circuits.
resolveCapturedBaseCommitSha gains an optional trailing integrationBranch param
defaulting to "main", so existing single-repo callers + base-commit-capture
real-git tests stay green. New audit events worktree:workspace-repo-acquire-busy
/-failed. 6 new real-fixture tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workspace tasks (no task.worktree, populated workspaceWorktrees) now render in
existing task views without crashing or going blank. New read-only
WorkspaceWorktreesSummary component (placeholder "N repos acquired" + a flat
repo→worktree/branch list — within the "doesn't look broken" ceiling, not a rich
status UI); TaskCard and TaskDetailModal nil-guard on isWorkspaceTask. Single-repo
rendering unchanged. CONCEPTS.md notes workspace-task merges are non-atomic
(repos land independently on local integration refs; partial-land is local and
operator-resettable). Tests 8/8; TaskCard regression 251/251.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In workspace mode (loadWorkspaceConfig present), the executor now skips the
root acquireTaskWorktree({rootDir}) and every intervening rootDir git preflight
(base-commit capture, contamination, identity-guard, verifyWorktreeInvariants),
runs the agent session rooted at the non-git workspace root (cwd=rootDir,
browse-only; task.worktree never set), and tracks activeWorktrees as a per-task
Set<path>. scopePromptToWorktree is a no-op in workspace mode. The non-workspace
path is unchanged (every change branches on this.workspaceConfig; a single-repo
task holds a one-element Set).
Converted every activeWorktrees consumer to membership semantics (feasibility-
verified list): findActiveWorktreeOwner, hasActiveWorktreeBinding, the FN-6736
phantom-binding reclaim, listWorktreeHolders (flat-maps a Set into N holder rows
— verified the FN-6782 reaper keys off taskId only, so slot accounting is
unaffected), the conflict-set iteration, the three deleteActive* unregister
resolvers (loop every path), cleanup, getWorktreePath (undefined for a
multi-worktree workspace task), and the verifyWorktreeInvariants singular
resolution (gated off in workspace mode — per-repo verify returns in Phase B).
Rewrote executor-workspace.test.ts from vi.mock-the-subject to a real two-repo
git fixture harness (_workspace-fixture.ts, shared with later units), 13 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-fix verification review (correctness + adversarial + reliability, unanimous
P0) found that the earlier retry-burn fix introduced an infinite loop: parking a
WorkspaceTaskMergeError task with status:null + mergeRetries:0 passes every
auto-merge eligibility gate (canMergeTask short-circuits only on status==='failed'),
so the cooldown sweep re-enqueues it every tick → guard re-throws → re-park, forever.
- Park with status:'failed' (keep mergeRetries:0). canMergeTask now blocks the
auto-sweep; a human's manual merge still works because it flows through the
manual-resolver branch (rejectMergeResolvers), which bypasses canMergeTask — so
'failed' does not block manual retry (the original comment's worry was wrong).
- Detect the error via `err instanceof Error && err.name === "WorkspaceTaskMergeError"`,
matching the VerificationError/MergeAbortedError convention and bundle-safe across
the @fusion/core→@fusion/engine boundary (drops the now-unused class import).
- Document that the dispatch door guard is a fast-fail only; the unconditional
chokepoint guard inside runAiMerge is the authoritative enforcement.
- Add a regression test asserting the auto-merge park sets status:'failed' (not null).
Gate green: lint, typecheck, build, test:gate (649+58), project-engine (81).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies ce-code-review (autofix) feedback — 5 reviewers, P1s corroborated.
F1 [P1, ×4 reviewers] Guard the merge chokepoint, not just the 4 doors. The
per-caller `getTask().catch(()=>null); if(t) assert` pattern failed open on a
transient read, and runAiMerge re-read the task unguarded — so a workspace
task could reach git work against the non-git root. Added a named
WorkspaceTaskMergeError and call assertNotWorkspaceTaskMerge inside runAiMerge
(the sole merge path) and the deprecated aiMergeTask body; door guards remain
as fast-fail defense-in-depth.
F2 [P1] The dispatch catch treated the guard throw as a merge failure and set
mergeRetries=MAX, permanently blocking manual retry. It now recognizes
WorkspaceTaskMergeError and parks without burning retries.
F3 [P2] Deprecation-warning test asserted toBeLessThanOrEqual(1) — vacuously
true on zero emissions. Now resets the per-project flag and asserts the
warning fires exactly once and not again on a second deterministic merge.
F6 [P2] The once-per-process warning flag suppressed the notice for all other
projects in a multi-project host; now keyed per project (Set by cwd).
F5/F7/F8 [P3] @deprecated propagated to the aiMergeTask barrel re-export; CLI
runTaskMerge guard moved inside the formatted try/catch; FNXC placeholder
timestamps corrected; test .at(-1) -> length index.
Documented as residual (deferred to master-plan U8, not bugs in U0's window):
self-healing auto-finalize + store.mergeTask are additional merge-completing
paths not hardened here — workspace tasks are not end-to-end runnable until
master-plan Phase A, and U8 makes self-healing workspace-aware.
Gate green: typecheck (29 projects), lint, build, test:gate (649+58),
affected tests (206+4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make runAiMerge (the FN-5633 clean-room AI merge path, already the default)
the sole merge path; soft-deprecate the legacy aiMergeTask pipeline. Phase 0
of the workspace-mode master plan — a standalone merge-consolidation refactor
that lands first so downstream workspace work targets one merge function.
U1 — collapse the dispatch: project-engine.ts now calls runAiMerge
unconditionally; the two direct callers that bypassed the dispatch
(dashboard.ts onMergeImpl in --no-engine mode, task.ts runTaskMerge /
`fn task merge`) now route to runAiMerge too. Export runAiMerge from
@fusion/engine.
U2 — soft-deprecate: aiMergeTask is @deprecated (body retained for a later
deletion pass; shared helpers runAiMerge imports, e.g.
captureSingleCommitLandedMetadata, left intact). merger.mode "deterministic"
is annotated deprecated and made inert (type + field kept — published
@runfusion/fusion surface); the dispatch logs a one-time deprecation warning
and routes to runAiMerge. Changeset added (minor).
U3 — R7 workspace merge-boundary guard: shared @fusion/core predicate
assertNotWorkspaceTaskMerge(task) rejects tasks with populated
workspaceWorktrees at all four merge entry points (dispatch, store.mergeTask,
onMergeImpl, runTaskMerge) with an error naming master-plan U6. Covers the
window until per-repo merge support lands; U6 removes it.
U4 — deterministic-mode blast-radius audit: no production project, CI config,
or seeded setting pins merger.mode "deterministic"; only four engine tests
used it to drive the dispatch to aiMergeTask as a mockable seam — migrated to
mock runAiMerge instead. Other module-level aiMergeTask mocks were dead under
the default "ai" mode or test aiMergeTask directly (body retained), so they
are unaffected.
Also removes an unused acquireWorkspaceRepoWorktree import inherited from the
foundation branch (executor.ts) that was failing lint; master-plan U1 re-adds
it with its per-repo usage.
Merge gate green: lint, typecheck (29 projects), build, test:gate
(649 + 58), plus the migrated (114) and new predicate (4) tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow registering a non-git parent directory that contains multiple git
repositories as a single Fusion project. The agent acquires per-repo
worktrees on demand via a new `fn_acquire_repo_worktree` tool as it
discovers it needs to work in each sub-repo.
This commit lays the foundation:
- detectWorkspaceRepos / loadWorkspaceConfig / saveWorkspaceConfig in
@fusion/core (config persisted to .fusion/workspace.json)
- Task.workspaceWorktrees data model + store plumbing (per-repo
worktree/branch map, distinct from the singular task.worktree)
- acquireWorkspaceRepoWorktree wraps acquireTaskWorktree per sub-repo,
clearing the singular worktree/branch fields so each sub-repo gets a
fresh worktree instead of resuming a sibling repo's worktree
- fn_acquire_repo_worktree agent tool + workspace prompt injection
- executor git-repository validation bypassed when a workspace config
is present
- CLI `fn init` detects a non-git dir containing sub-repos and writes a
workspace config
Known gap (intentionally left for design discussion, see PR): the
executor's main worktree-acquisition path still assumes a single git
root and is not yet workspace-aware. End-to-end execution (skipping the
root acquisition, per-repo merge, per-repo session scoping) is a
follow-on once the execution model is agreed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
stuckKillCount is a lifetime counter — incremented by self-healing on each stuck-kill
and reset ONLY by a manual retry — so a long task that genuinely advances between
intermittent stalls could be terminalized by accumulation toward maxStuckKills (default 6),
even though it is making progress.
Reset the streak in TaskStore.updateStep when a step reaches a terminal forward status
(done/skipped), the single chokepoint every step-completion surface routes through (legacy
fn_task_update, graph markStepDone, fn_task_done). It deliberately does NOT rescue a task
wedged re-running the same failing step — no step completes between those kills, so it still
terminalizes as designed; it only bounds the budget to consecutive stalls. Complements the
verification fan-out cap (PR #1708) that keeps verification fast in the first place.
New regression suite asserts reset on done/skipped/graph-source-done and NO reset on
in-progress advance or ignored out-of-order transitions. Merge gate + full @fusion/core
suite (6242 tests) + engine self-healing/stuck-detector (442 tests) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten the executor agent guidance and the fn_run_verification tool guidance so
verification stays scoped to changed files instead of running the full workspace
test suite, which (for a foundational-package edit) reverse-expands across the
whole workspace and stalls the task.
- agent-prompts.ts: remove the "during final integration" blanket permission to
run workspace-wide suites; name the forbidden full-run commands explicitly.
- run-verification-tool.ts: strengthen BOUNDED_VERIFICATION_GUIDANCE to forbid
`pnpm test:full` / `pnpm verify:workspace` / whole-package runs as verification.
Engine + core typecheck pass; run-verification-command and executor-review-verdicts
prompt-assertion suites stay green (93 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-test `createTaskStoreTestHarness` recreated a TaskStore from scratch
every test — mkdtemp ×2 + new store + recursive rm with retry backoff — which
dominated wall-clock across these high-test-count files. Switch the files that
tolerate a shared store to `createSharedTaskStoreTestHarness`, which builds the
store once and resets state between tests via full table truncation + FTS
rebuild + filesystem reset, preserving isolation.
Measured on store-create.test.ts (53 tests): 4.44s → 1.78s wall
(test execution 3.59s → 0.93s, ~3.9x). Full @fusion/core suite stays green
(346 files, 6236 tests) and typecheck passes.
19 files that test disk persistence / migration / workflow-definition state are
incompatible with a shared in-memory store and were intentionally left on the
per-test harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Read hasResumableProgress from latestTask (the store snapshot fetched
at ~9226), not the frozen dispatch-time task param, so a freshly
dispatched task that commits step progress mid-session is preserved on
a hard-cancel teardown — not just the re-dispatch case (greptile P1)
- Add companion regression test for the fresh-task-with-in-session-progress
case, which fails against the stale-task snapshot (greptile P1)
- Reformat the requirement comment block to the canonical FNXC heading
(FNXC:WorkflowLifecycle yyyy-MM-dd-hh:mm:) convention (CodeRabbit)
Fusion-Task-Id: FN-6722
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the engine aborted in-flight work mid-execution and bounced the task
back to `todo`, the single-session teardown cleared the task `branch` and
re-queued without `preserveResumeState` — resetting every step to `pending`
(store.moveTaskInternal resetAllStepsToPending) and dropping the pointer to
the commits already on the task branch. The next dispatch then re-planned
from Step 0 and the committed work was stranded, observed as FN-6722 losing
all its progress and getting stuck in todo.
The teardown now keeps the branch and moves with `preserveResumeState`
whenever the task has resumable step progress, matching the sibling
step-session (executor ~8065) and pause-park (executor ~1826) paths, so
execute() resumes onto the existing branch from the first incomplete step.
The worktree is still removed to free its concurrency slot (FN-6782) — only
the durable pointers (branch + step state) are kept.
Adds a regression test driving the exact single-session catch teardown.
Fusion-Task-Id: FN-6722
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Many loading states rendered bare "Loading…" text with no spinner
element, and a couple rendered an unstyled `loading-spinner` div with no
matching CSS (invisible). The global spin animation was never broken —
it rotates in Chromium and WebKit — so prior transform-box edits chased
a non-bug, validated only by a string-matching CSS test.
Add a shared <LoadingSpinner> (self-contained animated SVG, no
lucide-react import so it survives partial test mocks) and adopt it
across ~47 loading placeholders so every loading state shows a
consistent animated spinner. Includes a component test that asserts the
svg carries the animate-spin utility.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
discoverDroidModels ran `droid models`/`droid model list`, which aren't real
droid commands — they parse as a prompt and launch a persistent
`droid exec --stream-jsonrpc` agent session that never exits, leaking a process
per call. The dashboard reloads the droid extension on every chat-send, so these
piled into dozens of orphaned `droid` processes.
Switch discovery to parse `droid exec --help` (lists Available + Custom models,
exits cleanly) via new parseDroidModelsFromHelp, and add a SIGKILL-on-timeout
guard so a wedged spawn can never leak. Verified against the real binary: 46
models, 0 leaked processes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the left sidebar collapse control into the footer so it sits above Settings with row-style behavior.
- Replace the floating border collapse button with a footer row button that keeps accessible expanded/collapsed labels.
- Retokenize collapse toggle CSS around shared sidebar item styling and remove the obsolete floating modifier.
- Extend sidebar tests and dashboard guide coverage for the footer placement and collapsed rail behavior.
Files changed:
docs/dashboard-guide.md | 4 +-
.../dashboard/app/components/LeftSidebarNav.css | 24 ++--------
.../dashboard/app/components/LeftSidebarNav.tsx | 32 ++++++-------
.../components/__tests__/LeftSidebarNav.test.tsx | 54 ++++++++++++++++++++--
4 files changed, 72 insertions(+), 42 deletions(-)
Fusion-Task-Id: FN-6847
Fusion-Task-Lineage: 5d7781a1-afc9-4e5c-bf7b-0648bb874285
Refine the desktop list split pane so narrower sidebars still show readable task titles.
- Lower the list-view sidebar minimum width from 280px to 200px across resize and keyboard handling.
- Clamp desktop list task titles to two wrapped lines with overflow protection.
- Update ListView coverage for the smaller persisted/keyboard minimum and title-clamp CSS.
Files changed:
packages/dashboard/app/components/ListView.css | 10 +++++++++-
packages/dashboard/app/components/ListView.tsx | 2 +-
.../app/components/__tests__/ListView.test.tsx | 22 +++++++++++++++++-----
3 files changed, 27 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-6831
Fusion-Task-Lineage: ac3e455d-8cb0-4f42-ac5c-d735e56d2b89
Ensure the Command Center theme selector renders above neighboring views while preserving mobile in-flow behavior.
- Add an open state class to the theme dropdown root for local stacking control.
- Raise the open dropdown and popover above Command Center sibling cards without exceeding app chrome layers.
- Cover desktop elevation and mobile static popover behavior in ThemeDropdown tests.
Files changed:
.../dashboard/app/components/ThemeDropdown.css | 15 +++++++-
.../dashboard/app/components/ThemeDropdown.tsx | 2 +-
.../components/__tests__/ThemeDropdown.test.tsx | 40 ++++++++++++++++++++++
3 files changed, 55 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-6826
Fusion-Task-Lineage: ff43c06e-f3a7-4b4c-8e29-46deb66e9145
Move externalEngines.delete(projectId) to immediately after acquiring the
singleton lock instead of after engine.start() succeeds. If a project was
marked external, the holder exits, acquire succeeds, but start() then throws,
the success-path delete never ran and hasRunningEngine() reported a phantom
engine forever. Added a regression test for the failed-takeover path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Stop reconciliation/startAll/onProjectAccessed from warning every tick for
externally-owned engines: swallow EngineAlreadyRunningError in the outer
catches (it's expected and already logged once in createAndStart)
- Add FNXC:DashboardHealth requirement-trace comments on the externalEngines
field and the dashboard hasRunningEngine health check
- Add regression test: reconciliation stays quiet across ticks for an
externally-owned engine (inner refusal logged once, outer failure suppressed)
- Add regression test: hasDashboardEngine legacy fallback to getAllEngines when
hasRunningEngine is unavailable on the manager
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the false "engine not running" banner root cause and fix as a
docs/solutions learning, and add an "Engine Singleton Lock" entry to
CONCEPTS.md: a failed per-machine lock acquisition is proof an engine is
running elsewhere, not "no engine."
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- FNXC requirement-trace comments on the stepwise workflow-step seam,
the flowToIr v2-signal/byte-identity contract, and TaskForm's
optional-steps create-mode behavior
The dashboard's engine-availability health check only counted engines
this process started. A second launch (e.g. `pnpm dev dashboard`
alongside an already-running `fusion`) is correctly refused the
per-machine engine singleton lock, so its engine map stays empty and
the dashboard showed a false "engine not running" banner even though an
engine was live on the machine.
ProjectEngineManager now records projects whose singleton lock is held
by another process (via EngineAlreadyRunningError) and exposes
hasRunningEngine(), which the health endpoint consults so the banner
reflects machine-level truth. Reconciliation still retries so this
process takes over if the other exits, and the "refusing to start" log
fires once per project instead of on every 30s reconciliation tick.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Dropdown: aria-multiselectable, drop dead aria-labelledby, ArrowUp opens panel
- Dirty-state: NewTaskModal tracks enabledWorkflowSteps so toggles trigger discard prompt
- TaskForm: reset optionalStepsLoading on the no-workflow early return
- Node editor: pass plugin step templates into the optional-steps panel (both layouts)
- FNXC requirement comments on the new optional-steps components