Extracted the duplicated file-watch-with-polling-fallback logic from TaskStore and AgentStore into a shared controller.
- Added packages/core/src/fs-watch-poll-controller.ts implementing a reusable fs.watch + polling-fallback controller
- Refactored packages/core/src/store.ts (TaskStore) to use the shared controller instead of its own watch/poll implementation
- Refactored packages/core/src/agent-store.ts (AgentStore) to use the shared controller instead of its own watch/poll implementation
- Added packages/core/src/__tests__/fs-watch-poll-controller.test.ts covering the new controller's behavior
- Updated docs/architecture.md to document the shared controller
Files changed:
docs/architecture.md | 1 +
.../src/__tests__/fs-watch-poll-controller.test.ts | 187 +++++++++++++++++++++
packages/core/src/agent-store.ts | 66 +++-----
packages/core/src/fs-watch-poll-controller.ts | 123 ++++++++++++++
packages/core/src/store.ts | 66 +++-----
5 files changed, 364 insertions(+), 79 deletions(-)
Fusion-Task-Id: FN-7726
Fusion-Task-Lineage: 69be6dc3-5414-44f2-a3f1-3eb72c2d7391
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes a task_agent_mutation policy example drift: the read-only GitLab browse
tools were incorrectly listed as mutation examples even though they were
never part of ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS and are already
classified read-only.
- Remove fn_task_browse_gitlab_project_issues, fn_task_browse_gitlab_group_issues,
and fn_task_browse_gitlab_merge_requests from AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES.task_agent_mutation
in packages/core/src/types.ts, keeping the mutating fn_task_import_gitlab_* variants
- Add an FNXC:ToolGovernance comment documenting the invariant and rationale
- Add regression coverage asserting the browse tools are excluded from
task_agent_mutation examples and are pinned as READONLY_FN_TOOLS
Files changed:
.../src/__tests__/agent-permission-policy.test.ts | 19 +++++++++++++++++++
packages/core/src/types.ts | 6 ++----
2 files changed, 21 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7733
Fusion-Task-Lineage: 3a54faa1-89dd-48bc-978a-4a53f06706be
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Board mutations (fn_task_update, CEO override, direct SQL) issued from a pi-extension tool session could silently write into a task's throwaway, never-synced worktree-local .fusion/fusion.db instead of the true project root when git CLI resolution failed (missing git binary, Docker "dubious ownership" refusal, or a non-default settings.worktreesDir). This fixes root-cause resolution and adds regression coverage plus a docs writeup.
- getProjectRootFromGitLinkedWorktree now resolves a linked worktree's project root from git's own on-disk .git/commondir metadata via pure filesystem reads before falling back to the git rev-parse CLI, so writes no longer fall through to a local hydrated copy on git-invocation failure.
- Added getMainRepoRootFromGitFile and resolveCommonGitDirFromWorktreeGitFile helpers with FNXC:Storage comments documenting the FN-7730 root cause and fix rationale.
- Added packages/core/src/__tests__/pi-extensions-write-path-durability.test.ts regression coverage for the write-path durability invariant.
- Extended packages/core/src/__tests__/pi-extensions.test.ts with additional resolution-path assertions.
- Documented the failure mode and fix in docs/storage.md ("Silent board-mutation write loss (FN-7730)").
- Added a patch changeset for @runfusion/fusion describing the user-facing fix.
Files changed:
.changeset/fn-7730-worktree-project-root-resolution.md | 7 ++
docs/storage.md | 54 ++++++++++
packages/core/src/__tests__/pi-extensions-write-path-durability.test.ts | 98 ++++++++++++++++++
packages/core/src/__tests__/pi-extensions.test.ts | 87 +++++++++++++++-
packages/core/src/pi-extensions.ts | 114 +++++++++++++++++++++
5 files changed, 359 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7730
Fusion-Task-Lineage: 00753a2d-a934-42cf-8fde-0f9b8ad98142
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Introduces a dedicated review_gate_bypass permission-policy category so operators can govern who may bypass a failed pre-merge review gate independently of ordinary task-mutation permissions.
- Add review_gate_bypass as a new sensitive-action category in packages/core/src/types.ts, distinct from task_agent_mutation, with fn_task_bypass_review as its sole example tool
- Default review_gate_bypass to require-approval even under the unrestricted preset (stricter than the preset's uniform disposition) in packages/core/src/agent-permission-policy.ts, while approval-required/locked-down already cover it uniformly
- Classify fn_task_bypass_review into the new category via a shared REVIEW_GATE_BYPASS_FN_TOOLS set in packages/engine/src/gating-classifications.ts, consumed identically by both evaluateAgentActionGate and the permanent-agent gate to prevent path drift
- Render the new category as its own row in the dashboard's project-default and per-agent AgentPermissionPolicyEditor, surfaced in AgentDetailView
- Update docs/settings-reference.md and add unit tests across core/engine/dashboard covering the new category, its stricter default, and gate-classification alignment
- Add changeset (@runfusion/fusion: minor) documenting the new operator-facing permission category
Files changed:
.changeset/fn-7728-review-gate-bypass-rbac.md | 7 +++
docs/settings-reference.md | 8 +--
.../src/__tests__/agent-permission-policy.test.ts | 54 ++++++++++++++++++-
packages/core/src/agent-permission-policy.ts | 12 ++++-
packages/core/src/types.ts | 8 +++
.../dashboard/app/components/AgentDetailView.tsx | 2 +
.../app/components/AgentPermissionPolicyEditor.tsx | 8 +++
.../__tests__/AgentPermissionPolicyEditor.test.tsx | 5 ++
.../engine/src/__tests__/agent-action-gate.test.ts | 45 ++++++++++++++++
.../src/__tests__/gating-classifications.test.ts | 63 ++++++++++++++++++++++
.../src/__tests__/permanent-agent-gating.test.ts | 41 ++++++++++++++
packages/engine/src/agent-action-gate.ts | 7 +++
packages/engine/src/gating-classifications.ts | 8 ++-
packages/engine/src/permanent-agent-gating.ts | 6 +++
14 files changed, 266 insertions(+), 8 deletions(-)
Fusion-Task-Id: FN-7728
Fusion-Task-Lineage: 100c8563-2897-4d53-9546-5c2faa6ab7d8
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Add a policy-gated review-lane bypass primitive so operators can unstick cards stranded by a failed pre-merge review step (e.g. the no-feedback review-engine defect), without exposing it to agent-driven lanes.
- Add `store.bypassFailedPreMergeReviewStep(id, { reason, actor })` in @fusion/core plus `getLatestFailedPreMergeReviewStep` in task-merge.ts, and new `bypassedBy`/`bypassedAt`/`bypassReason`/`bypassedFromStatus`/`bypassedFromVerdict` fields on `WorkflowStepResult`
- Add operator-only `fn_task_bypass_review` CLI/pi-extension tool; explicitly withheld from executor/reviewer/triage agent tool lists
- Add `POST /tasks/:id/bypass-review` dashboard API route and wire it through `register-task-workflow-routes.ts` and legacy API compatibility layer
- Add dashboard UI affordance (context menu action + task detail modal + right-dock controller wiring) to trigger the bypass with a reason
- Add i18n strings for the bypass action/labels across en/es/fr/ko/zh-CN/zh-TW locales
- Update `gating-classifications.ts` to recognize the bypassed state
- Add unit tests: `store-bypass-review.test.ts`, `task-merge-bypass.test.ts`, extension test coverage, and `useTasks` hook test coverage
- Update docs (`docs/workflow-steps.md`, `docs/dashboard-guide.md`, AGENTS.md, fusion skill references) to describe the new bypass tool/route
- Add changeset `.changeset/fn-7720-review-lane-bypass-primitive.md` (minor)
Files changed:
$(git diff --cached --stat)
Fusion-Task-Id: FN-7720
Fusion-Task-Lineage: 590b020a-ae02-4b51-8189-df8f54bf3044
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds opt-in cross-process change detection to AgentStore so the engine reacts to CLI-driven agent stop/start mutations promptly instead of waiting for the periodic audit sweep.
- AgentStore gains fs.watch-based (with poll fallback) cross-process notification, modeled on TaskStore's existing mechanism
- Re-emits existing agent:updated/agent:stateChanged events in the engine process when another process (the fn CLI) mutates an agent row
- HeartbeatTriggerScheduler listeners now fire immediately instead of waiting up to 60s for the auditTimerRegistrations sweep; the sweep remains as durable backstop
- in-process-runtime.ts wires up the new notification bus
- Adds unit tests for agent-store cross-process notifications and heartbeat-scheduler reaction behavior
- Updates docs/agents.md and docs/architecture.md
- Adds changeset (patch) for @runfusion/fusion
Files changed:
.changeset/fn-7723-cross-process-agent-notify.md | 7 +
docs/agents.md | 1 +
docs/architecture.md | 1 +
packages/core/src/__tests__/agent-store.test.ts | 177 +++++++++++++++++
packages/core/src/agent-store.ts | 210 ++++++++++++++++++++-
.../src/__tests__/heartbeat-scheduler.test.ts | 162 ++++++++++++++++
packages/engine/src/runtimes/in-process-runtime.ts | 30 +++
7 files changed, 587 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7723
Fusion-Task-Lineage: d3a7fa05-b40d-4388-8e98-140f9d8861c9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Bounds durable-agent heartbeat worktree acquisition to a fixed retry count instead of requeuing to todo indefinitely across heartbeat cycles.
- Add MAX_HEARTBEAT_WORKTREE_ACQUISITION_RETRIES (3) in agent-heartbeat.ts, reusing Task.recoveryRetryCount as a cross-heartbeat counter (no schema migration)
- On cap exhaustion, terminally mark the task status:"failed" with an explanatory error, log the entry, and reopen to todo with preserveStatus so the failed status isn't wiped by reopen-to-todo semantics
- Add onTaskAcquisitionExhausted callback wired in in-process-runtime.ts to CentralCore.recordTaskCompletion(taskId, false) so exhausted acquisitions count toward totalTasksFailed
- Add regression tests in agent-heartbeat-worktree.test.ts and in-process-runtime.test.ts covering the retry cap and completion recording
- Add changeset (patch) and a docs/solutions/logic-errors writeup documenting the investigation and other worktree-collision sub-gaps found not to reproduce on HEAD
Files changed:
.changeset/fn-7721-worktree-heartbeat-retry-cap.md | 7 ++
docs/solutions/logic-errors/heartbeat-worktree-acquisition-unbounded-requeue.md | 84 ++++++++++++++++++++++
packages/engine/src/__tests__/agent-heartbeat-worktree.test.ts | 58 +++++++++++++++
packages/engine/src/__tests__/in-process-runtime.test.ts | 11 +++
packages/engine/src/agent-heartbeat.ts | 72 ++++++++++++++++++-
packages/engine/src/runtimes/in-process-runtime.ts | 12 ++++
6 files changed, 242 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7721
Fusion-Task-Lineage: caad671c-f360-4c1c-8aaa-5b48fca5a55b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Ensures stopping and restarting an agent durably clears its heartbeat timer instead of relying on the later FN-7645 watchdog repair.
- HeartbeatTriggerScheduler.auditTimerRegistrations now unregisters lingering timers for non-eligible (stopped/paused/disabled) agents
- syncTimerForAgent force-re-arms a stale present timer on a start transition so no orphaned timer entry lingers
- Added 308 lines of new heartbeat-scheduler regression tests covering the stop/start zombie-timer scenarios
- Added changeset (patch) documenting the fix
- Updated docs/agents.md and docs/architecture.md to describe the new invariant
Files changed:
.changeset/fn-7718-zombie-timer-invalidate.md | 7 +
docs/agents.md | 2 +
docs/architecture.md | 1 +
.../src/__tests__/heartbeat-scheduler.test.ts | 308 +++++++++++++++++++++
packages/engine/src/agent-heartbeat.ts | 49 +++-
5 files changed, 364 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7718
Fusion-Task-Lineage: fc834ccd-495e-4294-805d-325b4cb536a2
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Triage planning-model retries no longer dead-end on "no fallback configured" when a provider primary-model call fails (e.g. a 404 wrapped in a 429 for nvidia/moonshotai/kimi-k2.6).
- Add resolveImplicitPlanningFallbackModel() to agent-session-helpers.ts: derives a fallback from the resolved project/global default (execution) model when neither planningFallback*/global fallback* is set, guarding against self-swap and skipping in test mode.
- Wire the implicit fallback into TriageProcessor.specifyTask() in triage.ts so a retryable primary planner-model failure swaps once via the derived fallback instead of failing triage outright.
- Add unit test coverage in agent-session-helpers.test.ts and triage.test.ts for the new implicit-fallback resolution and its triage integration.
- Document the new implicit-fallback behavior in docs/settings-reference.md.
- Add a patch changeset for @runfusion/fusion describing the fix.
Files changed:
.changeset/fn-7719-triage-planning-implicit-fallback.md | 7 +
docs/settings-reference.md | 2 +
packages/engine/src/__tests__/agent-session-helpers.test.ts | 75 +++++++
packages/engine/src/__tests__/triage.test.ts | 237 +++++++++++++++++++++
packages/engine/src/agent-session-helpers.ts | 41 ++++
packages/engine/src/triage.ts | 31 ++-
6 files changed, 389 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-7719
Fusion-Task-Lineage: 69e797e1-5bac-47f3-8dce-505b9d64d83c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Grok CLI provider readiness now mirrors the Cursor CLI provider: it is derived from the `grok` binary being available rather than requiring a Fusion-visible GROK_API_KEY or ~/.grok/user-settings.json, since the CLI manages its own auth.
- probeGrokBinary now derives `authenticated` from binary availability (readiness) instead of API-key/user-settings presence; key detection surfaces as a non-blocking `apiKeyDetected` hint
- /auth/status treats the grok-cli provider as authenticated when enabled + binary available
- GrokCliProviderCard drops the blocking "Set GROK_API_KEY" state
- Direct xAI streaming path is unchanged and still uses $GROK_API_KEY when present (FN-7711/FN-7714)
- Added changeset for @runfusion/fusion (patch)
Files changed:
$(cat /tmp/diffstat_fn7716.txt)
Fusion-Task-Id: FN-7716
Fusion-Task-Lineage: ac0efc79-2510-465e-9cd2-4938c08989c9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Archiving a task from triage/planning/todo (not just in-progress) previously left leaked active-session-registry entries, so a successor task could hit ActiveSessionPathHeldByForeignTaskError and get blocked from Plan Review.
- Add an explicit `to === "archived"` branch in the task-move handler that awaits abort of in-flight task work and sweeps any leftover activeSessionRegistry paths for the task, checked before the narrower `from === "in-progress"` branch so direct in-progress→archived transitions are covered too.
- Deliberately exclude `to === "done"` / `to === "in-review"` from this sweep since those columns legitimately hold ai-merge / workspace-repo-land merge leases that must survive the transition.
- Add regression test coverage for archive releasing active sessions across originating columns.
- Add changeset and architecture doc note.
Files changed:
.../fn-7717-archive-active-session-release.md | 7 +
docs/architecture.md | 1 +
...xecutor-archive-releases-active-session.test.ts | 167 +++++++++++++++++++++
packages/engine/src/executor.ts | 35 +++++
4 files changed, 210 insertions(+)
Fusion-Task-Id: FN-7717
Fusion-Task-Lineage: 7cff6821-7bb3-4b75-b502-a26467ca7f51
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fall back to the Grok CLI's user-settings file for the API key so pi's $GROK_API_KEY provider reference resolves even when the env var isn't exported.
- Add hydrateGrokApiKeyFromUserSettings() in grok-provider.ts, called from registerBuiltInGrokProvider(), which hydrates process.env.GROK_API_KEY from ~/.grok/user-settings.json { apiKey } only when the env var is unset/empty
- Env var always wins; a missing (ENOENT), malformed, or empty-apiKey settings file is fail-soft (no throw, no env mutation), mirroring the grok-runtime probe's fallback behavior
- Add regression tests covering env-precedence, fallback hydration, and fail-soft error paths (grok-provider-user-settings.test.ts)
- Document the fallback in docs/settings-reference.md
- Add a patch changeset for @runfusion/fusion
Files changed:
.changeset/fn-7714-grok-user-settings-apikey.md | 7 +
docs/settings-reference.md | 2 +-
.../__tests__/grok-provider-user-settings.test.ts | 156 +++++++++++++++++++++
packages/core/src/grok-provider.ts | 47 +++++++
4 files changed, 211 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7714
Fusion-Task-Lineage: 5450b480-3a32-4331-9494-867b84605464
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Documents that GrokRuntimeAdapter.promptWithFallback is an intentional no-op rather than unfinished work, and updates its regression test to assert that contract explicitly.
- Add FNXC:GrokCli comment on promptWithFallback explaining Grok streaming already flows through the pi/xAI OpenAI-compatible path from FN-7711, that the grok CLI has no documented non-interactive prompt/stream subcommand, and that this stub is only reached via an unused runtimeConfig.runtimeHint === "grok" path
- Remove the stale TODO(FN-7705) comment
- Rename/expand the promptWithFallback test to assert the intentional no-op contract (resolves without throwing, returns undefined)
Files changed:
.../src/__tests__/runtime-adapter.test.ts | 11 ++++++++++-
.../fusion-plugin-grok-runtime/src/runtime-adapter.ts | 18 ++++++++++++++++--
2 files changed, 26 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7715
Fusion-Task-Lineage: 118639d3-5530-45d5-bc66-de9b1f18fbc4
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the Grok CLI model picker showing raw prompt/preamble text instead of real model names by rewriting parseModelLines to match the actual verified `grok models` output shape.
- Rewrote parseModelLines in process-manager.ts to strip the login/"Default model:"/"Available models:" preamble
- Strip `*`/`-` bullet markers and the `(default)` annotation from each model line
- Preserve existing legacy `id - Label`, columnar, and JSON parsing paths
- Added regression tests covering the real grok models output shape
- Added changeset (patch) documenting the fix
Files changed:
.changeset/fn-7712-grok-model-parse.md | 7 ++++
.../src/__tests__/process-manager.test.ts | 39 ++++++++++++++++++++++
.../src/process-manager.ts | 34 ++++++++++++-------
3 files changed, 68 insertions(+), 12 deletions(-)
Fusion-Task-Id: FN-7712
Fusion-Task-Lineage: 93e34513-07b9-41b3-8b8b-ecdb763b4208
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Agents that must edit files beyond a task's declared ## File Scope had no
way to keep the scope in sync, so those edits were stranded at merge (the
squash merge is scoped to ## File Scope, and cross-task overlap blocking +
the merge file-scope invariant both read it).
New executor tool fn_task_file_scope_add validates repo-relative
paths/globs with isValidFileScopeEntry, de-dupes against existing scope,
appends them to the ## File Scope section of PROMPT.md, and persists via
store.updateTask({ prompt }) (same validation + task.json/PROMPT.md sync as
fn_task_prompt_write). Registered in the main coding-agent tool list; the
base executor prompt now instructs the agent to call it when editing beyond
the declared scope. Merge-time peer-claim refusal is unchanged and remains
the cross-task backstop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FN-7690 changed resolveApiType() to return the registered pi-ai key
"anthropic-messages" for anthropic-compatible providers (the bare
"anthropic" key is never registered and throws at stream time), but left
behind a stale JSDoc and a stale test expectation:
- custom-provider-registry.ts: update the FN-7689 buildCustomProviderModels
comment that still described the anthropic/anthropic-messages drift as
unresolved.
- provider-registration.test.ts: assert config.api === "anthropic-messages"
(was still asserting the pre-fix "anthropic").
Also de-slow a retry-exhaustion test: the describe uses fake timers with
shouldAdvanceTime, so awaiting a 3-retry backoff (1s+5s+15s) burned ~21s of
real wall time. Drive the backoff with advanceTimersByTimeAsync instead
(Standing Rule: prefer fake timers over real time waits).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prevents short-lived CLI processes from being held open by background SQLite integrity checks.
- unref the sqlite3 child process (and its stdio) spawned by integrityCheckSqliteFileAsync via the shared unrefQmdChildProcess helper, immediately after spawn
- unref the 60s scheduling timer in scheduleBackgroundIntegrityCheck so a short-lived caller isn't pinned waiting for a background check it never asked to block on
- add regression test coverage (db-integrity-check-unref.test.ts) plus a CLI fixture (db-integrity-check-fixture.mjs) that exercises the fix in a real short-lived process
- add changeset documenting the fix and the audit of other spawn sites across @fusion/core/@fusion/engine/@fusion/dashboard/cli confirming they are safe
Files changed:
.changeset/fn-7709-db-integrity-check-unref.md | 7 ++
.../src/__tests__/db-integrity-check-unref.test.ts | 135 +++++++++++++++++++++
.../fixtures/db-integrity-check-fixture.mjs | 28 +++++
packages/core/src/db.ts | 26 ++++
4 files changed, 196 insertions(+)
Fusion-Task-Id: FN-7709
Fusion-Task-Lineage: 6594aca4-0268-4bba-9a7f-af96d695f1e9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
The OAuth expiry monitor and validity logger iterated the un-aliased
getOAuthProviders() id `anthropic` and evaluated get("anthropic"), which
can resolve to a stale legacy/supplemental row (e.g. ~/.pi/agent/auth.json)
even when the fresh, actually-used token lives under `anthropic-subscription`.
That fired a false "Anthropic OAuth expired" notification while the real
subscription token had refreshed successfully.
Both surfaces now resolve the freshest of the two aliased ids via a shared
resolveEffectiveOAuthCredential helper (mirroring the refresh scheduler's
getRefreshCandidateIds alias handling), so a live subscription token
suppresses the false alert. Notification throttle/cadence unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes searchWithQmd's inline promisify(execFile) copy that could hold a caller open by reusing the already-hardened, synchronously-unref'd executor established for the background refresh path.
- searchWithQmd now calls getDefaultExecFileAsync() instead of building its own promisify(execFile) executor inline
- Removes the second un-unref'd execFile executor that could keep a short-lived caller (e.g. one-shot CLI memory search) open up to the awaited timeout
- Adds regression test fixture and test coverage (qmd-search-fixture.mjs, qmd-search-unref.test.ts) asserting the shared executor is used
- Adds changeset (patch) for @runfusion/fusion
Files changed:
.changeset/fn-7707-qmd-search-unref.md | 7 +
packages/core/src/__tests__/fixtures/qmd-search-fixture.mjs | 30 ++++
packages/core/src/__tests__/qmd-search-unref.test.ts | 166 +++++++++++++++++++++
packages/core/src/memory-backend.ts | 16 +-
4 files changed, 216 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7707
Fusion-Task-Lineage: 3f9f94a7-5613-4b9a-a3ba-8e9bcdd6b687
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Replaces promisify(execFile) with a hand-rolled spawn()-based qmd executor that unrefs the child process and its stdio, so a fire-and-forget scheduleQmd* memory-index refresh never keeps a short-lived caller process (e.g. CLI) alive; long-lived callers like the dashboard server still see refresh resolve/reject normally.
- memory-backend.ts: replace promisify(execFile) qmd exec path with spawn()-based executor that unrefs child + stdio
- Add qmd-refresh-unref.test.ts covering unref behavior with a qmd-refresh-fixture.mjs test fixture
- Add changeset fn-7706-qmd-unref.md (patch, fix category)
Files changed:
.changeset/fn-7706-qmd-unref.md | 7 ++
.../src/__tests__/fixtures/qmd-refresh-fixture.mjs | 28 +++++
.../core/src/__tests__/qmd-refresh-unref.test.ts | 136 +++++++++++++++++++++
packages/core/src/memory-backend.ts | 123 ++++++++++++++++++-
4 files changed, 291 insertions(+), 3 deletions(-)
Fusion-Task-Id: FN-7706
Fusion-Task-Lineage: 713c23c2-d7da-42c4-b066-31883ba78321
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fix CLI process exit so `fn agent stop`/`fn agent start` no longer hang up to 60s and eventually time out on repeated retries against the same agent.
- Root cause: `resolveProject()` cached an unclosed `TaskStore`, and `createAgentStore()` never closed the `AgentStore` it opened, leaving SQLite handles alive after the command's real work was done.
- Add `resolveProjectPathOnly`/`closeProjectStore` helpers in `project-context.ts` so path-only callers never leak a `TaskStore`.
- Explicitly close `AgentStore` on every exit/return path in `agent.ts`, since `process.exit()` skips pending `finally` blocks.
- Add a bounded fast-fail timeout around the state-store write (default 10s, overridable via `FUSION_AGENT_CMD_TIMEOUT_MS`) so a genuinely stuck operation fails fast with a clear error and non-zero exit instead of hanging.
- Add regression tests covering process-exit/store-closing behavior and update CLI reference docs.
- Add changeset for the patch release.
Files changed:
.changeset/fn-7704-agent-cmd-hang-fix.md | 7 +
docs/cli-reference.md | 3 +
.../commands/__tests__/agent-process-exit.test.ts | 114 +++++++++++
packages/cli/src/commands/__tests__/agent.test.ts | 111 +++++++++-
packages/cli/src/commands/agent.ts | 223 ++++++++++++++++-----
packages/cli/src/project-context.ts | 44 ++++
6 files changed, 444 insertions(+), 58 deletions(-)
Fusion-Task-Id: FN-7704
Fusion-Task-Lineage: 4679d1a0-3ab8-48ce-86b7-5919bba805fb
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Fixes the search icon overlapping placeholder/typed text in the Files — Project search input under the compact spacing theme.
- Anchor .file-browser-search-input padding-left to the icon's own --space-sm offset + 16px icon width + a real gap, instead of the unrelated calc(--space-lg + --space-md) formula that collided exactly with the icon's occupied width under compact spacing
- Add FNXC:FileBrowser comment documenting the collision math and why the padding is now theme-invariant
- Add a regression test asserting padding-left exceeds icon offset + width against the compact spacing scale
- Add a patch changeset for @runfusion/fusion
Files changed:
.changeset/fn-7703-search-icon-overlap.md | 7 +++
packages/dashboard/app/components/FileBrowser.css | 11 ++++-
.../app/components/__tests__/FileBrowser.test.tsx | 52 ++++++++++++++++++++++
3 files changed, 69 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-7703
Fusion-Task-Lineage: d7de7f82-f0e7-4086-a8ef-2fed2b4704ec
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Thread the machine-local cursorCliBinaryPath operator override into /api/models Cursor CLI discovery so the model picker spawns the same cursor-agent binary already validated by sign-in/status/probe.
- register-model-routes.ts reads globalSettings.cursorCliBinaryPath, trims and normalizes blank to undefined (preserving PATH auto-detection)
- passes the normalized binaryPath through to getCursorPickerModels({ binaryPath }) for model discovery
- adds regression tests covering override-set and override-blank/unset behavior in register-model-routes-cursor-cli.test.ts
- adds changeset fn-7699-cursor-cli-binary-path-model-picker.md (patch, fix) documenting the follow-up to FN-7696
Files changed:
$(cat /tmp/diffstat.txt)
Fusion-Task-Id: FN-7699
Fusion-Task-Lineage: 9895af8e-447d-425b-af58-1af4748c013c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Corrects the FN-3396 preflight's assumed Cursor CLI commands with the real, verified contract captured and implemented in FN-7697.
- Documents model discovery as plain-text `cursor-agent models` (no --json flag), including output shape, empty-account state, and the unreliable --list-models alternative
- Documents the parsing strategy: extract id before first ' - ' per line, filtering header/tip/empty-state lines
- Documents authentication as derived from `cursor-agent status --format json` via `isAuthenticated`, distinct from the --version availability probe
- Updates the Windows shell-backed probe list to include the auth-status probe and the corrected model-discovery command
- Marks the FN-3396 contract-freeze section as superseded by the verified contract, retaining accurate parts (binary candidates, expected failure states, dynamic-first principle)
- Adds an update-history note and FNXC:CursorCli comment documenting the correction
Files changed:
docs/cursor-cli-contract.md | 43 +++++++++++++++++++++++++++++--------------
1 file changed, 29 insertions(+), 14 deletions(-)
Fusion-Task-Id: FN-7698
Fusion-Task-Lineage: ae30b81c-f750-4011-85ae-883b1c5eb48b
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Adds an end-to-end integration test proving the artifact pipeline works against a real TaskStore, not just a mocked one.
- New test file exercises createArtifactRegisterTool/createArtifactListTool/createArtifactViewTool bound to a real TaskStore (inMemoryDb, real filesystem writes) instead of a mocked store
- Pins the register -> list -> view invariant for a real base64 PNG image artifact, verifying disk persistence, SQLite row fields (type, mimeType, sizeBytes, uri, taskId), and the list/view text surfaces
- Pins the invalid-base64-payload rejection path (non-image bytes for an image-typed artifact) to confirm no artifact row is persisted
- Pins the empty-state list text for a task with no registered artifacts
Files changed:
packages/engine/src/__tests__/agent-artifact-tools-real-store-integration.test.ts | 131 +++++++++++++++++++++
1 file changed, 131 insertions(+)
Fusion-Task-Id: FN-7693
Fusion-Task-Lineage: fd3493aa-6fb2-4e18-a735-c4a9d87c9c6c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
Round 3 of full-suite greening on `main`. The post-#1965 full-suite
still failed on 6 UI/CSS test files in shards 3+4 (pre-existing
test-drift from recent UI commits, surfaced after the chat/i18n fixes
landed). All fixed.
## Fixes (all test-only; no production change)
- **TaskCard badge/footer tests** (`TaskCard.badge-height`,
`TaskCard.badge-wrap`, `TaskCard.footer-wrap`) — `RuntimeFallbackBadge`
now calls the dashboard `useToast()` hook, but these suites render
`<TaskCard>` without a `ToastProvider`. Added the `useToast` mock (same
pattern as the sibling `TaskCard.test.tsx` and `PlanningModeModal`
suites).
- **`TaskDetailModal.github-tracking-header`** — the github/gitlab
tracking header CSS rules were consolidated into a shared selector list
(`.detail-github-tracking-section .detail-source-header,
.detail-gitlab-tracking-section .detail-source-header {…}`), so the
test's `\s*\{` (selector immediately followed by `{`) no longer matched.
Updated the 3 CSS regexes to `[^{]*\{` to tolerate the selector list
while still pinning the layout contract.
- **`GraphTaskNode` tests** (`fusion-plugin-dependency-graph`) — same
`useToast` issue: `GraphTaskNode` renders the REAL `TaskCard` (to verify
prop pass-through, unlike sibling suites that mock it), hitting
`RuntimeFallbackBadge`→`useToast`. Added the
`@fusion/dashboard/app/hooks/useToast` mock to both files.
## Note on shard-2 engine[2/2]
Shard 2 still times out (watchdog 900s) on `@fusion/engine [2/2]`. This
is the engine-reliability real-git tier running single-threaded under
4-shard concurrent load — locally `[2/2]` is ~96s and green. It's
slow-test-debt / CI-load, not a code bug in these commits; I'm
investigating the specific slow/hanging file separately (the silent CI
reporter hides it).
## Verification
- TaskCard badge/footer: 14/14 ✅
- TaskDetailModal.github-tracking-header: 1/1 ✅
- GraphTaskNode + GraphTaskNode.drag: 29/29 ✅
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Stabilized several dashboard and dependency-graph test suites by
mocking toast behavior to prevent provider-related failures.
* Improved robustness of task card and graph node interaction tests.
* Updated task detail modal CSS/layout assertions to better align with
current responsive styling and selector patterns.
* Reduced test flakiness for step-session retry timing by using
controlled fake-timer advancement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The 'publishes failed terminal workflow step activity' test awaited executeAll()
directly while the executor retried a failing step 3x with sleep() delays. Under
useFakeTimers({ shouldAdvanceTime: true }) those sleeps consumed REAL wall-clock
time (~22.6s locally, ballooning under CI load and busting the shard-2 watchdog).
Fast-forward the retry sleeps via vi.advanceTimersByTimeAsync like sibling retry
tests; the loop now completes in milliseconds.
## Summary
`003948033` ("harden runtime-fallback agent-card viewport gating")
landed on `main` **without a changeset**, but it affects published
`@runfusion/fusion`. This PR adds the missing patch changeset so the fix
shows up in release notes.
## Context
Supersedes the now-closed #1963, whose code was fully redundant with
`main` — all four FUX-039 findings plus every Greptile/CodeRabbit review
comment already shipped via `003948033` and the preceding FUX-039
commits. The only remaining gap was this release-notes entry.
## What the changeset documents
- **summary (user-facing):** Prevent redundant polling and a re-render
loop in agent-card runtime-fallback badges.
- **category:** `fix`
- **dev:** `AgentsView` caches one stable ref callback per viewport key
(avoids an infinite re-render loop when `IntersectionObserver` is
unavailable) and evicts it on unmount; the test-only toast-dedupe reset
is guarded to a no-op in production builds.
## Status
- Diff: a single new file under `.changeset/`. No production code
changes.
- Gate: ✅ green — Lint, Typecheck, Build, and Gate all pass on
`d8ce3f408`.
- An earlier revision also trimmed `fn-7692`'s over-length summary to
unblock the gate, but `main` since fixed that itself in `5815cd170`, so
this branch was rebased to drop the now-redundant commit. The diff is
now purely the FUX-039 changeset.
🤖 Generated with an autonomous coding agent
Main landed the FUX-039 runtime-fallback agent-card hardening (003948033)
without a changeset, but it affects published @runfusion/fusion. This adds
the missing patch changeset so the fix appears in release notes.
## Summary
Follow-up to #1947. The full-suite on `main` is still red on 3 surfaces
introduced by post-#1947 commits. This PR fixes the two real test
failures and the i18n parity gap.
## Fixes
- **i18n key parity (FN-7658)** —
`settings.scheduling.autoArchiveDuplicateTasks` + `...Help` were added
to `en` but not the 5 non-en catalogs, breaking the i18n parity gate
(`parity.test.ts`, `i18n-gate-coverage.test.ts`). Added the 2 keys
(empty-string per the untranslated-entry convention) to `zh-CN`,
`zh-TW`, `fr`, `es`, `ko` in `packages/i18n/locales` (the single source
of truth; `dashboard/app/locales` is gitignored and synced in CI).
- **chat.test.ts (FN-7675)** — `chat.ts` now imports
`FUSION_RUNTIME_SELF_AWARENESS` from `@fusion/core` (CHAT_SYSTEM_PROMPT
embeds it); the hand-written core mock didn't stub it, so the module
failed to load. Added a stub (importOriginal intentionally avoided to
preserve the fs-cascade block).
- **verification-followup-dedup.test.ts (FN-7658)** — the "remains
additive with FN-4892 same-agent duplicate intake" test asserts the
ARCHIVE path, but FN-7658 made same-agent auto-archiving opt-in
(`autoArchiveDuplicateTasksEnabled` defaults false → flag-in-place in
triage). The test now opts into the legacy archive behavior it asserts.
## Note on shard-2 engine[2/2] timeout
The full-suite shard 2 times out on `@fusion/engine [2/2]` (watchdog
900s). Locally `[2/2]` runs in ~96s and is green (the lone
`provider-registration.test.ts` failure is local-only `pi-ai@0.79.9`
staleness — the lockfile pins `0.80.3` which exports `/compat`, so CI
resolves it). The `verification-followup-dedup` failure above is the
only real `[2/2]` defect; this PR fixes it. If the CI timeout persists
it's aggregate real-git load, which I'll address separately (not a code
bug).
## Verification
- i18n `parity` + `i18n-gate-coverage`: 7/7 ✅
- `chat.test.ts`: 14/14 ✅
- `verification-followup-dedup`: 5/5 ✅
- engine `--shard=2/2` (excluding the local-staleness file): 363 files /
4483 tests ✅ in ~96s
No production behavior change; no changeset needed (i18n catalog +
test-only).
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added scheduling settings for automatic duplicate-task archiving,
including label and help text entries (currently placeholders) across
Spanish, French, Korean, Simplified Chinese, and Traditional Chinese.
* **Bug Fixes**
* Updated “awaiting confirmation” merger messaging to better reflect
when auto-merge proceeds automatically.
* **Tests**
* Updated reliability interaction tests to explicitly opt into legacy
duplicate-task archiving behavior.
* Adjusted chat-related tests by extending the runtime mock to satisfy a
new core import requirement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Follow-up hardening on the RuntimeFallbackBadge viewport-gating work:
- AgentsView.tsx: registerAgentCardRef now returns a cached, stable callback per
key (agentCardRefCallbacksRef) instead of a fresh closure each render. A fresh
closure reads as unmount+remount to React; in environments without
IntersectionObserver the mount path calls setVisibleAgentCardKeys -> re-render
-> another fresh closure -> an infinite re-render loop (including jsdom). The
cached entry is evicted on true unmount (el === null) so the Map cannot grow
unbounded across created/deleted agents.
- ActiveAgentsPanel.tsx: document the viewport-gated badge polling with an FNXC
comment (behavior unchanged).
- useRuntimeFallbackStatus.ts: guard __resetRuntimeFallbackToastDedupeStoreForTests
to a no-op outside the test build (import.meta.env.MODE !== "test") so the
test-only dedupe reset can never affect production code paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Every animated README GIF opened on the app's page-load sequence — dark
skeleton placeholder boxes, then a "Loading files…" spinner (some also
an "AI engine is not running" banner) — before real content appeared,
making the looping GIFs look broken on first paint. This trims those
leading frames so each GIF starts on fully-rendered, populated content.
**17 dashboard-capture GIFs trimmed** (leading loading frames dropped,
per-GIF):
| GIF | dropped | GIF | dropped |
|---|---|---|---|
| command-center | 16 | chat-rooms | 18 |
| command-center-light | 10 | chat-rooms-light | 9 |
| command-center-gray | 9 | chat-rooms-gray | 10 |
| command-center-ember | 11 | chat-rooms-ember | 10 |
| workflows | 10 | agent-mail | 27 |
| workflows-light | 9 | agent-mail-light | 8 |
| workflows-gray | 16 | agent-mail-gray | 4 |
| workflows-ember | 3 | agent-mail-ember | 4 |
| agent-chat | 3 | | |
Loading-intro length varied widely per recording (3 frames to 27), so
each cut point was determined individually via frame-by-frame inspection
and visually verified.
**Left untouched:** `fusion-reel.gif`, `fusion-company-reel.gif`,
`fusion-mesh.gif` — edited reels that open on designed title cards
("From a rough idea.", "Import a company."), not loading skeletons.
## Technical notes
- Re-encoded with `gifsicle --unoptimize … --optimize=3 --lossy=60`.
`--unoptimize` is required — a naive frame cut leaves the new first
frame as a broken transparency-delta (renders as white garbage).
- `--lossy=60` keeps files at or below original size (a plain
re-optimize bloated them ~30–50%); at 2× zoom it's visually
indistinguishable from lossless and text stays crisp.
- Net total: **40.0 MB → 38.1 MB**.
- Verified frame 0 of all 17 outputs shows clean populated content — no
skeletons, no delta corruption.
No README edits needed (filenames unchanged).
🤖 Generated with [Claude Code](https://claude.com/claude-code)