Address PR #1739 review feedback:
- P1 (greptile): When workspaceMode is explicitly false in config.json,
skip the auto-detection fallback so toggling workspace mode off via the
dashboard has a lasting effect (was being re-enabled on next registration).
- CodeRabbit: node_modules exclusion test now includes a real sibling
sub-repo to prove the exclusion is the gate, not just absence of
detection.
- Add test for workspaceMode:false config.json guard.
Add workspaceMode as a first-class ProjectSettings boolean that controls
whether the project root is treated as a workspace parent (multi-repo)
or a single git repo.
- ProjectSettings type + DEFAULT_PROJECT_SETTINGS: workspaceMode?: boolean
- CLI registerProjectInteractive: when sub-repos are detected, ask the
user to confirm workspace mode instead of auto-applying
- TaskStore.updateSettings: when workspaceMode is toggled on, detect
sub-repos and persist workspace.json; when toggled off, remove it
- Dashboard SettingsModal GeneralSection: workspace mode toggle checkbox
This lets users change workspace mode per-project at any time via the
dashboard Settings or PUT /settings API.
Address PR #1739 review feedback:
- P1: Exclude node_modules, .fusion, .pi from detectWorkspaceRepos so
packages installed from git sources don't produce false-positive
workspace members.
- P2: Wrap saveWorkspaceConfig in try/catch so a write failure (permissions,
disk full) doesn't fail the current registration.
- Nitpick: Thread runner/timeout through detectWorkspaceRepos so custom-runner
callers are consistent across all code paths.
The initial fix only checked loadWorkspaceConfig, but the dashboard
POST /api/projects and `fn project add` routes never create workspace.json
(only registerProjectInteractive does). So re-adding a workspace project
through the dashboard still triggered git init because the guard saw no
workspace.json.
Add detectWorkspaceRepos as a fallback: after loadWorkspaceConfig and
isInsideGitWorkTree both miss, probe for git sub-repos. If found, persist
workspace.json and return 'existing' without running git init. This covers
all registration surfaces.
## Problem
`ensureGitRepositoryForProjectPath` unconditionally ran `git init` on
non-git paths, including workspace roots. This created a stray empty
repo with unborn HEAD at the workspace root, poisoning every downstream
git command:
- Executor sets session cwd to the workspace root (browse-only mode)
- `git rev-parse --abbrev-ref HEAD` fails with `fatal: ambiguous
argument 'HEAD'` on the unborn HEAD
- All workspace task execution breaks (`fn_task_done refused:
wrong_branch`, `expected swarmclaw/ repository is absent`)
## Fix
Add an early-return guard in `ensureGitRepositoryForProjectPath` that
checks `loadWorkspaceConfig(projectPath)` before any git operation. When
`.fusion/workspace.json` is present (workspace mode), the function
returns `"existing"` immediately, keeping the workspace root non-git as
intended by the workspace execution contract.
## Files Changed
- `packages/core/src/git-repository.ts` — workspace-mode early-return
guard + FNXC:Workspace comment
- `packages/core/src/__tests__/git-repository.test.ts` — regression test
verifying no `.git` is created at workspace root
## Testing
- `pnpm typecheck` — pass
- `pnpm lint` — pass
- `pnpm test` (affected `git-repository.test.ts`) — 5/5 pass (4 existing
+ 1 new)
## Remediation for existing broken workspaces
Users with an already-registered broken workspace project should remove
the stray repo:
```bash
rm -rf <workspace-root>/.git
```
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1738">
<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**
* Improved project setup so workspace-root projects are no longer
initialized as Git repositories when workspace configuration is present.
* Prevents unexpected `.git` creation at the workspace root and
preserves existing workspace behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Add scripts/lib/distill-release-notes.mjs with:
- distillDeterministic(): groups parsed changesets by category,
renders clean markdown release notes (Keep a Changelog headings)
- buildDistillationPrompt(): builds the context input for AI mode
- DISTILLATION_SYSTEM_PROMPT: system prompt for AI distillation
- Category display order: New → Fixed → Breaking → Security →
Performance → Internal; empty categories omitted
The AI mode (createFnAgent integration) is deferred to U4 where the
engine runtime is available. The deterministic mode is the fallback
contract: it always produces valid output regardless of model access.
14 tests covering category grouping, ordering, empty/edge cases,
prompt building, and legacy entry handling.
ensureGitRepositoryForProjectPath unconditionally ran `git init` on
non-git paths, including workspace roots. This created a stray empty
repo with unborn HEAD at the workspace root, poisoning every downstream
git command (executor session cwd: `fatal: ambiguous argument 'HEAD'`).
Add an early-return guard that checks loadWorkspaceConfig before any git
operation, keeping the workspace root non-git as intended by the
workspace execution contract.
Add the reviewed, completed plan for the dashboard App.tsx module-breakup
refactor (planning + two doc-review rounds). Status: completed — all 8
implementation units shipped on this branch.
App.tsx dropped from 2,729 to 1,636 lines through the module-breakup refactor
(U1-U7), now well under the 2,000-line cap. Remove its grandfathered entry
from scripts/line-count-baseline.json so the file is subject to the cap going
forward and can never regress above 2,000. Scoped change: only the App.tsx
entry is removed; every other ceiling is untouched (a full `--update` would
have re-derived the whole baseline and re-raised ceilings for unrelated
grown files, which AGENTS.md forbids).
U8 (App.tsx module-breakup plan).
Extract the conditional banner cluster (~14 banners each gated on
viewMode === "project" && currentProject) into a presentational component
packages/dashboard/app/components/dashboard/DashboardBanners.tsx with a typed
DashboardBannersProps interface (39 fields) added to types.ts. The
TaskIdIntegrityBanner setDashboardHealth updater and all FNXC comments move
verbatim; banner components are imported directly from siblings.
App.tsx: 1,704 -> 1,636 lines. Behavior-preserving; App.test.tsx identical
(5 pre-existing experimental-flag failures, none introduced). Verified by
typecheck and eslint.
Completes U7 (App.tsx module-breakup plan).
Extract the inline renderMainContent() view-switch (~647 lines, ~24 view
branches) into a presentational component
packages/dashboard/app/components/dashboard/MainContent.tsx with a typed
MainContentProps interface (141 fields) in types.ts. All 18 lazy view const
DECLARATIONS stay unchanged in App.tsx (the lazy-loaded-views-docs inventory
guard regex-scans App.tsx for them — R5) and are threaded to MainContent as
LazyExoticComponent props. Branch-local consts and render-prop arrows stay
co-located inside MainContent.
App.tsx: 2,219 -> 1,704 lines, now under the 2,000-line file-count ratchet
(R3 achieved). Behavior-preserving; App.test.tsx identical (5 pre-existing
experimental-flag failures, none introduced). Verified by typecheck, eslint,
and the lazy-loaded-views-docs inventory guard.
U7a (App.tsx module-breakup plan).
Extract three AppInner state clusters into hooks:
- useMainPanelTaskDetail: the main-panel task-detail snapshot + initial tab;
setTask accepts the SetStateAction updater form so the embedded detail can
patch the snapshot on task updates.
- useBoardScrollRestore: the board scroll snapshot refs, capture, and the
double-requestAnimationFrame restore effect keyed on taskView; exposes
requestRestore for App to schedule a restore on detail close.
- usePoppedOutTasks: the popped-out task-detail windows (dedupe-by-id popOut,
close-by-id).
App keeps the navigation-history composition (openTaskDetailInMainPanel /
closeTaskDetailMainPanel) and now consumes the hooks' primitives.
Behavior-preserving; App.test.tsx identical (5 pre-existing failures, none
introduced). Verified by typecheck, eslint, and 5 renderHook tests.
Completes U6 (App.tsx module-breakup plan).
> ⚠️ **Draft — do not merge until the stack lands.** Final phase; stacks
on foundation #1710 + U0 #1711 + Phase A #1713 + Phase B #1714 + Phase C
#1717; targets `main` with the whole stack. **Review only the Phase-D
commits** (`7cd204e` U1, `78d7a28` U2, `8e70d69` review fixes).
## Workspace mode — Phase D (self-healing + e2e) — FINAL PHASE
Implements Phase D of the [master
plan](docs/plans/2026-06-21-002-feat-workspace-mode-execution-model-plan.md)
— units U8/U9 ([Phase-D
plan](docs/plans/2026-06-22-001-feat-workspace-phase-d-plan.md)). Closes
the workspace-mode lifecycle: the self-healing layer is now
**workspace-aware** (it was either wrongly finalizing or silently
skipping workspace tasks), and an **e2e harness** proves the whole path
with no remote push. After this the feature is operationally complete.
### What changed
- **U1 — workspace-aware self-healing.** The feasibility pre-check
caught a **P0**: `landWorkspaceTask` sets status `"merging"`, and the
existing `recoverInterruptedMergingTasks` would (via the singular
`findLandedTaskCommit`) finalize a **partial-landed** workspace task to
done on *one* repo's commit. Fixed — and the review's FN-5893 audit
found and gated **four more** single-commit-finalize sites
(`recoverStuckMergeDeadlocks`, `recoverOrphanOnlyScopeViolations`,
`recoverAlreadyMergedReviewTasks`,
`recoverBranchMisboundInReviewTasks`). `recoverMergeableReviewTasks` now
admits workspace tasks (it gated on `Boolean(task.worktree)`, null for
them). Three new reconcilers: partial-land recovery (re-enqueue via the
idempotent `enqueueMerge`, guarded + starvation-bounded), phantom
`workspace-repo-land` lease reclaim (new
`activeSessionRegistry.entriesByKind` seam, terminal-owner-only), and
orphaned per-repo worktree cleanup (from the **stored** `worktreePath`,
no temp-root walk). A workspace-aware liveness predicate + an
`isMergePending` merge-queue guard prevent moving a live/dispatching
task backward.
- **U2 — e2e harness.** A real two-repo lifecycle test (engine-default
lane, `describeIfGit`) asserting the **no-push invariant** directly
(snapshot every origin/remote-tracking ref before+after
`landWorkspaceTask`, assert byte-for-byte equality while local refs
advance) plus per-repo `landedSha`, finalize-once, and partial-land
recovery through the actual reconciler (no double-land).
### Review (4 personas)
**No P0 shipped.** The reviewers verified the backward-safety invariants
and caught the FN-5893 all-surfaces miss (the P0 guard had been applied
to one of several finalize paths) and a TOCTOU (the reconciler was blind
to the merge-queue dispatch window → a same-task
double-`landWorkspaceTask` could double-squash). Both fixed; every guard
reuses `allowsAutoMergeProcessing` (FN-5147) + the workspace
liveness/merge-pending checks and only ever skips (`-no-action`), never
moves a live or human-gated task backward.
### Deferred
- Extracting `self-healing-workspace.ts` (the file is 10.7k lines) and
`workspace-merger.ts` (Phase-C residual). Per-sub-repo cwd reachability
verification; store-level atomic per-repo merge; rich dashboard per-repo
merge UI. Remote push (out — D2/D5 local-ref only).
### Verification
Gate green: lint, typecheck (29 projects), build, `test:gate` (649+58);
self-healing + e2e + project-engine + merger **724**.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1718">
<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
## Release Notes
* **New Features**
* Workspace multi-repository merges now land per-repository with
automatic recovery if individual repos fail.
* Dashboard and CLI merge operations now fully support workspace tasks.
* **Bug Fixes**
* Improved reliability of partial-land recovery with bounded retry logic
and state persistence.
* Fixed race conditions in concurrent merge coordination across
sub-repositories.
* **Deprecations**
* Deterministic merge mode is deprecated; all merges now use the unified
AI merge path.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
> ⚠️ **Draft — do not merge until the stack lands.** Stacks on
foundation #1710 + U0 #1711 + Phase A #1713 + Phase B #1714; targets
`main` with the whole stack diff. **Review only the Phase-C commits**
(`744ed09` U1, `7544346` U2, `64e87f9` U3, `627bdcf` review fixes).
## Workspace mode — Phase C (per-repo merge loop, land-as-you-go on
local integration refs)
Implements Phase C of the [master
plan](docs/plans/2026-06-21-002-feat-workspace-mode-execution-model-plan.md)
— units U5/U6/U7 ([Phase-C
plan](docs/plans/2026-06-21-006-feat-workspace-phase-c-plan.md)).
Replaces U0's R7 guard (which *threw* on workspace-task merges) with the
real **per-repo land loop**. A workspace task now runs → captures →
reviews → **merges**: each acquired sub-repo's `fusion/<id>` branch
lands onto **that repo's own LOCAL integration ref** (CAS +
fast-forward, **no remote push** — D2/D5), land-as-you-go.
### What changed
- **U1 — `landOneRepo` + `landWorkspaceTask`.** Extracted the per-repo
land mechanics out of `runAiMerge`'s inline clean-room closure into an
exported `landOneRepo`; `runAiMerge` is now its byte-for-byte
single-repo caller (the 56-test merger-ai oracle stays green).
`landWorkspaceTask` loops the acquired sub-repos (sorted), re-resolves
each repo's integration branch (override-stripped → own `origin/HEAD`),
lands each, aggregates repo-tagged results. The engine dispatch +
user-facing CLI `fn task merge`/dashboard merge doors route workspace
tasks here; `store.mergeTask`/`aiMergeTask`/the `runAiMerge` chokepoint
stay throwing (defense-in-depth).
- **U2 — landed predicate + finalize-once + auto-retry/park.** Each
landed repo's tip is persisted as `workspaceWorktrees[repo].landedSha`;
`isRepoLanded` skips it on retry (idempotent). The task finalizes to
done exactly once after *all* repos land. A partial land raises
`WorkspacePartialLandError` → the engine consumes a `mergeRetry` and
re-runs (skipping landed repos) up to MAX, then operator-parks.
- **U3 — per-repo land lease.** A new `activeSessionRegistry`
`workspace-repo-land` kind serializes concurrent same-sub-repo lands.
### Review (5 personas)
**No P0.** Adversarial verified the two crown-jewel invariants clean:
**no remote push** anywhere in the land loop (CAS + FF-only), and
**exactly one mergeRetry per attempt** with the hard-fail guard unable
to enter the retry loop. Fixed in-branch (`627bdcf`): a **double-land**
bug (a swallowed `landedSha` write could produce a second squash commit
— now propagated, with a `Fusion-Task-Id`-trailer landed-fallback); a
**cross-phase lease clobber** (a merging task could overrun an executing
task's acquire lease — now `taskId`-aware); a **retry storm** under DB
outage (now fails closed); a **status `'merging'` leak**; a
**reachability-gate poison** that demoted fully-merged workspace tasks
(the fast-path now skips them — they're verified by per-repo
`landedSha`); the **dashboard `merged:false`** contradiction;
busy-contention no longer burns the retry quota; backoff capped;
`WorkspacePartialLandError` promoted to a real class.
### Deferred to Phase D
- Self-healing reconcilers for **partial-landed / stuck** workspace
merges (the landed predicate `isRepoLanded` is exported for this).
- The e2e workspace harness.
- Per-repo worktree teardown; extracting a `workspace-merger.ts` module
(`merger-ai.ts` is large); per-sub-repo cwd reachability verification.
### Verification
Gate green: lint, typecheck (29 projects), build, `test:gate` (649+58);
workspace-merger + merger-ai oracle + project-engine **174**.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1717">
<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**
* Workspace merges now process repositories independently, tracking
per-repo completion for idempotent retries.
* Concurrent land operations on the same sub-repository are serialized
to prevent conflicts.
* Dashboard and CLI now report full merge status for workspace tasks,
including per-repository outcomes.
* **Bug Fixes**
* Improved handling of partial workspace land failures with proper
backoff and retry logic.
* Stricter validation of workspace configuration repository arrays.
* **Documentation**
* Added comprehensive Phase C plan for workspace merge behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Move the capacity-risk signal (computeCapacityRisk), the settings-hydrate
guard, the re-enable-clears-dismissal behavior, the per-project dismiss
state, and the dismiss action into app/hooks/useCapacityRiskBanner.ts.
App computes agentStats / inProgressCount / inReviewCount / settings and
passes them in. Uses the named CapacityRiskSignal type (no ReturnType<typeof>).
Behavior-preserving; App.test.tsx identical (5 pre-existing failures, none
introduced). Verified by typecheck, eslint, and 3 renderHook tests (signal
computation, dismiss, re-enable-clears-dismissal after hydrate).
Part of U5 (App.tsx module-breakup plan).
Extract three more AppInner state clusters into hooks:
- useDashboardHealth: dashboard health state + mount fetch + on-demand refresh;
exposes setHealth for the TaskIdIntegrityBanner remediation callback.
- useAuthTokenRecovery: the auth-token-recovery dialog open state driven by the
AUTH_TOKEN_RECOVERY_REQUIRED_EVENT window listener.
- useScopedDismissFlag: a generic per-project dismissable banner flag (scoped
storage + project-change re-read + dismiss); backs the setup-warning banner.
Capacity-risk dismiss stays inline pending its dedicated useCapacityRiskBanner
hook. Behavior-preserving; App.test.tsx identical (5 pre-existing failures,
none introduced). Verified by typecheck, eslint, and 7 renderHook tests.
Part of U5 (App.tsx module-breakup plan).
Consolidate the working/base branch-filter state, the per-project scoped-load
effect, the change handlers, and the branchOptions/baseBranchOptions/
filteredBoardTasks memos (including the NO_BRANCH_FILTER_VALUE "no branch"
sentinel) into app/hooks/useBranchTaskFilters.ts. App computes the
remote-aware boardSourceTasks and passes it in. Behavior-preserving;
App.test.tsx identical (5 pre-existing failures, none introduced). Verified
by typecheck, eslint, and 6 renderHook tests.
Part of U4 (App.tsx module-breakup plan).
Split AppInner's single /api/events subscriber (which drove mailbox counts,
the approval banner, and the first-done GitHub-star prompt) across two hooks:
- useMailboxUnread owns the unread/pending counts, the fetchUnreadCount
refresh, and the message:*/approval:* count SSE handlers; exposes refresh
(for the approval hook) and setMailboxUnreadCount (MailboxView reports its
own count via onUnreadCountChange).
- useApprovalBanner owns the approval-banner dedupe/dismiss state machine,
the task:updated + approval:requested SSE handlers, and the star + mailbox
side effects via onStarPrompt / onMailboxRefresh callbacks (KTD4: the single
task:updated subscriber is preserved; the awaiting-approval mailbox refresh
is preserved via the callback).
The showGitHubStarPrompt boolean stays inline in App (minimal surface) wired
through a stable onStarPrompt callback. App.tsx no longer imports
fetchUnreadCount, parseDateMs, the dismissal helpers, or
didEnterAwaitingApproval/didEnterDone (moved into the hooks; still
re-exported from App for the unit-test contract).
Behavior-preserving; App.test.tsx identical to before (5 pre-existing
experimental-flag failures, none introduced). Verified by typecheck, eslint,
and 8 new renderHook tests. Completes U2 + U3.
Move the chat-unread-response badge (clear-on-chat-view effect plus the
chat:message:added / chat:room:message:added SSE subscriber) out of AppInner
into app/hooks/useChatUnreadBadge.ts (byte-faithful extraction). App.tsx no
longer imports ChatRoomMessage (the chat SSE was its only user).
Behavior-preserving; verified by dashboard typecheck, eslint, and a new
renderHook test (assistant/user message gating + clear-on-view).
Part of U2 (App.tsx module-breakup plan).
Move the 30s stash-recovery orphan poll out of AppInner into
app/hooks/useStashOrphanCount.ts (byte-faithful extraction). App.tsx now
consumes the hook and no longer imports `api` directly (the stash poll was
its only direct call site). Behavior-preserving; verified by dashboard
typecheck, eslint, and a new renderHook test (fake-timer poll assertions).
Part of U2 (App.tsx module-breakup plan).
Move the module-level pure functions (approval-banner dedupe helpers, CLI
banner actions, boot-loader/shell-onboarding guards, remote-dashboard URL
builder) and storage-key constants out of App.tsx into
app/utils/appLifecycle.ts. App.tsx re-exports the seven unit-tested symbols
so existing `from "../../App"` test imports resolve unchanged.
Behavior-preserving; no functional change. Verified: dashboard typecheck
(both passes), eslint clean on both files, 16 pure-function unit tests
pass, App.test.tsx identical to pristine (5 pre-existing experimental-flag
failures, none introduced by this change).
U1 of the App.tsx module-breakup plan
(docs/plans/2026-06-24-001-refactor-dashboard-app-tsx-module-breakup-plan.md).
Chat sessions now open via data-testid selectors (chat-session-chat-* and
chat-room-item-leads) and the right dock is closed before capture to prevent
pointer interception. Direct chat threads and the #leads room are now properly
selected and visible in all agent chat and chat room captures.
> ⚠️ **Draft — do not merge until the stack lands.** Stacks on the
workspace foundation (#1710) + U0 (#1711) + Phase A (#1713). Targets
`main`; its diff **includes the whole stack**. **Review only the Phase-B
commits** (`fc9423e` U1, `81edbee` U2, `453ed92` review fixes). Retarget
once the stack merges.
## Workspace mode — Phase B (per-repo capture, contamination, review,
completion verify)
Implements Phase B of the [master
plan](docs/plans/2026-06-21-002-feat-workspace-mode-execution-model-plan.md)
— units U3, U4 ([Phase-B
plan](docs/plans/2026-06-21-005-feat-workspace-phase-b-plan.md)). The
executor's change-capture, contamination, worktree-invariant, review,
and completion-verify paths now iterate `task.workspaceWorktrees` **per
sub-repo**, each against that repo's own `baseCommitSha` (Phase A), with
repo-prefixed file lists. After Phase B a workspace task can run **and**
be captured + reviewed + completion-verified — short of merge (Phase C).
A feasibility pre-check corrected the plan before implementation:
capture/contamination/scope-leak were **not gated** — they silently
degraded to empty against the non-git root. Phase B adds the missing
workspace branches and **reuses the existing `captureModifiedFiles`**
machinery per repo (its `resolveDiffBaseRef` merge-base fallback handles
undefined per-repo bases; its `filterFilesToOwnTaskCommits` divergence
audit restores contamination for free) rather than hand-building diffs.
### What changed
- **U1 — per-repo capture + verify.** Post-session capture loops
`workspaceWorktrees`, reusing `captureModifiedFiles(repo.worktreePath,
repo.baseCommitSha, …)` and repo-prefixing into `task.modifiedFiles`;
branch-attribution runs per sub-repo. `verifyWorktreeInvariants`
un-stubbed to iterate every acquired worktree, **preserving its
`{ok|reason|observed|expected}` union** (the `reason` enum drives
requeue/handoff) with an additive `repo` field. The no-op
`assertCleanBranchAtBase` is not iterated.
- **U2 — per-repo review (both sites) + completion verify.** A shared
`reviewWorkspacePerRepo` loops the existing single-cwd `reviewStep` once
per sub-repo (the reviewer agent runs its own `git diff` at that cwd —
**N reviewer agents**, an accepted cost) and aggregates repo-tagged
verdicts as a **conjunction**. Both entry points iterate —
`fn_review_step` and the step-inversion seam (FN-5893). `fn_task_done`
runs the per-repo `verifyWorktreeInvariants` and a per-repo
`evaluateTaskDoneScopeLeak`. New `workspace-paths.ts` repo-prefix
helper.
### Review
4-persona `ce-code-review`. **No P0**; the review conjunction was
confirmed safe (no false-done — empty map and per-repo throws both route
to UNAVAILABLE → blocks). Fixed in-branch: the scope-leak guard now
**fails closed** on a per-repo throw (was fail-open) and **blocks a
scoped task that acquired zero worktrees** (was silently passing); the
review loop breaks on first non-APPROVE (preserving the verdict); the
`.changeset` always-allowed carve-out is honored in workspace mode
(wiring in the repo-local helper); deterministic offending-repo
ordering. Verified safe: semaphore release on throw + reviewer abort via
session disposal.
### Deferred to Phase C
- Extract a `workspace-executor.ts` module (executor.ts is 16k+ lines) —
to land **at the start of Phase C, before the merge loop**
(maintainability P1).
- Committed off-scope changes can still slip past per-repo scope-leak
when a repo's `baseCommitSha` is undefined and no merge-base resolves
(`captureModifiedFiles` returns `[]`) — partially pre-existing,
amplified by treating undefined base as normal.
- Store-level **atomic** per-repo `workspaceWorktrees` merge (becomes
reachable here with multi-repo acquisition).
- The merge loop / landed predicate / file-scope leases (master
U5/U6/U7).
### Verification
Gate green: lint, typecheck (29 projects), build, `test:gate` (649+58);
workspace tests 28+ (capture, review conjunction, fail-closed scope
guard, zero-acquire block, `.changeset` carve-out).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1714">
<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
## Release Notes
* **New Features**
* Added workspace mode support for multi-repository task execution with
per-repo worktree management.
* Dashboard now displays workspace task status with "N repos acquired"
summary and per-repo worktree details in task cards and detail modals.
* **Bug Fixes**
* Fixed workspace tasks rendering blank on the dashboard; task cards and
detail views now display acquired repository information.
* **Improvements**
* Enhanced workspace acquisition reliability with identity guards,
per-repo base commit tracking, and serialized acquisition controls to
prevent conflicts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Collapse the left sidebar in all captures for full-width content
- Ensure a chat thread is selected in agent chat screenshots
- Add board Graph view (task dependency node graph) screenshot
- Add Command Center Overview, Activity, and Signals tab screenshots
- Re-capture all GIFs and PNGs with sidebar collapsed at 1600x1000
Closes#1675
## Summary
Adds `X-Session-Id` and `X-Session-Affinity` request headers to all
outbound LLM chat completion requests. These headers are widely
understood by LLM gateways, proxies, and observability tooling:
- **Gateway sticky routing** — keep consecutive requests from one
conversation on the same backend or cache instance
- **Observability trace grouping** — tools like Langfuse and Arize group
individually stateless API calls into a single cohesive multi-turn chat
trace
- **Memory/proxy middleware** — fetch and append conversation history
matching the session id
Both headers carry the same stable identifier: the **task id** when
available (stable across pause/resume), otherwise the **pi session id**
for non-task sessions (chat, summarizer, reviewer).
## Implementation
The headers are injected by wrapping `modelRegistry.getApiKeyAndHeaders`
— the single chokepoint pi-coding-agent uses to resolve per-request auth
and headers for both the main stream and compaction. Verified against
the pi-coding-agent source (`sdk.js:171`,
`agent-session.js:145/162/1490`) that all HTTP-based provider paths
route through this method. The wrapper merges routing headers into the
resolved output, preserving provider-specific headers and never
disturbing API-key resolution.
The `modelRegistry` is created fresh per `createFnAgent` call, so the
mutation is session-scoped.
## Changes
- **`packages/engine/src/pi.ts`** — `buildSessionRoutingHeaders()` and
`attachSessionRoutingHeaders()` helpers; wiring call inside
`createFnAgent` resolving `sessionRoutingId = options.taskId ??
piSessionId`. Warns (instead of silently no-oping) if the pi API changes
and `getApiKeyAndHeaders` is absent.
- **`packages/engine/src/executor.ts`** — propagate `taskId` to four
secondary task-scoped sessions (retry, verification-fix, workflow-step,
child-agent) that previously fell back to a per-instance pi id,
fragmenting per-task observability grouping.
- **`packages/engine/src/__tests__/pi-session-routing-headers.test.ts`**
— unit tests for the helpers (header shape, merge, failed-auth
passthrough, absent-method no-op).
- **`packages/engine/src/__tests__/pi-create-fn-agent.test.ts`** —
end-to-end wiring tests asserting the `taskId ?? piSessionId` precedence
inside `createFnAgent`.
- **`.changeset/session-routing-headers.md`** — `@runfusion/fusion:
minor` (new feature).
## Test plan
- [x] Engine typecheck clean
- [x] ESLint clean
- [x] `pi-session-routing-headers.test.ts` (5 tests)
- [x] `pi-create-fn-agent.test.ts` wiring tests (3 new, 79 total)
- [x] Engine-core gate suite (610 tests)
- [x] Executor + pi test suites (946 tests)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1736">
<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 session-routing HTTP headers (`X-Session-Id`,
`X-Session-Affinity`) to all outbound LLM chat completion requests.
* Uses a stable routing identifier derived from `taskId` when available
(preserving consistency across pause/resume), otherwise falls back to
the current session id.
* Ensures follow-up and spawned sessions reuse the same routing id for
consistent sticky routing and trace correlation.
* **Tests**
* Added coverage to verify header construction, merge behavior, and
fallback/no-op cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Re-captured all showcase GIFs and screenshots from the running Fusion
dashboard at 1600x1000 (16:10) to match original aspect ratios and avoid
the vertical squashing caused by vh-based CSS at shorter viewports.
Media changes:
- Dashboard board + Agents roster shots (Tokyo Night + Ember)
- Command Center, Workflows, Chat, Chat Rooms, Agent Mail GIFs refreshed
across Tokyo Night, Shadcn Light, Shadcn Dark Gray, and new Ember theme
- 6 mobile screenshots (board, agents, missions, command center, chat)
- 8 new Ember-themed assets added to every showcase table
Also extended demo/seed.ts to seed chat messages and agent mail (agents,
a direct chat session, a #leads multi-agent room, and mailbox messages
covering triage summaries, approvals, and hand-offs) and fixed two
pre-existing task-transition bugs so the full seed runs to completion.
README updated to add a 'board & agent team' showcase section and expand
every theme table from 2 columns to 3 (Light / Dark Gray / Ember).
> ⚠️ **Draft — do not merge until the stack lands.** Stacks on the
workspace foundation (#1710) + U0 (#1711), which live on a fork and
can't be PR bases here, so this targets `main` and its diff **includes
#1710 + #1711 + Phase A**. **Review only the Phase-A commits**
(`09bd01b` U1, `023e4b0` U3, `12d33c5` U2, `d5fa865` review fixes).
Retarget to a clean Phase-A-only diff once the stack merges.
## Workspace mode — Phase A (make a workspace task *run*)
Implements Phase A of the [master
plan](docs/plans/2026-06-21-002-feat-workspace-mode-execution-model-plan.md)
— units U1, U2, U10 ([Phase-A
plan](docs/plans/2026-06-21-004-feat-workspace-phase-a-plan.md)). A
workspace task (Project rootDir = non-git parent of sub-repos) can now
acquire per-repo worktrees and browse/edit in them. Capture/review/merge
are Phases B–D.
Settled design (from the master plan): **land-as-you-go on each repo's
LOCAL integration ref** (no remote push), session-time coherence. The R7
merge-boundary guard from U0 stays at the merge chokepoint; Phase A
doesn't route around it.
### What changed
- **U1 — executor session scoping.** In workspace mode the executor
skips the root `acquireTaskWorktree` and every rootDir git preflight
(base-commit, contamination, identity-guard,
`verifyWorktreeInvariants`), roots the agent session at the non-git
workspace root (browse-only; `task.worktree` unset), and tracks
`activeWorktrees` as a per-task `Set<path>` — every consumer converted
to membership semantics (incl. `listWorktreeHolders` flat-mapping N
holder rows, the unregister resolvers looping all paths). Per-repo
acquired paths are registered into the Set via the
`fn_acquire_repo_worktree` tool. Non-workspace path byte-for-byte
unchanged. The foundation's self-mocking test was replaced with a real
two-repo git fixture harness reused by later units.
- **U2 — per-repo acquisition hardening.**
`acquireWorkspaceRepoWorktree` now installs the identity guard (executor
parity args), captures a per-repo `baseCommitSha` **local-first**
against the repo's own resolved integration branch (stripping the shared
`integrationBranch`/`baseBranch` override so each sub-repo falls through
to its own `origin/HEAD`), persists it into `workspaceWorktrees[repo]`,
and registers same-sub-repo exclusivity via `activeSessionRegistry`
path-keying (a recycle pool isn't a cross-task lock). Post-acquire steps
are non-fatal; idempotent re-acquire.
- **U10 — dashboard floor.** Workspace tasks render a placeholder / flat
per-repo list instead of a blank card; CONCEPTS.md notes the non-atomic
merge semantics.
### Review
5-persona `ce-code-review` (correctness, adversarial, reliability,
api-contract, maintainability). **No P0** — the workspace-root-removal
path was specifically ruled out, and the contract additions
(`baseCommitSha?`, `GitMutationType`/`ActiveSessionKind` unions,
optional base-capture param, `WorkspaceRepoAcquireBusyError`) verified
additive/non-breaking. Corroborated P1s fixed in-branch:
`fn_acquire_repo_worktree` now returns a sanitized retryable error (was
an uncaught throw); per-repo paths are actually added to
`activeWorktrees`; post-acquire failures no longer strand the worktree.
Plus P2s (baseBranch strip, sequential-acquire clobber, busy-path
logging, memo key-set, observability).
### Deferred to Phase B (by scope)
Per-repo worktree teardown on completion, `scanIdleWorktrees` orphan
coverage for sub-repo worktrees, FN-6782 reaper dedup of multi-row
holders, a store-level atomic per-repo `workspaceWorktrees` merge. Also:
don't run a workspace task end-to-end until Phase B re-adds per-repo
contamination/verify.
### Verification
Gate green: lint, typecheck (29 projects), build, `test:gate` (649 +
58); workspace tests 25, dashboard 251.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1713">
<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
## Release Notes
* **New Features**
* Workspace mode execution now properly displays task status in
dashboard with "N repos acquired" placeholder.
* Improved workspace task acquisition with per-repository exclusivity
controls and automatic retry logic.
* **Bug Fixes**
* Fixed blank rendering of workspace tasks in dashboard and detail
views.
* Enhanced error handling for workspace task acquisition failures with
improved visibility.
* **Documentation**
* Updated workspace mode execution plan with implementation details and
phase breakdown.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Two workflow-editor improvements (the branch-group work this branch is
named for is already on `main`):
### FN-6880 — Graph-native optional steps + add-ons as subgraphs
Optional steps move from an execution-inert *declaration*
(`optionalSteps: [{templateId}]` run through a hidden `workflow-step`
seam) to a real graph construct:
- **`optional-group` container node** (mirrors `foreach`/`loop`) holding
a `template:{nodes,edges}` subgraph. The graph executor runs it **once**
when the group is enabled for a task and **bypasses** it (passes
through, runs no template node) when disabled. (U1 IR+validation, U2
executor)
- **Enable state reuses the per-task `enabledWorkflowSteps` facet** + a
workflow-level `defaultOn`, keyed by the group node id; new tasks seed
from `defaultOn`. (U3)
- **Editor authoring**: the container is a registered React-Flow group
node with a `defaultOn` toggle; round-trips through
`flowToIr`/`irToFlow`. (U4)
- **All 7 built-in add-ons** (documentation-review, qa-check,
security-audit, performance-review, accessibility-check,
browser-verification, frontend-ux-design) are insertable from the
palette as a node **or** wrapped in an optional-group. (U5)
- **Built-ins migrated**: coding + stepwise-coding express
`browser-verification` as an optional-group (default OFF); coding is now
interpreter-deferred. Parity oracles updated. (U6)
- **Legacy declaration surface retired** (U7a): the
`WorkflowOptionalStep` type + `optionalSteps` IR field +
`validateOptionalSteps`, and the editor's declaration authoring UI
(`WorkflowOptionalStepsPanel`, `optionalStepsOf`, `flowToIr` threading).
A legacy persisted `optionalSteps` key is tolerated at parse. The
per-task toggle surfaces (dropdown, inline card, modal, Workflow tab)
are unchanged — they consume the distinct
`ResolvedWorkflowOptionalStep`.
### FN-6879 — Workflow node Help in the detail pane
Every node kind (including engine-managed graph-only nodes: merge gate,
branch-group integration/promotion, PR/recovery) gets an in-editor Help
section: what it does, how to configure it, inputs/outputs/edges.
## Code review
4 reviewers (correctness, testing, API-contract, reliability) over the
feature diff. One **P1** found and fixed: enabling a built-in
optional-group whose node id collides with a `WORKFLOW_STEP_TEMPLATES`
id was silently bypassed (the toggle id was remapped to a materialized
step row id the executor never matched) — fixed by passing
optional-group ids through enable resolution untouched, with
colliding-id regression tests on the create + update paths. Triaged P3s
(description/icon drop, switch-time seeding, selection-row id mix) are
benign/deferred.
## Verification
- Lint clean · `tsc --noEmit` 0 errors in core/engine/dashboard · `pnpm
build` succeeds · `pnpm test:gate` green (engine-core 649, ci-shape 58).
- Feature suites: core IR/validation/resolver/seeding, engine
run-once/bypass + two-task divergence + built-in execution, dashboard
editor round-trip + palette + node-help.
## Deferred (documented follow-ups)
- **`workflow-step` seam infrastructure removal** — the seam is a shared
`WorkflowSeam` union member woven through ~9 engine runtime files
(`runtime-primitives`, `step-session-executor`,
`workflow-node-handlers`, compiler seam-anchor, `runWorkflowSteps`). Now
orphaned (no built-in uses it) but inert; excising it is its own unit.
See the plan's Deferred section.
- Minor: optional-group `description`/`icon` on the toggle UI;
`defaultOn` seeding on post-create workflow switch.
## Not run
- Browser/visual testing was not run in this environment
(worktree-bundle dashboard setup). UI is covered by jsdom render tests
including the node-type registration test (the exact
stale-bundle/unregistered-type failure mode the team's learnings flag).
Recommend a real-browser pass before merge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1712">
<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
## Release Notes
**New Features**
- Optional-group container nodes now power optional workflow behavior
(run once per enabled task; bypass when disabled).
- Added a Help section to the workflow node inspector, including
“Engine-managed” read-only nodes.
- Added “insert as optional group” to wrap palette templates quickly.
**Improvements**
- Built-in coding/stepwise workflows now use optional-group for browser
verification.
- Task enablement supports optional-group `defaultOn` seeding.
**Bug Fixes**
- Fixed cases where optional-group toggles could be silently bypassed
when ids collide.
**Changes (Breaking)**
- Retired the legacy optional-step model and its authoring UI; migrate
to optional-group nodes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Add X-Session-Id and X-Session-Affinity headers to all outbound LLM chat
completion requests so LLM gateways can sticky-route consecutive requests
from the same conversation and observability tools (Langfuse, Arize) can
group stateless API calls into a single multi-turn trace.
The headers carry a stable identifier: the task id when available (stable
across pause/resume), otherwise the pi session id. The implementation wraps
modelRegistry.getApiKeyAndHeaders -- the single chokepoint pi-coding-agent
uses for both the main stream and compaction -- merging routing headers into
the resolved output. This covers all HTTP-based providers (built-in, custom,
and HTTP-streaming extensions) without disturbing auth resolution.
Also propagates taskId to four secondary executor sessions (retry,
verification-fix, workflow-step, child-agent) that previously fell back to
a per-instance pi id, fragmenting per-task observability grouping.
Closes#1675