Commit Graph

12293 Commits

Author SHA1 Message Date
gsxdsm
d3002d1453 FN-8617: restore authoritative dashboard modal inventory
Restore the committed source of truth for dashboard modal migration coverage.

- Add the evidence-backed inventory of dashboard modal classifications and migration ownership.
- Link the dashboard guide to the canonical modal inventory.

Files changed:
 docs/dashboard-guide.md           |   4 ++
 docs/dashboard-modal-inventory.md | 103 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 107 insertions(+)

Fusion-Task-Id: FN-8617
Fusion-Task-Lineage: 87159726-d574-4f71-bd6f-66cf9f2f9a9e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 16:05:08 -07:00
gsxdsm
743dc5f46e FN-8612: remove tablet task modal padding
Keep task modals dense on tablets without reducing their touch resize targets.

- Move the Task Detail drag target out of layout flow while preserving its 44px hit area.
- Restore desktop-density New Task header and body padding on tablet resize surfaces.
- Add CSS, unit, and browser coverage for tablet geometry and generic floating windows.

Files changed:
 .changeset/fn-8612-tablet-modal-padding.md         |   7 +
 docs/dashboard-guide.md                            |   8 +-
 .../task-detail-modal-tablet-width.test.ts         |   8 +
 .../dashboard/app/components/FloatingWindow.css    |  40 ++++-
 packages/dashboard/app/components/NewTaskModal.css |  14 ++
 .../FloatingWindow.touch-geometry.test.tsx         |  14 ++
 .../app/components/__tests__/NewTaskModal.test.tsx |   8 +
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  54 ++++++-
 .../task-modal-touch-resize-browser.test.ts        | 177 ++++++++++++++++++++-
 packages/dashboard/vitest.config.ts                |   6 +
 10 files changed, 322 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-8612

Fusion-Task-Lineage: fecd7c48-7b6c-434e-9002-f8f21241120c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 15:42:53 -07:00
gsxdsm
a9b30013bb fix(core): approval dedupe lookup matches on PostgreSQL instead of minting duplicates
`findLatestByDedupeKey` read `targetContext` through the string-only `fromJson`.
In backend (PostgreSQL) mode that column is jsonb and Drizzle returns it ALREADY
PARSED, so the dedupe scan never matched: every gate retry minted a duplicate
approval request, and an approved grant could never be redeemed. The live
database shows the signature plainly — 17 approved requests, 0 completed.

Normalize both shapes in one place (`normalizeTargetContext`), applied at
`rowToRequest` and both dedupe scan sites, so a row resolves whether it arrives
as a JSON string (SQLite) or a parsed object (Postgres).

The regression test asserts shape-independence rather than the single reported
case: the same stored key must resolve in BOTH shapes, and must not match a
different key or an absent context in either. Mutation-checked — reverting the
scan sites fails exactly the parsed-object case.

Cherry-picked ahead of #2457, which carries the wider approval/permission
hardening pass, because this one is an active production defect on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:38:05 -07:00
gsxdsm
30f81ac0cd fix(engine): compact verification failure output
Keep successful verification responses quiet and return bounded, high-signal diagnostics for failures without hiding zero-work or green-while-red warnings.
2026-07-26 15:36:33 -07:00
gsxdsm
65f6748e5b FN-8613: theme mailbox markdown links
Theme mailbox message anchors with dashboard tokens and verify link coverage.

- Style markdown anchor default, visited, hover, and focus-visible states with design tokens
- Cover inline, autolink, list, and table links while preserving file-path controls
- Add a patch changeset for themed mailbox links

Files changed:
 .changeset/theme-mailbox-markdown-links.md         |  7 ++++
 packages/dashboard/app/components/MailboxModal.css | 29 +++++++++++++
 .../__tests__/MailboxMessageContent.test.tsx       | 49 ++++++++++++++++++----
 3 files changed, 77 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8613

Fusion-Task-Lineage: a8c7e8bb-8238-4018-bf95-38193f908eb4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:49:17 -07:00
gsxdsm
07c8c95b10 FN-8614: cap agent tool output
Bound every engine-injected tool result to preserve agent context capacity.

- Add shared 16,000-character total text budgets with deterministic truncation markers and validated overrides.
- Apply outermost output clamps to Pi and non-Pi plugin tool paths, with semantic caps for high-volume reads.
- Cover budget behavior and document the operator-facing configuration contract.

Files changed:
 .changeset/fn-8614-tool-output-budget.md           |  7 ++
 docs/agents.md                                     |  8 ++
 .../core/src/__tests__/tool-output-budget.test.ts  | 58 +++++++++++++
 packages/core/src/index.gate.ts                    |  7 ++
 packages/core/src/index.ts                         |  7 ++
 packages/core/src/tool-output-budget.ts            | 97 ++++++++++++++++++++++
 .../src/__tests__/agent-artifact-tools.test.ts     | 10 +++
 .../src/__tests__/agent-document-tools.test.ts     | 10 +++
 .../__tests__/agent-task-logs-read-tools.test.ts   |  8 ++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 67 +++++++++++++++
 packages/engine/src/agent-session-helpers.ts       |  7 +-
 packages/engine/src/agent-tools.ts                 | 43 ++++++++--
 packages/engine/src/pi.ts                          | 54 +++++++++++-
 13 files changed, 374 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8614

Fusion-Task-Lineage: b6a76ccd-d7b4-4b43-af7e-cfd16ffb7fc8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:42:06 -07:00
gsxdsm
a6885b73f2 FN-8606: migrate core workflow modals to shared floating windows
Migrate dashboard dialogs to the shared movable and resizable FloatingWindow contract.

- Move core, workflow, Git, planning, and automation modal surfaces to stable floating-window identities with persisted geometry.
- Suspend geometry and floating controls for phone and short-viewport sheets, including Quick Chat.
- Add accessibility wiring, tablet touch targets, migration tests, and operator documentation.

