- Close first TaskStore before creating second in interactive registration (P1)
- Revert defaultWorkflowId default to undefined; set explicitly in onboarding only (P1)
- Add alpha-only filter + 2-char min to interactive prefix input (P2)
- Move suggestTaskPrefix to @fusion/core, share between CLI and dashboard (P2)
- Fix suggestTaskPrefix JSDoc to match implementation (P2)
- Fix workspace detection: change workspaceMode default from false to
undefined so isWorkspaceModeExplicitlyDisabled no longer blocks
auto-detection on fresh projects (config.json was being written with
workspaceMode:false during store.init(), causing the guard to skip
detection before it ever ran)
- Derive task prefix from project name (first 2-4 chars) instead of
hardcoded 'FN' as the suggested default
- Default workflow is now builtin:coding instead of undefined
- CLI registerProjectInteractive: onboarding prompt for task prefix
confirmation after project name
- Dashboard POST /api/projects: auto-derive prefix and set default
workflow for new registrations
## Problem
The previous fix (#1738) only checked `loadWorkspaceConfig` to skip `git
init` on workspace-mode roots. However, the dashboard `POST
/api/projects` and `fn project add` routes never create `workspace.json`
(only the CLI interactive setup in `registerProjectInteractive` does).
So re-adding a workspace project through the dashboard still triggered
`git init` because the guard saw no `workspace.json`, creating a stray
`.git` at the workspace root.
## Fix
Add `detectWorkspaceRepos` as a fallback in
`ensureGitRepositoryForProjectPath`: 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.
## Files Changed
- `packages/core/src/git-repository.ts` — `detectWorkspaceRepos`
fallback + `saveWorkspaceConfig` + FNXC comment
- `packages/core/src/__tests__/git-repository.test.ts` — regression test
verifying sub-repo detection skips `git init` and persists
`workspace.json`
## Testing
- `pnpm typecheck` (`@fusion/core`) — pass
- `pnpm lint` — pass
- `vitest run git-repository.test.ts` — 6/6 pass (5 existing + 1 new)
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1739">
<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 “Workspace mode (multi-repo)” checkbox in the dashboard to
enable/disable workspace behavior.
* Updated CLI interactive mode to prompt for using workspace mode;
non-interactive mode continues to auto-detect sub-repositories.
* **Bug Fixes**
* Improved workspace auto-detection when workspace config is missing,
including persisting detected repositories.
* Workspace mode now skips creating a root `.git` when sub-repositories
are detected.
* When workspace mode is explicitly disabled, auto-detection is skipped;
disabling also removes the workspace config.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Address PR #1739 review round 3:
- Major (coderabbit): Reorder writes so setWorkspaceModeInConfig runs
before saveWorkspaceConfig. If the config write fails, no stale
workspace.json is left behind.
- Major (coderabbit): setWorkspaceModeInConfig only treats ENOENT as
empty config (not parse errors or permission errors). Validates
settings is a plain object before merging to prevent clobbering.
Address PR #1739 review round 2:
- P1 (greptile): Auto-detection fallback now sets workspaceMode: true in
config.json so the dashboard toggle reflects the actual state.
- Major (coderabbit): Let saveWorkspaceConfig errors propagate instead of
silently returning 'existing' when the write fails. A failed write would
leave the project with no git repo and no workspace config.
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 -->
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.
> ⚠️ **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 -->
> ⚠️ **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 -->
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 -->
> ⚠️ **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
Raise CustomModelDropdown portal z-index from 1200 to 11000 so model
selection popups render above the shared floating-window stack (10100+)
instead of being obscured by popped-out ChatView and other floating modals.
Let ntfy test notifications use the current Settings form values before they are saved.
- Send unsaved ntfy enabled/topic/server/token values from all Settings ntfy test buttons.
- Resolve request-scoped ntfy config on the backend for general, message, and room test notifications.
- Publish message and room ntfy tests directly with the resolved config instead of the active persisted notification service.
- Cover unsaved ntfy test behavior across Settings UI and API routes.
Files changed:
.changeset/fn-6953-ntfy-test-unsaved-config.md | 5 +
.../dashboard/app/components/SettingsModal.css | 5 +-
.../dashboard/app/components/SettingsModal.tsx | 21 +-
.../components/__tests__/SettingsModal.test.tsx | 60 +++-
.../src/__tests__/routes-settings.test.ts | 181 ++++++++---
.../src/routes/register-settings-memory-routes.ts | 355 +++++++++------------
6 files changed, 380 insertions(+), 247 deletions(-)
Fusion-Task-Id: FN-6953
Fusion-Task-Lineage: 90557168-3b11-4ab4-b5c1-4261225600ba