- useTasks debounced effect bailed when searchQuery transitioned back to
undefined (App passes `searchQuery || undefined`), so clearing the
input never refetched the unfiltered list. Track previous value via
ref and only skip the initial mount.
- FTS5 search used bare tokens (exact term match), so "frob" did not
match "frobnicator". Append `*` to each token (and to quoted tokens)
for prefix matching. LIKE fallback already did substring matching.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merges FN-3009 (remote settings migrated to global scope with updated schema and docs), FN-3034 (legacy routines compatibility layer: agentId backfill on load and schema validation for old format), and FN-3039 (Nerd Font glyph asset bundled into the terminal with CSS prioritization). Core settings s
Fusion-Task-Id: FN-3034
This merge migrates Fusion's settings architecture to a unified global scope (FN-3009), consolidating settings types, defaults, merge semantics, updater logic, and routing into a centralized system that aligns dashboard remote settings with global configuration. Secondary changes include adding the
Fusion-Task-Id: FN-3009
This merge adds research settings to the settings modal and dashboard (FN-2839, FN-3029), implements a dashboard font scale setting with persistence and documentation (FN-3027), refactors AgentDetailView with a new AgentLogViewer CSS module (FN-2839), and improves NewAgentDialog with custom model dr
Fusion-Task-Id: FN-3027
Exports the research settings resolver from `@fusion/core` types to fix dashboard alias builds, ensuring the resolver is available when imported through the dashboard's module alias configuration.
Fusion-Task-Id: FN-2995
The vitest child-process guard wrapped exec/execFile without preserving the
`[util.promisify.custom]` symbol, so awaited `execAsync` resolved to a raw
stdout string instead of `{stdout, stderr}`. That single regression cascaded
through ~60 "failing" tests across cli, core, engine, and dashboard whose
production code was actually correct. Also relax the AI-CLI blocklist for
cheap introspection (--version/--help/which …), give SIGTERM'd subprocesses a
brief grace period before being flagged as "left running", fix a few real
test-side bugs uncovered along the way (executor mock step transitions, iOS
last-resort keyboard path, mission SSE replay tests racing with the real AI
agent), and convert dashboard route tests' dynamic `await import("../server.js")`
to static imports so first-test timings drop from 2–5s to <200ms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Nerd Font as the terminal font stack default with a migration to update existing user settings, updates the terminal and settings modals to reflect the new font choice, and includes corresponding tests plus a patch changeset for `@runfusion/fusion`.
Fusion-Task-Id: FN-3026
Workflow-step REVISE retries, pause→todo handoffs, and the
context-overflow fresh-session requeue were all routing tasks back to
`todo` before returning to `in-progress`. The default reopen-to-todo
path reset every step to pending and rewrote PROMPT.md checkboxes, so
each retry restarted from step 0 even when earlier steps had already
been done — the symptom seen on FN-2978, where every workflow REVISE
or pause cycle wiped the task's progress.
- Add `preserveResumeState` to `TaskStore.moveTask`. When set, skip
`resetAllStepsToPending` + `resetPromptCheckboxes` and keep
`worktree` and `executionStartedAt` so the resumed run reattaches to
the same checkout. `status`, `error`, and `blockedBy` still clear.
- Use it on the workflow-rerun bounce, the three pause-graceful
handoffs, and the context-overflow requeue. The agent-terminated
pause path still discards (it nukes worktree+branch by design).
- Context-overflow requeue clears `sessionFile` synchronously in the
awaited `updateTask` immediately before `moveTask`, so the next
dispatch cannot reopen the saturated session via a stale pointer.
- `fn_task_update` no longer silently regresses `done`/`skipped` steps
to `in-progress`, no longer captures a stale rewind checkpoint when
it does, and tells the agent honestly when a regression is ignored.
- Mobile chat keyboard: ChatView/QuickChatFAB gate layout on the new
`keyboardOpen` flag so focused-input + viewport-shrink iOS cases
still adjust when the computed overlap is zero.
Tests:
- New `preserveResumeState` coverage in store.test.ts; updated
workflow-rerun + pause-graceful assertions in executor.test.ts.
- Restructured the previously-flaky "routes exhausted prompt-mode
workflow hard failures" test to drive the bounce inline; passes in
isolation and in the wider workflow/pause/context sweep (59/59).
- Added regression tests in ChatView.test.tsx and QuickChatFAB.test.tsx
for the iOS last-resort `keyboardOpen=true, keyboardOverlap=0` case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated fixes for the FN-2978 incident class — auto-requeues
that orphaned committed work and watchdog kills on long verification runs.
**Auto-requeue branch reuse** (executor.ts, worktree-pool.ts)
- executor.ts:1782 now uses `task.branch || fusion/<id>` so persisted
branches are honored on requeue. Previously the hardcoded fallback
always tried to re-create the original branch, hit a conflict with
the prior run's ref, and got suffix -2/-3. Other call sites already
honor task.branch — this aligns the worktree-acquisition path.
- worktree-pool.ts:181 prepareForTask now probes existing branches with
`git rev-parse --verify` and checks them out as-is. Falls through to
suffixed creation only when the branch is genuinely in use by another
live worktree. Previously force-reset with `checkout -B`, destroying
prior commits.
- New private reconcileStepsFromGitHistory walks `git log
baseCommitSha..HEAD` for `feat(FN-X): complete Step N` commits and
marks matching steps[] as done so resumes don't redo committed work.
**Manual reset endpoint + UI** (dashboard)
- POST /api/tasks/:id/reset (requires `confirm: true`) — clears worktree,
branch, all retry counters, resets steps[] to pending, moves to todo.
Distinct from /retry which is the soft-resume path.
- Reset button alongside Retry in TaskDetailModal with confirm dialog,
wired through useTasks → AppModals → API.
**fn_run_verification tool** (run-verification-tool.ts, executor.ts)
- New custom tool wrapping test/lint/build commands with a heartbeat
callback (per-line + 60s synthetic), 200KB head+tail output cap, hard
timeout with SIGTERM→SIGKILL escalation, and auto-bootstrap detection
for missing node_modules. Prevents the inactivity watchdog from
killing sessions during long compiles.
- Cross-platform via `shell: true` (Node picks /bin/sh on POSIX,
cmd.exe on Windows). Prompt section in EXECUTOR_SYSTEM_PROMPT and
EXECUTOR_PROMPT_TEXT instructs agents to prefer package-scoped
verification first and reserve workspace-scoped runs for final
integration.
**Tests** (64 passing)
- detect-pseudo-pause.test.ts (27 tests) — covers all 7 regex patterns,
structural fallback, FN-2978 regression text.
- reconcile-step-regex.test.ts (25 tests) — pins the commit-message
regex against a wide variant set.
- run-verification-command.test.ts (12 tests) — basic execution, output
capture, heartbeat callbacks, timeout, error handling. POSIX-specific
cases (multi-cmd `;`, `>&2`, `\$USER`) gated behind itPosix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agents sometimes end turns by asking permission instead of calling tools.
Add turn-ending rules to both executor prompts and runtime detection via
detectPseudoPause() to identify these stalls and issue targeted retry
prompts that correct the behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace bare `feat(FN-XXXX): merge fusion/fn-XXXX` subjects with an
AI-generated summary describing what landed (e.g. `feat(FN-XXXX): add
webhook handler`). Calls the existing `summarizeCommitSubject` lane
alongside the body summarizer; falls back to `merge <branch>` when the
summarizer is disabled, unavailable, or returns nothing.
Default for `useAiMergeCommitSummary` flips to true so existing
projects without an explicit override pick up the new behavior. The
Settings UI already exposes the toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each agent now gets its own .fusion/agents/<id>/HEARTBEAT.md procedure
file instead of sharing a single project-wide file. A one-shot
migration in AgentStore.init() re-points existing agents off the legacy
shared path and copies the legacy file's contents into each agent's
new per-agent location so operator edits are preserved.
The HeartbeatTriggerScheduler now phase-aligns the first tick to
lastHeartbeatAt + intervalMs so a process restart resumes each agent's
existing schedule rather than waiting up to a full interval before
firing again. Overdue ticks fire promptly within a small jitter window
to avoid a thundering herd at boot.
Also fixes three pre-existing QuickChatFAB test failures introduced by
739e899b5: auto-select default model now switches to model mode whether
or not agents are present, the model tag only renders in model mode,
and one test scopes its option lookup to role="option" to disambiguate
the in-header tag from the dropdown entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The merge commit message was built from `commitLog`/`diffStat` computed
against `merge-base(branch, main)`. Under squash-merge workflows, when an
earlier task is squash-merged onto main first, branches that forked off
the pre-squash main no longer share ancestry with it — `merge-base`
resolves to a point before the earlier task, and the message describes
work already merged via the prior squash. FN-2952's commit body claimed
11 files / 557 insertions when the actual diff was 2 files / 55 lines.
Subject was also a generic `merge <branch>` regardless of content.
- packages/engine/src/merger.ts: new `computeActualMergeCommitContext`
helper that derives commitLog/diffStat from the actual integration
delta (`git diff --cached <integrationTarget> --stat`), filtering
branch commits by patch-id against the target's recent history to
drop already-squashed siblings. Wired into both commit-finalization
sites (`commitOrAmendMergeWithFixes` uses `preAttemptHeadSha`; the
final amend in `runMergeAttempt` uses `HEAD~1`). Agent-context use of
the wide range is unchanged.
- packages/engine/src/merger.ts: `buildDeterministicMergeMessage` now
generates subject and body in parallel via `Promise.all`. Subject is
composed as `feat(taskId): <ai summary>`, capped at 72 chars, with
fallback to the legacy `merge <branch>` form on any AI failure.
- packages/core/src/ai-summarize.ts: new `summarizeCommitSubject` and
`sanitizeCommitSubject` mirroring the body summarizer's structure.
Same title-summarizer lane, 15s timeout. Sanitizer strips quotes,
bullets, re-added conventional-commit prefixes, and trailing periods;
hard-caps at 60 chars.
- packages/core/src/__tests__/ai-summarize.test.ts: 9 tests covering
the sanitizer's behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reset all task step statuses to pending and set currentStep to 0 when moving from in-progress, in-review, or done back to todo/triage
- Reset checked boxes in PROMPT.md on reopen so execution checklists match the reset step state
- Keep reopen cleanup scoped to reopen transitions while preserving existing behavior for non-reopen moves
- Add targeted store tests covering reopen paths, prompt checkbox reset, and no-op behavior for tasks without steps
Fusion-Task-Id: FN-2966
In PR-mode, processPullRequestMergeTask called gh pr create --head
fusion/<task-id> without ever pushing the branch to origin. PR creation
failed and the task stalled in in-review — and combined with the
recover-mergeable-review sweep bug, the stalled task got force-merged
locally instead. Push the branch via git push -u origin <branch>
immediately before createPr (skipped when an existing PR already covers
the branch).
Also remove the dead autoCreatePr setting: defined as a default in the
schema and Settings type but never read anywhere.
Related to https://github.com/Runfusion/Fusion/issues/21
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `triage` to VALID_TRANSITIONS["in-review"] so the dashboard's
`Request AI Revision` and `Rebuild Spec` actions work for in-review
tasks. moveTask now applies the same full reset on in-review → triage
as on in-review → todo (clears branch/baseBranch/baseCommitSha/summary/
recovery metadata and workflowStepResults) so the respec'd task starts
from scratch. The in-review task card's Move menu also gains Planning
as a destination.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Expand core test coverage for task node overrides, unavailable node policies, and defaultNodeId settings contracts
- Restrict dashboard task change views to files actually modified by the task, with updated API route handling and tests
- Improve quick chat UX and styling with extended component behavior and hook updates
- Harden memory dream extraction by reading assistant text from session state and covering undefined-output cases
- Enhance merger commit summarization and update architecture/settings/multi-project/task-management docs
Fusion-Task-Id: FN-2840
Update dashboard mocks to push assistant text into session.state.messages
(matching real session.prompt() void return) and add a regression test for
array-shaped content. Add core unit tests for extractDreamProcessorResult
to lock in undefined/null safety.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
session.prompt() resolves to void; the reply lives in session.state.messages.
The dream route was awaiting its return value, so parsing crashed with
"Cannot read properties of undefined (reading 'match')" once the dream
completed. Extract the assistant text post-prompt and fail soft on
non-string input in extractDreamProcessorResult.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Squash merge commits previously landed with only a bare "merge
fusion/fn-XXXX" subject and a single bullet from the branch's commit
log, leaving git log readers without insight into what actually
changed. Now buildDeterministicMergeMessage calls summarizeCommitBody
(title-summarizer lane when configured, default model otherwise) with
the step commits + diffstat, and emits a three-section body: AI summary
+ Commits merged + Files changed. The deterministic sections always
ship so AI failure / timeout still yields a substantive message.
Also extends summarizeCommitBody to take an optional commitLog and
loosens its prompt for more detail when the change warrants it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the commit-body AI helper out of merger.ts into the existing
core/ai-summarize.ts module so all short-summary AI work (titles,
chat titles, fallback merge commit bodies) shares one home with
consistent dispatch semantics, error handling, and session lifecycle.
Core (ai-summarize.ts):
- New `summarizeCommitBody(diffStat, rootDir, provider, modelId, opts)`
exported alongside `summarizeTitle`. Same shape (provider/modelId
args), same get-engine-or-bail dynamic loading via `getFnAgent`,
same readonly-tools session, same disposal-in-finally pattern.
- Differs from `summarizeTitle` in three deliberate ways suited to the
commit-body job:
1. Returns null on any failure instead of throwing — the caller is
always the merger, which has a deterministic fallback chain
behind it. Throwing would force the merger to wrap every call
in try/catch.
2. Accepts an optional `signal` to forward engine-pause / shutdown
cancellation, plus a configurable `timeoutMs` (default 30s)
so a wedged AI session can't stall a merge indefinitely.
3. Larger output ceiling (2000 chars vs title's 60) and larger
input ceiling (4000 chars truncated diff) — commit bodies are
multi-line and need more room than a 60-char title.
- Exported alongside `summarizeTitle` from `@fusion/core`. Constants
(`COMMIT_BODY_SYSTEM_PROMPT`, `MAX_COMMIT_BODY_INPUT_LENGTH`,
`MAX_COMMIT_BODY_LENGTH`, `DEFAULT_COMMIT_BODY_TIMEOUT_MS`) re-exported
for callers that want to override behavior.
Engine (merger.ts):
- Dropped the local `aiGenerateCommitBody` function (~70 lines) — it
duplicated the session-creation pattern from `summarizeTitle` while
living in a place where future maintainers wouldn't think to look.
- `resolveSafeCommitBody` now imports `summarizeCommitBody` from
`@fusion/core` and delegates. The cascade behavior is unchanged
(commitLog → AI → diff stat → synthetic) and the title-summarizer
model preference is preserved (provider/modelId resolved here, then
passed through).
Tests:
- 6 new test cases in `ai-summarize.test.ts` covering:
empty input → null, missing engine → null (graceful, never throws),
missing engine + model selection → null, pre-aborted signal → null,
custom timeout (returns quickly under 1s ceiling), exposed constants.
- Core: 3136/3136 pass (was 3130 — +6 new). Engine: 2887/2887 pass.
- Typecheck clean, workspace lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tasks were getting stuck in `in-review` forever when auto-merge could not
resolve conflicts within MAX_AUTO_MERGE_RETRIES. The conflict-exhaustion
branch silently cleared `status` (no error, no log entry, no comment),
and the 30-min cooldown sweep would reset retries and re-attempt the
same impossible merge — looping silently with no user-facing surface.
Why:
- FN-2918 and FN-2903 both spent hours in this loop with no error/comment
visible on the task. The only log evidence was repeated
"Auto-merge retry cooldown elapsed (30m idle)" entries with no
follow-up outcome.
How to apply:
- Every merge failure now writes a `<Manual|Auto>-merge failed: <msg>`
entry to the task log so the dashboard surfaces the reason.
- Conflict-retry exhaustion now bounces the task back to `in-progress`
with a comment + log entry so the executor re-rebases against main
and retries — mirroring the verification-failure-bounce pattern.
- New `mergeConflictBounceCount` task field caps outer bounces
(`MAX_MERGE_CONFLICT_BOUNCES = 2`); past the cap, the task is parked
in `in-review` with `status="failed"` and a follow-up triage task is
created so a human can resolve the conflict manually.
- Non-conflict and non-direct-strategy errors now also set
`status="failed"` so the cooldown sweep can't re-pick them up.
- `canMergeTask` skips tasks with `status="failed"` so terminal
failures (verification cap, bounce cap, non-conflict error) are no
longer eligible for cooldown re-attempts.
Schema migration v52 adds the `mergeConflictBounceCount` column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>