Files changed:
 .changeset/fn-8606-floating-window-core-modals.md  |   7 +
 docs/dashboard-guide.md                            |   4 +
 packages/dashboard/app/App.tsx                     |   8 +-
 .../dashboard/app/components/ActivityLogModal.tsx  |  30 ++--
 packages/dashboard/app/components/AddNodeModal.tsx |   8 +-
 .../dashboard/app/components/ChangesDiffModal.tsx  |  35 +++--
 .../dashboard/app/components/ConnectNodeModal.tsx  |   9 +-
 .../dashboard/app/components/FloatingWindow.css    |  71 ++++++++-
 .../dashboard/app/components/FloatingWindow.tsx    |  27 +++-
 .../dashboard/app/components/GitManagerModal.tsx   |  30 +++-
 .../dashboard/app/components/GroupTaskModal.tsx    |   8 +-
 .../app/components/ModelOnboardingModal.tsx        |  28 ++--
 .../dashboard/app/components/NodeDetailModal.tsx   |   9 +-
 .../dashboard/app/components/PlanningModeModal.css |   1 -
 .../dashboard/app/components/PlanningModeModal.tsx |  57 +++----
 .../app/components/ScheduledTasksModal.tsx         |   9 +-
 packages/dashboard/app/components/ScriptsModal.css |   1 -
 packages/dashboard/app/components/ScriptsModal.tsx |  32 ++--
 .../dashboard/app/components/SettingsModal.css     |   1 -
 .../dashboard/app/components/SettingsModal.tsx     |  58 ++++---
 .../app/components/WorkflowAddStepModal.css        |  15 --
 .../app/components/WorkflowAddStepModal.tsx        |  46 +++---
 .../components/__tests__/ActivityLogModal.test.tsx |  33 ++--
 .../app/components/__tests__/AddNodeModal.test.tsx |  12 +-
 .../components/__tests__/ChangesDiffModal.test.tsx |  78 +++++-----
 .../components/__tests__/ConnectNodeModal.test.tsx |   7 +
 .../components/__tests__/FloatingWindow.test.tsx   | 173 ++++++++++++++++++++-
 .../components/__tests__/GitManagerModal.test.tsx  |  30 ++--
 .../components/__tests__/GroupTaskModal.test.tsx   |   9 ++
 .../__tests__/ModelOnboardingModal.test.tsx        |  18 ++-
 .../components/__tests__/NodeDetailModal.test.tsx  |   9 ++
 .../__tests__/PlanningModeModal.autosize.test.tsx  |  30 ++--
 .../__tests__/ScheduledTasksModal.test.tsx         |  21 ++-
 .../app/components/__tests__/ScriptsModal.test.tsx |  15 +-
 .../__tests__/SettingsModal.mobileClose.test.tsx   |  25 ++-
 .../__tests__/WorkflowAddStepModal.test.tsx        |   7 +
 .../floatingWindowMigration.test-helpers.ts        | 126 +++++++++++++++
 37 files changed, 828 insertions(+), 259 deletions(-)

Fusion-Task-Id: FN-8606
Fusion-Task-Lineage: dab0df2d-73f4-4b0f-bec3-a45016310c91
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:37:35 -07:00
gsxdsm
cf5575a319 Add new items to the project roadmap 2026-07-26 14:01:00 -07:00
gsxdsm
4c3b3c14b7 Update roadmap with new tasks and workflows
Added new items regarding tasks, workflows, and CEO role.
2026-07-26 13:56:45 -07:00
gsxdsm
82f85ce863 Update roadmap with new planned and completed items 2026-07-26 13:53:45 -07:00
gsxdsm
93a403af67 fix(dashboard): import delete-attribution constants via browser-safe subpath
The client bundle aliases `@fusion/core` to the leaf `core/src/types.ts` to
keep Node-only dependencies out of the browser, so a package-root import of
`FUSION_CLIENT_HEADER`/`FUSION_DASHBOARD_UI_CLIENT` typechecked but failed
`vite build`:

  "FUSION_CLIENT_HEADER" is not exported by "../core/src/types.ts"

Follow the documented pattern instead of widening the root alias: declare a
`./task-delete-attribution` subpath export, add the matching Vite alias ahead
of the broader `@fusion/core` key (Vite matches in order), register the module
in the browser-safe-core allowlist, and import the subpath from the client.
`task-delete-attribution.ts` has no imports at all, so it is a safe leaf.

`app/utils/detectContentLanguage.ts` already warned about exactly this trap;
the miss was mine for verifying with typecheck, lint and test:gate but not
`pnpm build`, which is one of the four checks CI blocks on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:22:18 -07:00
gsxdsm
15a2fb18cc Merge branch 'fix/incomplete-pg-ports'
Wire incomplete PostgreSQL ports for archive, reconcile, health, settings
cache, agent cache, and async prompt overrides.
2026-07-26 13:16:13 -07:00
gsxdsm
ab87d0d803 fix(api): return 404 for missing tasks, and make task deletions attributable
Three related fixes, all originating from a `[api:error] Request failed`
log line showing a 500 on `GET /api/tasks/FN-8610/runtime-fallback`.

1. Missing/deleted tasks now return 404 instead of 500.
   `getTaskImpl` signalled a miss with a bare `Error`, and route catches
   only mapped errno `ENOENT` to 404 — a leftover from the file-backed
   storage era. In Postgres mode nothing sets an errno code, so every
   unknown/missing/soft-deleted/wrong-project read returned 500. Adds a
   typed `TaskNotFoundError` (message byte-identical) plus a shared
   `task-lookup-error` mapper applied across the task, session-diff,
   git/GitHub, workflow and file-workspace route registrars. The same
   bare throw existed on both archive-lifecycle delete paths, so
   `DELETE /tasks/:id` was affected too.

2. 5xx logs now carry the origin stack.
   `rethrowAsApiError` constructed a fresh `ApiError` from the message
   and discarded the original, so the `FNXC:ApiErrorDiagnostics`
   contract logged the rethrow site rather than the throw site — the
   reported log entry had no stack at all. Threads `cause` through the
   error factories and walks the chain (bounded, cycle-guarded).

3. Task deletions are attributable, and non-operator deletes notify.
   `task:deleted` audit rows recorded `agentId: "system"` for every HTTP
   delete, making an operator click indistinguishable from a script or
   an agent; the calling agent's task id was accepted by the store and
   then never persisted. Adds a `callerKind` union recorded in audit
   metadata, tags every delete call site, and stamps a self-reported
   `x-fusion-client` header from the dashboard client. When the caller
   is `agent-tool` or `api-unattributed`, a best-effort notice is sent
   to the operator mailbox; operator and engine deletes stay silent.

`x-fusion-client` is attribution, not authentication — anything can send
it. No delete-blocking, gating or permission logic is added here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:15:36 -07:00
gsxdsm
2b55077546 fix: wire incomplete PostgreSQL ports for archive, reconcile, health
Replace empty backendMode stubs with real AsyncDataLayer paths: archive ID
reservation and isTaskArchivedAsync, orphaned task.json re-import, health
snapshots via checkPostgresHealth, settings/agent memory caches for sync
readers, async builtin prompt overrides, and self-healing audit/health
callers that previously used dead sync SQLite fallbacks.
2026-07-26 13:10:20 -07:00
gsxdsm
827b145fa1 FN-8611: persist manual import translations
Persist manual GitHub and GitLab import translations across preview sessions.

- Add durable translation cache reads and identity-aware translation requests.
- Route manual translations through the translation budget with actionable API errors.
- Restore cached previews automatically and cover API/UI behavior with tests.

Files changed:
 .changeset/fn-8611-manual-import-translations.md   |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 packages/dashboard/app/api/ai-text.ts              |  52 ++++++--
 packages/dashboard/app/api/legacy.ts               |   2 +
 .../dashboard/app/components/GitHubImportModal.tsx |  14 ++-
 .../components/GitHubImportTranslateControls.tsx   |  39 +++++-
 .../__tests__/GitHubImportModal.test.tsx           |  42 +++++++
 packages/dashboard/src/ai-translate.ts             |  23 ++++
 .../register-ai-text-assistant-routes.test.ts      | 135 +++++++++++++++++----
 .../routes/register-ai-text-assistant-routes.ts    | 131 ++++++++++++++------
 10 files changed, 370 insertions(+), 77 deletions(-)

