Commit Graph

11439 Commits

Author SHA1 Message Date
gsxdsm
9d791b3bf1 FN-8009: preserve approved plans through prompt hygiene
Keep manually approved plans idempotent when deterministic prompt hygiene is applied.

- Document normalized fingerprint comparison at the approval gate
- Cover approval reuse after Original Description and Frontend UX injection

Files changed:
 packages/engine/src/__tests__/triage.test.ts | 37 ++++++++++++++++++++++++++++
 packages/engine/src/triage.ts                |  8 ++++++
 2 files changed, 45 insertions(+)

Fusion-Task-Id: FN-8009

Fusion-Task-Lineage: 8474bc13-61e0-421a-8e99-080f99382285

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 21:52:06 -07:00
gsxdsm
a048a619fc fix(core): resolve unbound project ids to a real partition or no filter
Six of the eight postgres-suite failures shared one root cause: writes
normalize project_id, reads did not. The fusion_assign_project_id trigger
(migration 0006) rewrites a blank project_id to the session's fusion.project_id
or '__legacy_unscoped__', but helpers reached as `layer.projectId ?? ""` then
filtered on the literal '' -- a value the database never stores. Every unbound
read missed rows it had just written.

AsyncDataLayer.projectId is optional by design (undefined = project-agnostic),
so `?? ""` is the bug: it turns "no scope" into a scope that matches nothing.

The resolution differs by what the rows are, and conflating them corrupts data:

