- probeWorktrunk: also refuse a bare wt/wt.exe override on Windows (resolves to
Windows Terminal via PATH), and tighten the package-dir match from a broad
'windowsterminal' substring to 'microsoft.windowsterminal' so a genuine
worktrunk under an unrelated *windowsterminal* folder is still probed.
- Fix worktrunk enable deadlock/save-race: probe status when the user views the
Worktrees section (not gated on 'enabled', which deadlocked since the toggle is
disabled until status==installed), and re-verify on Save so a fast enable+save
can't silently persist enabled:false. Hook 'refresh' now returns the fetched
status and is exposed.
- Tests: bare-wt refusal, forward-slash Windows Terminal path, unrelated
windowsterminal-folder is probed, and a rerender enabled false->true probe-once
transition; update SettingsModal mocks for the new refresh().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 1882/1883 terminal-auto-create guard fixed the wrong subsystem; the
embedded terminal was already guarded. The actual trigger: worktrunk's CLI is
named 'wt', colliding with Windows Terminal (wt.exe) on PATH, so probing it with
'wt --version' launched Windows Terminal and popped its native version dialog —
fired automatically by the Settings worktrunk-status fetch on mount.
- useWorktrunkInstallStatus: only auto-fetch /api/worktrunk/status when the
integration is enabled (user opt-in), never on a plain Settings/dashboard mount.
- probeWorktrunk: refuse to exec a resolved 'wt' that is the Windows Terminal
alias (WindowsApps / WindowsTerminal package dir), covering every resolution
surface (cached/override/PATH/install/settings-route). Basename computed
host-independently so the guard holds when the build host is POSIX.
- Tests for both guards; report updated with corrected root cause + Symptom
Verification + Surface Enumeration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace real-timer waits with a deterministic bounded microtask flush for
the fire-and-forget task lifecycle hook chain (FN-5048: no slow tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Windows: local runtime hung at "Starting local Fusion runtime…"
### Root cause
Desktop startup had two independent persisted sources of truth that
could disagree:
- `desktop-launch-mode.json` — decides whether **main** *starts* the
embedded local runtime
- `shell-connections.json` (`desktopMode`) — decides whether the
renderer **launch gate** *waits* for it
`shell:setDesktopMode` persists shell settings **before** the fallible
`startLocalRuntimeOnce()` / `saveDesktopLaunchMode()`. So a first
"local" selection whose runtime start threw or was interrupted left
`shell=local` / `launch-mode=choose` **permanently**. Every later launch
then sat at "Starting local Fusion runtime…" polling a runtime nobody
started → 30s timeout.
### Fix (defense in depth) — `78f0bc31`
- `initializeApp` reconciles: a completed shell `local` selection is
authoritative → heals the launch-mode file and starts the runtime.
- `onDesktopModeChange` / `onDesktopLaunchModeChange` persist
launch-mode **before** the fallible start so it can't re-desync.
- `DesktopLaunchGate` no longer assumes main started the runtime — if
it's not running/starting it actively `setDesktopMode("local")` before
polling.
- Env-gated startup trace (`FUSION_STARTUP_TRACE`) so packaged builds
(which log nothing) are diagnosable.
- Regression tests: split-brain → runtime starts + file heals;
agreement-on-choose → no start.
**Verified end-to-end under real Electron 35 / Node 22.16**: from the
exact split-brain state the runtime now reaches `RUNNING` and the
launch-mode file heals.
### Also: Windows root-build breakages — `bd24bd4c8`
- `scripts/build-workspace.mjs` "run as main" guard compared
`import.meta.url` to `` `file://${process.argv[1]}` ``, which never
matches on Windows → root `pnpm build` silently no-opped (exit 0, no
dist). Now uses `pathToFileURL(process.argv[1]).href`.
- `spawn('pnpm', …)` without `shell:true` (ENOENT on Windows) in
`build-workspace.mjs` and `packages/cli/tsup.config.ts` → pass `shell`
on win32.
### Notes
- `@fusion/desktop` and `@fusion/dashboard` are private → no changeset.
- Build the Windows installer via the `desktop-windows` workflow
(`electron-builder --projectDir deploy`); local `pnpm deploy` staging
hits an unrelated directory-rename race on managed-workspace
filesystems.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added optional desktop runtime startup tracing (enabled via
environment variable).
* **Bug Fixes**
* Improved local desktop handoff to prevent reload loops and navigate
directly to the embedded local runtime.
* Added “split-brain” healing between persisted launch mode and shell
settings.
* Prevented auto-registration of runtime root/CWD during
desktop/dashboard startup.
* Improved Windows compatibility for CLI/workspace command spawning and
npm install process handling.
* **Tests**
* Expanded local/Electron integration, navigation, and onboarding
regression coverage; improved async flushing for more reliable
initialization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
createServer's modelRegistry is optional and, when absent, the /api/models endpoint returns an
empty list — so after connecting a provider (e.g. Anthropic) the onboarding model picker showed
"no models". The desktop wired authStorage but no ModelRegistry.
Add a shared createFusionModelRegistry(authStorage) factory to @fusion/engine (which already depends
on @earendil-works/pi-coding-agent) so non-CLI hosts don't need that dep directly, and pass its
result as modelRegistry from both desktop server paths. Verified against a clean embedded server:
/api/models now returns 24 Claude models instead of an empty list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a task adds/removes a dependency without regenerating the lockfile,
the inferred `pnpm install --frozen-lockfile` (and yarn/bun equivalents)
in the AI-merge clean room fails with ERR_PNPM_OUTDATED_LOCKFILE, dead-
ending the merge. Detect that specific frozen-refusal and retry once
non-frozen (pnpm gets explicit --no-frozen-lockfile to override any CI
default), regenerating the lockfile and recomputing the install marker.
A configured worktreeInitCommand keeps its authoritative frozen intent
and still hard-fails. Surfaced via the merge:ai-deps-sync run-audit event.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Missing mock export (5 files, 17 tests):
- reviewer-prompt-single-source, plan-review-unavailable-recovery,
triage-fast-mode-workflow-variant, triage-stuck-requeue-preserve-draft,
restart.integration: add formatModelMarkerDetails to the ../pi.js mock.
Production code calls this after resolving an agent session, but the test
mocks were missing the export, causing all reviewer/triage/restart paths
to throw before reaching finalization assertions.
Outdated worktree assertions (2 files, 5 tests):
- worktree-acquisition-backend, worktree-acquisition-worktrunk: update
assertions for the new git symbolic-ref origin/HEAD resolution call and
the appended start point in git worktree add (worktree isolation fix).
Stale workflow compiler tests (1 file, 2 tests):
- workflow-routes: FN-7360 removed the linear compiler /compile endpoint
and made parseWorkflowIr the sole validity gate. Branching custom
workflows are now valid on the graph interpreter. Updated the two stale
tests that asserted 422 for branching IR to assert 201/200 instead.
Reviews no longer fail on formatting. Three changes to how reviewer/gate
verdicts are parsed and how retries reset state:
- Approval leniency: a review that clearly approves in prose passes even
without a structured verdict (proseSignalsClearApproval, with a
revise/reject/negated-approval guard so a rejection is never flipped). Any
APPROVE*/APPROVAL verdict token classifies as approved. Shared by the
reviewer/plan-review parser and the code-review/browser-verification gate.
- Prose + trailing JSON: extractJsonObjectCandidates does a string-aware
balanced-brace scan and prefers the last object, so a model that emits
reasoning prose then a trailing {"verdict":...} payload parses correctly.
An explicit "Verdict:" heading/line still takes precedence over an
incidental/example JSON object.
- Malformed handling: executeWorkflowStep retries the fallback model on
malformed output (not just timeout); malformed gate output becomes a
non-blocking advisory (a genuine parsed REVISE still blocks).
- Retry clears prior terminal step failures (incl. optional gate nodes like
code-review) after the task leaves the mergeable in-review column, so a
retry starts clean without an auto-merge race.
Fail-closed merge / PR-review / mission-verification gates are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add TaskExecutor.blockOuterDispatchWhenEphemeralDisabled, gating all three
workflow dispatch paths (graph / authoritative / work-engine) on
ephemeralAgentsEnabled at the top of execute(). Previously the toggle was
enforced only on the legacy scheduler/EphemeralWorkerManager path — whose
onTaskStart spawn refusal is a fire-and-forget callback that runs after
execution begins — so unassigned tasks reaching execute() off a non-scheduler
path still ran. Unassigned tasks are now re-queued for permanent-agent
assignment; permanent-agent-bound tasks still run. Adds regression coverage
across all three entry points.
Also includes the ephemeralAgentsCanCreateTasks project setting (default on)
gating fn_task_create for ephemeral callers in both the pi extension and the
executor task-worker tool.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A pre-merge-remediation/plan-replan node (e.g. code-review-remediation) is a
fire-and-forget async scheduler with no failure out-edge. When its schedule call
can't re-arm (missing rehydrated failureContext after restart,
remediation-not-scheduled, or an exhausted rework budget), the failure bubbled
out as the terminal graph outcome and handleGraphFailure stamped status:"failed"
— surfacing a spurious "Task Failed" even while the previously-scheduled
fix/reviewer session was still live.
Guard the terminal sink: skip the failed park when the failed node is a
remediation node AND a live agent session surface is still registered for the
task. Scoped via isRemediationGraphNode (IR workflowAction + built-in node-id
fallback) and hasLiveTaskSessionSurface; genuine execute/merge failures and
remediation failures with no live session still park failed unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the pnpm-lock.yaml conflict. Main's #1865 (review-checkout routing)
auto-merged cleanly with the completion-summary backstop in executor.ts.
Main independently pinned pi-claude-cli's pi-ai/pi-coding-agent to ^0.80.3
(e15489259) but kept the top-level `getModels` import, which 0.80.3 removed —
this branch's migration to `getBuiltinModels` from `/providers/all` is retained
as the working fix. Lockfile regenerated against the merged package.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- Add explicit `sourceMetadata.externalReviewCheckout` review routing
with fail-closed validation.
- Log the selected review checkout so operator/runtime tasks can verify
where review ran.
- Cover metadata extraction, invalid metadata fallback,
`fn_review_step`, and workflow `stepReview` routing.
## Test Plan
- `cd packages/engine && corepack pnpm exec vitest run
src/__tests__/review-checkout.test.ts
src/__tests__/reviewer-workspace.test.ts`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Review routing now supports explicit external checkout overrides via
review checkout metadata, using the resolved external checkout as the
review working directory when valid.
* Logging now highlights the selected review routing target and warns
when external checkout metadata is present but invalid.
* **Bug Fixes**
* Missing/blank/invalid/relative/nonexistent/non-git paths now fail
closed to the task worktree instead of falling back.
* Metadata priority is enforced strictly (custom fields → branch context
→ source metadata → root), preventing lower-priority fallback when
higher-priority data is invalid.
* **Tests**
* Added/expanded automated coverage for override and fail-closed
behavior, including workspace-mode routing semantics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Two distinct v0.52.0 regressions reported in issue #1863.
1. Triage loop (engine): the best-effort completion-summary graph node is
wired into every built-in workflow with a success-only edge. A thrown
handler exception or a failed summary projection write bypassed the
advisory `!blocking -> success` coercion, terminated the graph at
'completion-summary', and routeGraphFailureToExecutionResume bounced the
in-review task back to todo forever (token usage 0, execution NOT STARTED).
The graph executor now degrades a completion-summary node failure to
success (ensureWorkflowCompletionSummary still backfills task.summary), with
a routeGraphFailureToExecutionResume backstop. Shared isCompletionSummaryNode
predicate exported from @fusion/core.
2. i18n object-key crashes (dashboard): three views called t() with keys that
resolve to nested objects (taskDetail.executionMode, routing.source,
nodes.dockerHost), so i18next returned "returned an object instead of
string" and crashed the render. Added leaf label keys across all locales and
switched the callers.
Tests: engine non-fatal completion-summary regression (fails without the fix),
dashboard invariant guard scanning t("literal") callers against real en/app.json,
and a Stats-panel reproduction against the real bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sonnet 5 had disappeared from every surface: pi-ai 0.79.9 (the installed
version) lacks it, and FN-7374 removed the static row expecting the live
registry to carry it. Live-verified that claude-sonnet-5 returns 200 on
api.anthropic.com/v1 with a raw ANTHROPIC_API_KEY and runs via the Claude
CLI (it 403s on subscription-OAuth /v1 — scope-gated; runtime fallback
applies). Note: pi-ai 0.80.3 ships sonnet-5 natively, so this SUPPLEMENTAL
row dedupes once the install catches up.
- core: re-add claude-sonnet-5 to SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION
and restore its static pricing (revert FN-7374); update pricing tests.
- engine/dashboard tests: flip the FN-7374 "withheld" assertions to the
restored "advertised" behavior.
PR feedback:
- Trim the two FNXC comments (auth-storage.ts, pi.ts) to concise
requirement prose per coding guidelines (CodeRabbit).
- Replace the now-inert getApiKey mock in two subscription routing tests
with a clarifying note (Greptile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude subscription (Max/Pro) chats regressed to 404/502/429 because
FN-7396 rerouted subscription OAuth to a /v1-based `anthropic-subscription`
runtime provider — reintroducing issue #1857 that FN-7391 had fixed. Both
routed the OAuth token to api.anthropic.com/v1, the surface that broke.
Proven in code that v0.51.0 (working) sent subscription OAuth directly to
/v1 via pi-ai's built-in `anthropic` provider (Claude Code impersonation:
Bearer + anthropic-beta oauth headers), NOT through the CLI. Restore that:
- auth-storage: getApiKey("anthropic") resolves subscription/legacy OAuth
again (raw API key still wins), so the built-in provider gets the token.
- pi.ts: remove the runtime reroute and the /v1 `anthropic-subscription`
execution provider so anthropic/* selections stay on the built-in provider.
- register-model-routes: advertise `anthropic` for OAuth users so direct
OAuth is selectable in the picker.
Three independent surfaces, no rerouting: direct OAuth, raw ANTHROPIC_API_KEY
(precedence), and explicit pi-claude-cli.
Fixes#1857
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep Anthropic OAuth subscription credentials away from the direct /v1 provider while preserving raw API-key routing.
- Treat direct anthropic auth as raw API-key-only in auth storage and model discovery.
- Re-route persisted anthropic model selections to the Claude CLI provider when no raw Anthropic API key is configured.
- Add regression coverage for OAuth-only routing, raw API-key preservation, and provider list separation.
- Document the distinct raw API key, OAuth subscription, and Claude CLI routes.
Files changed:
.../fn-7391-anthropic-subscription-cli-routing.md | 7 ++
docs/settings-reference.md | 2 +-
.../dashboard/src/__tests__/routes-auth.test.ts | 74 ++++++++++++++
.../dashboard/src/routes/register-model-routes.ts | 27 +++++-
packages/engine/src/__tests__/auth-storage.test.ts | 108 +++++++--------------
.../src/__tests__/pi-create-fn-agent.test.ts | 100 +++++++++++++++++++
packages/engine/src/auth-storage.ts | 45 ++++-----
packages/engine/src/pi.ts | 61 ++++++++++++
8 files changed, 319 insertions(+), 105 deletions(-)
Fusion-Task-Id: FN-7391
Fusion-Task-Lineage: d1fecc2d-7f31-408c-bf65-0e65b7fd88b1
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
- add an opt-in `allowAnswerQuestionIdDrift` flag for interactive AI
sessions
- keep strict question-id validation by default
- enable the tolerance only for Compound Engineering recovered sessions
so persisted session rows can answer after dashboard
restarts/non-deterministic rehydration
## Test Plan
- `corepack pnpm --filter @fusion/engine exec vitest run
src/__tests__/interactive-ai-session.test.ts --silent=passed-only
--reporter=dot`
- `corepack pnpm --filter @fusion/engine typecheck`
- `corepack pnpm --filter @fusion-plugin-examples/compound-engineering
build`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved interactive session recovery so persisted answers can
continue after dashboard restarts, even if the question ID changes
during rehydration.
* Keeps strict question-ID validation by default; mismatches still fail
unless drift is explicitly allowed.
* **New Features**
* Added `allowAnswerQuestionIdDrift` option to permit accepting the
persisted question ID during recovered session answering.
* **Tests**
* Added/updated coverage for strict mismatch error behavior and the
successful completion path when drift is enabled.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The step execution engine was already gone (runWorkflowSteps deleted,
workflow_steps table dropped in migration 132). This removes what remained:
the linear step compiler (compileWorkflowToSteps/validateLinearity/
WorkflowCompileError), which survived only as a validator + step-preview
generator.
parseWorkflowIr/validateV2 (which accepts branching graphs) is now the sole
workflow validity gate at save/select/refine and in the graph task runner.
Custom branching workflows are now selectable and run on the graph
interpreter instead of being rejected as non-linear.
- core: delete workflow-compiler.ts; rework store.validateWorkflowCompilable
onto parseWorkflowIr; move MERGE_REGION_NODE_KINDS into
workflow-lifecycle-validation; retag workflow-steps-to-ir as legacy lowering
- engine: drop the compiler double-validation in workflow-graph-task-runner
- dashboard: remove POST /api/workflows/:id/compile + client wrapper; drop the
interpreterOnly response field and editor banner; no post-save compile check
- i18n: remove the orphaned workflowNodes.interpreterOnly key across locales
- tests: reframe two workflow-selection tests whose premise inverted; fix a
pre-existing red in builtin-lead-generation (completion-summary node)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-merge finalization now ignores stale branch-only residue once durable merge proof exists, so squash-landed tasks do not stay stuck in review because their task branch history is noisy.
Fusion-Task-Id: FN-7360