**Test-only.** One file. No production changes.
`executor-workspace.test.ts`: **2 failed / 10 passed → 13 passed**.
(This commit was pushed earlier and I failed to open its PR — the work
was finished and sitting on a dangling branch, which is why
`executor-workspace` still shows in main's failure census.)
## Why it was red
Both cases asserted that `clearPhantomExecutorBinding` **succeeds**
while session-registry paths are held.
PR #2531 (FN-6756, P0: *"stop reaping worktrees out from under live
planners"*) inverted that. `hasLiveSessionSurface` now includes
`activeSessionRegistry.pathsForTask(taskId).length > 0`, and that guard
runs **before** both branches — so any registered path refuses the
clear. The FNXC note at `executor.ts:2729` says the kind-blind guard is
deliberate: *"A leaked entry now blocks THIS sweep rather than a live
planner losing its worktree — the strictly safer failure."*
So the old expectations describe the pre-#2531 contract. Rewritten to
the current one, which had **no direct coverage**: a refusal leaves
`activeWorktrees` and the registry entries untouched. The FN-6736 KTD2
invariant ("every held path, not one") is kept, exercised with no
registry paths so the guard permits it.
**Verified the new tests guard:** removing the registry term from
`hasLiveSessionSurface` fails 2 of them (`NEW-failures=2`), and only
them.
## Flagged, not fixed — a possible dead branch
The guard appears to make **both branches it precedes** unreachable for
their stated purpose:
- the default branch exists to unregister every held registry path
(FN-6736);
- `preserveWorktrees: true` exists to **keep** those paths so a
`moveTask(preserveWorktree: true)` re-dispatch reattaches to the same
worktree (FN-7249) — and its **only** production caller is the
self-healing reclaim at `self-healing.ts:3565`.
Both need registered paths to do anything, and the guard rejects exactly
that case. With none registered, one sweeps nothing and the other
preserves nothing.
The third new test pins this so the conflict is **executable rather than
prose**: `preserveWorktrees: true` returns `false` while the path it
exists to preserve is registered.
I did not "fix" it by rewriting the assertion to match production — that
would bury a possible regression in FN-7249's reattach path. Resolving
it (exempting the non-destructive `preserveWorktrees` path, or narrowing
the guard by kind) is a product decision for the FN-6756 owner.
## Main's engine-default census, measured just now
| Point | Failed | Files |
|---|---|---|
| when I started this sweep | 283 | 28 |
| after the logger-mock fix (#2573) | 106 | 23 |
| **now** | **65** | **12** |
This PR clears one of the remaining 12.
`executor-review-verdicts.test.ts` is newly red and in my lane — taking
that next.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
reconcilePhantomCommittedReservations emitted a task:reconcile-phantom-committed-reservation audit row every maintenance tick for the same orphaned committed reservations even when zero child rows were pruned (~19k no-op writes/day). Gate the audit emission on actual pruned work; the FN-7069 contract is unchanged (committed reservation stays committed so the ID is never reused).
clearPhantomExecutorBinding unconditionally unregistered held worktree paths from activeSessionRegistry, defeating the moveTask(preserveWorktree:true) the self-healing phantom reclaim pairs it with: re-dispatch then acquired a brand-new worktree instead of reattaching (FN-7249 lost its first worktree this way). Add a preserveWorktrees option that clears only the stale in-memory executor/lock bookkeeping and leaves the session-registry path entries intact; the self-healing caller opts in. Non-self-healing callers keep the default full-clear behavior.
- 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>
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>
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>