Move the host-agnostic bundled-plugin auto-install logic (manifest loading, entry-path
resolution, install/update/enable flow) out of the CLI package into @fusion/core so the
desktop embedded runtime can auto-install bundled runtime plugins without depending on
the CLI package; the CLI module becomes a thin adapter that supplies its own bundle-dir
resolution to the shared helper.
- Add packages/core/src/plugins/bundled-plugin-install.ts with the shared, host-agnostic
ensureBundledPluginInstalled / ensureBundledDependencyGraphPluginInstalled /
ensureBundledCursorRuntimePluginInstalled implementation and BUNDLED_PLUGIN_IDS/
isBundledPluginId/resolvePluginEntryPath, exported from @fusion/core's index.
- Slim packages/cli/src/plugins/bundled-plugin-install.ts to a CLI-specific
candidate-bundle-dir resolver that delegates to @fusion/core and re-exports the same
public surface dashboard.ts/serve.ts/daemon.ts already depend on.
- Remove the now-redundant packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
(coverage moved with the implementation to @fusion/core).
- Add packages/desktop/src/bundled-plugin-dirs.ts to resolve each bundled plugin's staged
package directory via import.meta.resolve, mirroring the CLI's dist/plugins/<id> resolver.
- Wire local-runtime.ts and local-server.ts to call ensureBundledPluginInstalled before
loadAllPlugins() and expose a lazy-install callback for PUT /api/plugins/:id/settings,
mirroring the CLI dashboard command's startup auto-install pass.
- Update docs/PLUGIN_AUTHORING.md to describe the shared bundled-plugin-install location.
Files changed:
docs/PLUGIN_AUTHORING.md | 11 +
.../__tests__/bundled-plugin-install.test.ts | 619 ++-------------------
.../resolve-plugin-entry-path-sync.test.ts | 97 ----
packages/cli/src/plugins/bundled-plugin-install.ts | 250 +--------
packages/core/src/index.ts | 8 +
.../__tests__/bundled-plugin-install.test.ts | 391 +++++++++++++
.../core/src/plugins/bundled-plugin-install.ts | 186 +++++++
.../src/__tests__/bundled-plugin-dirs.test.ts | 59 ++
.../desktop/src/__tests__/local-runtime.test.ts | 183 +++++-
.../desktop/src/__tests__/local-server.test.ts | 96 +++-
packages/desktop/src/bundled-plugin-dirs.ts | 61 ++
packages/desktop/src/local-runtime.ts | 66 ++-
packages/desktop/src/local-server.ts | 36 +-
13 files changed, 1171 insertions(+), 892 deletions(-)
Fusion-Task-Id: FN-7637
Fusion-Task-Lineage: 953c5b82-a079-4600-b3af-45c974cd5014
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fix the Planner Chat composer's streaming Stop button rendering narrower than the idle Send button by giving both variants a shared width floor.
- Declare a locally-scoped --chat-input-control-size on .task-planner-chat-composer (same formula as ChatView.css's .chat-input-row so the shared .chat-input-send/.chat-input-stop classes no longer read an undefined custom property and fall back to width: auto.
- Add a min-inline-size floor bound to that property on .task-planner-chat-send (present on both send and stop variants) so neither button renders narrower than the other on desktop.
- Add a regression test asserting the desktop control-size floor, the pre-existing mobile square sizing, and the FN-7594 stop-icon visibility contract.
- Add a changeset documenting the fix as a patch release.
Files changed:
.changeset/fn-7634-planner-stop-button-width.md | 7 +++++
.../app/components/TaskPlannerChatTab.css | 8 ++++++
.../__tests__/TaskPlannerChatTab.test.tsx | 33 ++++++++++++++++++++++
3 files changed, 48 insertions(+)
EOF
)
Fusion-Task-Id: FN-7634
Fusion-Task-Lineage: e73c836f-3c2d-4871-ac93-fdf5e52de5f6
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Align the Priority chip, Execution-mode toggle, and Oversight dropdown trigger to the exact same box height in the task detail metadata cluster, closing a gap where a shared min-height floor still let controls diverge.
- Add explicit `height: var(--detail-priority-control-min-height)` to `.detail-priority-chip`, `.detail-execution-mode-toggle`, and `.detail-oversight-menu-trigger` alongside the existing `min-height`, so none can outgrow or undershoot the others regardless of flex stretch/content differences
- Keep `min-height` as a safety-net floor for edge cases like font scaling
- Add regression test asserting all three controls share the same fixed height token on desktop and mobile, and that the Oversight popover itself remains unaffected
- Add changeset documenting the fix as a patch-level bug fix
Files changed:
.../fn-7633-priority-execution-oversight-height.md | 7 ++++
.../dashboard/app/components/TaskDetailModal.css | 44 ++++++++++++++++---
...etailModal.responsive-and-dependencies.test.tsx | 49 ++++++++++++++++++++++
3 files changed, 95 insertions(+), 5 deletions(-)
Fusion-Task-Id: FN-7633
Fusion-Task-Lineage: 7a89743e-117f-4032-9614-a9a15f2a9b08
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the desktop app's plugin subsystem, which was never wired into createServer(), breaking Settings > Plugins Browse registry and plugin install.
- local-runtime.ts: construct a PluginStore + PluginLoader (mirroring the CLI dashboard command), load enabled plugins, run plugin schema-init hooks, and pass pluginStore/pluginLoader/pluginRunner into createServer()
- local-server.ts: apply the same wiring to the legacy desktop local server path for consistency
- Both paths fail soft: a broken plugin subsystem (e.g. corrupt manifest) is logged/traced but no longer blocks embedded dashboard startup
- Extend local-runtime.test.ts and local-server.test.ts to cover plugin wiring and the fail-soft path
- Add changeset (patch) documenting the fix
Files changed:
.changeset/fn-7623-desktop-plugin-wiring.md | 7 ++
.../desktop/src/__tests__/local-runtime.test.ts | 123 ++++++++++++++++++++-
.../desktop/src/__tests__/local-server.test.ts | 69 +++++++++++-
packages/desktop/src/local-runtime.ts | 50 ++++++++-
packages/desktop/src/local-server.ts | 41 ++++++-
5 files changed, 286 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7623
Fusion-Task-Lineage: c6f291fb-e6aa-4ac1-a3f3-4189fc831c60
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Authentication settings previously enumerated providers straight from pi AuthStorage's live runtime registry, so connecting a runtime plugin (e.g. Hermes Runtime) could narrow/collapse the visible provider list. This adds a static, hand-maintained supported-provider catalog and unions it with storage-reported providers so presence in the list is deterministic while status stays live.
- Add packages/dashboard/src/routes/auth-provider-catalog.ts with STATIC_OAUTH_PROVIDER_CATALOG, STATIC_API_KEY_PROVIDER_CATALOG, and unionProviderCatalog() (catalog always wins on presence; runtime-only extras still surface; runtime name wins on name conflicts).
- Update register-auth-routes.ts's GET /api/auth/status to union the static catalogs with storage.getOAuthProviders()/getApiKeyProviders() instead of relying solely on runtime-reported providers.
- Extend routes-auth.test.ts coverage for the new catalog-union behavior (provider presence stable across narrowed runtime registries, extras preserved, name precedence).
- Add changeset fn-7625-static-auth-provider-catalog.md (patch, fix).
Files changed:
.changeset/fn-7625-static-auth-provider-catalog.md | 7 +
.../dashboard/src/__tests__/routes-auth.test.ts | 180 +++++++++++++++++++--
.../dashboard/src/routes/auth-provider-catalog.ts | 93 +++++++++++
.../dashboard/src/routes/register-auth-routes.ts | 25 ++-
4 files changed, 291 insertions(+), 14 deletions(-)
Fusion-Task-Id: FN-7625
Fusion-Task-Lineage: be984497-b881-4a8f-9860-544617e2b5f7
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the onboarding/settings GitHub step so it never offers a dashboard OAuth login for github (no github OAuth provider is ever registered; pi only ships anthropic/github-copilot/openai-codex), replacing the broken Connect OAuth button with gh CLI guidance and a clearer server-side error.
- Remove the "Connect OAuth (optional)" button and its login-instructions panel from the onboarding branch that runs when hasGithubProvider is false (ModelOnboardingModal.tsx), since it always called handleLogin("github") against a non-existent provider
- Update ModelOnboardingModal tests to cover the new gh-CLI-only flow
- Make POST /api/auth/login return a clear, actionable 400 naming the requested provider, the registered dashboard OAuth providers, and that GitHub integration uses gh CLI/token auth instead of a generic "Unknown provider" / model-not-found error
- Add routes-auth.test.ts coverage for the improved unknown-provider error message
- Add a patch changeset documenting the fix
Files changed:
.changeset/fn-7624-github-onboarding-auth.md | 7 +++++
packages/dashboard/app/components/ModelOnboardingModal.tsx | 32 +++++++---------------
packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx | 32 +++++++++++++++++++++-
packages/dashboard/src/__tests__/routes-auth.test.ts | 22 +++++++++++++++
packages/dashboard/src/routes/register-auth-routes.ts | 14 +++++++++-
5 files changed, 83 insertions(+), 24 deletions(-)
Fusion-Task-Id: FN-7624
Fusion-Task-Lineage: e7be1b0a-6e50-479d-b56e-1284e8a94b7f
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds a close affordance to the embedded Settings header on mobile, since only a bottom nav bar (no sidebar) is available to exit there.
- Render a mobile-only `modal-close` button in the embedded Settings header when `isEmbedded && viewportMode === "mobile"`, wired to the existing `onClose` prop.
- Leave desktop/tablet embedded and the standalone modal presentation unchanged.
- Add regression tests covering the new mobile close button.
- Add a patch changeset documenting the fix.
Files changed:
.changeset/fn-7627-mobile-settings-close.md | 7 ++
.../dashboard/app/components/SettingsModal.css | 16 +++
.../dashboard/app/components/SettingsModal.tsx | 17 +++
.../__tests__/SettingsModal.mobileClose.test.tsx | 125 +++++++++++++++++++++
4 files changed, 165 insertions(+)
Fusion-Task-Id: FN-7627
Fusion-Task-Lineage: b39c7172-bce3-4fb2-9095-58ae376c43ef
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Root-caused and fixed the third recurrence of the mobile terminal shortcut bar not scrolling horizontally: styles.css's mobile lockdown resets touch-action to pan-y across ancestors, and touch-action's used value is the intersection of the touched element's and every ancestor's value, so the leaf .terminal-shortcut-panel's pan-x was silently defeated even though it was already correct.
- Opt the terminal overlay and modal ancestors (.modal-overlay.terminal-modal-overlay, .modal.terminal-modal--mobile, plain-media-query mobile modal, and the shortcut/status footer) into touch-action: pan-x pan-y so descendant leaf touch-action values can take effect
- Add FNXC:Terminal comments documenting the ancestor-intersection root cause and recurrence history (FN-7550/FN-7560)
- Add a documented solution note under docs/solutions/ui-bugs/ for the ancestor-intersection touch-action pattern
- Add regression tests asserting the modal/overlay/footer ancestors carry the pan-x pan-y opt-in
- Add a changeset for the fix
Files changed:
.../fn-7621-mobile-terminal-shortcut-scroll.md | 7 ++
...on-ancestor-intersection-defeats-leaf-scroll.md | 57 +++++++++++
.../dashboard/app/components/TerminalModal.css | 38 ++++++++
.../components/__tests__/TerminalModal.test.tsx | 106 +++++++++++++++++++++
4 files changed, 208 insertions(+)
Fusion-Task-Id: FN-7621
Fusion-Task-Lineage: 771fd79e-e193-43b0-908b-0e8fe2fc2c70
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the mobile dashboard terminal sometimes rendering completely blank on open by having TerminalModal recover from a zero/collapsed container box.
- TerminalModal now attaches a persistent ResizeObserver directly on the xterm container, mirroring SessionTerminal's existing pattern
- When the container reports a zero/collapsed box on the first post-open fit, it now re-fits once the real box settles instead of staying stuck at FitAddon's degenerate 2x1-cell floor
- Added regression tests covering TerminalModal and SessionTerminal zero-geometry recovery
- Documented the root cause and fix in docs/solutions/ui-bugs/mobile-terminal-blank-render-zero-geometry-container.md
- Added a patch changeset for @runfusion/fusion
Files changed:
.changeset/fn-7620-mobile-terminal-blank-render.md | 7 +
docs/solutions/ui-bugs/mobile-terminal-blank-render-zero-geometry-container.md | 112 +++++++
packages/dashboard/app/components/TerminalModal.tsx | 49 +++
packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx | 66 ++++
packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 358 +++++++++++++++++++++
5 files changed, 592 insertions(+)
Fusion-Task-Id: FN-7620
Fusion-Task-Lineage: 103f5b17-9a6e-4e9a-ab61-65ccb2203a8d
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
Fixes the failing **full-suite** CI run on `main` ([run
28874651861](https://github.com/Runfusion/Fusion/actions/runs/28874651861))
— all 4 test shards were red. ~32 test files failing across engine +
dashboard (src + app), rooted in ~13 distinct causes from recent main
commits. All resolved; the merge gate and full engine/dashboard suites
are green locally.
## Root causes & fixes
### Engine (shards 1 & 2)
- **`appendAgentLog` 6th timing arg (FN-7503, `2797803c0`)** —
`agent-logger.ts` now passes an optional
`{durationMs,timeToFirstTokenMs}` 6th arg; many
executor/heartbeat/merger tests asserted the old 5-arg form. Added a
shared timing-tolerant helper `agent-log-assertions.ts` (asserts
`taskId/text/type`, tolerant of the timing object) and applied it across
affected files — so future timing fields won't re-break every executor
test.
- **`reconcileSupersededGeneratedFixFeatures` (mission)** —
`mission-execution-loop.ts` calls a method the test's missionStore mock
lacked; added a no-op stub (the real `MissionStore` already implements
it).
- **`ModelFallbackExhaustedError` / `proseSignalsClearApproval` /
`extractJsonObjectCandidates` missing from `vi.mock`** — converted stale
hand-written mocks (`../pi.js`, `../reviewer.js` in
`executor-test-helpers.ts`) to `importOriginal`-spread so real exports
carry through.
- **Workspace product fixes (2):**
- `merger-ai.ts` — `landWorkspaceTask` now recovers the integration-tip
sha as `landedSha` when the A1 trailer-fallback proved a sub-repo landed
but its sha was never persisted, so `finalizeWorkspaceTask` can build
merge proof (was stranding partial-land retries in-review).
- `worktree-acquisition.ts` — `acquireWorkspaceRepoWorktree` strips the
shared project `integrationBranch/baseBranch` overrides before
forwarding to `acquireTaskWorktree` (FN-7360's `freshStartPoint` was
resolving an absent shared branch).
- **FN-7360 extra `git symbolic-ref` exec** — updated worktree
exec-count assertions for the new `resolveIntegrationBranch` call.
- **Planner-overseer / stepwise-workflow / workflow-graph /
workflow-prompt / executor-step-session / liveness-gate / checkout /
ce-workflow / triage-split** — test-alignments for intentional behavior
changes (FN-7229 retry-cap, FN-7265 review-node removal, FN-7335
pause-abort logging, FN-7577 recovery-budget, FN-7577 overseer denial
loop, specifyTask single promptWithFallback call, FN-4944
already-on-main noop log, FN-7486 ownership short-circuit).
### Dashboard API (shard 3)
- **`store.on('task:moved')` (FN-7337)** — `createServer` now registers
the listener; backed the 4 affected MockStores with EventEmitter (shared
root cause across chat-routes.rooms, register-git-github,
routes-run-cited-goals, routes-sandbox-audit).
- **`routes-agent-import`** — core mock converted to
`importOriginal`-spread (was missing FN-7444 planning-deepening
constants).
- **`session-resume-history`** — engine mock missing
`resolveMcpServersForStore`.
- **`task-create-workflow-route`** — `builtin:legacy-coding`
defaultSteps now include `plan-review` (FN-7224/7226).
- **GitLab parity** — added the missing `[GitLab Parity Inventory]`
cross-link in `docs/signals-connectors.md`.
### Dashboard app (shard 4)
- Test-alignments for intentional product changes: FN-7057 (workflow
selection preservation), FN-7340 (footer concurrency geometry), FN-7156
(Missions overview default), FN-7342/FN-6825 (board scroll + workflow
switcher), FN-7352 (openDetailTask 3rd arg), FN-7261 (backdrop dismiss
default-off), FN-7234 (non-authoritative fetch failures), plus a missing
`fetchWorkflowOptionalSteps` mock.
### MCP coverage
- `mcp-surface-coverage` forwarding needle updated for FN-7446's
`resolvePlanningMcpServers` helper.
## Approach notes
- Each fix is the **minimal** change at the correct source (test-update
where a recent commit intentionally changed behavior; product-fix for
the 2 real regressions). No assertion was loosened/deleted to force a
pass; no timeout appeasement.
- Coordination: work was partitioned by package across parallel
subagents (engine / dashboard-src / dashboard-app) with Main as the sole
git committer (path-scoped commits) after an early shared-index reset
wiped in-progress edits — process was tightened mid-flight.
## Verification
- **Full engine suite**: green (9231 passed; the lone local-only
`custom-providers-openai-completions` import error is stale local
`pi-ai@0.79.9` vs the lockfile's `0.80.3` — CI's fresh install resolves
`/compat`; it passed in the original CI run).
- **Dashboard API** (`dashboard-api-quality-backfill`): 242 files / 3185
tests / 0 failures.
- **Dashboard app** (`dashboard-app-quality-backfill`): all targeted
files green (37 + 95 tests).
- **Merge gate** (`pnpm test:gate`): engine-core 326 + ci-shape 63, plus
nohup/4040/appeasement/changeset-format checks — all pass.
- 2 changesets added for the published-`@runfusion/fusion` behavior
fixes (workspace landedSha, sub-repo worktree branch-strip).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved reliability for partial workspace land retries by recovering
the exact proven landed commit so durable merge proofs can complete.
* Fixed per-sub-repo worktree creation by removing invalid branch
override settings, preventing worktree-add failures.
* Dashboard stability updates: preserve mobile board scroll during
stabilization/restore, correct task filtering when workflows are
missing, ensure the Chat tab appears for done tasks, and refine
modal-dismiss and responsive popover behavior.
* **Documentation**
* Expanded the GitLab connector section with GitLab parity context and a
GitLab Parity Inventory reference.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- In direct-merge mode with `pushAfterMerge: true`,
`pushToRemoteAfterMerge` previously did one preemptive `pull --rebase`
before the first push, then allowed exactly **one** additional retry
when the push failed as non-fast-forward.
- On a busy repo, origin can move again during that single retry's
pull/push window, so the retry itself can also lose the race — leaving
the merge unpushed with no further attempt.
- This generalizes the single retry into a bounded loop
(`PUSH_NON_FF_MAX_RETRIES = 3`, backoff `2s/5s/10s`), re-running
`pullWithRebaseAndResolveConflicts` before each attempt, and breaking
out early if a retry's failure is no longer classified as
non-fast-forward (so unrelated errors surface immediately instead of
being retried needlessly).
- Abort-signal checks (`throwIfAborted`) and merge-abort rethrow
(`rethrowIfMergeAborted`) are preserved at every step of the loop.
## Test plan
- [x] Existing test `"retries push once after non-fast-forward
rejection"` still passes unmodified (loop returns on first successful
retry, same attempt counts as before).
- [x] New test `"retries push multiple times across repeated
non-fast-forward rejections"` — 2 consecutive non-ff failures then
success on the 3rd push attempt, proving the loop goes beyond the old
single-retry ceiling.
- [x] New test `"gives up after exhausting non-fast-forward retries"` —
all attempts fail as non-ff, proving retries are bounded (`pushed:
false` after 1 initial + 3 retries) rather than looping forever.
- [x] `packages/engine` full test suite: 37/37 passing (`npx vitest run
src/__tests__/merger-prompt-and-utils.test.ts`).
- [x] `npx tsc --noEmit -p .` clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved reliability when sending merged changes to a remote by
retrying after non-fast-forward push failures.
* Added configurable backoff and retry limits to recover via pull +
rebase and provide a clear failure when retries are exhausted.
* **Tests**
* Added coverage for repeated non-fast-forward retries, including the
case where the retry limit is reached and the operation ultimately
fails.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
findProvenLandedCommit now keeps --grep as a prefilter but verifies each
candidate carries an actual 'Fusion-Task-Id: <taskId>' trailer line via
git show -s --format=%B, so a later commit that merely mentions the trailer
text in its body cannot be selected. Regression covers a body-mention
intervening commit.
findProvenLandedCommit returns the task's own trailer commit (or recorded
landedSha when still an ancestor) instead of rev-parse on the integration
tip, so an intervening sub-repo land can't attribute a later unrelated
commit. Regression: intervening commit after lost persist recovers tipAfterFirst.
Update five dashboard app test files whose assertions drifted from
intentional product changes that landed on main without updating them:
- graph-workflow-header: FN-7057 treats stale/missing workflow ids as the
default workflow, so FN-unknown now shows under the default selection.
- EngineControlMenu.css: FN-7340 added a 768px range-thumb touch-target
block; narrow the popover-breakpoint assertion to that selector.
- MissionManager.delete-confirm: FN-7156 removed first-mission auto-select;
explicitly select the mission before the detail-delete flow.
- board-mobile-initial-render: FN-7342 preserves board column scroll during
stabilization; FN-6825 renders the workflow toolbar on options, not callbacks.
- workflow-auto-layout: FN-7265 removed the stepwise review node (per-step
review lives in the foreach); the connected run ends at completion-summary.
No assertion was loosened or deleted to force a pass; each change cites the
breaking commit via an FNXC comment. packages/dashboard is private (no changeset).
recoverActiveMissions (mission-execution-loop.ts:263) calls
missionStore.reconcileSupersededGeneratedFixFeatures per slice; the 5
MissionExecutionLoop-backed mocks here omitted it, so recovery threw
(TypeError) at the slice loop and aborted before processTaskOutcome /
ensureFeatureAssertionLinked / startValidatorRun ran — 4 tests failed.
Add a no-op stub (matches mission-execution-loop.test.ts reference) with
an FNXC:MissionReconcile note. No-op is correct: supersession is not
exercised by these tests.
## Summary
In direct-merge mode (`mergeStrategy: direct` + `pushAfterMerge: true`),
when the post-merge `git push` to origin fails or throws, `aiMergeTask`
in `packages/engine/src/merger.ts` only logged the failure to the
process-wide `mergerLog` and set a transient field
(`result.pushedToRemote` / `result.pushError`) on the local
`MergeResult` object — neither of which is persisted anywhere. The task
was still unconditionally marked done/merged.
Because nothing durable recorded the failure, a diverging local `main`
could go unnoticed indefinitely. This is exactly the mechanism that let
a local `main` drift 162 commits behind `origin/main` in our deployment
before it was caught by hand.
## Fix
Both push-failure branches (the non-throwing `else` and the throwing
`catch`) now also:
- call `audit.git({ type: "push:origin", ... })`, activating the
existing-but-previously-unused `"push:origin"` `GitMutationType` in the
run-audit schema (populated with `remote`, `integrationBranch`,
`outcome: "failed"`, `stderrPreview`)
- call `store.logEntry(taskId, ..., "PushToRemoteFailed")` so the
failure is visible on the task itself, not just in an ephemeral process
log
Both calls are wrapped in `.catch(() => undefined)` — they're
best-effort recording, not new failure paths, so a problem persisting
the failure record can't abort the outer merge flow. Task-completion
behavior (mark done even when the push failed) is intentionally
unchanged; this fix is about visibility, not about blocking completion.
## Test plan
- [x] Added assertions to the existing `merger-prompt-and-utils.test.ts`
test `"records push error but still completes merge when push fails"`,
asserting `store.recordRunAuditEvent` and `store.logEntry` are called
with the expected shape
- [x] `npx vitest run src/__tests__/merger-prompt-and-utils.test.ts` —
35 passed
- [x] `npx tsc --noEmit` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved handling of merge completion when pushing changes to the
remote repository fails.
* Failures are now consistently recorded and surfaced with clearer error
logging, making it easier to identify when a push did not succeed after
a merge.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
### Problem
In **Project Settings → Plugins**, the **Browse registry** panel fails
to load with:
```
Failed to load registry: Plugin "registry" not found
```
The registry listing never appears, so one-click plugin
discovery/install is unusable.
### Root cause
In `createApiRoutes` (`packages/dashboard/src/routes.ts`), the generic
project-scoped route **`GET /plugins/:id`** is registered **before** the
plugin sub-router (`createPluginRouter`) — which owns **`GET
/plugins/registry`** — is mounted (`router.use("/plugins",
createPluginRouter(...))`).
Express matches routes in registration order, so a request to
`/api/plugins/registry` matches `:id` with `id === "registry"`, calls
`pluginStore.getPlugin("registry")`, and throws `Plugin "registry" not
found`. The real registry handler (which builds the curated registry
manifest) is never reached.
### Why not just reorder the mount?
The two handlers are **not** equivalent:
- the inline `GET /plugins/:id` is **project-scoped** via
`getProjectContext(req)`,
- the sub-router uses the **global** plugin store.
Mounting the sub-router first would shadow the project-scoped routes and
change scoping semantics for other endpoints. So reordering is the wrong
fix.
### Fix
Let the reserved static path fall through. In the inline `GET
/plugins/:id` handler, when `id === "registry"`, call `next()` so the
later-mounted sub-router serves the registry listing (it already handles
the `projectId` query param). Minimal and targeted — `"registry"` is the
only static GET path under `/plugins` that collides with the
single-segment `:id` pattern.
```ts
router.get("/plugins/:id", async (req, res, next) => {
if (req.params.id === "registry") { next(); return; } // fall through to the registry sub-route
...
});
```
### Tests
Adds a regression test in `plugin-routes.routes.test.ts` (using the
existing `createApiRoutes` harness that reproduces the real mount order)
asserting `GET /api/plugins/registry`:
- returns **200** with a `plugins` array, and
- **never** calls `pluginStore.getPlugin("registry")` (i.e. is no longer
shadowed by `:id`).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed the `/plugins/registry` endpoint so it responds correctly
instead of being treated like a plugin ID.
* Improved route handling to ensure the registry page/API is reached
even when generic plugin routes are registered first.
* **Tests**
* Added regression coverage for the `/plugins/registry` route to verify
the correct response and prevent the generic plugin lookup from
intercepting it.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Fixes#1939.
Adds the single missing `COPY
plugins/fusion-plugin-linear-import/package.json
./plugins/fusion-plugin-linear-import/package.json` line to the
Dockerfile's pre-install COPY block.
## Root cause
Commit dfb6e5270d added
`plugins/fusion-plugin-linear-import` as a new pnpm workspace member,
but didn't add a matching Dockerfile COPY line for its package.json. As
a result that member's devDependencies (`@types/node`, `@types/react`)
are never installed inside the image, and the later `pnpm build` step
fails with TS2688 once the compiler reaches that plugin.
## Fix
One line, alphabetically placed alongside the other
`plugins/fusion-plugin-*` COPY lines.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Updated the container build setup to include an additional package
manifest needed during image creation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
### Problem
Two settings that already exist in the `Settings` schema and are honored
by the engine have **no control anywhere in the dashboard UI**:
- `buildTimeoutMs` — max time for build/verification commands (default
`300000` = 5 min)
- `insightExtractionEnabled` (+ `insightExtractionSchedule`) — periodic
extraction of durable insights from completed tasks into memory
Because there's no UI, the only way to change them is to hand-edit
`config.json` — but that has to be done with the app fully closed, since
the running app rewrites `config.json` from its database on
reload/shutdown and silently clobbers live file edits. That's a
confusing footgun (edits appear to "not stick"), and 5 minutes is too
low a build timeout for large monorepo / Docker builds.
### Change
Expose both via controls that mirror the existing patterns in the same
sections — no schema, route, or persistence changes needed (the keys
already flow through `form`/`setForm` → `updateSettings`).
- **Scheduling section:** "Build/Verification Timeout (minutes)", placed
next to the existing "Stuck Task Timeout (minutes)", using the same
minutes↔ms conversion.
- **Memory section:** "Enable Insight Extraction" checkbox + conditional
cron "Schedule" field, placed next to the existing Auto-Summarize
controls (matching how `insightExtractionSchedule` is already documented
in `types.ts`).
Both use the existing `t(...)` i18n fallback style used throughout these
sections.
### Scope note (feedback welcome)
Both live in their existing **project-scoped** sections (Scheduling,
Memory), which is the natural home. If maintainers want these settable
as **global/user defaults** too, I'm happy to add a global-defaults
counterpart in a follow-up — just wanted to keep this PR focused and
non-duplicative.
### Testing
- Both keys are pre-existing fields on the `Settings` type
(`buildTimeoutMs?: number` at `packages/core/src/types.ts`;
`insightExtractionEnabled?`/`insightExtractionSchedule?` likewise), so
the additions are type-safe.
- Controls follow the exact JSX/handler pattern of adjacent fields
already in each section.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added an **Insight Extraction** option in memory settings, including
an enable/disable toggle and a schedule field that appears when enabled.
* Added a **Build/Verification Timeout** setting in scheduling, with
minute-based input and helpful guidance text.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
### Problem
`docker build` (and `docker compose up --build`) fails during the `RUN
pnpm build` layer while compiling `plugins/fusion-plugin-linear-import`:
```
error TS2688: Cannot find type definition file for 'node'.
error TS2688: Cannot find type definition file for 'react'.
```
even though the prior `RUN pnpm install --frozen-lockfile` layer reports
success.
### Root cause
The builder stage pre-copies each workspace package's `package.json`
**before** `RUN pnpm install --frozen-lockfile` (a layer-caching
optimization), then does `COPY . .` afterward. `pnpm-workspace.yaml`
lists `plugins/fusion-plugin-linear-import` as a workspace member, but
the Dockerfile's pre-copy list has a `COPY plugins/<name>/package.json`
line for **every plugin except** `fusion-plugin-linear-import`.
Because that manifest is missing during the frozen-install layer, pnpm
never links the plugin's devDependencies (`@types/node`, `@types/react`)
into its resolution scope. When `pnpm build` later runs `tsc` for that
package (its `tsconfig.json` uses `types: ["node", "react"]`),
TypeScript can't find the type definitions — the `TS2688` failure above.
### Fix
Add the single missing pre-copy line so `fusion-plugin-linear-import`'s
manifest is present during the install layer, exactly like every other
workspace plugin:
```dockerfile
COPY plugins/fusion-plugin-linear-import/package.json ./plugins/fusion-plugin-linear-import/package.json
```
One-line, additive change; no behavior change beyond making the image
build.
### Verification
- Confirmed the omission is present on `main` and at the latest release
tag `v0.56.1`.
- With the line added, the pre-copied manifest set matches
`pnpm-workspace.yaml`, so `pnpm install --frozen-lockfile` links
`@types/node`/`@types/react` for the plugin and the `pnpm build` `tsc`
step no longer reports `TS2688`.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Chores**
* Updated the build setup to include an additional package manifest
during installation, helping ensure the container build completes
consistently.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
A single retry can still lose the race on busy repos if origin moves again
in the pull-rebase/push window. Generalize the one-shot retry into a bounded
loop (3 attempts, 2s/5s/10s backoff), re-pulling+rebasing before each push
attempt and bailing early once the failure is no longer non-fast-forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
When pushAfterMerge fails (or throws) in direct-merge mode, the daemon
only wrote to the process-wide mergerLog and a transient MergeResult
field, then unconditionally marked the task done. There was no durable
record on the task or in the audit trail, so a diverged local main
could go unnoticed indefinitely — this is how our local main drifted
162 commits from origin before it was caught by hand.
Record the failure through the two channels merger.ts already has for
this: the dormant "push:origin" GitMutationType via audit.git(), and a
task log entry via store.logEntry(). Both calls are best-effort
(.catch(() => undefined)) so a logging failure can't abort the merge
flow itself — task completion behavior is unchanged, the failure is
just no longer invisible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Dockerfile's pre-install COPY block was missing a line for
plugins/fusion-plugin-linear-import, which was added as a workspace
member in dfb6e5270d but never got a
matching COPY line. Without it, that plugin's package.json (and its
@types/node / @types/react devDependencies) is never staged before
'pnpm install --frozen-lockfile', causing the later 'pnpm build' step
to fail with TS2688.
Fixes#1939
The Plugins settings 'Browse registry' feature failed with
'Failed to load registry: Plugin "registry" not found'.
Root cause: in createApiRoutes (routes.ts), the generic project-scoped
'GET /plugins/:id' route is registered BEFORE the plugin sub-router
(createPluginRouter) that owns 'GET /plugins/registry' is mounted. Express
matches in registration order, so a request to /api/plugins/registry matched
':id' with id='registry', called pluginStore.getPlugin('registry') and threw
'Plugin "registry" not found' — the real registry handler was never reached.
Reordering the mount is unsafe: the inline ':id' route is project-scoped via
getProjectContext, while the sub-router uses the global plugin store, so
moving it would change scoping semantics. Instead, let the reserved static
path fall through: when id === 'registry', call next() so the mounted
sub-router serves the registry listing (which already handles projectId).
Adds a regression test asserting GET /api/plugins/registry returns 200 with a
plugins array and never calls getPlugin('registry').
Both settings already exist in the Settings schema and are honored by the
engine, but neither had a control in the dashboard, so users could only
change them by hand-editing config.json while the app was closed (the
running app rewrites config.json from its DB, clobbering live file edits).
- Add 'Build/Verification Timeout (minutes)' to the Scheduling section,
next to the existing stuck-task timeout (same minutes<->ms conversion).
- Add 'Enable Insight Extraction' + cron schedule to the Memory section,
next to the existing auto-summarize controls.
Both are placed in their existing project-scoped sections. Follow-up: a
global-defaults counterpart could be added if maintainers want these
settable at the user/global level too.