Fusion-Task-Id: FN-8611

Fusion-Task-Lineage: 00a84f43-4469-4bc5-bbb6-8245d8d79781

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 13:05:23 -07:00
gsxdsm
c5a38d8884 test: cover orphan reconcile and database-health PG stubs
Pin reconcileOrphanedTaskDirsImpl empty result and getDatabaseHealthImpl
always-healthy sentinel under backendMode so inventory category (e) stays
aligned with production self-healing and health call sites.
2026-07-26 12:58:48 -07:00
gsxdsm
d8123dc340 test: expand SQLite incomplete-PG-port inventory coverage
Drive real sync-reader stubs that empty-return under backendMode
(merge request, workflow selection/overrides/settings, run audit,
legacy step snapshot, settings/health) so category (e) of the
migration inventory stays pinned to shipped behavior.
2026-07-26 12:53:44 -07:00
gsxdsm
47087342fc FN-8605: harden tablet touch window controls
Make shared floating dashboard windows reliably movable and resizable on tablet touch viewports.

- Apply a tablet-aware 44px drag and resize hit-target contract to FloatingWindow.
- Preserve phone full-screen sheets below 768px while retaining desktop-hybrid geometry.
- Add geometry, browser touch, and viewport boundary coverage with captured visual evidence.
- Document the shared modal touch contract and release the dashboard fix.

Files changed:
 .changeset/fn-8605-floating-window-touch.md        |   7 ++
 docs/dashboard-guide.md                            |   5 +
 docs/testing.md                                    |   2 +-
 .../dashboard/app/components/FloatingWindow.css    | 116 ++++++++++++++----
 .../dashboard/app/components/FloatingWindow.tsx    |  56 ++++++++-
 .../components/__tests__/FloatingWindow.test.tsx   |  80 +++---------
 .../FloatingWindow.touch-geometry.test.tsx         | 136 +++++++++++++++++++++
 .../FloatingWindowStack.cross-type.test.tsx        |   2 +-
 .../app/hooks/__tests__/useViewportMode.test.ts    |  18 ++-
 packages/dashboard/app/hooks/useViewportMode.ts    |   7 +-
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  39 +++++-
 .../__screenshots__/fn-8605/phone-fullscreen.png   | Bin 0 -> 11393 bytes
 .../e2e/__screenshots__/fn-8605/tablet-after.png   | Bin 0 -> 14275 bytes
 .../e2e/__screenshots__/fn-8605/tablet-before.png  | Bin 0 -> 14308 bytes
 .../task-modal-touch-resize-browser.test.ts        |  83 +++++++++++++
 15 files changed, 453 insertions(+), 98 deletions(-)

Fusion-Task-Id: FN-8605

Fusion-Task-Lineage: 993c7ed8-c3e9-4673-b0b7-5c50746991a7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:51:39 -07:00
gsxdsm
1290948530 test: ratchet authorized production SQLite DatabaseSync readers
Inventory analysis found exactly six read-only legacy openers; pin them in a
structural scan and assert incomplete archive guards stay SQLite-free in
backend mode so new production SQLite construction fails CI.
2026-07-26 12:45:58 -07:00
gsxdsm
f01461a70e feat(engine): thread the node id onto review-gate leases, activating pre-boot reclaim
Completes 3b83282273. The classifier and the self-healing reader landed there but
nothing stamped `leaseNodeId`, so the pre-boot reclaim path was unreachable.

Wiring: InProcessRuntime -> TaskExecutor -> WorkflowGraphTaskRunner ->
WorkflowGraphExecutor, which writes the field onto the pending lease.

The executor takes `getLocalNodeId`, a GETTER rather than a value, because the
runtime resolves the node id asynchronously (a CentralCore read) partway through
start() while `executorOptions` is built earlier in the same method. A snapshot
taken at construction would freeze `undefined` and silently disable attribution
forever -- the failure mode where the feature looks wired, typechecks, and never
fires. Reading it at runner-construction time picks up the resolved id.

With this, a review gate whose session dies to an engine restart is reclaimed on
the next self-healing pass instead of waiting out the 15-minute staleness floor.
Peer-owned and legacy unattributed leases still take the floor, so the
double-dispatch protection multi-node depends on is unchanged.

Adds five classifier cases: own-node pre-boot reclaims; peer-node, unattributed,
own-node-post-boot, and no-identity-supplied all still adopt. Verified the first
is not vacuous -- disabling the branch fails exactly that case (1 failed / 13
passed) and no other.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green
(299 + 10 + 70), plan-review-lease + plan-review-single-owner +
self-healing-orphaned-pending-step-results green (27).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:29:54 -07:00
gsxdsm
3b83282273 feat(engine): attribute review-gate leases to a node so dead local leases reclaim fast
Groundwork for FN-8603's remaining ~14-minute wait. Liveness for a pending
review gate is judged purely by a 15-minute staleness floor because a lease
records WHO took it (`leaseOwner` = run id) but not WHERE, and under multi-node
every engine sees every other engine's leases. A fresh-but-unknown lease might
be running on a peer, so the floor was the only safe test -- and a lease left by
this node's own crashed process is indistinguishable from it.

Adds `WorkflowStepResult.leaseNodeId` plus an optional `LocalNodeLeaseIdentity`
argument to `classifyReviewLease`. One narrow new case: a lease stamped with the
caller's OWN node id whose `startedAt` predates the caller's process boot is
provably dead -- the process that could have owned it is gone -- so it
classifies as `reclaim` immediately rather than aging out. Deliberately narrow,
because widening it is a double-dispatch risk: absent (legacy) or peer node ids
keep the floor, and a lease taken by this process after boot is still adopted.

InProcessRuntime.start() resolves the local node id from CentralCore (fail-soft;
on error it stays undefined and floor-only semantics apply) and passes it to
SelfHealingManager. The graph executor stamps the field when deps.localNodeId is
set.

NOT YET WIRED, so this is inert in production and behavior is unchanged end to
end: `localNodeId` is not threaded from WorkflowGraphTaskRunner /
WorkflowTaskRuntime down into the executor deps, so no lease actually carries a
`leaseNodeId` yet. The reader is ready; the writer needs that pass-through
(WorkflowGraphTaskRunnerDeps gains the field, the runner forwards it, and the
runtime supplies this.localNodeId). Stopping here rather than half-threading it.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green
(299 + 70), core workflow-step-results suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:24:29 -07:00
gsxdsm
fde3b76a8c FN-8602: enable tablet touch resizing for task modals
Enable touch-driven resizing for task modals on tablet viewports.