- Data and analytics reads (usage events, agent runs, research runs) take
  projectScopeFor(): a bound id filters, an unbound one reads across projects.
  This matches the contract taskProjectScope already documents ("when undefined
  the scope filter is a no-op").
- __meta migration guards (project-identity stamps, agent-store markers) take
  projectPartitionId(): an unbound id resolves to the shared sentinel
  partition. projectScopeFor would be wrong here -- dropping the predicate lets
  an unbound getMetaValue return whichever project's marker it finds first, so
  on the shared cluster project A's "migration complete" marker would tell
  project B to skip a migration it never ran. upsertMetaValue already documented
  this: "the empty binding remains the explicit project-agnostic compatibility
  partition". Writing the sentinel explicitly also keeps the partition
  deterministic -- a blank write from a session carrying fusion.project_id would
  otherwise land in that project's stamp.

Names the sentinel (LEGACY_UNSCOPED_PROJECT_ID) instead of open-coding it, and
puts both helpers next to taskProjectScope so the convention has one home.

Fixes taskstore-remaining (24/24), project-identity (6/6), and
satellite-fusiondir-stores (16/16).

The remaining two failures are a different bug and are NOT addressed here: the
child tables research_run_events and experiment_session_records never declared
project_id in schema-as-code, though migration 0006 added the column and
rewrote their FKs to composite (project_id, parent_id). Drizzle therefore cannot
write the parent's partition, the trigger stamps '__legacy_unscoped__', and the
FK fails against a project-owned parent. That needs a schema-as-code change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:47:01 -07:00
gsxdsm
a588c38784 fix(core): read usage events across projects when the layer is unbound
An unbound (project-agnostic) data layer read zero usage events it had just
written. AsyncDataLayer.projectId is optional by design -- undefined means a
project-agnostic layer for single-project / global / analytics reads -- but
helpers taking `projectId: string` are called as `layer.projectId ?? ""`, which
turns "no scope" into a literal '' scope.

'' never matches: the fusion_assign_project_id BEFORE INSERT trigger (migration
0006) rewrites a written '' to the session's fusion.project_id or
'__legacy_unscoped__', so a read filtering on '' looks for a value the database
never stores. Writes normalize, reads did not. Proven by probe: the row is
present with project_id '__legacy_unscoped__', emitUsageEvent returns true, and
queryUsageEvents returns [] even with no other filters.

Treat blank as unbound and drop the scope predicate, matching the contract
taskProjectScope already documents ("when undefined the scope filter is a
no-op"). Restricting an unbound reader to '__legacy_unscoped__' rows instead
would make an unscoped analytics read silently partial.

Adds projectScopeFor() next to taskProjectScope so the convention has one home
rather than a third open-coded variant.

Note the write path is already live: remaining-ops-7.ts emits with
`layer.projectId ?? ""` under backendMode, so unscoped events are accumulating
under the sentinel today. The async reader has no production caller yet, which
is why nothing user-facing broke.

Fixes taskstore-remaining.test.ts (24/24). The remaining failures in that suite
share this root cause but not this resolution -- see the follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:37:44 -07:00
gsxdsm
3dcb62f40f FN-8008: normalize plan approval fingerprints
Keep approval recovery idempotent when deterministic prompt hygiene is injected.

- Normalize plan approval fingerprints around Original Description and Frontend UX sections.
- Preserve re-approval for operator-authored plan changes and cover recovery behavior.
- Document the normalization contract and add a patch changeset.

Files changed:
 .changeset/fn-8008-plan-approval-fingerprint.md   |  7 +++
 docs/workflow-steps.md                            |  2 +-
 packages/core/src/__tests__/plan-approval.test.ts | 53 +++++++++++++++-
 packages/core/src/plan-approval.ts                | 73 ++++++++++++++++++++++-
 packages/engine/src/__tests__/triage.test.ts      | 45 ++++++--------
 packages/engine/src/triage.ts                     | 40 ++-----------
 6 files changed, 153 insertions(+), 67 deletions(-)

Fusion-Task-Id: FN-8008

Fusion-Task-Lineage: 9c0f415d-662a-455a-a4bd-b873307e53bc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 21:33:11 -07:00
gsxdsm
a646ae3f36 FN-8005: normalize Quick Add action icon sizes
Unify the Quick Add primary action cluster around the shared icon-only sizing treatment.

- Apply btn-icon styling and 14px SVG sizing to GitHub, session advisor, priority, and Fast controls.
- Cover primary cluster uniformity across mobile, toggle, and priority states.
- Add a patch changeset for the Quick Add visual fix.

Files changed:
 .changeset/fn-8005-quick-add-icon-size-parity.md   |  7 +++
 .../dashboard/app/components/QuickEntryBox.tsx     | 19 ++++---
 .../components/__tests__/QuickEntryBox.test.tsx    | 63 ++++++++++++++++++++--
 3 files changed, 79 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-8005

Fusion-Task-Lineage: c912fb90-1e49-4c4d-8664-4caf1489f890

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 21:25:51 -07:00
gsxdsm
d1bda3683c fix(core): reap the losing wrapper and stop self-joining on a startup race
Two related leaks on the embedded Postgres startup-race join.

The flagged one: the catch dropped `nonAdminHandle` to null without stopping
it, so a wrapper that onLaunched had already published leaked. The obvious fix
-- call handle.stop() first -- is worse than the leak. stop() runs killAll(),
which resolves its target by reading line 1 of the data dir's postmaster.pid.
On this path that file belongs to the process that WON the race, so stop()
would taskkill the instance we are joining. pg.stop() is the same trap via
pg_ctl -D on the shared dir, which is why settleCancelledStart (it calls both)
cannot be reused here. Added NonAdminServerHandle.stopWrapperOnly(), which
kills only our wrapper pid and its children, and called it before the handle is
dropped. A racing winner is another process's child, so /t cannot reach it.

The one found while making that safe: the catch joined on ANY start failure. A
start that took the lock and then failed later (readiness timeout, non-admin
poll error) reads back its OWN postmaster.pid, so isAlreadyRunning hands back
our own port and we "join" ourselves with ownsProcess=false -- nothing ever
stops it, orphaning a live postmaster for the life of the host. The join now
fires only on a lock-collision error, which is the one failure proving our
postgres refused to start and someone else owns the dir. Every other failure
returns to the existing cancellation/cleanup paths, which stop what they
started. That is also what makes the wrapper-only kill provably safe: on this
path our postgres never took the lock.

Tests: a non-lock failure must propagate even with a postmaster.pid present
(fails without the fix -- the old catch swallowed it and joined), and a lock
collision must still join. Both always-on with a mocked ctor.

Pre-existing and unrelated: taskstore-remaining.test.ts fails identically on a
clean tree with these changes stashed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:11:40 -07:00
gsxdsm
402b3a91fa fix(FN-8004): treat heartbeat soft-delete races as benign instead of stranding agents
A task soft-deleted concurrently with a heartbeat-driven moveTask raised
TaskDeletedError from the engine's own board path, leaving the agent in `error`
with a non-empty lastError and requiring a stop/start cycle to recover.

The race is benign by construction: the task is gone, so the move is a no-op.
The heartbeat now classifies it via isConcurrentSoftDeleteRaceError (matching the
canonical message and serialized/typed forms), keeps the agent active, clears
stale error/recovery state, and emits agent:heartbeat-move-skipped-soft-delete
with ids/counts-only metadata. Concurrent operator pauses are preserved.

Squash-merged by hand from fusion/fn-8004. The engine's AI merge approved this
content twice (squash a3a3cc6a8) but could not land it: main advances every ~8
minutes and each merge cycle took ~10, so every attempt lost to a concurrent
advance and rebuilt. Each cycle also burned a corrective pass on a first-pass
review rejection with no stated reason — the issue #1946 class of bug that this
task's own report cites as a sibling.

Reconciled against #2157, which refactored transient-error-detector.ts: the new
classifier coexists with the extracted transient-error-patterns.ts leaf. Verified
on the merged tree — 123 tests green across FN-8004's suites and #2157's,
engine typecheck clean.

Fusion-Task-Id: FN-8004

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:00:45 -07:00
gsxdsm
08a10bf486 fix(FN-8006): back off and pause Plan Review on provider rate limits
A rate-limited Plan Review re-ran every 30s for hours (~1,900 requests
per 5h window, reviewerFallbackRetryCount observed past 100), which is
the request volume that trips a provider's low-interactivity throttle —
so the retry storm prolonged the very outage it was retrying.

Root cause: runPlanReviewBeforeExecution catches every reviewStep throw
inline to keep triage alive, which converts them all to an UNAVAILABLE
verdict. That laundering had two consequences the earlier fixes missed:
FN-8006 terminalized RetryStormError and the reviewer started throwing
ReviewerProviderError for 429s, but a ReviewerProviderError still landed
in the UNAVAILABLE park — a FIXED 30s nextRecoveryAt with no attempt
counter and no cap. The reviewer's own escalation contract ("escalate so
UsageLimitPauser pauses every lane") held only on the executor path,
because the inline catch hid the error from triage's usage-limit handler
in specifyTask.

- triage: fire usageLimitPauser.onUsageLimitHit for usage-limit reviewer
  failures, so a 429 pauses every lane instead of re-parking one task.
- triage: re-park via computeRecoveryDecision (60s/120s/240s, ±10%
  jitter) and terminalize at MAX_RECOVERY_RETRIES. A reviewer that never
  yields a verdict is a real failure and must surface, not spin.
- triage: clear the borrowed recoveryRetryCount budget on any real
  verdict, so surviving an outage cannot shorten the executor's later
  transient budget.
- core: RetryStormError takes an optional cause, surfaced as
  underlyingError in serializeRetryStormError and folded into the
  message, so a cap no longer masks the real error. recordRetry threads
  it from the reviewer's error path.

Surface enumeration: the park is driven by a thrown provider error, a
thrown generic error, and a plain UNAVAILABLE verdict with no throw.
All three are covered — a repro pinned only to the reported 429 would
leave the other two spinning on the old fixed timer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:53:05 -07:00
gsxdsm
130c70286b fix(core): create the database when joining a racing embedded Postgres
A lifecycle that joins an already-running instance returned a connection URL
before the owner had created the database. The owner calls ensureDatabase()
only after its own start() resolves, but the signals a joiner detects the
instance by -- the runningInstances entry and, decisively, postmaster.pid,
which postgres itself writes -- both appear earlier. A joiner landing in that
window handed back a URL to a database that did not exist and failed at the
caller's first connect.

Reordering the owner's publish does not fix it: isAlreadyRunning falls back to
the pid file, whose timing postgres owns, so the joiner must verify. Both join
paths (preflight and the startup-race catch) now create the database if absent.
Creating from the joiner is safe rather than a second writer -- CREATE DATABASE
is atomic and both sides tolerate the duplicate, so whoever loses treats the
winner's database as its own success.

Verification takes the joined instance's port explicitly. getPort() resolves to
`options.port ?? resolvedPort`, which on a join with an explicitly configured
port is this instance's requested port, not the one being joined.

It is best-effort by contract: isAlreadyRunning joins optimistically without
probing (a stale pid file from a crash still resolves to a port), so a probe
failure logs and returns the URL exactly as before, letting the connection
layer report an unreachable cluster. A hard throw would turn every stale-pid
start into a startup failure.

Duplicate tolerance covers both codes a real cluster produces: 42P04
duplicate_database when the winner committed before our catalog probe, and
23505 unique_violation on pg_database_datname_index when the two CREATEs
collide inside the catalog insert. The concurrent-ensureDatabase test caught
the 23505 arm -- tolerating only 42P04 left the tighter half of the race
throwing.

Tests: a real-process test proving a joiner creates the database the owner has
not (drop-the-database reproduces the window), a real-process concurrent
ensureDatabase race, and an always-on test pinning the best-effort contract for
an unreachable join. All three fail without the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:46:01 -07:00
gsxdsm
71dd191c7c FN-8006: terminalize Plan Review retry storms
Plan Review now fails tasks when reviewer fallback retry limits are exceeded.

- Detect RetryStormError from Plan Review workflow execution
- Serialize the terminal retry error, clear recovery scheduling, and preserve workflow results
- Add retry-storm regression coverage, architecture guidance, and a patch changeset

Files changed:
 .changeset/fn-8006-plan-review-retry-storm.md      |  7 ++++
 docs/architecture.md                               |  2 +-
 packages/engine/src/__tests__/triage-plan-review-unavailable-retry.test.ts | 47 +++++++++++++++++++++-
 packages/engine/src/triage.ts                      | 33 +++++++++++++++
 4 files changed, 87 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8006

Fusion-Task-Lineage: 932e7930-2069-4b0c-9cd1-9db39c2de5a3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 20:37:31 -07:00
gsxdsm
728eb1adaf FN-8003: add planning prompt recovery copy action
Preserve original planning prompts for recovery from stalled or off-track interviews.

- Add Copy prompt actions to active interview and error recovery surfaces.
- Restore original prompts for resumable sessions and provide clipboard feedback.
- Cover prompt copying across active, error, resumed, and absent-prompt states.

Files changed:
 docs/dashboard-guide.md                            |   3 +
 .../dashboard/app/components/PlanningModeModal.css |  33 +++++
 .../dashboard/app/components/PlanningModeModal.tsx | 116 +++++++++++++----
 .../PlanningModeModal.planning-flow.test.tsx       | 139 +++++++++++++++++++++
 4 files changed, 266 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-8003

Fusion-Task-Lineage: 11c0ec9d-290b-4793-9571-002d4b429c5e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 20:32:19 -07:00
gsxdsm
e3f98253cc feat: Quality plugin — Task QA tab, preview servers, tests, and suggested cases (#2127)
## Summary

Adds a bundled **Quality** plugin (`fusion-plugin-quality`) that makes
task QA easier and more visual:

- **Task QA tab** (action-first): preview/test server for the task
worktree, allowlisted test runs, report viewer, screenshots CTA,
suggested test cases, CI handoff
- **Quality hub** (left sidebar): project-wide run history and preset
launches
- Host **task-detail slot context** (`taskId`, worktree, `projectId`) so
plugin tabs can scope correctly
- `superviseSpawn` re-exported on the plugin packaging shim for
published plugins
- Plan: `docs/plans/2026-07-14-001-feat-quality-plugin-plan.md`

## Design constraints

- Does **not** replace the merge gate — advisory orchestration only
- Composes Dev Server process patterns and artifact registry (no second
browser stack)
- Never free-form shell; never port 4040
- Full-suite requires explicit confirm

## Test plan

- [x] `pnpm --filter @fusion-plugin-examples/quality test` (15 tests)
- [x] PluginSlot unit tests still pass
- [ ] Enable Quality plugin in dashboard Settings → Built-in Plugins
- [ ] Open Task Detail → **QA** tab with a worktree; start preview, run
verify:fast, generate suggestions
- [ ] Open left sidebar **Quality** hub and list runs
- [ ] Confirm merge gate / PR checks unchanged

## Residual / follow-up (same plan, later units)

- Deeper hub CI (host route)
- Full browser-verification toggle UX + agent QA sessions (U7/U9/U10)
- Richer screenshots gallery wiring to live artifacts API
- Test plans CRUD polish

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added the Quality plugin with a project Quality hub and task-focused
QA tab.
* Added test runs, reports, preview server controls, suggested test
cases, and run history.
* Added configurable test presets, cancellation, status tracking, and
safe command execution.
* Added experimental-feature controls for enabling Quality
functionality.
* Bundled Quality with the CLI and made it available through the plugin
manager.

* **Documentation**
* Added Quality plugin guidance, terminology, configuration details, and
implementation planning documentation.

* **Bug Fixes**
* Improved process supervision so command failures and shutdown timers
are handled safely.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 20:28:11 -07:00
gsxdsm
1b9c7a7ca2 fix(FN-7575): stop double-commenting when a task is both imported and tracked
A task can carry BOTH linkages at once, pointing two services at ONE issue:
- GitHub: maybeCreateTrackingIssue() ADOPTS a github sourceIssue as
  githubTracking.issue (github-tracking.ts, `source_issue_linked`).
- GitLab: buildGitLabTaskProvenance() always returns sourceIssue AND
  gitlabTracking.item for the same item, so on GitLab EVERY imported task with
  gitlabCommentOnDone on was double-commented.

With comment-on-done enabled the issue-comment service and the tracking-comment
service both posted. Reproduced against the real wiring: two comments on
acme/widgets#42 ("✅ Task FN-1 ... resolved." then "✅ Done — ...").

The issue-comment services now suppress themselves when the tracking service
provably posts to the SAME target, and the tracking comment wins — it carries
commit/branch/PR/files/merged plus the release lines.

Identity, never "both linked": the two may legitimately target DIFFERENT issues
(a tracking issue linked separately from the source issue), which is two comments
on two issues and must keep working. GitHub matches on case-insensitive
owner/repo + number; GitLab is identical by construction because
resolveGitLabTarget() prefers the tracked item.

Both guards mirror the tracking services' `from === to` no-op guard: on a
same-column re-emit the tracking service stays silent, so suppressing there would
drop the only comment rather than dedupe it.

The net split is now disjoint: issue-comment owns "imported but not tracked",
tracking owns "tracked". Suppression is logged (once per completion, not the
high-frequency skip-noise FN-8024 removed) because a custom comment template
silently not rendering on a tracked issue is otherwise unexplainable.

Behavior change, documented in settings-reference.md: githubCommentTemplate /
gitlabCommentTemplate no longer render on a tracked issue.

Tests updated where they encoded the double-post path (they exercised the
services in isolation, so the duplicate was invisible). GitLab fixtures now
distinguish tracked vs imported-not-tracked shapes. Also asserts a PRE-EXISTING
gap left unchanged: resolveGitLabTarget() early-returns on an unresolvable item
and never falls back to sourceMetadata, so neither service comments there.

Verified non-vacuous: the 4 suppression tests fail against the pre-fix source;
the "still posts" tests pass either way by design. Gate green (294/122/63).

Fusion-Task-Id: FN-7575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:24:49 -07:00
gsxdsm
cae7847085 fix(FN-8004): retry ACP provider blips in auto-merge instead of parking failed (#2157)
## What happened

FN-8004's implementation work finished and passed review. The auto-merge
then failed with `Grok ACP turn failed: Internal error` — a ~20 second
provider blip — and the task was parked `status: "failed"` with 8 files
of complete, reviewed work stranded on its branch.

The park is the interesting part: `status: "failed"` is precisely what
tells recovery to stop. So a misclassification here isn't a missed
retry, it's **terminal**. Both recovery paths were disabled by the same
wrong verdict:

- `maybeRetryTransientMerge` (inline, 3 retries w/ backoff) — never
fired once (`mergeTransientRetryCount: 0`).
- `recoverTransientMergeFailures` (self-healing sweep, exists exactly to
rescue parked in-review tasks) — skipped it, gated on the same
classifier.

## Three defects fixed

**1. No AI-provider failure class existed.** The AI merge drives a real
LLM turn, but `classifyTransientMergeError` only modeled git/lease/spawn
faults. Adds `ai-provider-turn-failure`.

**2. ACP dropped the error detail.** `promptAcpSession` rethrew the SDK
error unchanged, discarding the JSON-RPC `code`/`data` — the only
evidence the fault was provider-side. ("Internal error" is just the
standard text for `-32603`.) It now preserves them, keeping the original
as `cause`:

```
Internal error (acp rpc code -32603, retryable)
```

Classification anchors on that envelope, **not** on the bare `"Internal
error"` — matching that unanchored would disguise genuine application
defects as retryable blips. Only provider-fault codes (`-32603`,
`-32000`..`-32003`) are retryable; caller-fault codes
(`-32600`..`-32602`) stay permanent, since retrying just repeats the
failing call.

**3. Sweep/inline asymmetry** (found while tracing; latent and
unreported). The inline gate accepted `isTransientError(msg) ||
classify(msg)`, but the sweep consulted **only** the classifier. So
`ECONNRESET` / `socket hang up` during a merge earned inline retries and
then went **invisible to the sweep** once parked — stranded forever. The
classifier now delegates to `isTransientError`, so both gates agree by
construction.

To keep that delegation from importing the detector's
`usage-limit-detector → logger` chain (the chain FN-5627 split the
classifier out to avoid, which would break
`notification-service.test.ts`'s partial `vi.mock`), the pure predicates
moved to the import-free leaf `transient-error-patterns.ts`, re-exported
from `transient-error-detector.ts`. All 13 exports preserved, verified
programmatically.

## Loosened budgets

Per request, so more self-heals. Both apply **only** to errors already
proven transient; the ceiling and
`merger:transient-failure-budget-exhausted` audit path remain.

| Budget | Before | After |
|---|---|---|
| `MAX_AUTO_MERGE_TRANSIENT_RETRIES` | 3 | 5 (backoff
5s/10s/20s/40s/80s) |
| `MAX_TRANSIENT_MERGE_RECOVERIES` | 2 | 5 |

The bump broke two suites that had hardcoded the old `3`. Rather than
swap in another magic number, both now derive the cap from the constant
so future tuning doesn't re-break them.

## Verification

- `pnpm test:gate` green · `pnpm lint` clean · engine + ACP typecheck
clean · `pnpm verify:fast` PASS (5/5)
- ACP plugin 230 tests green · Grok plugin 64 green · engine
transient/merge suites 136 green
- Regression tests assert the **invariant across every surface** (per
*Fix the Invariant, Not the Repro*), not just the reported Grok string:
both ACP runtime prefixes, all retryable/non-retryable rpc codes, both
SDK error shapes, network delegation, class-ordering, and negative cases
proving bare `"Internal error"` and real defects stay permanent.
- A test caught a genuine bug in my own code mid-review (nested-shape
message shadowing), now fixed.
- `notifier.test.ts > "awaiting approval"` fails — **confirmed
pre-existing on clean main**, unrelated.

## Note

FN-8004's own branch (`fusion/fn-8004`) is still unmerged and its work
looks complete. Once this lands, its merge should be retried separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:14:25 -07:00
gsxdsm
e9537c9e85 docs(core): correct the ensureDatabase comment on the postgres join path
The preflight join carried "// Ensure the database exists on the running
instance" above a line that only builds a URL. No ensureDatabase() call has
ever followed it, so the comment described behavior the code does not have.

Replace it with why the call is absent: a joiner has no cluster of its own to
ensure, the owning process creates the database after its own start(), and
ensureDatabase() would throw here anyway because it requires `this.running` --
which the join path leaves false by design so stop() never reaps an instance
we did not start.

Also records the ordering assumption the path rests on: the owner publishes
runningInstances / writes postmaster.pid before its ensureDatabase() resolves,
so a joiner winning that window fails at the connection layer rather than
silently using a missing database.

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:14:04 -07:00
gsxdsm
8023aa2d08 fix(core): do not rescue a cancelled embedded Postgres start into a success
The startup-race join added in e33039ad0 catches a failed start, re-reads
postmaster.pid, and joins the competing instance. `startServerAsNonAdminUser`
rejects on abort from inside that same try, so a timeout-cancelled non-admin
launch that happened to observe a postmaster.pid would be rescued into a
published joined instance instead of propagating.

That contradicts the cancellation contract the post-start phases enforce a few
lines below (FNXC:PostgresResourceLifecycle 2026-07-14-18:42), which checks the
signal after every delayed phase specifically to stop a late instance before it
can publish running state or registry ownership.

Rethrow when the signal is aborted, restoring the pre-join behavior for that
path. The genuine race (no cancellation) still joins as intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:10:53 -07:00
gsxdsm
e33039ad0f fix(core): join competing postmaster when embedded Postgres startup races
Starting a second Fusion process could fail with `lock file "postmaster.pid"
already exists`. The singleton preflight check and `pg.start()` are not atomic,
so another process can create the lock in between — the loser surfaced the
collision to the TUI as an error instead of simply joining the live instance.

`EmbeddedPostgresLifecycle.start()` now wraps the start path in a try/catch. On
failure it re-reads `postmaster.pid` via `isAlreadyRunning()`; when a live
instance is found it connects to that port with `ownsProcess=false` (so this
process never stops a server it did not start) and logs the race. Failures with
no live instance rethrow unchanged, so genuine startup errors are unaffected.

Regression test lives outside the real-process `embeddedDescribe` block — it uses
a mocked ctor, and nesting it there would skip it under FUSION_EMBEDDED_TEST_SKIP=1
(the gate/CI default), leaving the fix unprotected.

Verified: 35/35 embedded-lifecycle tests pass, core typecheck clean, lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:07:25 -07:00
gsxdsm
0b332816d5 FN-7986: raise plan review replan cap to 8
Allow more automatic Plan Review revisions before escalating tasks for human approval.

- Raise the consecutive REVISE replan cap from 3 to 8.
- Cover the seven- and eight-revision boundaries in triage tests.
- Add a patch changeset describing the revised default.

Files changed:
 .changeset/fn-7986-plan-review-cap.md              |  7 +++++++
 .../triage-plan-review-replan-cap.test.ts          | 23 +++++++++++++---------
 packages/engine/src/triage.ts                      |  8 ++++----
 3 files changed, 25 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-7986

Fusion-Task-Lineage: 3b61f333-be9a-414a-bd27-aabdbb45caa0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:57:34 -07:00
gsxdsm
d4553be678 FN-8029: compact task live-log thinking blocks
Make task live-log thinking blocks more compact while retaining readable expanded reasoning.

- Reduce desktop and mobile thinking container padding.
- Tighten thinking summaries and bodies with tokenized spacing.
- Update CSS regression assertions for compact thinking blocks.

Files changed:
 packages/dashboard/app/components/TaskChatTab.css  | 34 ++++++++++++++++------
 .../app/components/__tests__/TaskChatTab.test.tsx  | 28 +++++++++---------
 2 files changed, 40 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-8029

Fusion-Task-Lineage: fd9a9443-3a01-48ce-b965-227104404a79

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:47:37 -07:00
gsxdsm
753b1bb710 fix(engine): honor graph cancellation at the merge node
The merge node could not observe a graph abort. WorkflowPrimitiveContext
carried no signal, so requestMerge raced the merge only against its own
30-minute GRAPH_MERGE_TIMEOUT_MS using a controller it owned. A hard-cancel
(user cancel, engine restart, pause/resume) aborted the graph controller and
the walk kept sitting inside the merge node for the full timeout. When the
timeout finally fired it aborted the still-running AI merge -- surfacing as
"Manual-merge failed: Request was aborted" -- and the walk reported
value=merge-timeout for a cancellation it had missed half an hour earlier.
An abort landing between merger-ai's `worktree: null` write and
mergeConfirmed then stranded the card as no-worktree-no-merge-confirmed.

Thread the graph AbortSignal from WorkflowNodeExecutionContext (where it
already existed) through primitiveNodeContext/primitiveContextForNode into
the primitives, and honor it on both merge surfaces:

- requestMerge fails fast when the walk is already cancelled, before
  ensureWorkflowMergeBoundaryTask mutates the row or the requester enqueues
  a merge, and links the graph signal into its timeout controller via
  AbortSignal.any -- raced separately so the walk returns on the abort
  rather than waiting on a requester that may never settle.
- The legacy merge seam had the identical unguarded race and gets the same
  treatment.

The timeout stays: it bounds a wedged merge queue, which is a different
failure from cancellation. Both signals must stay live -- dropping either
silently restores the stall with no type error.

Cancellation returns a distinct `merge-cancelled` rather than reusing
merge-timeout. Returning `data.status: "failed"` would let classifyMergeFailure
read the unknown reason as merge-failed and route the cancellation into
bounded auto-merge retry, re-requesting the merge the operator just cancelled.

Regression test covers both merge surfaces, both cancel timings (pre-flight
and mid-flight), the no-signal back-compat path, the signal plumbing itself,
and the classification boundary. Verified by removing the fix: 7 of 9 cases
fail, with the mid-flight cases hanging until timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:43:08 -07:00
gsxdsm
1043e44bc2 FN-8025: stabilize planning retry status tests
Keep retry-state assertions synchronized with the planning stream lifecycle.

- Hold mocked planning streams in the retry loading window
- Await transient retry-status rendering for resumed and sidebar sessions

Files changed:
 .../__tests__/PlanningModeModal.planning-flow.test.tsx  | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8025

Fusion-Task-Lineage: 4cb91dc1-8132-4c33-b8fc-203e335a6ae1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:37:56 -07:00
gsxdsm
b9ad5b0cec docs(FN-7575): correct the FNXC rationale on the issue-comment services
My previous commit's comments claimed GitHubIssueCommentService "effectively
never fires" and was not the surface that posts. That is wrong, and acting on it
would have deleted working, documented behavior.

The issue-comment and tracking-comment services gate on different, disjoint
linkages:
- {GitHub,GitLab}IssueCommentService -> task.sourceIssue, the IMPORT linkage set
  unconditionally by buildGitHubIssueSource, plus the documented
  githubCommentOnDone / gitlabCommentOnDone settings (settings-reference.md:637,
  651 — no Settings UI, but reachable via the settings API/file).
- {GitHub,GitLab}TrackingCommentService -> githubTracking.enabled /
  gitlabTracking.item, the explicit TRACKING linkage.

resolveImportedIssueGithubTracking() returns undefined unless
githubLinkImportedIssuesToTracking or the tracking defaults resolve on, so an
imported issue with tracking off has sourceIssue and NO tracking linkage — the
issue-comment service is then the ONLY surface that comments. Neither service is
redundant; record that so neither is deleted as a "duplicate" later.

Also records the pre-existing overlap: a task carrying BOTH linkages with
comment-on-done enabled receives two comments, one per service. Orthogonal to the
release lines and left undeduped.

Comments only — no behavior change; the 76 comment-surface tests still pass.

Fusion-Task-Id: FN-7575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:29:02 -07:00
gsxdsm
adcba0eed2 FN-8017: add imported-item filter to task import
Add a persisted control that declutters Import Tasks by hiding items already on the board.

- Filter imported GitHub issues, pull requests, and GitLab resources while retaining full imported counts.
- Clear hidden selections, show an all-imported empty state, and preserve the preference per project.
- Document the toggle, add styling, coverage, and a minor changeset.

Files changed:
 .changeset/fn-8017-hide-imported-toggle.md         |   7 ++
 docs/dashboard-guide.md                            |   4 +-
 .../dashboard/app/components/GitHubImportModal.css |  21 ++++
 .../dashboard/app/components/GitHubImportModal.tsx |  70 ++++++++++++-
 .../__tests__/GitHubImportModal.test.tsx           | 108 +++++++++++++++++++++
 packages/dashboard/app/hooks/modalPersistence.ts   |   6 ++
 6 files changed, 209 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-8017

Fusion-Task-Lineage: 5f03fe92-6dce-4de7-a745-0c8a46dc2dbb

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:27:00 -07:00
gsxdsm
5523c19fb4 FN-8023: align ChatView thinking text tokens
Align ChatView thinking-section text with the muted token guard.

- Replace legacy secondary text tokens in thinking labels, controls, and empty states
- Document the ChatView token-guard requirement

Files changed:
 packages/dashboard/app/components/ChatView.css | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8023
Fusion-Task-Lineage: 7f0a6da0-f15d-4dc8-a935-308a11ee9f14
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:21:20 -07:00
gsxdsm
71275abffe fix(FN-7575): post release version lines on the surface that actually comments
FN-7575 (issue #1916) added "Current version:" / "Target release:" lines to
GitHubIssueCommentService, but that service is gated on `githubCommentOnDone`
— default false, with no Settings UI — so it effectively never fires. The
"✅ Done —" comments on linked issues are posted by GitHubTrackingCommentService,
which had no version logic. The lines were invisible in production for ~10 days;
issue #1916's own close comment is the proof.

Extract the self-repo check and next-minor computation into a shared
fusion-release-version.ts and apply it across all four done-comment surfaces
(GitHub/GitLab x tracking/issue) so they cannot drift again.

- Release lines join `optionalLines` rather than being appended to the finished
  string, so they count against DONE_COMMENT_MAX_LENGTH and shrink the title
  budget; appending would silently blow the cap on long titles.
- Version resolution is a lazy resolver, so getCliPackageVersion()'s filesystem
  walk only runs for self-repo comments.
- GitLab self-repo matching uses item.projectPath: resolveGitLabTargetFromItem()
  prefers the numeric projectId, which never matches the slug.
- Non-self repos stay byte-for-byte unchanged (asserted).

Per the Surface Enumeration rule, regression tests assert the invariant across
every done-comment surface — both the pure formatters and the services that
post — plus case-insensitive slug matching (issue #1916 is "Runfusion/Fusion"),
in-progress transitions, the 0.0.0 sentinel, unparseable versions, the
lazy-resolution guarantee, and the truncation ladder under the length cap.
Verified non-vacuous: 10 of the new tests fail against the pre-fix source.

Fusion-Task-Id: FN-7575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:07:51 -07:00
gsxdsm
7cec078054 FN-8016: scope task popups to their opening view
Scope task-detail popups to their origin dashboard view by default.

- Default per-view popup scoping while retaining a legacy global-popup opt-out.
- Key popup lifecycle, navigation, and Escape dismissal by task and origin view.
- Update settings copy, documentation, localization, and regression coverage.

Files changed:
 .changeset/fn-8016-task-popup-view-scoping.md      |   7 ++
 docs/dashboard-guide.md                            |   4 +-
 .../core/src/__tests__/settings-defaults.test.ts   |   4 +-
 packages/core/src/settings-schema.ts               |   6 +-
 packages/core/src/types.ts                         |   6 +-
 packages/dashboard/app/App.tsx                     |  67 ++++++-----
 .../app/__tests__/App.keyboard-shortcuts.test.tsx  |  14 ++-
 .../app/__tests__/App.taskPopupViewGating.test.tsx | 125 +++++++--------------
 .../dashboard/app/components/SettingsModal.tsx     |   2 +-
 .../settings/sections/AppearanceSection.tsx        |   6 +-
 .../sections/__tests__/AppearanceSection.test.tsx  |  18 ++-
 .../app/hooks/__tests__/useAppSettings.test.ts     |  15 +++
 .../app/hooks/__tests__/usePoppedOutTasks.test.ts  |  28 ++---
 packages/dashboard/app/hooks/useAppSettings.ts     |   8 +-
 packages/dashboard/app/hooks/usePoppedOutTasks.ts  |  14 +--
 packages/i18n/locales/en/app.json                  |   4 +-
 packages/i18n/src/resources.d.ts                   |   4 +-
 17 files changed, 158 insertions(+), 174 deletions(-)

Fusion-Task-Id: FN-8016

Fusion-Task-Lineage: e33beeae-0ce3-4202-95dc-6fb2d26f9770

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 19:03:44 -07:00
gsxdsm
66ae82af5b FN-8007: align concurrency current-use markers
Align dashboard and footer concurrency markers with their native range thumbs.

- Map running counts in min-relative slider coordinates and clamp them to the configured cap
- Standardize native slider thumb dimensions and marker geometry across browsers
- Add dashboard coverage and document the marker behavior

Files changed:
 .changeset/fn-8007-concurrency-dot-alignment.md    |   7 +
 docs/dashboard-guide.md                            |   8 +-
 .../dashboard/app/components/EngineControlMenu.css |  20 ++-
 .../dashboard/app/components/EngineControlMenu.tsx |  19 ++-
 .../__tests__/EngineControlMenu.test.tsx           |  96 +++++-------
 .../command-center/CommandCenterControls.css       |  22 ++-
 .../command-center/CommandCenterControls.tsx       |  19 ++-
 .../__tests__/CommandCenterControls.test.tsx       | 164 +++++++++++++++++++++
 8 files changed, 277 insertions(+), 78 deletions(-)

Fusion-Task-Id: FN-8007

Fusion-Task-Lineage: 9ad8ee0b-09da-413e-96bc-530c897cb32e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 18:43:37 -07:00
gsxdsm
05e499b019 fix(dashboard): make import auto-translate settings findable, native, and non-blocking (#2147)
Follow-up to #2141 (merged). Three operator-reported problems with the
shipped import auto-translate feature, plus a real bug found while
testing them.

## 1. The settings were unfindable

> "where are the translate settings? I can't find them and search in
settings isn't finding them"

The controls rendered fine — but Settings **search** only matches
curated keywords and advertised i18n keys per section, and "Project
General" says nothing about translation. So searching `translate`
matched nothing.

Now advertised on **Project General** (where the controls live) and on
**Project/Global Models** (where the lane is picked). Verified against
the real `filterSettingsSectionsForSearch`, not by eye:

| query | surfaces |
|---|---|
| `translate` / `translation` | general, project-models, global-models |
| `auto translate`, `target language` | general |

For reference, they live in **Settings → Project General**, directly
below "Always link imported GitHub issues to GitHub tracking".

## 2. The checkbox looked foreign

> "the auto translate checkbox needs to be the left of the text and it
needs to be styled like other check boxes"

It used `SettingsToggleRow`, which renders a **right-aligned toggle
switch**, while every other GitHub/import setting in that section uses a
plain `checkbox-label` with the input **before** its text. Two checkbox
idioms in one section read as a bug regardless of which is nicer in
isolation.

Both controls now use the section's native `form-group` +
`checkbox-label` / `select.select` markup. A test asserts my checkbox's
class and structure are **identical to the neighbouring
`githubLinkImportedIssuesToTracking` checkbox**, so it can't silently
drift back.

## 3. Auto-translation was async but not incremental

> "ensure the auto translate is non blocking and runs async in the
background"

The list never blocked (it rendered originals immediately; import is
cache-read only). But a **single request translated all 50 issues**, so
nothing appeared until every issue finished — minutes on a large page —
and one timeout discarded the whole page's work.

It now streams in chunks of 8: titles appear as each chunk lands, a
failure costs one chunk instead of the page, and chunks are sequential
so opening the panel can't fan 50 model calls at the provider at once.

## Also: a real infinite-render loop (found by testing #3)

`items` and `eligible` are fresh **array identities** on most renders,
and both sat in the effect's dependency list — effect → `setState` →
re-render → new array → effect. It manifested as a **heap OOM** under
`renderHook`.

Effect dependencies are now string/scalar only, with live issue data
read from a ref, and the reset path preserves state identity so it
cannot re-trigger itself. This bug shipped in #2141; it needed a
re-render with a fresh `issues` identity to trigger, but it was live.

## Changeset

Folded into the **existing unreleased** `github-import-auto-translate`
changeset rather than adding a second one for the same unshipped
feature.

## Verification

- ✅ `pnpm lint`, root `pnpm typecheck`, `pnpm verify:fast`, `pnpm
test:gate` (479 tests)
- ✅ 54 tests across the translate suites, including new coverage pinning
the checkbox idiom + neighbour parity, and that translations **stream
per chunk** rather than all-or-nothing (a regression to one request
fails these)

## Reviewer note

Worth knowing for future test-writing here: `beforeEach(() =>
mock.mockReset())` **implicitly returns the mock**, and vitest treats a
function returned from `beforeEach` as a teardown callback — it then
invokes the mock with zero arguments and corrupts `mock.calls`. The test
file uses a block body and says why.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added project settings to enable GitHub import auto-translation and
choose a target language, including “follow dashboard language”.
* Auto-translation now runs in the background, chunked, and streams
translated issue content progressively into imported tasks.
* Translations are cached and reused to speed up repeated imports, with
incremental updates shown as they arrive.
* Settings search now includes translation and import auto-translation
terms.

* **Bug Fixes**
* Improved update handling so changes to issue text refresh
translations, while closed issues are never requested and failures don’t
erase already translated results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:08:32 -07:00
gsxdsm
3f426c3ef0 fix: isolate mDNS node broadcasts (#2155)
## Summary

- Make Fusion mDNS broadcast names node-unique to avoid same-name DNS-SD
collisions.
- Treat asynchronous Bonjour broadcast errors as non-fatal diagnostics
when no listener is registered.
- Add regression coverage for a service-name collision.

## Validation

- `pnpm --filter @fusion/core exec vitest run
src/__tests__/node-discovery.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/core typecheck`
2026-07-15 18:08:07 -07:00
Phil Larson
514ccd304c Recover malformed agent interview responses (#2146)
## Summary
- preserve valid onboarding JSON returned in Pi thinking-only assistant
blocks
- retry one bounded JSON-only reformat turn when the model returns prose
or malformed output
- keep streamed output as a final extraction fallback instead of
overwriting it with an empty content array

## Verification
- `pnpm --filter @fusion/dashboard exec vitest run
src/__tests__/agent-onboarding.test.ts` — 20 passed
- `pnpm --filter @fusion/dashboard typecheck`
- `pnpm lint`
- `pnpm check:changesets --strict`
- live local-runtime AI Interview produced a structured
Hermes/computer-use onboarding question after restart

Follow-up to #2142, which fixed the missing planning-model fallback and
runtime-hint prompt.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved agent onboarding recovery when assistant replies include
thinking-only content or malformed JSON.
* Added a single automatic retry that re-formats invalid output into
valid onboarding JSON.
* Preserved structured “thinking” content as part of valid onboarding
responses.
* Normalized optional onboarding fields so null/empty/whitespace-only
values are treated as missing.
* Tightened Hermes automation so the runtime hint is set exactly to
`hermes`.
* **Tests**
* Added onboarding event synchronization and expanded coverage for
recovery and field normalization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 18:07:37 -07:00
gsxdsm
40ae6ddb3a fix(FN-8024): stop logging skipped stale triage recovery writes
Skipping a stale planning-state write is the expected outcome of a normal
scheduler advancement, not an anomaly, so the warn was pure log noise.
Behavior is unchanged; only the two planLog.warn emissions are removed.

Fusion-Task-Id: FN-8024

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:35:32 -07:00
gsxdsm
7daa16fe30 FN-8002: suppress reconnecting hint on planning question screen
Gate Planning Mode's "Reconnecting…" indicator to the loading view so persisted awaiting-input questions stay free of transient SSE reconnect noise.

- Show planning.reconnecting only when view.type is "loading"
- Cover desktop and mobile resumed question screens without the hint
- Keep the hint during active generation loading reconnects
- Add patch changeset for the user-facing fix

Files changed:
 .changeset/FN-8002-planning-reconnecting-hint.md   |  7 +++
 .../dashboard/app/components/PlanningModeModal.tsx |  6 +-
 .../PlanningModeModal.planning-flow.test.tsx       | 73 ++++++++++++++++++++++
 3 files changed, 85 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8002

Fusion-Task-Lineage: 0a8682ee-922d-4acd-b7ce-bbf4f25dfce7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 17:12:32 -07:00
gsxdsm
22fde62510 FN-8001: open footer planning sessions in Planning view
Navigate Background Tasks footer planning rows into the embedded Planning view so resume actually loads planning mode.

- Call handleChangeTaskView("planning") when opening a background planning session
- Extend App tests for footer planning resume and unchanged non-planning session routes
- Update dashboard-guide planning resume entry-point docs

Files changed:
 docs/dashboard-guide.md                            |  4 +-
 packages/dashboard/app/App.tsx                     |  5 ++
 .../app/components/__tests__/App.test.tsx          | 94 +++++++++++++++++++---
 3 files changed, 91 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-8001

Fusion-Task-Lineage: 402ece21-5f29-4304-a3aa-ef7004a30155

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:58:56 -07:00
gsxdsm
0b06026c74 FN-7992: open GitHub import issue/PR details in FloatingWindow
Show GitHub/GitLab import item details in a draggable FloatingWindow instead of an embedded two-pane preview, simplifying the import modal layout.

- Replace inline list/preview split with FloatingWindow for issue and PR detail
- Remove two-pane resize handle, mobile list/preview switch, and related CSS
- Keep close confirmation when discarding detail-window changes
- Update FloatingWindow styles and dashboard guide for floating import details
- Slim GitHubImportModal tests while restoring core import-modal coverage

Files changed:
 docs/dashboard-guide.md                            |   6 +-
 .../dashboard/app/components/FloatingWindow.css    |  23 +-
 .../dashboard/app/components/GitHubImportModal.css | 359 +-------
 .../dashboard/app/components/GitHubImportModal.tsx | 347 ++------
 .../components/__tests__/FloatingWindow.test.tsx   |   2 +-
 .../__tests__/GitHubImportModal.test.tsx           | 909 ++-------------------
 6 files changed, 139 insertions(+), 1507 deletions(-)

Fusion-Task-Id: FN-7992

Fusion-Task-Lineage: 0991e28c-d793-4a41-9312-6e250e8a09c4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:51:34 -07:00
gsxdsm
c0bef0bfbe FN-7969: deprecate unused builtin Coding (Ideas) workflow
Hide builtin:coding-ideas from new selection after occupancy preflight, while keeping it resolvable for any existing task selections.

- Add builtin:coding-ideas to DEPRECATED_BUILTIN_WORKFLOW_IDS so it is excluded from defaultEnabledBuiltinWorkflowIds and listWorkflowDefinitions selection listings
- Keep getBuiltinWorkflow / direct resolution working for pre-existing Coding (Ideas) task selections
- Document deprecation and custom-workflow copy path in dashboard-guide and workflow-steps
- Extend builtin-workflows and settings-sections tests for hide-from-selection + management/resolution retention
- Add minor changeset for @runfusion/fusion

Files changed:
 .changeset/fn-7969-deprecate-coding-ideas.md       |  7 +++++++
 docs/dashboard-guide.md                            |  2 +-
 docs/workflow-steps.md                             |  2 +-
 .../core/src/__tests__/builtin-workflows.test.ts   | 28 ++++++++++++++--------
 packages/core/src/builtin-workflows.ts             |  9 +++----
 packages/core/src/types.ts                         |  9 ++++---
 .../app/__tests__/settings-sections.test.tsx       |  2 ++
 7 files changed, 43 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-7969

Fusion-Task-Lineage: 578ae727-e1b6-4ff9-a3a2-d1228c50fba6

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:47:01 -07:00
gsxdsm
f0485041c1 FN-8000: open mobile chat threads before streaming back-btn assertions
Fix ChatView mobile streaming tests that failed when chat-back-btn was missing after remount with an active session restored by useChat.

- Open the session via sidebar click before asserting chat-back-btn in streaming mobile tests
- Populate sessions/filteredSessions fixtures for the silent-request mobile case
- Assert empty-state copy stays hidden once the thread is open
- Document remount/sidebar restore requirement with FNXC comment

Files changed:
 packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8000

Fusion-Task-Lineage: 8893884c-b5ed-4c5a-8962-31688b859b9a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:44:00 -07:00
gsxdsm
96b1f21707 FN-7999: show failed-banner diagnostics and model/node retry
Expose a richer Task Failed banner with tool-error diagnostics and one-click retry using a different model or node.

- Always show the failed banner for failed tasks (including errorless failures) with a generic reason fallback
- Surface the latest agent-log tool_error detail and a retry hint for workflow/step-execute failures
- Add Retry and Retry with a different model/node actions with deferred model/node override save on confirm
- Style the banner recovery controls and cover them in TaskDetailModal tests
- Add minor changeset for @runfusion/fusion

Files changed:
 .changeset/fn-7999-failed-banner-retry.md          |   7 +
 .../dashboard/app/components/TaskDetailModal.css   |  47 +++++++
 .../dashboard/app/components/TaskDetailModal.tsx   | 143 ++++++++++++++++++++-
 .../__tests__/TaskDetailModal.test-helpers.ts      |   3 +-
 .../components/__tests__/TaskDetailModal.test.tsx  |  59 ++++++++-
 5 files changed, 248 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7999

Fusion-Task-Lineage: d4268c43-442f-4f39-afbe-c6f583ec0fc0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:35:26 -07:00
gsxdsm
0e84731d8a fix(FN-7965): let the overseer see executor-stage failures
`deriveSignalAndSources`'s executor branch never read `task.status`, so a row
parked `status: "failed"` — e.g. the terminal fn_task_done refusal/invariant
park — reported `signal: "progressing"` with the reason "Task is actively
executing in-progress work". The overseer observed a dead task as healthy and
took no action. `failed` was only ever derived for the merger/pull-request
stages, so the sole backstop was the FN-7743 2h stall proxy firing hours later.
This is exactly what FN-7965's audit trail shows: every intervention on a
terminally-parked task was action="observe", reason="Task is actively executing
in-progress work".

Report `failed` so recovery engages on the next poll. This adds no new policy:
a failed executor observation already routes to `retry_step` (executor sources
are `agent-log`, never an ERROR_SOURCE_KIND), bounded by
PLANNER_RECOVERY_MAX_ATTEMPTS and escalated on exhaustion.

Precedence and dedup preserved: `paused` still wins, so an operator/user-paused
row stays `blocked` and is never routed into autonomous recovery; and the reason
is a constant (never interpolating task.error/status) so the FN-7577
`stage|signal|reason` feed dedup still suppresses repeat observations.

Verified: the repro test fails with the branch disabled; paused-precedence,
healthy-card (FN-7577) and dedup guards added; overseer/recovery surfaces
93 passed + core planner-recovery 20/20; engine + dashboard typecheck clean;
`pnpm test:gate` green (294+122+63).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:32:17 -07:00
gsxdsm
214af98591 FN-7977: hold Plan Review provider failures without replan regression
Prevent provider, model, transport, and abort failures from bouncing tasks back to planning after they enter execution.

- Classify non-plan-defect Plan Review failures and skip needs-replan handoff
- Terminate graph traversal with plan-review-provider-failure-hold and retry in place
- Guard triage recovery so advanced column/worktree/step state is never overwritten
- Document planning-recovery no-regression invariant and add regression tests
- Add patch changeset for the operator-facing fix

Files changed:
 .changeset/fn-7977-planning-failure-no-regression.md |   7 ++
 docs/architecture.md                               |   1 +
 docs/workflow-steps.md                             |   2 +-
 packages/engine/src/__tests__/replan-target.test.ts     |  17 +++-
 packages/engine/src/__tests__/transient-error-detector.test.ts |  32 +++++-
 packages/engine/src/__tests__/triage.test.ts       | 110 +++++++++++++++++++++
 packages/engine/src/__tests__/workflow-graph-optional-group.test.ts          |  46 ++++++++-
 packages/engine/src/__tests__/workflow-graph-optional-step-fix.test.ts       |  36 +++++++
 packages/engine/src/executor.ts                    |  62 +++++++++++-
 packages/engine/src/replan-target.ts               |  22 +++++
 packages/engine/src/transient-error-detector.ts    |  37 +++++++
 packages/engine/src/triage.ts                      |  73 +++++++++++---
 packages/engine/src/workflow-graph-executor.ts     |  45 ++++++++-
 13 files changed, 466 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-7977

Fusion-Task-Lineage: 6d62d3ca-c6f3-4d02-a377-d7fd59f0c0f9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:29:10 -07:00
gsxdsm
1c02e683b7 FN-7970: deprecate unused builtin:brainstorming from new selection
Hide the built-in Brainstorming workflow from new selection after occupancy preflight, while keeping it resolvable for existing tasks.

- Add DEPRECATED_BUILTIN_WORKFLOW_IDS and isBuiltinWorkflowDeprecated helper
- Exclude deprecated built-ins from defaults and selection listings
- Hide deprecated built-ins from Settings workflow enablement toggles
- Update docs/tests and add a minor changeset for the operator-facing change

Files changed:
 .changeset/fn-7970-deprecate-brainstorming.md      |  7 ++++
 docs/workflow-steps.md                             |  2 +-
 .../core/src/__tests__/builtin-workflows.test.ts   | 40 ++++++++++++----------
 packages/core/src/builtin-workflows.ts             | 17 ++++++++-
 packages/core/src/index.gate.ts                    |  2 ++
 packages/core/src/index.ts                         |  2 ++
 packages/core/src/task-store/remaining-ops-8.ts    | 10 ++++--
 packages/core/src/types.ts                         |  9 +++++
 .../app/__tests__/settings-sections.test.tsx       | 28 ++++++++++++++-
 .../settings/sections/GeneralSection.tsx           |  9 +++--
 10 files changed, 101 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-7970

Fusion-Task-Lineage: 47f9cd6e-d843-4c14-b197-447ff2072e3b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:24:23 -07:00
gsxdsm
92a5587797 fix(FN-7987): restore chat.test.ts engine mock completeness
FN-7987 added the shared fusion toolset imports to chat.ts without extending
chat.test.ts's hardcoded `vi.mock("@fusion/engine")` factory, which red-lit
`check-mock-completeness` and blocked the whole merge gate on main.

Add the 11 missing exports with shapes matching their call sites: the singular
`create*Tool` factories return one tool each, while the plural factories are
spread (and `createMemoryTools(...)` is `.filter`ed by `tool.name`), so they
return arrays.

Test-only; no changeset. Verified: both mock-completeness checks green,
chat.test.ts 14/14, and `pnpm test:gate` now passes end-to-end (294+122+63).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:21:10 -07:00
gsxdsm
f1b528f4c5 fix(FN-7965): honor terminal fn_task_done park instead of resurrecting the session
The in-session `fn_task_done` handler parks a task terminally (status=failed,
worktree/branch/sessionFile cleared) once its refusal/invariant retry budget is
exhausted. That write happens inside the live agent session, so the executor's
no-fn_task_done retry loop never observed it and spawned a fresh session anyway.
The retry completed, marked the task done, and dragged a worktree-less row into
the pre-merge graph, where the first write-capable node failed on
`no-worktree-for-write-node` — surfacing as a misleading "Workflow graph
terminated with failure at node 'code-review-remediation'" instead of the real
refusal. Observed on FN-7965 and again live on FN-7981.

Re-read state at the top of the retry loop and honor the park. The status probe
covers all three park sites (invariant-check, explicit refusal, implicit
refusal) rather than the single reported repro.

Deliberately not routed through the FN-4806 reclaim branch: its silent todo
requeue would clear the park and, with the budget already spent, re-park on the
next pickup in a todo->execute->park loop.

The pre-existing reclaim probes could not catch this — they test
`worktree === null`, but the store maps a cleared column to `undefined`
(`task-store/serialization.ts`: `row.worktree || undefined`), so the existing
test only passed because its mock returned a value production never emits.
Tightening that probe regressed 7 fixtures and is left as separate work.

Verified: new tests fail with the guard disabled; engine reliability surfaces
show zero regressions vs baseline (17 pre-existing failures unchanged, 495->499
passing); engine-core gate suite 294/294.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:21:03 -07:00
gsxdsm
363916926d FN-7995: always persist tool_error detail for Activity feed diagnosis
Always persist bounded tool_error detail so the task Activity feed can surface underlying failure messages even when verbose tool-output persistence is off.

- Keep tool args and successful tool_result detail opt-in via persistAgentToolOutput
- Always include bounded tool_error detail in agent-log JSONL rows
- Document diagnostic retention in types, agent-logger, and storage docs
- Cover Activity reveal behavior and logger persistence with unit tests
- Add patch changeset for operator-facing Activity error detail fix

Files changed:
 .changeset/fn-7995-tool-error-detail.md            |  7 ++++
 docs/storage.md                                    |  1 +
 packages/core/src/agent-log-constants.ts           |  4 +++
 packages/core/src/types.ts                         | 10 ++++--
 .../app/components/__tests__/TaskChatTab.test.tsx  | 42 ++++++++++++++++++++++
 packages/engine/src/__tests__/agent-logger.test.ts | 41 ++++++++++++++++++---
 packages/engine/src/agent-logger.ts                |  9 ++---
 7 files changed, 104 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-7995

Fusion-Task-Lineage: 0fa063df-58b1-4991-a0d9-e8a77181d32a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 16:17:37 -07:00
gsxdsm
3d658059cb FN-7979: add route regressions for translated GitHub issue import
Lock GitHub import routes so tasks use cached translated title/body with source URL appended, and fail open to original prose when translation is off, missing, or for closed issues.

- Mock getImportTranslation on the route test store
- Cover single-issue and batch import happy paths for cached translations
- Assert fail-open when auto-translate is off, cache misses, or issue is closed
- Prefer persistent getSettings mocks so translation settings survive multi-call import flows
- Document CLI/GitLab exclusion in FNXC surface note

Files changed:
 .../dashboard/src/__tests__/routes-github.test.ts  | 216 ++++++++++++++++++++-
 1 file changed, 214 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-7979

Fusion-Task-Lineage: 7b0fe0a5-44f3-4131-b535-6b4c7602b5a8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:58:55 -07:00
gsxdsm
e83116a970 FN-7991: mark import-screen items as imported immediately
Mark successful GitHub/GitLab import rows as Imported right away via optimistic local URL state, without waiting for the parent tasks prop round-trip.

- Add optimisticImportedUrls unioned with tasks-derived importedUrls via isUrlImported
- Populate on successful GitHub issue/PR and GitLab imports; clear on modal reset and source change
- Disable re-import and show Imported badge on rows, counts, and import buttons for optimistic URLs
- Cover optimistic import surfaces in GitHubImportModal tests
- Document the behavior in the dashboard guide and add a patch changeset

Files changed:
 .changeset/fn-7991-import-screen-optimistic-imported.md   |  7 +++
 docs/dashboard-guide.md                                    |  2 +-
 packages/dashboard/app/components/GitHubImportModal.tsx    | 57 +++++++++++++++++-----
 packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx | 56 ++++++++++++++++++---
 4 files changed, 103 insertions(+), 19 deletions(-)

Fusion-Task-Id: FN-7991

Fusion-Task-Lineage: ddfb249a-e2e8-4723-a86d-7f6edc74305c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:51:54 -07:00
gsxdsm
80f202831d FN-7994: keep planning session sidebar populated during load
Speed up Planning mode session-list load so the sidebar never blanks while history refreshes.

- Seed the planning sidebar from already-loaded active sessions via initialSessions
- Filter GET /ai-sessions and store listAll by optional type=planning to skip non-planning payloads
- Show skeleton rows while the first authoritative session refresh is in flight
- Wire type through client fetchAiSessions, dashboard AiSessionStore, and core listAllAiSessions
- Add UI and route coverage for seeded/skeleton load and type-filtered listing
- Ship patch changeset for the operator-facing performance fix

Files changed:
 .changeset/FN-7994-planning-sidebar-fast-load.md   |  7 +++
 packages/core/src/async-ai-session-store.ts        |  7 ++-
 packages/dashboard/app/App.tsx                     |  1 +
 packages/dashboard/app/api/legacy.ts               |  3 +-
 .../dashboard/app/components/PlanningModeModal.css | 48 ++++++++++++----
 .../dashboard/app/components/PlanningModeModal.tsx | 27 ++++++++-
 .../PlanningModeModal.planning-flow.test.tsx       | 65 ++++++++++++++++++++++
 .../app/components/dashboard/MainContent.tsx       |  2 +
 .../dashboard/app/components/dashboard/types.ts    |  2 +
 .../src/__tests__/routes-planning.test.ts          | 29 ++++++++++
 packages/dashboard/src/ai-session-store.ts         |  2 +-
 packages/dashboard/src/routes.ts                   | 16 +++++-
 12 files changed, 201 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-7994

Fusion-Task-Lineage: 7c4cf98d-6dfe-4b9b-bc88-62257ed39507

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:49:18 -07:00
gsxdsm
dc7bb40948 FN-7990: share worktree classifier so Code Review acquires a worktree
Unify write-capability classification so graph preparation acquires a worktree for inline-fix Code Review before runtime runs, eliminating the immediate no-worktree-for-write-node failure.

- Add shared workflowNodeRequiresWorktree helper for preparation and runtime
- Plumb optional-group context and reviewerInlineFixes into graph preparation
- Acquire/reuse/reacquire worktrees for write-capable inline review nodes
- Keep Plan Review and disabled inline fixes read-only
- Add regression tests and a patch changeset

Files changed:
 .changeset/fn-7990-code-review-worktree.md         |  7 ++
 .../__tests__/ce-workflow-step-executor.test.ts    | 97 ++++++++++++++++++++++
 .../workflow-node-execution-needs.test.ts          | 47 +++++++++++
 packages/engine/src/executor.ts                    | 32 +++----
 packages/engine/src/workflow-graph-executor.ts     | 52 ++++++++----
 .../engine/src/workflow-node-execution-needs.ts    | 46 ++++++++++
 6 files changed, 243 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-7990

Fusion-Task-Lineage: f5d19181-0b98-4827-8adb-069f7dc05c03

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:34:43 -07:00
gsxdsm
5ff7a20738 FN-7976: fix mailbox artifact open and view-task popups
Fix Mailbox/Artifacts media auth and ensure View task always opens a usable popup.

- Add artifactMediaUrlWithToken for authenticated img/video/audio/link loads while keeping artifactMediaUrl token-free for fetch and HTML previews
- Load script-capable HTML artifact previews via Authorization + revocable blob URL so tokens never reach allow-scripts iframes
- Keep non-board/list task popups (Mailbox, Documents) visible even when board/list-only popup gating is enabled
- Upgrade duplicate popOut entries so reopening a task refreshes snapshot and origin
- Document the behavior and add a patch changeset

Files changed:
 .changeset/fn-7976-mailbox-artifact-fixes.md       |  7 +++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/App.tsx                     | 15 +++--
 .../app/__tests__/App.taskPopupViewGating.test.tsx | 10 ++-
 .../dashboard/app/__tests__/api-artifacts.test.ts  | 12 +++-
 .../api/__tests__/legacy-artifact-media.test.ts    | 27 ++++++++
 packages/dashboard/app/api/legacy.ts               | 21 +++++--
 .../dashboard/app/components/ArtifactsGallery.tsx  | 72 ++++++++++++++++++----
 .../dashboard/app/components/DocumentsView.tsx     |  4 +-
 .../app/components/MailboxArtifactAttachment.tsx   |  6 +-
 .../dashboard/app/components/TaskDocumentsTab.tsx  |  6 +-
 .../components/__tests__/DocumentsView.test.tsx    | 31 ++++++----
 .../__tests__/MailboxArtifactAttachment.test.tsx   | 24 ++++----
 .../app/components/__tests__/MailboxView.test.tsx  |  8 +--
 .../components/__tests__/TaskDocumentsTab.test.tsx | 16 ++---
 .../app/hooks/__tests__/usePoppedOutTasks.test.ts  |  9 ++-
 packages/dashboard/app/hooks/usePoppedOutTasks.ts  | 17 +++--
 17 files changed, 206 insertions(+), 81 deletions(-)

Fusion-Task-Id: FN-7976

Fusion-Task-Lineage: 4c25b3a6-5836-4629-b33e-647f213e3261

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:30:55 -07:00
gsxdsm
667f4c8a55 FN-7987: expose shared fusion toolset to chat agents and Grok CLI
Give dashboard chat and room responders the same safe coordination/productivity tools as other agent lanes, including via the Grok MCP bridge.

- Export chat coordination tool factories from @fusion/engine for public use
- Assemble createChatFusionToolset with board, delegation, web, goal, memory, and research tools
- Wire the shared toolset into model-loop chat and room-responder sessions
- Exclude destructive agent-lifecycle tools and fn_memory_append from chat
- Cover chat fusion parity and Grok bridge tool preservation with tests
- Document chat Grok tool parity and add a minor changeset

Files changed:
 .changeset/fn-7987-chat-fusion-toolset.md          |  7 ++
 docs/agents.md                                     |  1 +
 docs/grok-cli-contract.md                          |  2 +-
 packages/dashboard/src/__tests__/chat-manager.test.ts | 52 +++++++++++-
 packages/dashboard/src/chat.ts                     | 95 +++++++++++++++++++++-
 packages/engine/src/__tests__/agent-session-helpers.test.ts | 15 ++++
 packages/engine/src/index.ts                       | 26 ++++++
 plugins/fusion-plugin-grok-runtime/src/__tests__/tool-bridge.test.ts | 36 ++++++++
 8 files changed, 230 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-7987

Fusion-Task-Lineage: 4d8d3dbc-10b8-4b56-9b63-79fe85a13755

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:25:39 -07:00
gsxdsm
d9843f75bc FN-7983: colocate project summarization model with summarization settings
Move the Project Summarization model lane and title-summarizer fallback next to the AI title/commit summarization controls in Project Models.

- Extract shared project-lane renderer and keep default/merger/import-translate in the general Model Lanes list
- Render summarization + title-summarizer fallback inside the AI summarization section with the same models-available guard
- Add regression tests for colocation and empty-models guard
- Update settings reference docs and add a patch changeset

Files changed:
 .changeset/fn-7983-summarization-lane-colocation.md       |   7 ++
 docs/settings-reference.md                         |   6 +-
 .../app/__tests__/settings-sections.test.tsx       |  61 ++++++++++
 .../settings/sections/ProjectModelsSection.tsx     | 126 ++++++++++++---------
 4 files changed, 142 insertions(+), 58 deletions(-)

Fusion-Task-Id: FN-7983

Fusion-Task-Lineage: c473ba61-f003-401d-bc66-86f2078ba047

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 15:21:16 -07:00