- needsPrRepair no longer short-circuited by the open-state guard: legacy
fallback rows (finalized + prState open + prNumber null) now repair by
creating the real PR on re-promotion; regression test added
- no-PR abandon route test asserts last persisted call + response body
- goal-provenance fallback test clears missionId on its own in-memory store
so the feature-linkage path is genuinely exercised
Both sides bootstrapped CONCEPTS.md — kept main's broader structure (Missions,
Merge lifecycle, CE sessions) and folded the branch-group vocabulary into its
Merge-lifecycle cluster (enriched Shared branch group + Self-healing sweep;
added Branch assignment mode, Landed, Group promotion). Union-merged core
index/store imports; took main's AGENTS.md discoverability wording. Also adds
execFile to the two child_process test mocks that only stubbed exec (the
coordinator's argv-based git calls now require it).
- syncGroupPrCallback forwards owner/repo to updatePr (multi-project daemons
could 404 or edit an unrelated same-numbered PR via process-cwd fallback)
- merger background reconcile re-reads the group before persisting and skips
the write when the PR snapshot changed (stale-write race vs newer open PR)
- branchContext.groupId trimmed on metadata emit/parse round-trip
- triageSlice non-shared invariant assertions (no groupId, no group row)
Users can now watch everything the agent does while a CE stage works, steer
it mid-stage, and read the whole conversation as a proper chat surface.
Live output:
- New host capability: CreateInteractiveAiSessionOptions.onProgress — the
engine adapter streams thinking/text deltas + tool start/end markers from
the pi agent hooks (any plugin can use this).
- Orchestrator buffers per-session live activity (merged deltas, discrete
tool lines, capped), emits throttled progress events over SSE, and
GET /sessions/:id attaches it as liveActivity for the polling fallback.
- Routes detach turn execution: start/answer/resume return immediately
(status active) and clients converge via push/poll — the turn is watchable
instead of hidden inside a blocking POST.
- Turn timeout is now INACTIVITY-based: an actively-working long turn is
never killed; a quiet one interrupts with its working trace preserved.
- On settle the trace persists into history as a condensed record.
Steering:
- Stage protocol: responses may be a direct answer, {value, comment}
(answer + guidance), or {feedback} (guidance without answering); the
system prompt instructs agents to treat steering as first-class input.
- CeFlow: guidance textarea alongside selectable questions — attach to the
clicked answer, or "Send guidance" on its own.
Q&A UI:
- Transcript no longer hides control records: past questions/answers render
as chat bubbles (option ids → labels), steering turns marked, working
traces as collapsible "Agent work" blocks, completion marker.
- Live working pane (pulse + streaming thinking/tool lines) while a turn runs.
Tests: 130 plugin tests green (14 new: live buffer/flush ordering, inactivity
watchdog survives active work, detached convergence, steering payload shapes,
transcript rendering, live pane). Engine seam tests green; plugin/core/
engine/dashboard tsc clean. Core full suite OOMs locally (known orchestrator-
shell issue) — covered by CI shards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two compounding bugs made the memory-pressure vitest auto-kill a
30-second SIGKILL sweep of anything mentioning vitest:
1. False pressure: getAvailableMemory probed os.availableMemory, which
does not exist, and silently fell back to os.freemem() — on macOS
that reads ~99% used on an idle 256GB machine, permanently above the
90% threshold. Now reads process.availableMemory() (Node 22+) and
refuses to auto-kill when only the unreliable freemem fallback is
available.
2. Overbroad targeting: pgrep -f vitest matches full command lines, so
the sweep also killed wrapper shells (zsh -c '... npx vitest run'),
monitor loops, and anything else whose argv mentions vitest —
stranding exit handlers and taking out unrelated process trees.
New shared findVitestProcessIds (@fusion/core) filters matches to
actual node executables.
Surface enumeration (all vitest-process kill/count surfaces):
- TUI memory-pressure auto-kill (controller.killVitestProcesses)
- TUI manual kill-vitest command (same method)
- dashboard POST /api/kill-vitest
- dashboard GET /api/system-stats vitestProcessCount (display)
All four now route through findVitestProcessIds.
Expose operational log retention as a project setting in the dashboard.
- add an Operational log retention selector to the Project General settings section with supported retention options
- validate operationalLogRetentionDays in the settings API and cover accepted and rejected values in tests
- document the constrained retention values and assert project-scope/default parity for the setting
Files changed:
docs/settings-reference.md | 2 +-
packages/core/src/__tests__/settings-parity.test.ts | 7 ++++
packages/dashboard/app/components/SettingsModal.tsx | 47 ++++++++++------------
packages/dashboard/app/components/__tests__/SettingsModal.test.tsx | 8 ++++
packages/dashboard/src/__tests__/routes-settings.test.ts | 21 ++++++++++
packages/dashboard/src/routes/register-settings-memory-routes.ts | 10 +++++
6 files changed, 69 insertions(+), 26 deletions(-)
Fusion-Task-Id: FN-5939
Fusion-Task-Lineage: 2148dd88-1cef-4c6d-9696-31148fce97d3
Review residuals #5/#7/#8/#11/#12 + #3 wiring: forward the configured GitHub
token to the abandon route's client; guard abandon against finalized/merged
groups; reconcile an open group PR's state from GitHub on single-group reads
(merged out-of-band now flips prState); add fn branch-group abandon for
agent-native parity; block branchName shell injection (execFile argv push +
core-side branch-name validation at group creation); and collapse the
branch-groups list N+1 to a single task fetch via a shared
filterTasksByBranchGroup helper.
Engine half: real-git E2E covering planning- and mission-sourced groups —
members land on the group branch (never main/sibling), completion-gated single
PR via injected callback, re-promote idempotency, sync on later landing,
abandon→closed, and a self-healing finalize mid-flow staying group-anchored.
Core half: real triageFeature stamps the BG- id, member enumeration, and the
canonical completion gate flipping on landing.
Tasks with autoMerge explicitly enabled never auto-merged when the
project-level setting was disabled: the merge enqueue gate
(allowInReviewMergeProcessing) and all 19 in-review self-healing sweeps
checked only settings.autoMerge, and the board stall-signal hydration
passed the raw global into the diagnostic gates.
Introduce allowsAutoMergeProcessing(task, settings) in core — additive
relative to the global setting so configs with global auto-merge ON are
unchanged (explicit autoMerge:false tasks still flow to the merger's
manual-required parking) — and use it at the enqueue gate, every
self-healing sweep, and the store's stall/stalled signal contexts.
Route and coordinator disagreed on landed/complete: the route required
mergeConfirmed + matching mergeTargetBranch, the coordinator accepted bare
column===done/in-review and never checked the branch. Extract canonical
isBranchGroupMemberLanded/isBranchGroupComplete in @fusion/core (stricter
route semantics win — load-bearing for merge-target safety) and consume from
both sides. Tightens promotion gating to fire only when all members are
merge-confirmed onto the group branch.
Planning and mission entry points discarded the BranchGroup returned by
ensureBranchGroupForSource and stamped a synthetic planning:/mission: string
that never resolved against getBranchGroup, breaking member enumeration.
Capture and stamp the real BG- id; stop setTaskBranchGroup hardcoding
assignmentMode; add a removable legacy read-side shim. Export TaskBranchContext.
branch_groups.branchName is globally UNIQUE, but ensureBranchGroupForSource
only looked up an existing group by (sourceType, sourceId). When a second
mission's shared-branch triage resolved to a base branch (e.g. "main") that
another mission already owned a group for, createBranchGroup threw
"UNIQUE constraint failed: branch_groups.branchName". That error escaped
triageFeature and was swallowed by both callers (validation-failure
auto-triage and the reconcile sweep), leaving the mission's "defined"
features — including generated fix features — permanently un-triaged.
ensureBranchGroupForSource now reuses an existing open group for the same
branch name before attempting to create one, matching the established
getBranchGroupByBranchName(...) ?? ensureBranchGroupForSource(...) idiom.
Confirmed by reproducing against a snapshot of the affected mission DB:
triageFeature threw the UNIQUE error before, succeeds after.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces polling-only with a true server-push seam (any plugin benefits):
- core: createRouteContext accepts an emitEvent override (default still logs)
- dashboard: emitPluginCustomSseEvent forwards a plugin's ctx.emitEvent calls to
connected /api/events clients as a project-scoped 'plugin:custom' event; the
plugin route context's emitEvent is wired to it
- dashboard: PluginDashboardViewContext gains subscribePluginEvents so views
consume push via a host capability (no raw EventSource, no deep app import)
- CE view subscribes its session to push and refetches on each event; polling
stays as the fallback when push isn't wired or an event is missed
Also: skill-reachability test installs into a temp dir (no repo-dir writes).
Tests: dashboard sse +2 (plugin:custom relay + project scoping), plugin 99.
Closes the U2/U5 skill-discovery carry-forward so the plugin's interactive ce-*
sessions actually load the stage's bundled skill in a live agent (not just in
scripted-fake tests).
Root cause: createFnAgent built its DefaultResourceLoader without forwarding any
skill-discovery path, and the interactive seam options couldn't carry one. The
loader's skillsOverride only *filters* skills already discovered from cwd's
standard roots, so the plugin-local .fusion-ce-skills/<id>/SKILL.md was never
discoverable.
Fix (end-to-end):
- AgentOptions.additionalSkillPaths forwarded into DefaultResourceLoader
- CreateInteractiveAiSessionOptions gains requestedSkillNames + additionalSkillPaths
- the interactive engine adapter forwards them to createFnAgent (skills +
additionalSkillPaths)
- the orchestrator runs the session with cwd on the real project root and hands
it [stage.skillId] + the install root
Proven: a real DefaultResourceLoader with additionalSkillPaths discovers ce-plan
and filters out ce-work; the orchestrator passes the right id/path/cwd. Plugin 96,
engine 136, core 99 tests green.
Expose createInteractiveAiSession on route/loader PluginContext alongside the
one-shot createAiSession. The prompt->parse->retry->pause->resume loop is
reimplemented in an engine seam modeled on planning.ts (no engine await-input
primitive exists). Reuses PlanningQuestion/PlanningResponse; stays generic with
no plugin-specific concepts. Injected on route contexts only; tool/runtime
contexts omit it (parity with createAiSession).
Resolve goal tool lookups to the canonical project store from Fusion worktree directories.
- recognize both legacy .worktrees and .fusion/worktrees paths when resolving the project root for pi extensions
- add CLI coverage proving fn_goal_list and fn_goal_show return dashboard-created goals from a Fusion worktree cwd
- extend core worktree-resolution tests and keep the branch conflict recovery test worktree path isolated in tmpdir
- add a patch changeset for the published CLI fix
Files changed:
.changeset/fn-5851-goal-store-resolution.md | 5 +
packages/cli/src/__tests__/goal-store-resolution.test.ts | 104 +++++++++++++++++++++
packages/core/src/__tests__/pi-extensions.test.ts | 14 ++-
packages/core/src/pi-extensions.ts | 12 ++-
packages/engine/src/__tests__/branch-conflicts-recovery.test.ts | 2 +-
5 files changed, 130 insertions(+), 7 deletions(-)
Fusion-Task-Id: FN-5851
Fusion-Task-Lineage: ec4c8239-b953-4df7-9f72-02e17e4b0851
Whether `sqlite3 .recover` can rebuild a given byte-level corruption is
build-dependent — CI's sqlite3 returned "failed" (the safe outcome: leave
the corrupt original untouched rather than swap in an unverified rebuild)
where the local build returned "recovered". Assert the contract for whichever
branch is taken instead of hard-requiring "recovered".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause: node:sqlite SIGSEGVs inside pager_write leave the B-tree
malformed in a way that still opens but fails integrity checks; large
operational-log tables widen the write window where the crash strikes.
- backup: verify every copy with PRAGMA quick_check, quarantine corrupt
copies as *.corrupt, and never rotate out the last verified-good backup
- db: add Database.recoverIfCorrupt() startup guard (wired into
TaskStore.init, disk-backed only, opt out via FUSION_DISABLE_DB_AUTORECOVER)
that rebuilds a malformed db via sqlite3 .recover, preserving the corrupt
original; also fixes the latent `.recover main` invalid-option bug that made
recoverDatabase() always fail
- db: drop lost_and_found* scratch tables on init; add pruneOperationalLogs()
- settings: add operationalLogRetentionDays (default 30, 0 = off) and prune
activityLog/agentLogEntries/runAuditEvents/agentHeartbeats during maintenance
- dashboard: expose retention in Settings -> Backups -> Database Maintenance
Tests: backup 59/59, db 135/135 (incl. real corrupt->recover->reopen),
self-healing cleanup/corruption 10/10, settings 77/77, SettingsModal 460/460.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten test fixture cleanup so temporary test directories are reliably removed.
- track created test project directories and install process exit/beforeExit backstop cleanup hooks
- make destroyTestProject resilient with async rm plus rmSync fallback and tracked-dir removal
- add regression tests for sqlite wal/shm sibling cleanup and tracked-directory drain behavior
- make db lock-child teardown remove children from tracking during iteration
Files changed:
packages/core/src/__tests__/db.test.ts | 10 +++--
packages/core/src/__tests__/test-project.test.ts | 34 ++++++++++++++-
packages/core/src/__tests__/test-project.ts | 54 +++++++++++++++++++++++-
3 files changed, 92 insertions(+), 6 deletions(-)
Fusion-Task-Id: FN-5840
Fusion-Task-Lineage: 0977be1d-8334-413d-a652-318584d7058b
Add a new interactive onboarding CLI flow that guides first-time setup end to end.
- add new `fn onboard` command wiring in CLI entrypoint and usage help
- implement `runOnboard()` with sequential prompts for central DB, provider auth, init, test mode, and project maxConcurrent
- persist `cliOnboardingCompletedAt` marker in global settings with `--force` rerun support
- add onboarding command tests and global settings regression coverage
- document `fn onboard` usage and options in CLI reference
- add a minor changeset for published `@runfusion/fusion`
Files changed:
.changeset/fn-5805-onboard-command.md | 5 +
docs/cli-reference.md | 20 ++
packages/cli/src/bin.ts | 10 +
packages/cli/src/commands/__tests__/onboard.test.ts| 193 ++++++++++++++++
packages/cli/src/commands/onboard.ts | 249 +++++++++++++++++++++
packages/core/src/__tests__/global-settings.test.ts| 11 +
packages/core/src/index.ts | 2 +-
packages/core/src/settings-schema.ts | 1 +
packages/core/src/types.ts | 4 +
9 files changed, 494 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-5805
Fusion-Task-Lineage: b1bcaf27-9bd7-4569-b685-225a97fa1200