- Add tablet touch resize handles and pointer interactions for task and new-task modals.
- Preserve responsive modal sizing while keeping phone layouts fullscreen.
- Add unit, browser, and visual regression coverage with updated documentation.

Files changed:
 .changeset/fn-8602-tablet-touch-resize.md          |   7 +
 docs/dashboard-guide.md                            |   6 +-
 docs/testing.md                                    |   4 +
 packages/dashboard/app/components/NewTaskModal.css |  70 ++++++++++
 packages/dashboard/app/components/NewTaskModal.tsx |  19 ++-
 .../dashboard/app/components/TaskDetailModal.css   |  12 ++
 .../dashboard/app/components/TaskDetailModal.tsx   |   7 +-
 .../app/components/__tests__/NewTaskModal.test.tsx |   4 +
 ...etailModal.responsive-and-dependencies.test.tsx |   1 +
 .../hooks/__tests__/useModalResizePersist.test.tsx |  10 +-
 .../app/hooks/__tests__/useViewportMode.test.ts    |  26 +++-
 .../dashboard/app/hooks/useModalResizePersist.ts   |  26 +++-
 packages/dashboard/app/hooks/useViewportMode.ts    |  13 ++
 packages/dashboard/app/styles.css                  |  16 +++
 .../app/task-modal-touch-resize-e2e-fixture.html   |   5 +
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  60 ++++++++
 .../__screenshots__/fn-8602/phone-fullscreen.png   | Bin 0 -> 43987 bytes
 .../e2e/__screenshots__/fn-8602/tablet-after.png   | Bin 0 -> 52549 bytes
 .../e2e/__screenshots__/fn-8602/tablet-before.png  | Bin 0 -> 11500 bytes
 .../task-modal-touch-resize-browser.test.ts        | 154 +++++++++++++++++++++
 20 files changed, 424 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-8602

Fusion-Task-Lineage: f07ce22f-f529-4be3-ba1b-04063154a1b0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:12:35 -07:00
gsxdsm
00011b0113 fix(engine): recover restart-orphaned review steps in one cycle, raise fix budget
FN-8603 sat in-review for ~36 minutes after an engine restart killed its Code
Review session 34 seconds in. It did recover on its own; the cost was latency,
not a terminal park.

Sweep ordering. reconcile-orphaned-pending-step-results PRODUCES the failed
results that recover-failed-pre-merge-steps CONSUMES, but in the periodic
maintenance list it ran ~15 entries after it. A step orphaned in cycle N was
therefore rewritten to failed only after recovery had already scanned, so
nothing re-ran it until cycle N+1. Moved it immediately before its consumer and
removed the now-duplicated later entry. Startup recovery already ordered the two
correctly.

Post-review fix budget. Default raised 3 -> 10 per operator request. Three
passes is below the observed convergence length for the gates this fallback
actually governs -- Browser Verification and custom optional gates -- since Plan
Review and Code Review already resolve to "unbounded" when unset, and exhausting
the budget parks the card for a human. The declaration default and five inline
`settings.maxPostReviewFixes ?? 3` call sites in executor.ts/self-healing.ts had
drifted into separate literals, so raising one alone would have left every
unset-settings path on the old value; they now share the exported
DEFAULT_MAX_POST_REVIEW_FIXES.

Not done, and why. Re-dispatching a restart-orphaned lease immediately at
startup is the change that would close the remaining ~14-minute wait, but it is
unsound as specified: liveness is judged by a 15-minute lease-staleness floor
because leases carry no node attribution, so treating a pre-boot lease as dead
would let one node orphan another node's genuinely running review. Needs a node
id on the lease record first. Left the floor intact.

Verified: tsc clean on core and engine, pnpm lint clean, pnpm test:gate green,
self-healing orphaned-pending-step-results and optional-step-revision suites
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:08:03 -07:00
gsxdsm
cca13737b6 FN-8603: reduce steady-state diagnostic log noise
Route routine core, engine, and dashboard diagnostics through debug-gated shared loggers.

- Demote steady-state diagnostic sites while preserving warnings and errors for actionable failures.
- Add cross-package severity contracts and manifest coverage for demoted log sites.
- Document logging severity guidance and add a patch changeset.

Files changed:
 .changeset/fn-8603-log-severity.md                 |  7 ++
 docs/diagnostics.md                                | 20 ++++--
 .../__tests__/log-severity-spam-contract.test.ts   | 71 ++++++++++++++++++
 packages/core/src/activity-analytics.ts            |  5 +-
 packages/core/src/ai-summarize.ts                  | 61 +++++++---------
 packages/core/src/async-mission-store.ts           |  5 +-
 packages/core/src/async-secrets-store.ts           |  7 +-
 packages/core/src/central-core.ts                  | 17 ++---
 packages/core/src/docker-provisioning.ts           | 13 ++--
 packages/core/src/index.ts                         |  1 +
 packages/core/src/master-key.ts                    |  9 ++-
 packages/core/src/memory-compaction.ts             | 29 ++++----
 packages/core/src/memory-insights.ts               |  7 +-
 packages/core/src/migration-orchestrator.ts        |  7 +-
 packages/core/src/mission-store.ts                 |  5 +-
 packages/core/src/node-discovery.ts                |  7 +-
 packages/core/src/notification/dispatcher.ts       |  9 ++-
 .../core/src/plugins/bundled-plugin-install.ts     | 11 +--
 packages/core/src/reflection-store.ts              |  5 +-
 packages/core/src/secrets-store.ts                 |  7 +-
 packages/core/src/task-store/agent-logs.ts         | 21 +++---
 packages/core/src/task-store/async-events.ts       |  5 +-
 packages/core/src/task-store/async-maintenance.ts  |  7 +-
 packages/core/src/task-store/comments-ops.ts       |  7 +-
 packages/core/src/task-store/task-mutation-ops.ts  | 11 +--
 packages/core/src/task-store/workflow-integrity.ts |  9 ++-
 packages/core/src/types/merge-policy.ts            |  5 +-
 packages/core/src/usage-events.ts                  |  5 +-
 .../__tests__/log-severity-spam-contract.test.ts   | 48 +++++++++++++
 packages/dashboard/src/ai-refine.ts                |  5 +-
 packages/dashboard/src/ai-session-diagnostics.ts   | 10 +--
 packages/dashboard/src/chat.ts                     |  8 ++-
 packages/dashboard/src/devserver-manager.ts        |  9 ++-
 packages/dashboard/src/file-service.ts             |  5 +-
 packages/dashboard/src/github-tracking-comments.ts |  7 +-
 .../dashboard/src/github-tracking-reconciler.ts    |  5 +-
 packages/dashboard/src/github-tracking-state.ts    |  5 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |  5 +-
 packages/dashboard/src/insights-routes.ts          |  9 ++-
 packages/dashboard/src/issue-image-attachments.ts  |  5 +-
 packages/dashboard/src/knowledge-index.ts          |  5 +-
 packages/dashboard/src/plugin-routes.ts            |  7 +-
 packages/dashboard/src/routes/board-workflows.ts   |  5 +-
 packages/dashboard/src/routes/context.ts           |  5 +-
 .../dashboard/src/routes/register-auth-routes.ts   | 13 ++--
 .../routes/register-docker-provisioning-routes.ts  |  7 +-
 .../dashboard/src/routes/register-git-github.ts    | 21 +++---
 packages/dashboard/src/routes/register-gitlab.ts   |  7 +-
 .../src/routes/register-session-diff-routes.ts     |  9 ++-
 .../src/routes/register-settings-memory-routes.ts  |  7 +-
 .../src/routes/register-setup-activity-routes.ts   |  7 +-
 .../dashboard/src/routes/register-signal-routes.ts |  5 +-
 .../src/routes/register-task-workflow-routes.ts    | 11 +--
 packages/dashboard/src/runtime-logger.ts           | 11 +--
 packages/dashboard/src/server.ts                   |  7 +-
 packages/dashboard/src/sse.ts                      |  8 ++-
 packages/dashboard/src/terminal-service.ts         | 34 ++++-----
 packages/dashboard/src/view-chunk-manifest.ts      |  5 +-
 .../engine/src/__tests__/log-severity-manifest.ts  | 83 ++++++++++++++++++++++
 .../__tests__/log-severity-spam-contract.test.ts   | 40 ++++++++++-
 .../src/__tests__/logger-debug-gating.test.ts      |  7 +-
 packages/engine/src/goal-anchoring-audit.ts        |  5 +-
 packages/engine/src/plugin-runner.ts               | 44 ++++++------
 packages/engine/src/pty-native.ts                  |  9 ++-
 .../engine/src/runtimes/child-process-worker.ts    |  4 +-
 packages/engine/src/self-healing.ts                | 12 ++--
 packages/engine/src/worktree-hooks.ts              | 10 ++-
 67 files changed, 632 insertions(+), 250 deletions(-)

