Commit Graph

11477 Commits

Author SHA1 Message Date
gsxdsm
f3b68c9fff FN-8065: preview plans before refinement questions
Show a read-only plan preview at the Planning Mode deepening checkpoint.

- Persist pending plan details on the checkpoint question for fresh and restored sessions.
- Render formatted plan content and deliverables above refinement choices.
- Cover preview behavior and document the checkpoint flow.

Files changed:
 .../fn-8065-planning-checkpoint-plan-preview.md    |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 packages/core/src/types.ts                         | 11 ++++
 .../dashboard/app/components/PlanningModeModal.css | 60 ++++++++++++++++++++++
 .../dashboard/app/components/PlanningModeModal.tsx | 44 ++++++++++++++++
 .../PlanningModeModal.planning-flow.test.tsx       | 24 ++++++++-
 .../planning-interview-formatters.test.ts          | 19 +++++++
 packages/dashboard/src/planning.ts                 | 10 ++++
 8 files changed, 175 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8065

Fusion-Task-Lineage: 13cd52c3-af82-4723-a6b1-3366775213e1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:54:33 -07:00
gsxdsm
c2475b012d FN-8064: add proactive task chat status updates
Task-detail chat now narrates engine progress and review outcomes in real time.

- Emit bounded, redacted status rows for step lifecycle and review paths.
- Present status entries with a distinct task-chat treatment.
- Cover status narration and diagnostic sanitization with engine tests.

Files changed:
 .changeset/fn-8064-proactive-chat.md               |   7 +
 docs/architecture.md                               |   1 +
 packages/dashboard/app/components/TaskChatTab.css  |  16 ++
 packages/dashboard/app/components/TaskChatTab.tsx  |   9 +-
 .../engine/src/__tests__/executor-prompt.test.ts   |  28 +++-
 .../engine/src/__tests__/proactive-status.test.ts  |  54 +++++++
 packages/engine/src/executor.ts                    | 176 ++++++++++++++++-----
 packages/engine/src/proactive-status.ts            | 117 ++++++++++++++
 8 files changed, 365 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-8064

Fusion-Task-Lineage: c6d0a9b5-0946-4bf4-8338-e982e1cbfd53

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:47:59 -07:00
gsxdsm
39d76fdabe FN-8062: normalize room message ordering
Normalize room chat snapshots to chronological display order.

- Reverse newest-first API snapshots before cache hydration and rendering.
- Version the room message cache and cover initial, refresh, recovery, SSE, and host layouts.
- Add a patch changeset for the chat transcript ordering fix.

Files changed:
 .changeset/fn-8062-room-message-order.md           |  7 +++
 .../components/__tests__/ChatView.rooms.test.tsx   | 26 ++++++++
 .../app/hooks/__tests__/useChatRooms.cache.test.ts |  8 +--
 .../app/hooks/__tests__/useChatRooms.test.ts       | 73 +++++++++++++++++++++-
 packages/dashboard/app/hooks/useChatRooms.ts       | 45 ++++++++-----
 packages/dashboard/app/utils/swrCache.ts           |  3 +-
 6 files changed, 140 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-8062

Fusion-Task-Lineage: 99bb313c-fb24-40e2-b33b-4811bde6d6cb

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:41:27 -07:00
gsxdsm
e87b51bd07 FN-8054: add pinned chat conversations
Add durable, scoped pinning for Direct chat conversations.

- Add pinned session persistence, migration coverage, and archive-safe row locking.
- Enforce a three-conversation per-project pin limit through the chat API.
- Add desktop and mobile pin controls, sorting, indicators, and regression tests.

Files changed:
 .changeset/fn-8054-pin-conversations.md            |  7 ++
 docs/dashboard-guide.md                            |  2 +
 .../postgres/satellite-db-injected-stores.test.ts  | 13 ++++
 packages/core/src/async-chat-store.ts              | 27 ++++++++
 packages/core/src/chat-store.ts                    | 63 ++++++++++++++++--
 packages/core/src/chat-types.ts                    |  9 +++
 .../core/src/postgres/migrations/0000_initial.sql  |  1 +
 .../postgres/migrations/0012_chat_session_pins.sql |  8 +++
 packages/core/src/postgres/postgres-health.ts      |  3 +
 packages/core/src/postgres/schema-applier.ts       | 30 ++++++++-
 packages/core/src/postgres/schema/project.ts       |  3 +
 packages/dashboard/app/api/legacy.ts               |  1 +
 packages/dashboard/app/components/ChatView.css     | 32 +++++++++-
 packages/dashboard/app/components/ChatView.tsx     | 74 ++++++++++++++++++++--
 .../dashboard/app/hooks/__tests__/useChat.test.ts  | 21 ++++++
 packages/dashboard/app/hooks/useChat.ts            | 72 +++++++++++++++++----
 .../dashboard/src/routes/register-chat-routes.ts   | 25 +++++++-
 17 files changed, 366 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-8054

Fusion-Task-Lineage: 088cb01c-582b-4f56-a222-214da90ff356

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:35:29 -07:00
gsxdsm
e40f76fef9 FN-8057: render planning questions as sanitized Markdown
Render AI-authored planning and interview questions with readable, sanitized Markdown.

- Reuse MailboxMessageContent for active and historical planning questions.
- Preserve question typography while spacing Markdown blocks and lists consistently.
- Cover Markdown formatting across planning, mission, milestone, and slice interviews.

Files changed:
 .changeset/fn-8057-planning-question-markdown.md   |  7 ++++
 .../app/components/ConversationHistory.css         | 18 ++++++++++
 .../app/components/ConversationHistory.tsx         |  6 +++-
 .../components/MilestoneSliceInterviewModal.tsx    | 12 +++++--
 .../app/components/MissionInterviewModal.tsx       | 12 +++++--
 .../dashboard/app/components/PlanningModeModal.css | 27 +++++++++++--
 .../dashboard/app/components/PlanningModeModal.tsx | 18 +++++++--
 .../__tests__/ConversationHistory.test.tsx         | 20 +++++++++++
 .../MilestoneSliceInterviewModal.test.tsx          | 39 ++++++++++++++++++++
 .../MissionInterviewModal.test.tsx                 | 31 ++++++++++++++++
 .../PlanningModeModal.ui-interactions.test.tsx     | 41 +++++++++++++++++++++-
 11 files changed, 221 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-8057

Fusion-Task-Lineage: cf04fc77-c15c-4ca5-a99a-80a9631a1c8c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:07:43 -07:00
gsxdsm
8f176e3b89 FN-8055: align active-agent indicators with task activity
Keep board and list activity styling synchronized with actual agent work.

- Centralize task activity detection across cards, lists, routing, and model-resolution surfaces.
- Show running workflow activity while suppressing stale, paused, stuck-killed, and terminal task states.
- Cover status-null workflow activity and terminal-state regressions.

Files changed:
 packages/dashboard/app/components/ListView.tsx     | 34 ++---------
 packages/dashboard/app/components/RoutingTab.tsx   | 17 +-----
 packages/dashboard/app/components/TaskCard.tsx     | 28 +++------
 .../app/components/__tests__/ListView.test.tsx     | 66 +++++++++++---------
 .../app/components/__tests__/TaskCard.test.tsx     | 44 ++++++++++++++
 .../app/components/effective-model-resolution.ts   | 18 +-----
 .../app/utils/__tests__/taskActivity.test.ts       | 71 ++++++++++++++++++++++
 packages/dashboard/app/utils/taskActivity.ts       | 59 ++++++++++++++++++
 8 files changed, 228 insertions(+), 109 deletions(-)

Fusion-Task-Id: FN-8055
Fusion-Task-Lineage: 9ce38d60-06c8-4050-83f4-263e2f765c86
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 02:01:56 -07:00
gsxdsm
274318aebf FN-8056: enforce task token budgets
Enforce configured task token budgets whenever session usage is persisted.

- Apply soft alerts and hard pauses atomically from all executor persistence paths.
- Exclude cache-read tokens from budget usage and dispatch budget notifications once.
- Document budget semantics and add regression coverage.

Files changed: .changeset/fn-8056-token-budget-enforcement.md     |   7 ++
 docs/settings-reference.md                         |   2 +
 packages/core/src/types.ts                         |   4 +-
 .../src/__tests__/session-token-usage.test.ts      | 101 ++++++++++++++++++++-
 .../src/__tests__/token-budget-enforcer.test.ts    |  81 ++++++++++-------
 packages/engine/src/executor.ts                    |  22 ++++-
 packages/engine/src/session-token-usage.ts         |   8 +-
 packages/engine/src/token-budget-enforcer.ts       |  98 +++++++++++++++++---
 8 files changed, 262 insertions(+), 61 deletions(-)

Fusion-Task-Id: FN-8056

Fusion-Task-Lineage: 5f5ed522-f950-42ce-b4fd-e0b1d45b5815

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:52:10 -07:00
gsxdsm
d501c3b207 FN-8053: align ChatView mobile header tests with drill-in flow
Update ChatView header contracts to enter the direct-thread pane before assertions.

