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
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>
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>
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
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>
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>
Audit of task.status assignments across packages/core and packages/engine
surfaced five additional transient/in-flight statuses that should block
auto-merge but weren't in BLOCKING_TASK_STATUSES:
- awaiting-approval — triage spec awaiting user approval
- needs-replan — scheduler/executor/triage signaled re-plan needed
- mission-validation — mission-level validation in flight
- queued — scheduler-side transient state
- stuck-killed — defensive guard; task killed by stuck detector
Each represents a state where finalizing the merge violates user intent
or invariants. Group the set with comments explaining the intent of each
group so future additions can be slotted appropriately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A task in column "in-review" but with status "planning" (or the legacy
"specifying" alias) was passing the merge eligibility check, so the
auto-merger would finalize a branch the user had just moved back to
re-plan. The hardcoded BLOCKING_TASK_STATUSES set didn't include either
status — only failed / awaiting-* / merging-* were checked.
Add both to the set so getTaskMergeBlocker reports them as blockers,
matching the expected user mental model of "moved to planning = won't
auto-merge".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add Database APIs to rebuild the tasks_fts index and detect FTS5 corruption signatures
- Retry task upserts once after rebuilding FTS5 when corruption errors are encountered
- Extend TaskStore health checks to include FTS5 integrity verification
- Add regression tests for rebuild/integrity behavior and upsert recovery, plus a patch changeset for @runfusion/fusion
Previously this transition cleared only transient execution state
(status/error/worktree/blockedBy/workflowStepResults), but kept the
prior branch, baseBranch, baseCommitSha, summary, and recovery
counters. That meant retrying a reviewed task resumed on the old
branch with a stale summary instead of starting fresh. Now those
fields are also cleared on in-review → todo. Other reopen paths
(in-progress/done → todo/triage) keep their existing behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single "smart" strategy is now two flavors with the new default flipped
to prefer-main. Both share a pre-cascade `git fetch origin <currentBranch>`
+ best-effort fast-forward so a freshly-pushed sibling commit doesn't get
clobbered when the fallback resolves a conflict against a stale base.
- "smart-prefer-main" (new default): -X ours fallback. Protects just-merged
sibling work from being regressed by a concurrent task branch.
- "smart-prefer-branch": -X theirs fallback. Equivalent to legacy "smart".
Legacy "smart" / "prefer-main" enum values are accepted and normalized via
`normalizeMergeConflictStrategy()` so existing settings.json files migrate
seamlessly. The fast-forward step gracefully degrades on fetch failure or
divergent local main (logs and continues).
Updates settings UI dropdown, test helpers, and adds 5 fetch+ff regression
tests + 7 normalize-helper tests. Lint cleanup of two empty catch blocks
in scripts/release.mjs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subtask, mission-interview, and milestone/slice-interview sessions could pin
their `generating` state forever when the underlying provider stream stalled
silently or a tool call hung. Wrap each `agent.session.prompt()` in a new
GenerationGuard helper (per-session AbortController + timer) so a stuck turn
becomes a bounded error users can retry. Adds matching `stop*Generation`
exports and threads abort through cleanup so dismissing a modal cancels the
in-flight call instead of leaking it.
Also closes the gh-cli tool hang vector: `runGhAsync` / `runGhJsonAsync` now
accept `{ signal, timeoutMs }` (default 30s). Github-touching extension tools
forward the AI tool's signal so an aborted agent kills the `gh` child instead
of orphaning it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add effective node routing fields to task types/store, persist them in SQLite, and expand regression coverage for node override guard behavior
- Add dashboard and API support for manual memory dream processing/trigger actions plus expanded memory regression tests
- Improve task creation and task detail model/node UX, including quick chat default model selection and workflow/settings UI polish
- Apply mobile/dashboard UX fixes (form input zoom prevention, expand toggle styling, input layout tweaks) and update extension/docs/changelogs for the release
- Add a core node-override-guard module, export it from @fusion/core, and enforce conflicts in store updates
- Add API route and CLI extension safeguards so nodeId override updates are blocked when ownership would conflict
- Wire node override routing and validation through dashboard quick-create, list, modal, settings, and task form/detail surfaces
- Add focused unit and integration tests for core guard logic, workflow routes, and dashboard node override UX
- Define and export unavailable node policy types in core settings interfaces
- Add project-level unavailable node policy default and runtime validation helper
- Add core unit coverage for unavailable node policy parsing and acceptance cases
- Guard Paperclip mint requests to include companyId only when available for type-safe payloads
- Document unavailable node policy in the settings reference
- Improve automation startup diagnostics and route handling for manual execution steps
- Add support for full manual automation step execution in dashboard and engine flows
- Expand due-schedule coverage in automation store and dashboard route tests
- Add cron runner regression tests for edge cases and document the automation execution fix via changeset
Three fixes for the worktree-overflow / stuck-task incident:
1. Cap deterministic-verification-failure bounces (fix#2)
Auto-merge previously bounced an in-review task back to in-progress
on every verification failure with no upper bound. A single flaky test
could keep a task ping-ponging in-review→in-progress forever, holding
its worktree and consuming agent slots. Adds verificationFailureCount
on Task (DB migration v48), increments on each bounce, and after 3
failures marks the task failed and creates a follow-up triage task
so a fresh agent can investigate the underlying flake instead of
re-running the same fix loop.
2. Reap unregistered orphan worktree dirs even when recycle is on (fix#3)
cleanupOrphans previously bailed out entirely when recycleWorktrees
was true, leaving stale dirs (clear-hawk-broken, *-bak, leftover
crash debris) on disk forever. New reapUnregisteredOrphans pass
removes only directories that aren't registered git worktrees, so
the recycle pool keeps its warm worktrees but the trash gets cleared.
3. Idempotence guard on activity-log listener wiring (fix#6)
setupActivityLogListeners() was registering handlers on every call.
When init() ran twice, every task:created / task:moved event wrote
N rows to activityLog, producing the duplicate entries visible in
the DB. Added activityListenersWired flag so repeated calls no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the inMemoryDb opt-in established in 6e4797ff1 to single-instance
TaskStore/AgentStore/RoutineStore/PluginStore tests that were still
opening disk-backed fusion.db files. Cross-instance persistence tests
(open store A, close, open store B on same dir) and migration tests that
seed via a sibling Database instance keep their disk-backed stores —
swapping those would silently drop data between instances.
Sites flipped:
- core: store.test.ts (RunMutationContext, memory-toggle, diagnostics
blocks), store-sort, settings-export, backup, plugin-loader,
agent-instructions, agent-instructions-bundle, mission-store (all 10
triage subtests), mission-planning-context.integration
- dashboard: routes.test.ts (Messaging routes block),
session-reconnect, session-cross-tab, planning
Sites left disk-backed (cross-instance or sibling-Database dependency):
- run-audit*, task-documents, fts5-guard (sibling Database at same dir)
- mission-integration, mission-factory-parity (taskStore2 reopens)
- agent-store checkout-leasing (TaskStore + AgentStore at same dir)
- routes.test.ts AgentStore seed pattern (route handler opens its own)
- cli/extension.test.ts (makeCtx opens its own TaskStore)
All disk-backed tests continue to write to mkdtemp temp dirs — no live-db
risk introduced. Wall-clock impact: core 149s → 19s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The logEntry()-bounds test does 1005 sequential SQLite writes and was
timing out at 20s on a disk-backed TaskStore. It doesn't exercise
cross-instance persistence, so flip on inMemoryDb to bring it under 8s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in `inMemory` flag to `Database`/`ArchiveDatabase` (and
`{ inMemoryDb }` to TaskStore, AgentStore, RoutineStore,
AutomationStore, PluginStore) that swaps the on-disk fusion.db /
archive.db for SQLite's `:memory:` connection. Production callers
never set the flag, so behavior is unchanged.
Test files for each store now flip the flag in `beforeEach`. The
handful of tests that exercise cross-instance persistence (open store
A, close, open store B on same dir, expect data) construct disk-backed
stores explicitly inside the test body, marked with a comment at each
site.
Wall-clock impact:
- core: 69.4s → 18.5s (3.7× faster, 3038 tests)
- dashboard: 156.6s → 30.0s (5.2× faster — improvement ripples through
any test that constructs a TaskStore)
The refactor eliminates the per-test SQLite open + WAL fsync + tmp
dir cleanup loop that dominated setup cost: ~50ms/test → ~5ms/test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the standalone remoteEnabled setting from CLI, core settings defaults/types, and dashboard settings APIs/UI
- Treat remote access as enabled when an active provider is selected and that provider is configured as enabled
- Update remote auth and engine lifecycle checks to gate on provider activation instead of a global flag
- Adjust tests and add a changeset documenting the remote access configuration simplification