Fusion-Task-Id: FN-8603

Fusion-Task-Lineage: 53901db6-1af2-4bd7-b5ea-49507e048ef2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:01:19 -07:00
gsxdsm
0d40bc41d1 fix(dashboard): stop badging the merge-blocker in-review stall
A pre-merge check reporting a blocker is the ordinary in-review resting state
rather than an exceptional one, so badging it marked routine cards abnormal.
Operator-requested removal.

Suppression is expressed as a code list next to the existing
no-worktree-no-merge-confirmed entry, so both surfaces that gate on
shouldShowInReviewStallBadge -- the card header badge and the Task Detail
diagnostic block -- drop it from one place. The previous carve-out only
suppressed merge-blocker while isActiveMergeStatus(task.status) held; that is
gone, and the test row that used to expect a badge for status undefined now
asserts the suppression is unconditional.

task.inReviewStall keeps being computed and stored -- only the affordance is
withheld -- so the Review tab, run-audit, and self-healing are unaffected.
No dead CSS: the shared .in-review-stall classes still serve the remaining
codes and no --merge-blocker rule existed. The card test asserts no empty
badge shell is left behind.

Two TaskDetailModal cases used merge-blocker only as a fixture for the
diagnostics row and jump-to-activity-entry behavior; repointed at
transient-merge-status-no-owner so they still guard what they were written for.

Note for follow-up: this badge was the board's only signal for a card blocked
on a failed pre-merge step. self-healing's needsOperatorBypass comment already
flags that such cards "sit silently" behind a generic badge; with the badge
gone they show nothing at all on the board.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, 693 tests passing
across the 6 affected suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 11:47:04 -07:00
gsxdsm
f26cbedf4f fix(dashboard): close the code-review findings on the mobile tab-discard work
An 11-reviewer pass over f157bf7460..f5163d8351 found defects in the mobile
tab-discard change set itself. This fixes them.

Silent data loss (the recurring defect class):
- AgentDetailView reconnect refetched limit:100 and replaced wholesale, so 380
  displayed lines vanished with no "Load older" and no indicator; it now
  reconciles through the shared logStreamReconcile helper.
- useActivityLog.loadMore past the cap discarded the page it had just fetched
  while advancing the cursor and leaving hasMore true, so the feed silently
  stopped paginating behind a live-looking button.
- useAgentLogs: loadMore and resyncFromServer had no mutual exclusion, a
  no-overlap resync discarded explicitly paged-back history, a resync outliving
  the reconnect delay left an unmarked gap, and the live-tail trim could evict
  the gap marker itself.
- useLiveTranscript's resync overwrote live entries that raced the refetch.

The premise itself was not fully delivered:
- useProjects, useNodes, and useMeshState never called clearInterval, so they
  polled the whole time the tab was hidden. useProjects is mounted for the
  entire session, so the page never went idle -- the primary mechanism this
  work depends on. All three now use the shared visibility gate.
- sse-bus fired onReconnect twice per reconnect cycle and fanned out ~28
  subscribers in one tick, against a ~6-connection-per-origin cap on a waking
  radio. The successful open is now the single authority, and the fan-out uses
  the same exported stagger primitive as the polling path rather than a second
  copy of the slot formula.
- A channel first subscribed during the hidden window opened a live EventSource
  and keepalive; suspension is now a module-level condition openChannel
  consults, and a channel opened inside the grace window re-arms it.