- Add a shared mobile direct-thread helper for context-window tests
- Select the active session before mobile and floating header assertions
- Preserve rename coverage for the direct-thread session switcher

Files changed:
 .../components/__tests__/ChatView.context-window.test.tsx  | 14 ++++++++++++++
 .../components/__tests__/ChatView.core-contracts.test.tsx  |  9 +++++++++
 2 files changed, 23 insertions(+)

Fusion-Task-Id: FN-8053

Fusion-Task-Lineage: a491db31-04b1-4cb5-ac58-c3be297bfe45

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:45:39 -07:00
gsxdsm
a821bce236 FN-8030: move room thinking control into composer
Move room-level thinking effort from the crowded thread header into the composer Brain popover.

- Reuse the thinking-level control in a room-specific level-only mode.
- Preserve room thinking persistence and add desktop, mobile, and failure coverage.
- Update room-chat documentation and publish a patch changeset.

Files changed:
 .changeset/fn-8030-room-thinking-composer.md       |  7 +++
 docs/dashboard-guide.md                            |  2 +-
 .../app/components/ChatThinkingLevelControl.tsx    | 12 +++-
 packages/dashboard/app/components/ChatView.css     | 16 -----
 packages/dashboard/app/components/ChatView.tsx     | 54 +++++++---------
 .../__tests__/ChatThinkingLevelControl.test.tsx    | 20 ++++++
 .../components/__tests__/ChatView.rooms.test.tsx   | 72 +++++++++++++++++++---
 .../__tests__/ChatView.thinking-level.test.tsx     | 23 ++++---
 8 files changed, 137 insertions(+), 69 deletions(-)

Fusion-Task-Id: FN-8030

Fusion-Task-Lineage: 31054538-e620-412d-98ba-26fba34a6af8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:38:49 -07:00
gsxdsm
e46ffebde1 FN-7985: surface review budget exhaustion and configure replan cap
Expose exhausted Plan Review replan budgets for operator approval and allow workflows to configure the cap.

- Add validated numeric workflow setting support and a Plan Review replan-cap setting.
- Route configured cap exhaustion with a distinct approval reason and preserve fallback behavior.
- Display the budget-exhaustion state across task cards, lists, and details.
- Add tests, localized copy, documentation, and a minor changeset.

Files changed:
 .changeset/fn-7985-review-budget-approval.md       |  7 ++++
 docs/settings-reference.md                         |  9 +++--
 docs/workflow-steps.md                             |  2 +-
 .../builtin-workflow-settings-triage.test.ts       | 27 +++++++++++++--
 packages/core/src/builtin-workflow-settings.ts     | 17 ++++++++++
 packages/core/src/index.gate.ts                    |  1 +
 packages/core/src/index.ts                         |  1 +
 packages/core/src/workflow-ir-types.ts             |  4 +++
 packages/core/src/workflow-ir.ts                   | 27 +++++++++++++++
 packages/core/src/workflow-settings-resolver.ts    |  1 +
 packages/core/src/workflow-settings.ts             |  6 ++++
 packages/dashboard/app/components/ListView.css     | 19 +++++++++++
 packages/dashboard/app/components/ListView.tsx     | 22 +++++++++---
 packages/dashboard/app/components/TaskCard.css     | 18 ++++++++++
 packages/dashboard/app/components/TaskCard.tsx     |  6 ++--
 .../dashboard/app/components/TaskDetailModal.tsx   |  4 +--
 .../app/components/__tests__/ListView.test.tsx     | 30 +++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 17 ++++++++--
 .../app/components/workflow-setting-display.ts     | 11 ++++++
 .../dashboard/app/utils/reviewBudgetApproval.ts    | 11 ++++++
 .../triage-plan-review-replan-cap.test.ts          | 39 +++++++++++++++++++---
 packages/engine/src/triage.ts                      | 21 ++++++++----
 packages/i18n/locales/en/app.json                  |  2 +-
 packages/i18n/src/resources.d.ts                   | 19 ++++++++---
 24 files changed, 288 insertions(+), 33 deletions(-)

Fusion-Task-Id: FN-7985

Fusion-Task-Lineage: 125f101c-caca-45c2-8b40-996b2a31c019

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:32:23 -07:00
gsxdsm
375368e147 FN-8051: ensure PostgreSQL schemas initialize before plugin hooks
Ensure required PostgreSQL namespaces exist before plugin initialization on every boot.

- Create project, central, and archive schemas under the schema advisory lock before hooks run
- Cover marker-present databases with a plugin-hook schema availability regression test
- Add a patch changeset for the reliability fix

Files changed:
 .changeset/fn-8051-schema-init.md                  |  7 ++++
 .../src/__tests__/postgres/schema-applier.test.ts  | 43 ++++++++++++++++++++++
 packages/core/src/postgres/schema-applier.ts       | 12 ++++++
 3 files changed, 62 insertions(+)

Fusion-Task-Id: FN-8051

Fusion-Task-Lineage: a3b20683-a742-4a8c-9cfc-fbf316c5649b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:26:13 -07:00
gsxdsm
3f5d7c2147 FN-8049: preserve desktop geometry across sheet views
Prevent full-screen sheet opens from replacing saved desktop FloatingWindow geometry.

- Suspend geometry restoration and writes for sheet modal surfaces without adding storage keys.
- Preserve Artifact Gallery geometry at both narrow-width and short-height sheet breakpoints.
- Cover sheet persistence behavior and document desktop geometry restoration.

Files changed:
 docs/dashboard-guide.md                            |   3 +
 packages/dashboard/app/App.tsx                     |   4 +
 .../dashboard/app/components/ArtifactsGallery.tsx  |   3 +
 .../dashboard/app/components/FileBrowserModal.tsx  |   2 +
 .../dashboard/app/components/FloatingWindow.tsx    |  25 ++++-
 .../dashboard/app/components/GitHubImportModal.tsx |   4 +
 .../app/components/MissionInterviewModal.tsx       |   2 +
 .../dashboard/app/components/PrCreateModal.tsx     |   2 +
 .../app/components/ScheduledTasksModal.tsx         |   2 +
 .../app/components/WorkflowNodeEditor.tsx          |   2 +
 .../components/__tests__/FloatingWindow.test.tsx   | 118 ++++++++++++++++++++-
 .../app/hooks/__tests__/useViewportMode.test.ts    |  13 ++-
 packages/dashboard/app/hooks/useViewportMode.ts    |  15 +++
 13 files changed, 190 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8049

Fusion-Task-Lineage: 4c659046-694a-42fa-aca1-e702c1d3842a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 01:19:43 -07:00
gsxdsm
4b7f0d2d0e fix(dashboard): theme every Settings checkbox and move all inline help behind ? icons
- One settings-scoped checkbox rule (accent, size, focus ring) so the Advanced-settings
  toggle, SettingsToggleRow, ntfy/webhook card headers, and MCP toggle stop falling
  back to the browser-default accent.
- Fix the empty/off-screen help bubble on mobile: .notification-provider-header and
  .settings-field-label-row are now positioned ancestors for SettingsHelpTip.