Credentials and correctness:
- The service worker persisted every GET /api/* to durable Cache Storage,
  including /api/settings with daemonToken, githubAuthToken, gitlabAuthToken
  and ntfyAccessToken in plaintext, with no exclusion and no purge path --
  "Clear all cached data" only walked localStorage. Now gated, bounded, and
  genuinely purgeable.
- useTasks cleared its own snapshot when the mount revalidation failed on a
  waking radio, so the board blanked and the next restore was empty too.
  Suspension-class failures no longer destroy the cache.
- A single-row SSE update reset lastFetchTimeMs to now while an hours-old
  hydrated snapshot was on screen, re-marking every in-progress card stuck.
- ListView's "Select all visible tasks" acted on the full filtered set while
  only 50 rows rendered, so a bulk delete reached rows the operator could not
  see. Column's search window reset keyed on a boolean, so refining a query
  kept the expanded window.

Tests that could not fail:
- App.test.tsx mocked TerminalModal as isOpen ? <div/> : null, making the
  unmount-on-close invariant unobservable; MockEventSource kept its listeners
  after close(), so cases passed with their onReconnect handlers deleted.
- The SSE resync ratchet scanned only hooks/, exempting ~13 component call
  sites -- the exact regression it exists to prevent.
- MissionControlPanel's bespoke poll and the xterm scrollback constants and
  WebGL disposal had no coverage at all.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, pnpm
check:changesets clean, 877 tests passing across 36 scoped files.
Known unrelated red: MailboxView.test.tsx's FN-8407 CSS guard fails at HEAD
too -- this diff adds no @media rule and no .mailbox-view--mobile selector,
the only two things that assertion inspects. Left alone deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 11:17:52 -07:00
gsxdsm
9ff1587b84 fix(engine): keep the task worktree across replan bounces
`moveTask`'s reopen-to-todo/triage block clears `task.worktree` but leaves
`task.branch` intact, and `moveTaskToReplanColumn` called it with no options.
A replan bounce therefore left the row split-brained: no worktree pointer, but
still owning `fusion/<id>`, which was still checked out in the worktree it had
just orphaned. The next planning acquisition skipped its resume branch (gated on
`task.worktree`), re-created the same branch, collided, and fell into
`cleanupConflictingWorktree` — force-remove + `git branch -D` + fresh
`git worktree add` + init command, on every bounce. Observed on FN-8603: two
Plan Review REVISE bounces burned two full teardown/rebuild cycles for nothing,
since planning writes its spec to the task store, not the worktree.

Pass `preserveWorktree: true` at the shared seam, so this covers every replan
mover — Plan Review REVISE, required-artifact recovery, and the executor and
scheduler spec-staleness and filesystem-validation rebounds. Acquisition still
re-validates, so a preserved pointer to a removed checkout self-heals as before;
the rest of the replan contract (steps reset, status/error cleared) is unchanged.

Regression coverage asserts the invariant across both replan-column shapes
(triage and plan-in-place todo) and all three reopen origins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:53:04 -07:00
gsxdsm
71279ed042 fix(FN-8600): recover a duplicate verdict the planner reported in its reply
The prompt fix stops planners writing the verdict in prose, but it relies on
every model reading one sentence correctly. This closes the hole underneath it.

When the finalize read finds no spec at all, the planner's streamed reply is
searched for a line that is exactly `DUPLICATE: FN-NNNN`. If found, the engine
writes the canonical marker file and continues — so marker parsing, keep/delete
resolution, and the sourceMetadata.nearDuplicateOf that renders the operator's
decision all run on the unchanged file contract rather than a second code path
that could drift from it.

Deliberately narrow. The marker must occupy a whole line, only the first counts,
and recovery is gated on the plan being genuinely absent — a planner that wrote
a real spec is never overridden by something it said in passing. The text tail
is bounded because the verdict lands in the closing summary, and it tees off
onText rather than reading AgentLogger, whose buffer is flushed on a timer.

Verified both directions: the tests fail without the recovery block, and the
"wrote a real spec while mentioning a marker" case keeps its spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:23:00 -07:00
gsxdsm
a00f2633ce fix(engine): demote more TUI chatter across merger, self-heal, and ntfy
Route foreach/merger/worktree/self-healing skips, ntfy send bookkeeping, session-purpose runtime picks, planning using-model, and checkpoint rewind lines to debug so recoveries and failures stay visible in the operator log.
2026-07-26 10:15:18 -07:00
gsxdsm
f5163d8351 fix(dashboard): resync SSE state on reopen and bound the service-worker cache
Follow-up to f157bf7460, fixing the regressions an adversarial review found in
the mobile tab-discard work.

- SSE hidden-suspend dropped events silently: the per-task/run log streams are
  live-only with no replay, and many subscribers had no onReconnect, so a 60s+
  hidden window left invisible gaps in logs, a never-rendered approval banner,
  a diverged chat transcript, and a missed merge advance notice. Every
  subscriber now resyncs authoritative state on reopen.
- useAgentLogs refetches its authoritative page on reconnect and reports
  hasMore truthfully once paging reaches the first entry.
- Agent run logs are windowed rather than discarded, so the head of a long run
  stays reachable.
- lastFetchTimeMs is seeded from the cached envelope's savedAt, so a hydrated
  stale snapshot no longer renders every in-progress card as stuck.
- MAX_IMMUTABLE_CACHE_ENTRIES lands as 200; it was committed as Infinity, which
  left the cache-first bucket unbounded and the cap dead code.
- useAgentLogs.ts held a literal NUL byte that made git treat the file as binary
  and grep skip it; replaced with an escape sequence so it stays reviewable.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, pnpm check:changesets
clean, 25 scoped test files / 1337 tests passing. The xterm scrollback constants
and several components still lacking onReconnect remain untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:13:39 -07:00
gsxdsm
c7fa02f370 FN-8597: restore executor task-done invariant coverage
Restore the quarantined executor graph-completion invariant suite with real foreach projections.

- Exercise complete and partial expanded workflow-step projections at the merge boundary.
- Remove the rescued invariant suite from Vitest quarantine and clear its ledger entry.
- Extend the shared executor logger mock with the debug method required by the integration tip.

Files changed:
 .../__tests__/executor-task-done-invariant.test.ts | 267 +++++++++++++++++++--
 .../engine/src/__tests__/executor-test-helpers.ts  |   7 +
 packages/engine/vitest.config.ts                   |   7 -
 scripts/lib/test-quarantine.json                   |   8 +-
 4 files changed, 254 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8597

Fusion-Task-Lineage: 05a08e31-7da0-4c93-86a0-9baf8db7ce52

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 10:04:36 -07:00
gsxdsm
f157bf7460 fix(dashboard): harden visibility suspension, log caps, and mobile board UX
Suspend poll/SSE work when the tab is hidden, cap log buffers, restore board scroll more reliably, and improve list windowing/live tickers with related tests and a mobile-tab retention changeset.
2026-07-26 09:50:44 -07:00
gsxdsm
9bad0e1233 fix(engine): demote high-frequency TUI log spam to debug
Route process spawn/exit, verification success paths, MCP connect, skill info listings, createFnAgent/session bookkeeping, and executor dispatch chatter through FUSION_DEBUG so the operator log pane keeps real lifecycle outcomes.
2026-07-26 09:50:43 -07:00
gsxdsm
86c892b67c fix(FN-8600): make the duplicate-report instruction writable, not self-contradictory
The planning prompt said "do not write PROMPT.md" and, in the same breath,
"write DUPLICATE: {id} to the output file" — where the output file IS
PROMPT.md. A planner that took the first clause literally wrote no file and
reported the duplicate in prose.

The engine only ever reads the verdict from PROMPT.md's contents, so that
duplicate was invisible: the task failed deterministic validation as
"PROMPT.md file not found or empty", retried, terminalized to failed, emitted
a task-wedge mail, was recovered to todo by self-healing, and re-planned —
three full Opus planning cycles on FN-8600 before it was caught, with no
operator decision ever surfaced because sourceMetadata.nearDuplicateOf is only
set on the branch that parses the file.

Both prompt sites now say to write PROMPT.md with the marker as its entire
contents, and say why prose alone is not recorded.

Note the engine ordering is already correct — tryFinalizeExplicitDuplicateMarker
runs before validateGeneratedPrompt, and a worktree-local spec is recovered
first. Nothing to reorder; the file simply never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:42:24 -07:00
gsxdsm
ae512aec2b FN-8601: enforce foreach merge proof
Require complete foreach execution evidence before workflow merge review.

- Add reusable foreach instance coverage proof evaluation.
- Block checklist projection and merge admission on incomplete or failed node results.
- Cover core proof logic and PostgreSQL merge-boundary behavior.
- Add a patch changeset for the merge safeguard.

Files changed:
 .changeset/fn-8601-foreach-merge-proof.md          |   7 ++
 .../src/__tests__/workflow-merge-proof.test.ts     |  43 ++++++++
 packages/core/src/index.gate.ts                    |   2 +
 packages/core/src/index.ts                         |   2 +
 packages/core/src/workflow-merge-proof.ts          |  74 +++++++++++++
 ...xecutor-merge-boundary-foreach-proof.pg.test.ts | 111 +++++++++++++++++++
 packages/engine/src/executor.ts                    | 117 +++++++++++++--------
 7 files changed, 314 insertions(+), 42 deletions(-)

Fusion-Task-Id: FN-8601

Fusion-Task-Lineage: 40578171-0b13-4538-8f38-3948ed1e92c0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:40:37 -07:00
gsxdsm
a516c8b409 docs(FN-8600): capture the live-planning-worktree reclaim incident
Documents why self-healing force-removed a worktree a planning session was
using and parked the card branch-conflict-unrecoverable: planning gained a task
worktree but never took an active-session lease, so the reclaim sweep's liveness
guard had nothing to see, and a zero-commit branch classifies as
tip-already-merged by construction.

Captures the investigation's dead ends too — including reading maxConcurrent
from a multi-tenant config table without filtering by project_id, which produced
a confidently wrong root cause — and the three ways the first version of the fix
was itself wrong.

CONCEPTS.md: adds planning to the Active-session lease kinds (the entry had gone
stale), states the converse invariant that an unheld path reads as proof nothing
is running, and defines Top-level agent slot — the capacity concept whose
conflation with the worktree limit derailed the first hour of diagnosis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:22 -07:00
gsxdsm
2d263acc49 fix(FN-8600): keep self-healing from pausing live planners and unstick queued planning
Planning moved into the task's own worktree but never published that path to
activeSessionRegistry, so the self-owned-branch reclaim sweep's FN-4819 liveness
guard was blind to a live planner. A zero-commit task branch trivially reads as
tip-already-merged, so the sweep ran `git worktree remove --force` on the tree a
planning session was using, the removal failed, and the failure escalated to
branch-conflict-unrecoverable — parking a healthy card paused with no operator
action.

Planning now claims its worktree through acquireActiveSessionPath (new "planning"
session kind) and releases it only while it still owns the record, so a live
executor that took over the same path mid-teardown is never cleared.

Also fixes planning starvation and its diagnosability:
- admitOldest walks past candidates whose lane declines instead of ending the
  pass on candidates[0], unwinding each declined attempt's pre-held executor slot
  and reservation exactly so a decline cannot leak capacity past maxConcurrent.
- Withheld planning admission emits a deduped task:plan-admission-throttled
  run-audit event (ids/counts only), written fire-and-forget with the dedupe
  marker set only after the write lands. Previously the binding gate lived only
  in a log line that is persisted nowhere, so "why did this card sit queued to
  plan?" was unanswerable after the fact.

Reviewed by 8 review agents; every finding acted on or recorded. A proposed
STALE_SEMAPHORE_EXCESS_REPAIR_MS 600s->180s reduction was reverted under review —
nested runs are already excluded from the reclaim floor, so the window guards
uncounted top-level holders such as a merge body, and shortening it would trade a
bounded visible stall for an unbounded silent cap breach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:26:13 -07:00
gsxdsm
d4aa79b66c FN-8598: preserve legacy task cost badges
Restore cost badges for tasks with valid legacy token totals.

- Preserve usage records when optional timestamps and cache-write totals are absent
- Use task creation time to satisfy legacy usage timestamp requirements
- Cover card badge rendering, unpriced mixed usage, and mobile visibility
- Add a patch changeset for the restored badge behavior

Files changed:
 .changeset/fn-8598-cost-badge-fix.md               |   7 +
 .../task-token-usage-serialization.test.ts         |  45 +++++++
 packages/core/src/task-store/serialization.ts      |  16 ++-
 .../__tests__/TaskCard.cost-badge.test.tsx         | 146 +++++++++++++++++++++
 .../app/utils/__tests__/taskTokenCost.test.ts      |  11 ++
 5 files changed, 221 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8598

Fusion-Task-Lineage: 83fb4051-8e0f-4ee9-9f00-0e4d5cb8661e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:06:41 -07:00
gsxdsm
795a38c018 fix(engine): quiet graph review-entry audits and label engine aborts truthfully
Recognise workflow-graph moves into in-review so gate entry no longer emits handoff-invariant violations, and split pause-abort provenance so engine teardowns are engine-abort instead of hard-cancel.
2026-07-26 08:56:58 -07:00
gsxdsm
c76f276266 fix(dashboard): stop mobile board swipes jumping two columns at the edges
Columns are narrower than a phone viewport, so the first/last column's ideal
centered scrollLeft is outside the reachable scroll range. isColumnCentered
compared against that unreachable value, so an edge rest never read as
centered and commitDirectionalPage took its origin at release (already moved
onto the next column) instead of at gesture start — paging two columns.

Clamp the centering target to the reachable range, and clamp the mid-transit
origin against the gesture-start column so drag travel is never counted twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 08:48:36 -07:00
gsxdsm
9afd88dd9b FN-8599: fix task card size badge alignment
Keep the size badge aligned with the first task-card header row when status badges wrap.

- Anchor desktop and mobile size chips to the header start with fixed tokenized heights
- Cover size-badge geometry and paused/reviewing badge combinations
- Add a patch changeset for the task-card alignment fix

Files changed:
 .changeset/fn-8599-task-card-size-badge.md         |  7 +++
 packages/dashboard/app/components/TaskCard.css     | 25 +++++++--
 .../__tests__/TaskCard.badge-height.test.tsx       | 13 ++++-
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 62 +++++++++++++++++++--
 4 files changed, 98 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8599

Fusion-Task-Lineage: 062716b6-58ca-4f5b-8741-50856e933192

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 08:42:46 -07:00
gsxdsm
af897d9e3c FN-8596: isolate cross-root plugin MCP discovery
Prevent cross-root MCP discovery from unloading active plugin runtimes.

- Isolate discovery loader lifecycle and runtime-state persistence.
- Preserve shared plugin owners when non-owner loader participants stop.
- Cover core, dashboard, and engine cross-root discovery behavior.

Files changed:
 .changeset/fn-8596-plugin-discovery-isolation.md   |   7 ++
 .../plugin-loader-lifecycle-scope.test.ts          |  12 +++
 .../plugin-mcp-servers-discovery-isolation.test.ts | 115 +++++++++++++++++++++
 packages/core/src/plugin-loader.ts                 |  34 +++++-
 packages/core/src/plugin-mcp-servers.ts            |   8 +-
 .../context-plugin-mcp-discovery-isolation.test.ts |  48 +++++++++
 packages/dashboard/src/routes/context.ts           |  39 ++++++-
 ...-runtime-plugin-mcp-discovery-isolation.test.ts |  69 +++++++++++++
 packages/engine/src/runtimes/in-process-runtime.ts |  47 +++++++--
 9 files changed, 364 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-8596

Fusion-Task-Lineage: 231e53b6-a9a3-4a65-9732-3dabe44da198

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 08:35:04 -07:00
gsxdsm
d47d44c669 fix(engine): demote residual routine/peer-exchange poll chatter
Close log-spam skeptic gaps: routine-scheduler pause and re-entrance no-ops, and peer-exchange zero-work sync cycles, move to debug with contract-test locks.
2026-07-26 08:26:20 -07:00
gsxdsm
cfa84781d6 fix(engine): demote high-frequency TUI log spam to debug
Route steady-state chatter (maintenance batch, skill listings, activity heartbeats, stuck polls, SSE connect/disconnect, heartbeat timer skips, cron/routine de-dupe skips, hold-release capacity races) through FUSION_DEBUG so the operator log pane keeps real state changes and failures.
2026-07-26 08:20:37 -07:00
gsxdsm
beb83a1c1b fix(engine): close the unowned-card strand and harden the planning path
Second FN-8596 strand, found after the first fix shipped. Clearing the
stale `planning` status moved the card into a state owned by NOBODY:

  - planning excluded it: stale `firstExecutionAt` from its first pass made
    hasAdvancedPastPlanning true, and the previous fix only rescued cards
    that still carried a planning-stage status;
  - recoverAdvancedTriageTasks — the designated owner of that
    "stranded-advanced" class — also excluded it, because it bails on
    `workflowIrPinColumnId === "triage"`: it cannot resume a card into the
    column it already occupies (the pin was plan-replan, which lives in
    triage).

So the card sat indefinitely with no sweep, log, or audit event naming it.

hasAdvancedPastPlanning now decides on arrival order alone for any card in
the planner column: a stamp written BEFORE the card reached triage belongs
to a previous pass, whatever the status is now. A card that genuinely
advanced is still caught by the column check at the top, and one claimed by
execution AFTER landing here has a stamp newer than its arrival, so it
still reads advanced and stays with advanced-recovery. This flips one case
I added in the previous commit — production proved that classification
stranded the card.

Hardening, so this class cannot hide again:

  - detectStalledCards: a detect-only watchdog emitting
    `task:stall-watchdog-detected` for any non-terminal, unpaused card idle
    past 30m with no live session and no queued continuation. Deduped per
    shape. It deliberately does NOT mutate — a generic mutator racing the
    specialized sweeps is the bug class this file keeps re-fixing, so
    recovery stays with the sweep that owns each shape and this guarantees
    visibility.
  - The silent skips are now loud: runIfStillPlanningUnderTaskLock (all
    four callers inherit it), the planning handoff moveTaskIf, and the four
    requestPreMergeOptionalStepFix refusals now log why nothing was
    scheduled and that the card was left parked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:59:42 -07:00
gsxdsm
f005cee885 fix(engine): surface silent stalls and add stalled-card watchdog
Make planning-guard and remediation no-ops emit warnings, and detect idle non-terminal cards with no session or continuation so FN-8596-class strands show up in logs and run-audit.
2026-07-26 07:49:05 -07:00
gsxdsm
08f69745f9 fix(engine): quiet routine maintenance batch logs in TUI
Route per-step maintenance success/skip chatter to debug so the operator log pane keeps real recovery events instead of 50+ no-op lines every cycle.
2026-07-26 07:38:06 -07:00
Phil Larson
0643a64f0d fix(desktop): pin complete Pi runtime closure (#2439)
## Summary
- pin `pi-agent-core`, `pi-ai`, `pi-coding-agent`, and `pi-tui` to one
exact 0.82.0 workspace override set
- extend the Pi version policy guard to reject missing, ranged, or
mismatched desktop runtime closure overrides
- add a patch changeset for the legacy desktop packaging fix

## Test plan
- `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs` (5
passed)
- `node scripts/check-pi-versions-pinned.mjs`
- `corepack pnpm check:changesets --strict`
- focused engine fixtures: 4 files / 47 tests passed
- GitHub: Desktop packaging, Lint, Typecheck, Build, Gate, and Greptile
Review passed
2026-07-26 07:34:50 -07:00
gsxdsm
4633c6441b fix(engine): stop stranding replan cards on stale execution stamps
Root cause of the FN-8596 strand (card sat in Planning, doing nothing,
until an engine restart).

Plan Review returned REVISE, the graph rebounded the card to `triage` with
`needs-replan`, and triage claimed it — overwriting the status with the
TRANSIENT `planning`. `needs-replan` is a durable park that outranks the
execution timestamps, but `planning` is deliberately excluded from
REPLAN_PARK_STATUSES, so the card fell through to the stamp check. Those
stamps were written when it entered `in-progress` on its FIRST pass and are
never cleared, so the replanning card read as "advanced past planning" for
the rest of the session.

From there everything was a silent no-op:
updatePlanningStateIfStillCurrent returned false and its callers returned
with no log, no audit and no requeue. The revision session wrote the
revised PROMPT.md (via the store tool, which bypasses the guard) and the
finalize refused to hand the card off — "prompt written, then total
silence", status frozen at `planning`.

Stale stamps are now discriminated from a live claim by arrival order: a
stamp written BEFORE the card arrived in the planner column belongs to a
previous pass, while one written after arrival means execution genuinely
won the FN-8361 race and recovery must not clear the status out from under
it. A missing/unparseable columnMovedAt keeps the prior answer, so this can
only narrow the strand, never widen the race. The PR #2360
stranded-advanced class (stamps with no planning status) is untouched — all
30 pre-existing guard cases still pass.

Also warns when a planning finalize declines to hand off. That path was
completely silent, which is why this strand left nothing in any log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:33:37 -07:00
gsxdsm
581b7d0a49 fix(triage): clear stale planning statuses periodically, not only at startup
Observed on FN-8596: a plan-review REVISE routed to `plan-replan`, triage
claimed the card with `status:"planning"` and ran the revision session, and
the session wrote the revised PROMPT.md then died without finalizing. The
card sat in `triage` with `status:"planning"`, no live planner, and no
workflow continuation.

That status makes the card invisible to triage rediscovery (it looks
claimed), and the only sweep that cleared it ran at STARTUP — so the card
was unrecoverable short of an engine restart. The leaked-slot reaper then
reclaimed its concurrency slot, which made it look idle without making it
runnable.

Adds a periodic counterpart in the poll loop. Clearing the status is the
whole repair: the card is back in triage with a real spec, so ordinary
rediscovery re-picks it. It does not move, pause, or fail the card.

Guards against racing a healthy planner: the in-process `processing` set,
plus a 20-minute staleness floor that also covers a planner owned by
another node this process cannot see. Operator parks are never touched.

This fixes the recovery gap, not the trigger — why that session failed to
finalize is still under investigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 07:23:44 -07:00