- Migrate every remaining inline <small> description across settings sections to the
  shared SettingsHelpTip "?" affordance (validation errors and live status stay inline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:06:43 -07:00
gsxdsm
5372dcc37d fix(dashboard): center quick-add control rows on mobile
Center each wrapped row below the quick-add textarea (workflow/steps,
option chips, icons+Save) so all rows share equal left/right insets on
mobile, instead of options hugging the left edge and Save hugging the
right. Desktop/tablet keep the left-options / right-Save toolbar layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:48:23 -07:00
gsxdsm
a31c370375 FN-8045: add transactional handoff failure-injection seam
Ensure PostgreSQL review handoffs roll back all dependent writes after an injected late failure.

- Add a test-only failure injector after transactional handoff writes.
- Include workflow work in same-column retry transactions.
- Restore PG-backed handoff atomicity coverage and remove its quarantine.

Files changed:
 packages/core/src/store.ts                         |  24 +++
 packages/core/src/task-store/moves.ts              |  21 ++-
 .../in-review-handoff-atomic.test.ts               | 172 +++++++++++++--------
 packages/engine/vitest.config.ts                   |   1 -
 scripts/lib/test-quarantine.json                   |   5 -
 5 files changed, 151 insertions(+), 72 deletions(-)

Fusion-Task-Id: FN-8045

Fusion-Task-Lineage: 517e3000-9b88-4b0d-9b25-1a585eb8f322

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 00:44:35 -07:00
gsxdsm
c5aa5ecfc7 fix(dashboard): keep quick-add Save inline with its icon controls on mobile
Tighten the mobile primary-group gap to --space-xs and the icon-only
controls' min-width floor to 32px so the Save button no longer wraps to
its own line in the board quick-add composer. Touch-target height is
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:38:33 -07:00
gsxdsm
261901343e fix(core): split the domain project field from the RLS partition column (#2165)
## Problem

Migration 0006 made `project_id` the RLS isolation partition on every
`project`-schema table — stamped by a BEFORE INSERT trigger from the
`fusion.project_id` session GUC, with every PK/unique/FK rewritten to
composite `(project_id, …)`. Eleven tables **also** carried a
caller-supplied domain `projectId` on their TS types and wrote that
domain value into the same physical column.

When the domain value differs from the session GUC, the parent row lands
in the domain partition while child rows (`research_run_events`,
`experiment_session_records`, `eval_task_results`, …) land in the
session partition — and the composite FK fails with SQLSTATE 23503.
Appending an event to a project-owned research run could not persist.

## Fix

**Decision (operator): separate domain column; `project_id` stays the
partition.**

- **Migration `0011_owner_project_id.sql`** adds a nullable
`owner_project_id` domain column to the 11 conflated tables
(`research_runs`, `experiment_sessions`, `todo_lists`, `eval_runs`,
`chat_sessions`, `chat_rooms`, `ai_sessions`, `chat_token_usage`,
`project_insights`, `project_insight_runs`, `cli_sessions`), backfills
it from `project_id` (identical in production, so exact; the
`__legacy_unscoped__` sentinel backfills to NULL), and indexes it.
Idempotent, `to_regclass`-guarded per the 0007 pattern.
- **Stores** (`async-research-store`, `async-experiment-session-store`,
`async-todo-store`, `async-chat-store`, `async-ai-session-store`,
`async-eval-store`, `async-insight-store`, `cli-session-store`, …) stop
writing `project_id` entirely — the trigger/GUC owns the partition — and
map their domain `projectId` field to `owner_project_id` for both reads
and filters. TS types unchanged.
- **Applier** registers `OWNER_PROJECT_ID_SPLIT_VERSION = "0011"` and
advances `SCHEMA_BASELINE_VERSION`.

## Verification (re-run independently of the implementing agent)

- Core `tsc --noEmit`: exit 0 · `pnpm lint`: exit 0 · `pnpm
check:changesets`: exit 0 · `pnpm test:gate`: 185/185
- Full postgres suite: **5 failed / 807 passed** vs a **7 / 804**
baseline — the two conflation round-trips
(`satellite-db-injected-stores` ResearchStore + ExperimentSessionStore)
go green, zero new failures. The remaining 5 are pre-existing
unbound-harness `__meta`/identity failures, unrelated to this change.

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

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

* **Bug Fixes**
* Corrected project-scoped persistence and queries across AI sessions,
chats (rooms + token usage), evaluations/experiments, insights,
research, and todos by separating domain ownership from RLS
partitioning.
* Prevented foreign-key and row-level security violations when storing
or retrieving project-scoped data, including legacy records.
* **Database / New Features**
* Added migration 0011 introducing `owner_project_id` and backfilling
existing rows to preserve ownership while improving isolation.
* **Tests**
  * Updated migration-parity coverage to include the new baseline step.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:30:59 -07:00
gsxdsm
9b8edf06c4 fix(FN-2127): Quality experimental gate, done-task worktrees, hub layout (#2164)
## Summary

Follow-up on **#2127** (Quality plugin already on `main`). This PR only
lands the remaining Quality deltas that were not merged:

- **Experimental gate fix** — `TaskStore.getSettings()` is async; the
gate now awaits merged settings so enabling
`experimentalFeatures.qualityPlugin` actually works, and status-bearing
errors return structured `{ status, body }` instead of collapsing to
hard failures
- **Done-task QA worktrees** — when a task has no live worktree (typical
after land), preview/task runs create a disposable checkout under
`.fusion/quality-qa/` at the task branch or merge commit so processes
run the **done task’s code**, not project root
- **Hub layout** — shared `ViewHeader` + dashboard spacing/typography so
Quality matches Insights / Compound Engineering / Goals

Scoped to `plugins/fusion-plugin-quality/**` only (rebased onto current
`main`; duplicate plugin-landing commits dropped).

## Test plan

- [ ] Enable **Settings → Experimental → Quality Plugin**, restart if
routes were cold
- [ ] Quality hub: header matches other views; refresh + presets work
- [ ] Done task → QA tab → Start preview uses QA worktree at
branch/merge commit (not project root)
- [ ] Active task with live worktree still uses that worktree
- [ ] Flag off: clear experimental-disabled error (not generic empty
failure)
- [ ] `pnpm --filter @fusion-plugin-examples/quality test` (32 tests)
2026-07-16 00:16:12 -07:00
gsxdsm
9a34862586 refactor: package code organization waves 3–5 (#2148)
## Summary

Waves 3–5 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`).
Behavior-preserving peels after #2139 and #2143.

### Wave 3 — Merger + heartbeat recovery
- **`merger-errors.ts`** — verification/abort error classes
- **`merger-owned-landed.ts`** — ownership classification +
`Fusion-Task-Id` trailer
- **`merger-conflict-resolution.ts`** — conflict classify/auto-resolve
- **`agent-heartbeat-error-recovery.ts`** — durable error-recovery
budget helpers

### Wave 4 — Self-healing + dashboard API
- **`self-healing-constants.ts`** — public timing/budget constants
- **`self-healing-branch.ts`** — `isBranchAheadOfBase`
- **`app/api/client.ts`** — `api` / `ApiRequestError` / `buildApiUrl` /
`proxyApi`
- **`app/api/health.ts`** — health, engine status, updates +
`withProjectId`

### Wave 5 — Types tracking + merger parse + task CRUD
- **`types/task-tracking.ts`** — PR/issue/GitHub/GitLab tracking
contracts
- **`merger-git-parse.ts`** — `parseFailingFilesFromOutput`,
`parsePorcelainZ`, `parseShortstatSummary`
- **`app/api/tasks.ts`** — task list/detail/create/update/move client
surface
- Line-count baselines ratcheted down for `merger.ts`, `types.ts`,
`legacy.ts`

Public import paths stay on parent modules / `legacy.ts` / package
barrels via re-exports.

## Test plan
- [x] core/engine/dashboard typecheck (including `tsconfig.app.json`)
- [x] eslint on touched modules
- [x] `parse-porcelain-z` + merger parseFailing/getBranchChanged tests
- [x] dashboard `api-tasks` + legacy-prinfo/pr-types (69)
- [ ] CI merge gate

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

- **New Features**
- Added dashboard API support for task listing/detail, archiving,
creation, review updates, duplicate detection, bulk model updates,
moving tasks, and overlap repair.
- Added health/engine status and refresh/start controls, plus update
checking.

- **Bug Fixes**
- Improved dashboard API handling for non-JSON/HTML responses with
clearer errors, better URL routing for remote nodes, and project-scoped
queries.
- Strengthened automated recovery for heartbeat error/model-unavailable
scenarios and safer merge-conflict classification/auto-resolution.

- **Tests**
- Updated merge-conflict resolution and lifecycle test mocks to match
the updated git command behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 00:01:17 -07:00
gsxdsm
d6f94693b2 FN-8028: widen tablet chat message bubbles
Increase tablet agent-side chat bubble width while preserving responsive behavior.

- Raise the tablet assistant, streaming, and failure bubble cap to 92%.
- Update the responsive CSS contract and dashboard documentation.

Files changed:
 docs/dashboard-guide.md                                              | 2 +-
 packages/dashboard/app/components/ChatView.css                       | 5 ++++-
 packages/dashboard/app/components/__tests__/ChatView.core-contracts.test.tsx        | 4 ++--
 3 files changed, 7 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8028

Fusion-Task-Lineage: 2158a4c8-f07b-4e96-bcf2-80ebe6b08515

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 23:49:24 -07:00
gsxdsm
5ab46a6430 feat(dashboard): compact the GitHub import screen and move Import under the issue (#2163)
Mobile operator report: the import screen spent ~4 rows on chrome — a
provider row, a tab row, and a boxed ORIGIN/filter/Load stack — leaving
only **~9 issues visible**. Import also sat *above* the preview it acts
on, and a second Import in the list footer let you import an issue whose
body you had never opened.

## Before → After (measured at 412px)

| | before | after |
|---|---|---|
| control chrome | ~4 stacked bands (93px) | **one wrapping row (70px)**
|
| toolbar | boxed band, origin on its own line | **single 36px row** |
| issues visible | ~9 | **~13** |
| Import | list footer **and** preview header | **detail bottom bar
only** |

## Layout

- **Provider, type tabs, origin, filter and Load share one row.** It
wraps rather than clipping at the narrowest widths.
- **Load is icon-only** — the label survives as `aria-label`/`title`, so
the accessible name is unchanged (the pre-existing role+name query still
finds it, which is what proves nothing was lost for screen readers).
- **The labels filter is a popover.** Its trigger doubles as the readout
— it renders the active labels and takes an `is-active` cue — so
collapsing never hides applied state. Dismisses on outside pointerdown
or Escape; Escape `stopPropagation` keeps the modal from closing along
with it.
- **Origin stays visible as an inline chip** (per your call): it's
context for what you're importing, so it flattens from a stacked
ORIGIN/repo block rather than hiding in the popover.
- Removed the toolbar's `flex: 1 1 100%` mobile stacking — it dated from
when the toolbar was a full-width band and was forcing origin to claim
an entire line.
- Also neutralised `[data-theme="light"] .github-import-toolbar`, which
re-applied the band background later in the cascade at equal specificity
(light mode only).

## Actions — one place to import

Import + Close issue move from the preview header to a **bottom action
bar**: commit actions belong below the content they act on and within
thumb reach, matching the modal's own Cancel bar. The list footer's
duplicate Import is removed (Cancel stays — the modal still needs a
dismiss), so **an issue can no longer be imported sight-unseen**.

## Note for review — interaction with #551a2a3c1

`551a2a3c1` ("align import detail header") landed mid-work and
conflicted. Its panel-padding work is preserved untouched. Its header
rules were kept **as-is** because they remain correct for a lone label,
but its stated rationale — dropping `space-between` so "Close issue and
Import stay grouped as a pair at the end" — is moot now that those
actions live in the bottom bar. The comment is marked superseded rather
than left asserting something untrue.

## Verification

Measured in a real browser at 412px (jsdom has no layout, so the tests
pin structure and behaviour, not geometry): chrome 93→70px, toolbar a
single 36px row, ~13 issues visible, filter popover clamped inside the
viewport and autofocused, detail bar below the content with an unclipped
Import label. Re-verified after the rebase.

**90 tests pass** · gate green (294/122/63) · lint clean · typecheck
clean.

Seven existing tests encoded the old UI (labelled Load, always-open
filter input). They were **updated to the new contract rather than
appeased** — the filter assertions now exercise the whole affordance
(collapsed → open → filters), and FN-7657 persistence is asserted via
the *collapsed trigger*, proving both that state survives remount
**and** that a restored filter stays visible, which is the real risk
when a control collapses.

Two things worth knowing:

- The footer guard was **confirmed non-vacuous**: reintroducing the list
Import fails it.
- One new CSS rule is deliberately a child selector
(`.github-import-controls > *`) rather than naming
`.github-import-tabs`, because `GitHubImportModal.test.tsx` extracts
base rules with a naive first-match regex
(`/\.github-import-tabs\s*\{[^}]*\}/`) that a rule mentioning them
*above* the originals would silently hijack.

🤖 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 a more compact, responsive mobile layout for the GitHub import
screen.
* Moved Import and Close issue actions to the preview’s bottom action
bar.
* Added a collapsible labels filter popover with keyboard and
outside-click dismissal.
  * Consolidated mobile controls into a single compact row.
  * Simplified the Load action to an accessible icon-only button.

* **Bug Fixes**
  * Removed duplicate Import controls from the list footer.
* Improved control sizing, wrapping, and preview action positioning on
narrow screens.

<!-- 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 23:48:47 -07:00
gsxdsm
81d4bee59b fix(settings): Source Control nav icons, Authentication as the landing section, CLI binary behind Advanced (#2162)
Follow-up to #2158. That PR was squash-merged while this last commit was
still in flight, so these four fixes never landed on main.

### Source Control had no icon in the nav
A section's own `icon` now wins over the generic scope glyph, which
becomes the fallback. `icon` previously only rendered when `scope` was
`undefined`, so **any** scoped section was forced to the globe/folder
pair and could not identify itself — Source Control was
indistinguishable from MCP Servers and Scheduling. Both Source Control
entries now carry a git-branch icon. Nothing is lost: these sections
spell their tier out in the label (`· Global` / `· Project`), and the
aria-label and tooltip still announce it.

### Settings opens on Authentication
Previously "General · Global". Nothing else in the product works until a
provider is connected — the dashboard's own empty state sends operators
to Settings for exactly this. Authentication leads the **AI & Models**
group rather than sitting under Integrations: connecting a provider and
picking the models it gates are one task, in that order. A test asserts
the landing section is never one the Advanced switch hides, so a
default-configured operator cannot land on a section their own nav does
not list.

### The `fn` CLI binary panel moved behind Advanced, at the bottom
It used to render *first* inside "General · Global" — an
install/version/path panel was the first thing an operator saw on
opening Settings. It is machine plumbing touched once, or when an
install breaks, so it belongs with the other specialist surfaces.

### Changing section starts you at the top of it
Sections keep no scroll of their own, so the container's offset carried
over: leaving a long section scrolled halfway and picking a short one
landed mid-content — on mobile, often past everything, on an apparently
blank screen. Guarded by a ref rather than by reading the highlight key,
because a search jump also changes `activeSection` and that key
self-clears ~1.6s later, which would re-run the effect and yank the
operator off the row they just jumped to.

## Verification
- Typecheck clean (`tsconfig.app.json` — the one that covers `app/`).
- Settings suites run against this branch and against plain main:
**identical 67-failure set, 0 new**. Those 67 are pre-existing on main
(51 scheduling-merge + 16 remote-notifications) and are untouched here.
- Browser-verified: lands on Authentication; Source Control renders
`lucide-git-branch`; CLI Binary is last and hidden with Advanced off;
section change resets scroll 1200 → 0 while a search jump still holds
its row.

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


## Evidence

Both Source Control rows now carry a git-branch icon, sized and aligned
like their neighbours; Models keeps globe/folder.

![Settings nav showing git-branch icons on both Source Control
rows](https://raw.githubusercontent.com/Runfusion/Fusion/f70cda8eb75bcdf8e6a8d68bdaec430044383482/shots/nav-git-icons.png)


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

## Summary by CodeRabbit

* **New Features**
  * Settings now opens to **Authentication** by default.
  * Added a dedicated advanced-only **CLI Binary** settings section.
* Improved navigation icons for Authentication and Source Control
sections.

* **Improvements**
* Settings sections now reliably scroll to the top when switching
sections.
  * Search-result navigation preserves the selected result’s position.

* **Tests**
* Updated settings and mobile navigation coverage for the new default
section and labels.

<!-- 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 23:46:37 -07:00
gsxdsm
7aeefe21e3 fix(dashboard): stop clipping the composer Save button label on mobile (#2161)
The board composer's Save button rendered its label cut off on mobile
("Sav|").

## Root cause (measured, not guessed)

Reproduced in Chromium at a 412px viewport and measured the live layout:

- The primary action group needs **~275px** inside a **260px** column —
a 15px deficit.
- All five icon controls carry an explicit `min-width: 36px`
touch-target floor, so they **cannot** absorb it.
- Save's own automatic minimum size — which would normally floor a flex
item at its min-content width — is **zeroed by `overflow: hidden`**. Per
the flexbox spec, automatic minimum size applies only when `overflow` is
`visible`.

That `overflow: hidden` exists for a **vertical** reason
(FN-7680/FN-7683 height equalization). So a height fix silently made
Save the only horizontally-shrinkable control in the row: it absorbed
the entire deficit (**40px actual vs 55px needed**) and clipped its own
label.

## Fix

- Save is pinned to its content width (`flex: 0 0 auto; min-width:
max-content`) so it can never be squeezed. `overflow: hidden` stays — it
still owns the vertical clamp.
- The group may `flex-wrap: wrap` with `justify-content: flex-end`, so a
genuine deficit reflows to a second right-aligned line instead of
clipping.

**Not breakpoint-scoped** (FN-5751): the mechanism is width-driven, not
media-driven — mobile only trips it first because its 36px touch targets
are wider than the desktop chips. Where the row already fits, both rules
are inert.

This revises the older "wraps as one unit, never splitting Save from its
neighbors" intent: at widths where the row genuinely cannot fit, Save
wrapping to its own right-aligned line is strictly better than a clipped
label.

## Verification (in-browser, both breakpoints)

| | 412px (mobile) | 1400px (desktop) |
|---|---|---|
| label clipped | **no** (scrollWidth 67 === clientWidth 67) | no |
| Save width | 69px (full) | 69px |
| Save height | 36px — equal to icon siblings | unchanged |
| overflow | none (right edge 292 === container 292) | none |
| layout | wraps to a second right-aligned line | **no-op** — still one
line (group height 28px) |
| icon touch targets | all five still ≥36px | unchanged |

Gate green · lint clean · 288 dashboard composer tests pass.

## On the test

The CSS guard is a **string-match, not a layout proof** — jsdom has no
flex layout and cannot observe clipping, so the real proof is the
browser measurement above. The test exists so the invariant-bearing
declarations can't be silently dropped or re-scoped into a media query.
It was **confirmed non-vacuous**: 3 of its 4 cases fail against the
pre-fix CSS.

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

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

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed the Quick Entry Save button label being clipped on narrow mobile
screens.
* Improved action layout wrapping while preserving icon touch-target
sizing.

* **Tests**
* Added regression coverage to verify the Save button remains fully
visible across narrow layouts.

<!-- 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 23:45:53 -07:00
gsxdsm
db05a77fea FN-8026: fix idle reconnecting hints in interview modals
Keep reconnecting indicators scoped to active interview generation states.

- Hide transient reconnecting hints on persisted mission and milestone questions
- Limit subtask reconnecting hints to the generating state
- Cover active and persisted reconnecting behavior with modal tests
- Add a patch changeset for the dashboard fix

Files changed:
 .changeset/FN-8026-interview-reconnecting-hint.md  |  7 ++++
 .../components/MilestoneSliceInterviewModal.tsx    |  6 +++-
 .../app/components/MissionInterviewModal.tsx       |  6 +++-
 .../app/components/SubtaskBreakdownModal.tsx       |  6 +++-
 .../MilestoneSliceInterviewModal.test.tsx          | 37 ++++++++++++++++++++++
 .../__tests__/MissionInterviewModal.test.tsx       | 20 +++++-------
 .../__tests__/SubtaskBreakdownModal.test.tsx       | 19 ++++++-----
 7 files changed, 76 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-8026

Fusion-Task-Lineage: 60b2f741-6761-4fc0-bf50-cbddce5f5b6f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 23:39:12 -07:00
gsxdsm
513796b7e7 FN-8024: align insight task creation mock
Align the App task-creation mock with the current default-column behavior.

- Remove the obsolete triage column expectation from the insight task creation test.

Files changed:
 packages/dashboard/app/components/__tests__/App.test.tsx | 1 -
 1 file changed, 1 deletion(-)

Fusion-Task-Id: FN-8024

Fusion-Task-Lineage: f1dd7edd-8338-4367-b023-0adfe8ee4599

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 23:31:43 -07:00
gsxdsm
551a2a3c1e fix(dashboard): align import detail header and show translated titles in its title bar
The GitHub/GitLab import detail panel had no inset of its own, so it sat
flush against the window's left edge while FN-8015's resize gutter left a
gap on the right only — "Preview" was clipped and the whole header read as
misaligned on both the desktop window and the mobile sheet. Give the panel a
symmetric inset, letting that existing gutter supply the right side rather
than overriding it (which would push the inner scrollbar back into the
resize hot zone the gutter protects).

Also in the header: `space-between` spread three children apart and flung
"Close issue" into the middle, so the label now takes the free space and the
two actions stay grouped as a pair. Both actions size from one rule instead
of each inheriting its own .btn defaults, at a 40px touch target on mobile
where Import is the sheet's primary action. "Preview" becomes a muted
eyebrow matching the existing ISSUE #NNNN label.

Fix title truncation while here: `.floating-window__title` declared
text-overflow: ellipsis but was display:flex, which made the text an
anonymous flex item that text-overflow cannot act on, so titles hard-cut
mid-word. Every caller passes a plain string, so a block box makes the
existing declaration work as written.

Finally, the detail title bar kept the raw upstream title while the card
below showed the translation — one item displaying two different titles at
once. Both now read importTranslation.display.title, gated on activeTab to
match translateSelection so an item's number is never paired with the other
tab's title. The existing translation test now asserts both surfaces in both
directions; it fails without this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:22:33 -07:00
gsxdsm
43e03d3ac2 FN-8035: consolidate done actions into card menu
Done-card archive and revert actions now use the existing three-dot menu.

- Remove the duplicate inline Actions dropdown and its state handling.
- Cover done-card archive and revert menu behavior through the unified context menu.

Files changed:
 packages/dashboard/app/components/TaskCard.tsx     | 74 ++--------------------
 packages/dashboard/app/components/__tests__/TaskCard.test.tsx | 71 +++++++++++++--------
 2 files changed, 50 insertions(+), 95 deletions(-)

Fusion-Task-Id: FN-8035

Fusion-Task-Lineage: 64f57e12-829c-445b-aab0-df3996af5502

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 23:17:59 -07:00
gsxdsm
08c546dc75 test(core): bind the U14 postgres harness to a project, as production does
The usage-events round-trip failed because the harness ran unbound. Production
binds `fusion.project_id` per connection (connection.ts) and only falls back to
`fusion.project_bypass=on` when no projectId is given, so an unbound harness
wrote blank project_ids that the migration-0006 trigger rewrote to
'__legacy_unscoped__' -- and helpers scoping on `layer.projectId ?? ""` then
looked for a literal '' the database never stores.

Unbound is a shape production forbids: AgentStore.backendProjectId throws on it
("Reject unbound backend heartbeat/run access instead of silently reading or
writing the legacy empty-string partition"). The harness was wrong, not the
product -- an earlier attempt to make the product accommodate the unbound
harness was reverted in b51de02a5.

Binds both the layer and the admin connection: the admin connection seeds
fixtures the layer reads back, so it must sit in the same partition or the
layer cannot see its own setup. Three reads that relied on the unbound default
now pass the project id, matching how production callers thread
`layer.projectId` -- getLiveTaskColumn resolves a missing id to the sentinel
partition, so omitting it looked in the wrong place once rows were bound.

No product code changes. 24/24.

The same binding does NOT fit the satellite suites and they are left alone:
satellite-fusiondir has a test asserting the unbound APIs fail closed (binding
defeats its premise) and another that binds two projects itself, so that
harness needs an opt-out parameter rather than a blanket bind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 23:06:20 -07:00
gsxdsm
beae12a4bf FN-8034: enforce component and token reuse
Document standing guidance that keeps UI implementations consistent with existing systems.

- Require reuse of components, primitives, hooks, and helpers before creating alternatives.
- Require dashboard styling to use design tokens and component-scoped CSS.
- Link the styling guide, token source, and documented solution patterns.

Files changed:
 AGENTS.md | 8 ++++++++
 1 file changed, 8 insertions(+)

Fusion-Task-Id: FN-8034

Fusion-Task-Lineage: f0435e22-8e8e-471e-b188-7e1a732c2e41

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 23:01:27 -07:00
gsxdsm
1d6a0449ce fix(engine): keep replan cards plannable when they carry prior steps
Tasks piled up in the Planning column and never moved. hasAdvancedPastPlanning
counted steps.length > 0 as proof a card had advanced past planning, but a
replan card legitimately retains the steps its previous planning pass
materialized. The still-in-planning guard therefore failed for every card
Plan Review sent back, so triage's specifyTask claim silently skipped its
status:"planning" write and re-claimed the same cards every poll — never
planning them, and starving healthy cards out of the maxTriageConcurrent
slots they held.

Steps are no longer advancement evidence while a card sits in a planner lane:
the "triage" column, and the merged "todo" planner lane used by plan-in-place
workflows when the card carries a planning status. Worktrees and
execution/terminal columns remain durable advancement evidence, preserving
FN-7977's protection against a recovery write clobbering a card that raced
ahead into execution.

The primary claim path now warns instead of returning silently; recovery-write
skips stay silent by design. The silence is why this stalled the planner for
hours undiagnosed.

Regression coverage asserts the invariant across both planner surfaces rather
than the reported repro alone: triage cards with and without an explicit
needs-replan status, plan-in-place todo replans, every parked-for-planning
status, and the advancement signals that must still fire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:53:49 -07:00
gsxdsm
7b31d54b98 fix(FN-8004): make AI merge rejections actionable and stranded merges retryable (#2160)
Two follow-ups to FN-8004. Both were found by watching FN-8004's *own*
merge livelock for 40 minutes — it turned out to be blocked by the very
class of bug it was filed to fix.

## 1. AI merge rejections lost their reasons

The reviewer prompt said **both** of these:

> "**End with a single decision line**: `REVIEW_VERDICT:
approve|reject`"
> "**Then list each concrete reason as a bullet.**"

Those are impossible to satisfy at once. Reviewers obeyed "End with" and
wrote their reasoning *above* the verdict — but `extractRejectReasons`
only scanned lines *after* it. So every such rejection collapsed to the
placeholder `reviewer rejected the merge without a stated reason`, and
that placeholder was then handed to the corrective re-merge pass **as
its instruction**. The pass got no actionable feedback and just
re-rolled the merge.

The evidence, from FN-8004's own merge — the pattern repeated across
*both* attempts:

| | Attempt A | Attempt B |
|---|---|---|
| review pass 1 | rejected, no reason (03:46) | rejected, no reason
(03:57) |
| corrective pass | 1/3 | 1/3 |
| review pass 2 | **approved** `a3a3cc6a8` (03:49) | approved |

A reviewer that rejects and then approves identical content isn't
objecting — the reason was being thrown away. Each wasted cycle cost ~7
minutes, stretching the merge past main's ~8-minute churn window so
every attempt lost to a concurrent advance and rebuilt. **The livelock
was caused by the lost-reason bug.**

Fix: the parser recovers reasons from either side of the verdict (inline
→ after → before, nearest-first so the closing argument leads, capped at
8 so a long transcript can't flood the corrective prompt), skipping
severity/verdict/markdown scaffolding. The prompt ordering is now
unambiguous — reasons first, verdict last, nothing after it.

## 2. An orphaned merge-active stamp was un-retryable by hand

The Retry gate refused **every** merge-active status (`Task is not in a
retryable state (current status: landing)`), while self-healing cleared
stale stamps automatically minutes later. So a merger killed mid-flight
— crash, engine restart, operator SIGTERM — blocked the operator's own
escape hatch at exactly the moment they'd reach for it. FN-8004 hit
this: a killed merge left `landing` stamped and Retry 400'd for the full
sweep delay.

`isStaleMergeActiveStatus` now lives in the leaf
`merge-active-status.ts`, shared by `recoverStaleMergingStatus` and the
Retry gate — so **the manual path can never be stricter than the
automatic one**. This is the same one-concept-two-definitions bug as
FN-8004's transient classifier, which is why it's worth fixing
structurally rather than adding another special case.

A live merge stays protected by two independent signals: it holds the
in-process lease **and** refreshes `updatedAt` each phase. Staleness
fails closed on an unparseable timestamp.

One subtlety worth reviewing: the bypass feeds `isInReviewRetry` rather
than only the gate. A bare gate bypass would fall through to the generic
branch and move fully-executed work to `todo`, **re-running finished
work** — a bug this fix could easily have introduced.

## Verification

- Gate green (294 + 122 + 63) · lint clean · engine + dashboard
typecheck clean · `verify:fast` PASS
- 70 merger-suite tests green; all 7 pre-existing verdict-parser tests
still pass (backward compatible — none of them covered the verdict-last
layout, which is exactly why this shipped)
- **The route regression test was confirmed non-vacuous**: neutralizing
the fix fails the two "now retryable" cases while the three
live-merge-protection cases still pass, proving they guard real behavior
rather than the new code.
- Regression tests assert the invariant across every surface per *Fix
the Invariant, Not the Repro*: all five `ACTIVE_MERGE_STATUSES` (a
merger can die in any phase, not just the reported `landing`), both
live-merge signals, boundary conditions, fail-closed paths, and that
pre-existing retry paths are unchanged. Test files carry the required
`## Symptom Verification` and `## Surface Enumeration` sections.

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

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

* **Bug Fixes**
* AI merge rejections now reliably include concrete, correctly ordered
reasons, even when provided before the verdict line.
* Manual retry can recover tasks stuck in stale merge-processing states.
* Retry is still blocked for tasks tied to active merge activity or
recently updated/advancing merges.
  * Existing failed-merge retry behavior remains unchanged.

* **Reliability**
* Improved shared handling of “orphaned” merge-active detection across
the engine and dashboard.

* **Tests**
* Added/expanded coverage for merge-active staleness, retry eligibility,
and verdict/reason parsing.
<!-- 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 22:43:24 -07:00
gsxdsm
471835de3d FN-8031: render planning summaries as Markdown by default
Render planning summary descriptions as formatted Markdown on first display.

- Initialize the planning summary view in Markdown preview mode.
- Update interaction tests for preview-first description editing.
- Add a patch changeset for the planning summary behavior.

Files changed:
 .changeset/fn-8031-planning-summary-markdown.md     |  7 +++++++
 .../dashboard/app/components/PlanningModeModal.tsx  |  6 +++++-
 .../PlanningModeModal.ui-interactions.test.tsx      | 21 +++++++++++++++------
 3 files changed, 27 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-8031

Fusion-Task-Lineage: 4e0647a4-a44c-4774-937f-c9e6144837de

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 22:25:09 -07:00
gsxdsm
889fbf709e FN-8022: fix stale GitHub route tests
Refresh GitHub route tests for asynchronous audit lookup and project-scoped conflict reclaim.

- Mock asynchronous run-audit reads for done-task diff resolution
- Cover file-diff responses resolved from audit commit SHAs
- Provide the test project ID for PR conflict reclaim routes

Files changed:
 packages/dashboard/src/__tests__/routes-github.test.ts | 47 ++++++++++++++++++++--
 1 file changed, 44 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8022
Fusion-Task-Lineage: f7557c0d-f9fd-4f57-8b4f-aee9d0a29364
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 22:19:28 -07:00
gsxdsm
b51de02a54 Revert "fix(core): resolve unbound project ids to a real partition or no filter"
This reverts commit a048a619fc.
2026-07-15 22:14:14 -07:00
gsxdsm
959a7877c8 FN-8020: harden agent heartbeat health classification
Validate the four-interval heartbeat grace window and classify invalid persisted heartbeats safely.

- Cover reported field heartbeat ages in dashboard and engine health checks.
- Mark unparseable heartbeat timestamps as unresponsive and clamp future timestamps to fresh.
- Align dashboard health documentation with the existing four-interval grace window.

Files changed:
 .../app/utils/__tests__/agentHealth.test.tsx       | 48 +++++++++++++++++++++-
 packages/dashboard/app/utils/agentHealth.tsx       | 26 ++++++++++--
 .../src/__tests__/heartbeat-executor.test.ts       | 31 ++++++++++++++
 3 files changed, 100 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-8020

Fusion-Task-Lineage: 2bc0df78-d68c-489b-8bfb-9b09da10cdfa

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 22:11:26 -07:00
gsxdsm
e57f52bb0d FN-8015: keep floating window scrollbars clear of resize handles
Keep desktop floating-window scrollbars away from right-edge resize targets.

- Reserve a shared right-side gutter on floating-window bodies.
- Cover scrollbar clearance and right-edge resize behavior with component tests.

Files changed:
 .../dashboard/app/components/FloatingWindow.css    |  4 +
 .../components/__tests__/FloatingWindow.test.tsx   | 90 ++++++++++++++++++++++
 2 files changed, 94 insertions(+)

Fusion-Task-Id: FN-8015

Fusion-Task-Lineage: ef7eb1a6-7a8f-4b12-acd0-99a509902ad2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 22:08:13 -07:00
gsxdsm
55745af80f Settings: one type scale, every setting searchable, nav grouped by topic (#2158)
Settings had three compounding problems: you couldn't find a setting,
and once you found it, it didn't look like its neighbour.

## Organization — the nav was grouped by the wrong thing

It was grouped by **scope** (Global / Runtimes / Project). Scope is an
*attribute* of a setting, not a category — nobody opens Settings
thinking "I need a project-authority setting." That single choice split
five concepts across two groups, including **two nav entries both
labelled "MCP Servers"**, distinguishable only by a small icon.

Now grouped by topic, with paired sections adjacent and stating their
own scope:

![Topic-first settings
navigation](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/nav-topic-groups.png)

```
PREFERENCES     Appearance · Keyboard Shortcuts · Notifications · General · Global
PROJECT         General · Project · Commands & Scripts · Worktrees · Merge
AI & MODELS     Models · Global · Models · Project · CLI Agents · Agents & Permissions · Prompts · Memory
AUTOMATION      Scheduling & Capacity · Scheduled Evals
INTEGRATIONS    Authentication · MCP Servers · Global · MCP Servers · Project · Plugins · Runtimes · Secrets
INFRASTRUCTURE  Node Sync · Node Routing · Remote Access · Backups
ADVANCED        Experimental Features
```

## Search — it now finds settings, not just sections

Search matched only hand-written `searchableText` keyword arrays per nav
entry, and those demonstrably rotted: Project Models accumulated 20
keywords across **two separate fixes** (FN-7907, then
title-summarization on 2026-07-14) because operators searched
"summarize" and got nothing.

Every setting is now indexed from **its real label and help text** — 123
settings across 18 sections, zero curated keywords. The same query that
was patched twice by hand:


![Search returns individual
settings](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/search-summarize.png)

Two of those four hits match on *help text*, which a keyword index
structurally cannot do. Picking a result jumps to the exact control,
**opens its collapsed disclosure**, and highlights it:

![Jump to
field](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/jump-highlight.png)


**This can't rot again.** `settings-search-index.test.ts` extracts every
`descriptor={{ key }}` from section sources and fails the build if one
isn't indexed. "All settings are searchable" is an enforced invariant,
not a promise.

## Consistency — one type scale

A text field's label and a toggle's label in the same section were
styled **three different ways**: `.form-group label`
(12px/600/uppercase/muted), a `.settings-content` override narrowing it
to 0.72rem, and `.checkbox-label` (13px/500/sentence-case) whose every
declaration carried `!important` purely to out-specify the other two.

| | Before | After |
|---|---|---|
| Sections on shared primitives | 1 | **18** |
| Indexed searchable settings | 0 | **123** |
| `form-group` in settings | 226 | **98** |
| `checkbox-label` in settings | 79 | **40** |


![Appearance](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/appearance.png)

![Scheduling](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/scheduling.png)

![Backups](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/backups.png)

![Worktrees](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/worktrees.png)

![Memory](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/memory.png)

![Merge](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/merge.png)

![Notifications](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/notifications.png)
![General ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/general.png)

**Global `.form-group` is untouched** — 35 non-settings files style
forms with it, so settings migrate *off* it rather than restyle it
underneath the rest of the dashboard.

### A real latent bug this surfaced
`--font-size-sm` and `--font-size-md` were named by **12 declarations**
across AgentDetailView, DockTaskList, SetupWizardModal, and
ModelOnboardingModal — but **defined nowhere**. Those rules silently
no-op'd and inherited. The command-center token guard is the only thing
that catches this class of omission and it doesn't cover those files.
The scale is now complete (`2xs · xs · sm · base · md · lg`); `md` is
1.125rem to hold current rendering, since unstyled `h3` already inherits
the 1.17em UA default.

## What deliberately did *not* migrate

Some rows stay bespoke **on purpose**, not from incompleteness:

- **Password fields** (`ntfyAccessToken`, `githubAuthToken`,
`gitlabAuthToken`) — `SettingsTextRow` hardcodes `type="text"`, so
migrating would have **rendered stored tokens unmasked**.
- **Help text with embedded `<code>`/links** — `descriptor.help` is a
single string; flattening would drop markup or reword copy.
- **`<details>` progressive disclosure** in Merge — a descriptor's help
renders unconditionally, so migrating ~10 rows would delete the
disclosure and produce a wall of prose.
- **Dynamic flag lists** (Experimental) and bespoke editors/CRUD
(Prompts, Agents & Permissions, Plugins, KeyboardShortcuts' capture
widget) — no settings field name and no i18n key to anchor honestly.

## Data-model ambiguities surfaced (not papered over)

These need a human call and are **not** fixed here:

- **Five keys are declared in BOTH `DEFAULT_GLOBAL_SETTINGS` and
`DEFAULT_PROJECT_SETTINGS`**: `worktrunk`, `testMode`, `gitlabEnabled`,
`gitlabAuthToken`, `gitlabAuthTokenType`. No scope badge can be stamped
honestly, so those rows are left bespoke.
- **`globalMaxConcurrent` lives in the *project* blob** despite its
name, its dedicated global endpoint, and the "Global" header it renders
under. Its badge is omitted rather than assert a contradiction.
- **Two settings were editable from two screens**: `gitlabEnabled`
(General + Merge) and `githubTrackingDefaultRepo` (General + Global
General). Both are ambiguous-scope and custom widgets, so deduplication
is left as follow-up.


## Follow-ups from review

**`SettingsTextRow` gained `type`, so the token rows could migrate.** It
hardcoded `type="text"`, which is why every secret-bearing row (ntfy
access token, GitHub/GitLab tokens, the Cloudflare tunnel token) stayed
hand-rolled — migrating would have rendered stored secrets in plain
text. `password` rows now default to `autocomplete="off"` so a browser
never offers to save an API token, and masking is pinned by tests: a
regression there would not throw and would not look wrong in review, the
field would simply render the token.

That also made them findable. Searching "token" previously matched
nothing useful; it now returns 9 settings including *Access token* and
*Tunnel token*:


**Jump-to-field now reveals collapsed disclosures.** Rows inside a
closed `<details>` are in the DOM but invisible, so the jump scrolled to
and highlighted a control the operator could not see. The settings most
worth searching for are exactly the ones behind "Advanced".

**The scope banner is gone.** It claimed one scope for a whole section,
which was false wherever a section mixed them — Appearance is a "global"
nav entry whose task-presentation toggles are all project-scoped.
Per-row badges already say this accurately, so the banner and its dead
CSS are removed.

**Scheduling is split by scope.** `globalMaxConcurrent` moved to its own
`Scheduling · Global` section instead of sitting above the project
settings behind an in-section subheading. One section held two authority
levels, so "does this affect my other projects?" depended on which
subheading you had scrolled past — and a search result landing
mid-section shows no subheading at all.

![Scheduling ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/scheduling.png)

**Text-entry padding is now genuinely uniform.** Settings shipped two
input treatments: `.input`/`.select` (6px 10px at 13px) and the global
`.form-group input` rule (8px 12px at 14px). Padding depended on whether
an ancestor happened to be a `.form-group`, and because `.form-group
input` (0,1,1) out-specifies `.input` (0,1,0), naming the standard class
on a nested control did nothing. Measured in-browser after the fix: **51
controls across four sections, one appearance, zero outliers.** The same
specificity trap was silently re-imposing the uppercase/muted label
treatment on migrated rows nested in a `.form-group`; fixed as an
invariant rather than per-row, since sections legitimately keep
`.form-group` around bespoke content.

## Reconciling with #2147 (please review this call)

PR #2147 landed while this branch was open and deliberately moved the
two import auto-translate controls **off** `SettingsToggleRow` onto
`checkbox-label`, pinning that markup with a test written to survive *"a
refactor back onto the primitive"*. Its objection was that the primitive
rendered a right-aligned toggle switch clashing with the section's
native checkboxes — two idioms in one section.

Both halves of that objection are now gone: the primitive renders a
native checkbox **before** its label, and every checkbox in that section
— including the neighbour the test asserts parity against — renders
through it. The idiom split is resolved by migrating all of them rather
than de-migrating these two, so the markup assertions now track the
primitive. **Every behavioural contract from #2147 is kept and still
pinned**, and one was a real bug on this branch: switching
auto-translate off wrote `false` where it must write `undefined`,
leaving an explicit opt-out in the settings blob instead of staying
unset.

#2147's curated translate keywords are preserved for the genuine
vocabulary gaps ("localize", "localization", "foreign language issues")
that appear in no copy. FN-8016's rewritten `taskPopupsBoardListOnly`
copy (default now enabled) is adopted into the migrated row and its
search entry.

Worth noting: #2147's own FNXC says the translate controls were
*"effectively unfindable"* because *"settings search only matches
curated terms plus advertised i18n keys"* — 25 keywords hand-added days
ago. That is precisely the rot this PR's derived index removes.


## Source Control — the duplicate had a cause

GitLab settings were split across **three** sections (General: enable +
URLs; Merge: auth token + type; Global General: all five at global
scope) and GitHub across two. That split is *why* `gitlabEnabled` ended
up writable from both General and Merge — two enable toggles for one
key, last-save-wins.

A `Source Control · Global` / `· Project` pair now owns all 17 keys,
adjacent under Integrations, with **one** GitLab disclosure and **one**
enable toggle:

![Source Control ·
Project](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/source-control.png)

Key ownership moved with them in `section-keys.ts` / `save-split.ts`, so
every key has exactly one owner (`section-keys.test.ts` enforces
disjointness).

**A latent bug surfaced by the move:** nine sites outside the registries
hardcode `"global-general"`/`"general"` and silently gate **scope
routing** — four in `save-split.ts`, five in `SettingsModal.tsx`. Left
stale, global GitLab edits would have been written as project overrides.
Also verified against the schema: **all five `gitlab*` keys AND
`githubTrackingDefaultRepo`** are declared in both defaults (more than
the four originally identified), so those rows carry no scope badge
rather than assert a scope the data model can't support.

## Help moved behind a "?" beside every label

Rendering every description inline turned dense sections into walls of
prose — median help is ~100 chars, some past 400. That pressure is what
made Merge invent its own "More details" disclosure, so one section
showed two idioms. Measured across sections, Merge's help was **not**
unusually long (median 103 vs Appearance's 168, which rendered inline),
so the disclosure wasn't earning its keep.

The copy is deferred **visually, not removed**: the bubble is always
rendered and only fades in, so it stays in the accessibility tree for
`aria-describedby`, stays findable with in-page find, and **the search
index keeps matching on help text**. Errors are never deferred — a
validation message you must hunt for is one you won't see.

Only ~24 of 136 `<small>` blocks were actually row help. The rest stay
inline on purpose and aren't help: validation errors, live status,
empty-state explanations, per-option descriptions inside a multi-select,
and copy explaining *why* a control is disabled.

`children: ReactNode` (not a string) is what let the `<code>`-bearing
and link-bearing rows migrate without rewording your copy — a string API
is precisely why they were hand-rolled before.

### Mobile, verified on a 390px viewport

![Help tip on
mobile](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/mobile-help-tip.png)

Driving a real phone-sized viewport caught two bugs that neither jsdom
nor code review did:

- **Bubble rendered off-screen.** The label line reads "Name [scope] ?",
so the "?" sits well right of centre; anchored to the trigger, the
bubble spanned x=338→658 against a 390px screen — 268px unreachable.
`max-width` clamps width but can't help when the *anchor* is near the
edge. It now anchors to the row (`inset-inline: 0`): re-measured at
x=20→370 inside 390. An earlier comment claimed this behaviour; only the
comment existed.
- **Two bubbles open at once.** Outside-`pointerdown` dismissal misses a
path: `click` fires with **no pointer event** when Enter/Space activates
a focused trigger, so a keyboard user opening a second tip left the
first open underneath. Tips now broadcast on open and close each other;
the regression test asserts the bare-click path specifically.

Also verified on touch: tapping outside dismisses, and opening a second
tip closes the first — no stranded bubbles.

**Checkbox wrapping.** On a 390px viewport a long label ("Keep task
popups on the view where they were opened") stranded its checkbox alone
on line 1, with the text on lines 2-3 and the badge on line 4. The head
was a flex row and the label was a flex ITEM, so once it no longer fit
beside the checkbox the whole label wrapped rather than its text. Label
+ badge + tip now form one group that absorbs the wrapping, leaving the
checkbox as the only sibling item; continuation lines align under the
first word.

Audited every checkbox and radio on all 31 screens at 390px afterwards:
**101 controls, all on the first line of their label text.** (The
audit's first pass flagged 41 — all false positives from measuring
`<label>` elements whose text is a bare text node, i.e. the label box
included the checkbox. Re-measuring the text nodes themselves via Range
cleared them.)



### Every row, including the ones that stayed bespoke

Merge was the last holdout — 18 `<details>` "More details" disclosures
plus 4 inline blocks, an idiom it invented and no other section used.
All 22 now use the same "?":


![Merge](https://raw.githubusercontent.com/Runfusion/Fusion/468f71dfa065be6f46e4fccfe09f18518fbf4e81/shots/merge.png)

Project Models likewise rendered lane help as prose while the global
lanes next door already used the tip; its help now hangs on the existing
lane label row beside the Override/Inherited badge (the badge is live
state and stays visible; the fallback chain behind it is what you open
deliberately).

**Audited all 31 screens programmatically** (label treatments, control
padding, row overflow, leftover banners, horizontal scroll):
- **one label treatment on every screen**, one control treatment, zero
overflowing rows, zero banners, no horizontal scroll
- the copy still rendered inline is deliberately not row help:
validation errors, live status, block descriptions, per-option text
inside multi-selects, and copy explaining *why* a control is disabled

**Known gap:** the three plugin runtime screens (Hermes / OpenClaw /
Paperclip) still render inline help. They delegate to
`HermesRuntimeCard` / `OpenClawRuntimeCard` / `PaperclipRuntimeCard` —
separate card components outside the settings tree — and their copy *is*
genuine row help ("Leave blank to resolve hermes from your PATH"). They
are advanced-only and were left out of this pass rather than swept in at
the end without review.

## Padding and label consistency (measured, not eyeballed)

Two idioms were still visible on one screen — Merge rendered "PLAN
APPROVAL MODE" in caps directly above "Auto-merge conflict retries" in
sentence case. Rows that deliberately stay bespoke inherited the global
`.form-group label` treatment.

| | Before | After |
|---|---|---|
| Label treatments | uppercase/muted/11.5px **and** sentence/14px | **70
labels, one treatment** |
| Control padding | `6px 10px` **and** `8px 12px` | **81 controls, one
treatment** |
| Row gaps | 12 / 16 / 20 by adjacency | **0** — every row owns its
space |

The cause was a specificity trap: `.form-group input` (0,1,1)
out-specifies `.input` (0,1,0), so naming the standard class on a nested
control did nothing. Fixed settings-scoped; the global `.form-group` is
untouched (35 non-settings files depend on it, where uppercase is that
context's convention).

## Advanced settings toggle — verified

Confirmed in-browser after the regroup: **OFF → 15 sections / 5 groups;
ON → 31 sections / 7 groups**, `data-show-advanced` flips, preference
persists. The now-empty **Infrastructure and Advanced group headers are
correctly hidden** when off — the case the regroup could have broken,
since those groups contain only advanced sections.

## Pre-existing failures found (verified NOT caused by this PR)

Each verified by running the identical file on the parent commit and
diffing the **failure sets**, not just the counts:

| Failure | Verified |
|---|---|
| `SettingsModal.scheduling-merge.test.tsx` — 55 failures | identical
set before/after |
| `SettingsModal.remote-notifications.test.tsx` — 16 failures |
identical set before/after |
| `settings-default-descriptions.test.tsx` — `sqliteMigrationNotice`,
`postgresMigrationInboxMessageSentAt` | fails on clean tree |
| i18n `parity.test.ts` — 12 violations
(`settings.general.autoTranslate*`, `taskDetail.plan.*`) | 12 before, 12
after |

**This PR adds zero new failures.**

Fixed along the way: a stale `AppearanceSection` assertion testing copy
FN-7945 deliberately rewrote (failing silently), the ungrammatical "1
matching sections", and `&ldquo;` rendering literally on screen.

## Verification

- `tsc --noEmit -p tsconfig.app.json` → **0 errors** (note: the default
`tsconfig.json` only covers `src/` and does **not** typecheck `app/`)
- `pnpm test:gate` → **63 passed**
- Settings surface → 68 failures, every one a verified subset of the
pre-existing baseline, diffed by failure SET not count (this branch
incidentally fixes 4)
- `pnpm test:gate` 63/63 · lint clean across 83 changed files · `tsc -p
tsconfig.app.json` 0 errors
- Rebased onto `main`: conflicts with #2147 and FN-8016 resolved
- Driven in a real browser at 1440px and 390px: search, jump-to-field,
help tips, advanced toggle, and every migrated section
- Driven in a real browser: search, jump-to-field, highlight, and every
migrated section

🤖 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**
* Settings search now indexes individual settings controls, ranks
matches, and navigates directly to the exact row with a temporary
highlight.
* **UI Improvements**
* Migrated settings sections to shared row primitives
(toggle/select/number/text/textarea) for consistent spacing and
touch-friendly controls.
* Updated typography to a complete tokenized type scale; added the “?”
help tip and tokenized row highlight/error styling.
  * Navigation and search labels now show clear Global vs Project scope.
* **Bug Fixes**
* Improved search accuracy using label/help/keywords and fixed settings
search counts/pluralization and MCP scope labels.
* **Tests**
* Added/updated checks to ensure the settings search index stays
consistent with rendered rows.
<!-- 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 22:00:27 -07:00
gsxdsm
203557e7ae feat(dashboard): attach issue images on GitHub and GitLab import (#2151)
## Problem

An imported issue whose bug report *is* a screenshot arrived at the
agent as an unfetchable link.

Everything needed to show an agent an image already existed — the
executor builds a `## Attachments` section pointing at
`.fusion/tasks/<id>/attachments/` (`executor.ts:18887`), triage inlines
image attachments as base64 vision blocks, and `agent-prompts.ts:171`
explicitly permits reading that directory. But the import routes only
ever called `createTask()` with the issue body as text and never called
`addAttachment()`, so **that directory was always empty for imported
issues**.

The images can't be fetched later by the agent: GitHub
`user-attachments` assets redirect to a signed CDN URL and 404 on
private repos without credentials, and GitLab `/uploads/...` needs the
instance token. Import time is the only point where those credentials
are known to be present.

## Change

New `packages/dashboard/src/issue-image-attachments.ts` extracts images
from an issue's **body and comments**, downloads them, and stores them
via `addAttachment` — which already bridges images into the artifact
registry, so they also surface in the UI gallery.

Wired into every import surface:
- `POST /github/issues/import`
- `POST /github/issues/batch-import`
- All four GitLab routes, via the shared `importItem` chokepoint

Provider differences sit behind an `ImageImportPolicy` rather than one
shared host list, because the forges disagree on what matters:

| | GitHub | GitLab |
|---|---|---|
| URL form | absolute | usually relative `/uploads/<sha>/f.png` |
| Resolution | n/a | **project**-rooted, not instance-rooted |
| Trust boundary | fixed host allowlist | the configured instance origin
(self-managed = any host) |
| Auth | `Bearer` (gh CLI token) | `PRIVATE-TOKEN` |

Notable decisions:

- **Extraction runs on the original body, not the translated one.** The
translation model can rewrite or drop URLs — the same reason the
existing code appends `Source:` *after* translating.
- **`resolve()` returning null is the SSRF guard.** It's the single
place deciding a URL is ours to fetch, so
`![](http://169.254.169.254/...)` in an issue body is never requested.
- **Best-effort.** A failed download or comment fetch never fails an
import that already produced the task.
- **Batch stays cheap.** The REST `comments` count (free on the payload)
skips the comment fetch for issues with none, so a 50-issue batch
doesn't pay 50 round trips to discover empty threads.
- Capped at 10 images / 5MB each (matching `MAX_ATTACHMENT_SIZE`) / 15s
timeout.
- `GitLabClient.listNotes` is new and **read-only** — the client's
existing "no comment side effects" rule governs writes.

## Verification

- **30 new/updated tests pass** (23 helper + route-level wiring on both
forges). Route tests drive the real Express routes through to
`addAttachment`; the helper tests alone wouldn't prove the wiring.
- Typecheck clean (exit 0), lint clean, `check:changesets` passes.
- **Pre-existing failures confirmed against the untouched baseline, not
caused here:** 5 in `routes-github.test.ts` (`engine-unavailable`,
conflict-reclaim) and the `test:gate` `chat.test.ts` mock-completeness
failure both reproduce identically on `main` with this branch stashed.

One incidental test fix: `routes-gitlab.test.ts` used
`mockResolvedValue(jsonResponse(...))`, handing the **same** `Response`
instance to every call. A `Response` body is single-use, so the added
notes fetch got a consumed body. Switched to `mockImplementation` to
build a fresh one per call, matching the neighbouring test.

## Notes for the reviewer

Images are attached but the body's markdown links are left as-is — the
agent reads the files, and rewriting URLs in operator-visible text
seemed worse than leaving them.

🤖 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**
* GitHub and GitLab issue (and merge request) imports now convert
embedded screenshots in issue descriptions and comments/notes into real
task attachments.
* Works across single-issue and batch import workflows, including
project/group import flows.

* **Bug Fixes**
* Attachment extraction/import is resilient: per-image failures,
comment/notes fetch issues, and problematic/unsafe/oversized links won’t
break the overall import.

* **Tests**
* Added comprehensive coverage for URL extraction, provider policies,
attachment downloading/limits, redirects, and route integration for both
GitHub and GitLab.
<!-- 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 21:58:46 -07:00
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