513796b7e750d2680ec2b20eabbd5fa044a32cb2
11453 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
b51de02a54 |
Revert "fix(core): resolve unbound project ids to a real partition or no filter"
This reverts commit
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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:  ``` 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:  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:  **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** |         **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.  **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:  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  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 "?":  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 `“` 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> |
||
|
|
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 `` 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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 --> |
||
|
|
1b9c7a7ca2 |
fix(FN-7575): stop double-commenting when a task is both imported and tracked
A task can carry BOTH linkages at once, pointing two services at ONE issue: - GitHub: maybeCreateTrackingIssue() ADOPTS a github sourceIssue as githubTracking.issue (github-tracking.ts, `source_issue_linked`). - GitLab: buildGitLabTaskProvenance() always returns sourceIssue AND gitlabTracking.item for the same item, so on GitLab EVERY imported task with gitlabCommentOnDone on was double-commented. With comment-on-done enabled the issue-comment service and the tracking-comment service both posted. Reproduced against the real wiring: two comments on acme/widgets#42 ("✅ Task FN-1 ... resolved." then "✅ Done — ..."). The issue-comment services now suppress themselves when the tracking service provably posts to the SAME target, and the tracking comment wins — it carries commit/branch/PR/files/merged plus the release lines. Identity, never "both linked": the two may legitimately target DIFFERENT issues (a tracking issue linked separately from the source issue), which is two comments on two issues and must keep working. GitHub matches on case-insensitive owner/repo + number; GitLab is identical by construction because resolveGitLabTarget() prefers the tracked item. Both guards mirror the tracking services' `from === to` no-op guard: on a same-column re-emit the tracking service stays silent, so suppressing there would drop the only comment rather than dedupe it. The net split is now disjoint: issue-comment owns "imported but not tracked", tracking owns "tracked". Suppression is logged (once per completion, not the high-frequency skip-noise FN-8024 removed) because a custom comment template silently not rendering on a tracked issue is otherwise unexplainable. Behavior change, documented in settings-reference.md: githubCommentTemplate / gitlabCommentTemplate no longer render on a tracked issue. Tests updated where they encoded the double-post path (they exercised the services in isolation, so the duplicate was invisible). GitLab fixtures now distinguish tracked vs imported-not-tracked shapes. Also asserts a PRE-EXISTING gap left unchanged: resolveGitLabTarget() early-returns on an unresolvable item and never falls back to sourceMetadata, so neither service comments there. Verified non-vacuous: the 4 suppression tests fail against the pre-fix source; the "still posts" tests pass either way by design. Gate green (294/122/63). Fusion-Task-Id: FN-7575 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cae7847085 |
fix(FN-8004): retry ACP provider blips in auto-merge instead of parking failed (#2157)
## What happened
FN-8004's implementation work finished and passed review. The auto-merge
then failed with `Grok ACP turn failed: Internal error` — a ~20 second
provider blip — and the task was parked `status: "failed"` with 8 files
of complete, reviewed work stranded on its branch.
The park is the interesting part: `status: "failed"` is precisely what
tells recovery to stop. So a misclassification here isn't a missed
retry, it's **terminal**. Both recovery paths were disabled by the same
wrong verdict:
- `maybeRetryTransientMerge` (inline, 3 retries w/ backoff) — never
fired once (`mergeTransientRetryCount: 0`).
- `recoverTransientMergeFailures` (self-healing sweep, exists exactly to
rescue parked in-review tasks) — skipped it, gated on the same
classifier.
## Three defects fixed
**1. No AI-provider failure class existed.** The AI merge drives a real
LLM turn, but `classifyTransientMergeError` only modeled git/lease/spawn
faults. Adds `ai-provider-turn-failure`.
**2. ACP dropped the error detail.** `promptAcpSession` rethrew the SDK
error unchanged, discarding the JSON-RPC `code`/`data` — the only
evidence the fault was provider-side. ("Internal error" is just the
standard text for `-32603`.) It now preserves them, keeping the original
as `cause`:
```
Internal error (acp rpc code -32603, retryable)
```
Classification anchors on that envelope, **not** on the bare `"Internal
error"` — matching that unanchored would disguise genuine application
defects as retryable blips. Only provider-fault codes (`-32603`,
`-32000`..`-32003`) are retryable; caller-fault codes
(`-32600`..`-32602`) stay permanent, since retrying just repeats the
failing call.
**3. Sweep/inline asymmetry** (found while tracing; latent and
unreported). The inline gate accepted `isTransientError(msg) ||
classify(msg)`, but the sweep consulted **only** the classifier. So
`ECONNRESET` / `socket hang up` during a merge earned inline retries and
then went **invisible to the sweep** once parked — stranded forever. The
classifier now delegates to `isTransientError`, so both gates agree by
construction.
To keep that delegation from importing the detector's
`usage-limit-detector → logger` chain (the chain FN-5627 split the
classifier out to avoid, which would break
`notification-service.test.ts`'s partial `vi.mock`), the pure predicates
moved to the import-free leaf `transient-error-patterns.ts`, re-exported
from `transient-error-detector.ts`. All 13 exports preserved, verified
programmatically.
## Loosened budgets
Per request, so more self-heals. Both apply **only** to errors already
proven transient; the ceiling and
`merger:transient-failure-budget-exhausted` audit path remain.
| Budget | Before | After |
|---|---|---|
| `MAX_AUTO_MERGE_TRANSIENT_RETRIES` | 3 | 5 (backoff
5s/10s/20s/40s/80s) |
| `MAX_TRANSIENT_MERGE_RECOVERIES` | 2 | 5 |
The bump broke two suites that had hardcoded the old `3`. Rather than
swap in another magic number, both now derive the cap from the constant
so future tuning doesn't re-break them.
## Verification
- `pnpm test:gate` green · `pnpm lint` clean · engine + ACP typecheck
clean · `pnpm verify:fast` PASS (5/5)
- ACP plugin 230 tests green · Grok plugin 64 green · engine
transient/merge suites 136 green
- Regression tests assert the **invariant across every surface** (per
*Fix the Invariant, Not the Repro*), not just the reported Grok string:
both ACP runtime prefixes, all retryable/non-retryable rpc codes, both
SDK error shapes, network delegation, class-ordering, and negative cases
proving bare `"Internal error"` and real defects stay permanent.
- A test caught a genuine bug in my own code mid-review (nested-shape
message shadowing), now fixed.
- `notifier.test.ts > "awaiting approval"` fails — **confirmed
pre-existing on clean main**, unrelated.
## Note
FN-8004's own branch (`fusion/fn-8004`) is still unmerged and its work
looks complete. Once this lands, its merge should be retried separately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e9537c9e85 |
docs(core): correct the ensureDatabase comment on the postgres join path
The preflight join carried "// Ensure the database exists on the running instance" above a line that only builds a URL. No ensureDatabase() call has ever followed it, so the comment described behavior the code does not have. Replace it with why the call is absent: a joiner has no cluster of its own to ensure, the owning process creates the database after its own start(), and ensureDatabase() would throw here anyway because it requires `this.running` -- which the join path leaves false by design so stop() never reaps an instance we did not start. Also records the ordering assumption the path rests on: the owner publishes runningInstances / writes postmaster.pid before its ensureDatabase() resolves, so a joiner winning that window fails at the connection layer rather than silently using a missing database. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8023aa2d08 |
fix(core): do not rescue a cancelled embedded Postgres start into a success
The startup-race join added in
|
||
|
|
e33039ad0f |
fix(core): join competing postmaster when embedded Postgres startup races
Starting a second Fusion process could fail with `lock file "postmaster.pid" already exists`. The singleton preflight check and `pg.start()` are not atomic, so another process can create the lock in between — the loser surfaced the collision to the TUI as an error instead of simply joining the live instance. `EmbeddedPostgresLifecycle.start()` now wraps the start path in a try/catch. On failure it re-reads `postmaster.pid` via `isAlreadyRunning()`; when a live instance is found it connects to that port with `ownsProcess=false` (so this process never stops a server it did not start) and logs the race. Failures with no live instance rethrow unchanged, so genuine startup errors are unaffected. Regression test lives outside the real-process `embeddedDescribe` block — it uses a mocked ctor, and nesting it there would skip it under FUSION_EMBEDDED_TEST_SKIP=1 (the gate/CI default), leaving the fix unprotected. Verified: 35/35 embedded-lifecycle tests pass, core typecheck clean, lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0b332816d5 |
FN-7986: raise plan review replan cap to 8
Allow more automatic Plan Review revisions before escalating tasks for human approval. - Raise the consecutive REVISE replan cap from 3 to 8. - Cover the seven- and eight-revision boundaries in triage tests. - Add a patch changeset describing the revised default. Files changed: .changeset/fn-7986-plan-review-cap.md | 7 +++++++ .../triage-plan-review-replan-cap.test.ts | 23 +++++++++++++--------- packages/engine/src/triage.ts | 8 ++++---- 3 files changed, 25 insertions(+), 13 deletions(-) Fusion-Task-Id: FN-7986 Fusion-Task-Lineage: 3b61f333-be9a-414a-bd27-aabdbb45caa0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
d4553be678 |
FN-8029: compact task live-log thinking blocks
Make task live-log thinking blocks more compact while retaining readable expanded reasoning. - Reduce desktop and mobile thinking container padding. - Tighten thinking summaries and bodies with tokenized spacing. - Update CSS regression assertions for compact thinking blocks. Files changed: packages/dashboard/app/components/TaskChatTab.css | 34 ++++++++++++++++------ .../app/components/__tests__/TaskChatTab.test.tsx | 28 +++++++++--------- 2 files changed, 40 insertions(+), 22 deletions(-) Fusion-Task-Id: FN-8029 Fusion-Task-Lineage: fd9a9443-3a01-48ce-b965-227104404a79 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
753b1bb710 |
fix(engine): honor graph cancellation at the merge node
The merge node could not observe a graph abort. WorkflowPrimitiveContext carried no signal, so requestMerge raced the merge only against its own 30-minute GRAPH_MERGE_TIMEOUT_MS using a controller it owned. A hard-cancel (user cancel, engine restart, pause/resume) aborted the graph controller and the walk kept sitting inside the merge node for the full timeout. When the timeout finally fired it aborted the still-running AI merge -- surfacing as "Manual-merge failed: Request was aborted" -- and the walk reported value=merge-timeout for a cancellation it had missed half an hour earlier. An abort landing between merger-ai's `worktree: null` write and mergeConfirmed then stranded the card as no-worktree-no-merge-confirmed. Thread the graph AbortSignal from WorkflowNodeExecutionContext (where it already existed) through primitiveNodeContext/primitiveContextForNode into the primitives, and honor it on both merge surfaces: - requestMerge fails fast when the walk is already cancelled, before ensureWorkflowMergeBoundaryTask mutates the row or the requester enqueues a merge, and links the graph signal into its timeout controller via AbortSignal.any -- raced separately so the walk returns on the abort rather than waiting on a requester that may never settle. - The legacy merge seam had the identical unguarded race and gets the same treatment. The timeout stays: it bounds a wedged merge queue, which is a different failure from cancellation. Both signals must stay live -- dropping either silently restores the stall with no type error. Cancellation returns a distinct `merge-cancelled` rather than reusing merge-timeout. Returning `data.status: "failed"` would let classifyMergeFailure read the unknown reason as merge-failed and route the cancellation into bounded auto-merge retry, re-requesting the merge the operator just cancelled. Regression test covers both merge surfaces, both cancel timings (pre-flight and mid-flight), the no-signal back-compat path, the signal plumbing itself, and the classification boundary. Verified by removing the fix: 7 of 9 cases fail, with the mid-flight cases hanging until timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1043e44bc2 |
FN-8025: stabilize planning retry status tests
Keep retry-state assertions synchronized with the planning stream lifecycle. - Hold mocked planning streams in the retry loading window - Await transient retry-status rendering for resumed and sidebar sessions Files changed: .../__tests__/PlanningModeModal.planning-flow.test.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8025 Fusion-Task-Lineage: 4cb91dc1-8132-4c33-b8fc-203e335a6ae1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
b9ad5b0cec |
docs(FN-7575): correct the FNXC rationale on the issue-comment services
My previous commit's comments claimed GitHubIssueCommentService "effectively
never fires" and was not the surface that posts. That is wrong, and acting on it
would have deleted working, documented behavior.
The issue-comment and tracking-comment services gate on different, disjoint
linkages:
- {GitHub,GitLab}IssueCommentService -> task.sourceIssue, the IMPORT linkage set
unconditionally by buildGitHubIssueSource, plus the documented
githubCommentOnDone / gitlabCommentOnDone settings (settings-reference.md:637,
651 — no Settings UI, but reachable via the settings API/file).
- {GitHub,GitLab}TrackingCommentService -> githubTracking.enabled /
gitlabTracking.item, the explicit TRACKING linkage.
resolveImportedIssueGithubTracking() returns undefined unless
githubLinkImportedIssuesToTracking or the tracking defaults resolve on, so an
imported issue with tracking off has sourceIssue and NO tracking linkage — the
issue-comment service is then the ONLY surface that comments. Neither service is
redundant; record that so neither is deleted as a "duplicate" later.
Also records the pre-existing overlap: a task carrying BOTH linkages with
comment-on-done enabled receives two comments, one per service. Orthogonal to the
release lines and left undeduped.
Comments only — no behavior change; the 76 comment-surface tests still pass.
Fusion-Task-Id: FN-7575
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
adcba0eed2 |
FN-8017: add imported-item filter to task import
Add a persisted control that declutters Import Tasks by hiding items already on the board. - Filter imported GitHub issues, pull requests, and GitLab resources while retaining full imported counts. - Clear hidden selections, show an all-imported empty state, and preserve the preference per project. - Document the toggle, add styling, coverage, and a minor changeset. Files changed: .changeset/fn-8017-hide-imported-toggle.md | 7 ++ docs/dashboard-guide.md | 4 +- .../dashboard/app/components/GitHubImportModal.css | 21 ++++ .../dashboard/app/components/GitHubImportModal.tsx | 70 ++++++++++++- .../__tests__/GitHubImportModal.test.tsx | 108 +++++++++++++++++++++ packages/dashboard/app/hooks/modalPersistence.ts | 6 ++ 6 files changed, 209 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-8017 Fusion-Task-Lineage: 5f03fe92-6dce-4de7-a745-0c8a46dc2dbb Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
5523c19fb4 |
FN-8023: align ChatView thinking text tokens
Align ChatView thinking-section text with the muted token guard. - Replace legacy secondary text tokens in thinking labels, controls, and empty states - Document the ChatView token-guard requirement Files changed: packages/dashboard/app/components/ChatView.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-8023 Fusion-Task-Lineage: 7f0a6da0-f15d-4dc8-a935-308a11ee9f14 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
71275abffe |
fix(FN-7575): post release version lines on the surface that actually comments
FN-7575 (issue #1916) added "Current version:" / "Target release:" lines to GitHubIssueCommentService, but that service is gated on `githubCommentOnDone` — default false, with no Settings UI — so it effectively never fires. The "✅ Done —" comments on linked issues are posted by GitHubTrackingCommentService, which had no version logic. The lines were invisible in production for ~10 days; issue #1916's own close comment is the proof. Extract the self-repo check and next-minor computation into a shared fusion-release-version.ts and apply it across all four done-comment surfaces (GitHub/GitLab x tracking/issue) so they cannot drift again. - Release lines join `optionalLines` rather than being appended to the finished string, so they count against DONE_COMMENT_MAX_LENGTH and shrink the title budget; appending would silently blow the cap on long titles. - Version resolution is a lazy resolver, so getCliPackageVersion()'s filesystem walk only runs for self-repo comments. - GitLab self-repo matching uses item.projectPath: resolveGitLabTargetFromItem() prefers the numeric projectId, which never matches the slug. - Non-self repos stay byte-for-byte unchanged (asserted). Per the Surface Enumeration rule, regression tests assert the invariant across every done-comment surface — both the pure formatters and the services that post — plus case-insensitive slug matching (issue #1916 is "Runfusion/Fusion"), in-progress transitions, the 0.0.0 sentinel, unparseable versions, the lazy-resolution guarantee, and the truncation ladder under the length cap. Verified non-vacuous: 10 of the new tests fail against the pre-fix source. Fusion-Task-Id: FN-7575 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7cec078054 |
FN-8016: scope task popups to their opening view
Scope task-detail popups to their origin dashboard view by default. - Default per-view popup scoping while retaining a legacy global-popup opt-out. - Key popup lifecycle, navigation, and Escape dismissal by task and origin view. - Update settings copy, documentation, localization, and regression coverage. Files changed: .changeset/fn-8016-task-popup-view-scoping.md | 7 ++ docs/dashboard-guide.md | 4 +- .../core/src/__tests__/settings-defaults.test.ts | 4 +- packages/core/src/settings-schema.ts | 6 +- packages/core/src/types.ts | 6 +- packages/dashboard/app/App.tsx | 67 ++++++----- .../app/__tests__/App.keyboard-shortcuts.test.tsx | 14 ++- .../app/__tests__/App.taskPopupViewGating.test.tsx | 125 +++++++-------------- .../dashboard/app/components/SettingsModal.tsx | 2 +- .../settings/sections/AppearanceSection.tsx | 6 +- .../sections/__tests__/AppearanceSection.test.tsx | 18 ++- .../app/hooks/__tests__/useAppSettings.test.ts | 15 +++ .../app/hooks/__tests__/usePoppedOutTasks.test.ts | 28 ++--- packages/dashboard/app/hooks/useAppSettings.ts | 8 +- packages/dashboard/app/hooks/usePoppedOutTasks.ts | 14 +-- packages/i18n/locales/en/app.json | 4 +- packages/i18n/src/resources.d.ts | 4 +- 17 files changed, 158 insertions(+), 174 deletions(-) Fusion-Task-Id: FN-8016 Fusion-Task-Lineage: e33beeae-0ce3-4202-95dc-6fb2d26f9770 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
66ae82af5b |
FN-8007: align concurrency current-use markers
Align dashboard and footer concurrency markers with their native range thumbs. - Map running counts in min-relative slider coordinates and clamp them to the configured cap - Standardize native slider thumb dimensions and marker geometry across browsers - Add dashboard coverage and document the marker behavior Files changed: .changeset/fn-8007-concurrency-dot-alignment.md | 7 + docs/dashboard-guide.md | 8 +- .../dashboard/app/components/EngineControlMenu.css | 20 ++- .../dashboard/app/components/EngineControlMenu.tsx | 19 ++- .../__tests__/EngineControlMenu.test.tsx | 96 +++++------- .../command-center/CommandCenterControls.css | 22 ++- .../command-center/CommandCenterControls.tsx | 19 ++- .../__tests__/CommandCenterControls.test.tsx | 164 +++++++++++++++++++++ 8 files changed, 277 insertions(+), 78 deletions(-) Fusion-Task-Id: FN-8007 Fusion-Task-Lineage: 9ad8ee0b-09da-413e-96bc-530c897cb32e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
05e499b019 |
fix(dashboard): make import auto-translate settings findable, native, and non-blocking (#2147)
Follow-up to #2141 (merged). Three operator-reported problems with the shipped import auto-translate feature, plus a real bug found while testing them. ## 1. The settings were unfindable > "where are the translate settings? I can't find them and search in settings isn't finding them" The controls rendered fine — but Settings **search** only matches curated keywords and advertised i18n keys per section, and "Project General" says nothing about translation. So searching `translate` matched nothing. Now advertised on **Project General** (where the controls live) and on **Project/Global Models** (where the lane is picked). Verified against the real `filterSettingsSectionsForSearch`, not by eye: | query | surfaces | |---|---| | `translate` / `translation` | general, project-models, global-models | | `auto translate`, `target language` | general | For reference, they live in **Settings → Project General**, directly below "Always link imported GitHub issues to GitHub tracking". ## 2. The checkbox looked foreign > "the auto translate checkbox needs to be the left of the text and it needs to be styled like other check boxes" It used `SettingsToggleRow`, which renders a **right-aligned toggle switch**, while every other GitHub/import setting in that section uses a plain `checkbox-label` with the input **before** its text. Two checkbox idioms in one section read as a bug regardless of which is nicer in isolation. Both controls now use the section's native `form-group` + `checkbox-label` / `select.select` markup. A test asserts my checkbox's class and structure are **identical to the neighbouring `githubLinkImportedIssuesToTracking` checkbox**, so it can't silently drift back. ## 3. Auto-translation was async but not incremental > "ensure the auto translate is non blocking and runs async in the background" The list never blocked (it rendered originals immediately; import is cache-read only). But a **single request translated all 50 issues**, so nothing appeared until every issue finished — minutes on a large page — and one timeout discarded the whole page's work. It now streams in chunks of 8: titles appear as each chunk lands, a failure costs one chunk instead of the page, and chunks are sequential so opening the panel can't fan 50 model calls at the provider at once. ## Also: a real infinite-render loop (found by testing #3) `items` and `eligible` are fresh **array identities** on most renders, and both sat in the effect's dependency list — effect → `setState` → re-render → new array → effect. It manifested as a **heap OOM** under `renderHook`. Effect dependencies are now string/scalar only, with live issue data read from a ref, and the reset path preserves state identity so it cannot re-trigger itself. This bug shipped in #2141; it needed a re-render with a fresh `issues` identity to trigger, but it was live. ## Changeset Folded into the **existing unreleased** `github-import-auto-translate` changeset rather than adding a second one for the same unshipped feature. ## Verification - ✅ `pnpm lint`, root `pnpm typecheck`, `pnpm verify:fast`, `pnpm test:gate` (479 tests) - ✅ 54 tests across the translate suites, including new coverage pinning the checkbox idiom + neighbour parity, and that translations **stream per chunk** rather than all-or-nothing (a regression to one request fails these) ## Reviewer note Worth knowing for future test-writing here: `beforeEach(() => mock.mockReset())` **implicitly returns the mock**, and vitest treats a function returned from `beforeEach` as a teardown callback — it then invokes the mock with zero arguments and corrupts `mock.calls`. The test file uses a block body and says why. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added project settings to enable GitHub import auto-translation and choose a target language, including “follow dashboard language”. * Auto-translation now runs in the background, chunked, and streams translated issue content progressively into imported tasks. * Translations are cached and reused to speed up repeated imports, with incremental updates shown as they arrive. * Settings search now includes translation and import auto-translation terms. * **Bug Fixes** * Improved update handling so changes to issue text refresh translations, while closed issues are never requested and failures don’t erase already translated results. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3f426c3ef0 |
fix: isolate mDNS node broadcasts (#2155)
## Summary - Make Fusion mDNS broadcast names node-unique to avoid same-name DNS-SD collisions. - Treat asynchronous Bonjour broadcast errors as non-fatal diagnostics when no listener is registered. - Add regression coverage for a service-name collision. ## Validation - `pnpm --filter @fusion/core exec vitest run src/__tests__/node-discovery.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/core typecheck` |
||
|
|
514ccd304c |
Recover malformed agent interview responses (#2146)
## Summary - preserve valid onboarding JSON returned in Pi thinking-only assistant blocks - retry one bounded JSON-only reformat turn when the model returns prose or malformed output - keep streamed output as a final extraction fallback instead of overwriting it with an empty content array ## Verification - `pnpm --filter @fusion/dashboard exec vitest run src/__tests__/agent-onboarding.test.ts` — 20 passed - `pnpm --filter @fusion/dashboard typecheck` - `pnpm lint` - `pnpm check:changesets --strict` - live local-runtime AI Interview produced a structured Hermes/computer-use onboarding question after restart Follow-up to #2142, which fixed the missing planning-model fallback and runtime-hint prompt. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved agent onboarding recovery when assistant replies include thinking-only content or malformed JSON. * Added a single automatic retry that re-formats invalid output into valid onboarding JSON. * Preserved structured “thinking” content as part of valid onboarding responses. * Normalized optional onboarding fields so null/empty/whitespace-only values are treated as missing. * Tightened Hermes automation so the runtime hint is set exactly to `hermes`. * **Tests** * Added onboarding event synchronization and expanded coverage for recovery and field normalization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
40ae6ddb3a |
fix(FN-8024): stop logging skipped stale triage recovery writes
Skipping a stale planning-state write is the expected outcome of a normal scheduler advancement, not an anomaly, so the warn was pure log noise. Behavior is unchanged; only the two planLog.warn emissions are removed. Fusion-Task-Id: FN-8024 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7daa16fe30 |
FN-8002: suppress reconnecting hint on planning question screen
Gate Planning Mode's "Reconnecting…" indicator to the loading view so persisted awaiting-input questions stay free of transient SSE reconnect noise. - Show planning.reconnecting only when view.type is "loading" - Cover desktop and mobile resumed question screens without the hint - Keep the hint during active generation loading reconnects - Add patch changeset for the user-facing fix Files changed: .changeset/FN-8002-planning-reconnecting-hint.md | 7 +++ .../dashboard/app/components/PlanningModeModal.tsx | 6 +- .../PlanningModeModal.planning-flow.test.tsx | 73 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8002 Fusion-Task-Lineage: 0a8682ee-922d-4acd-b7ce-bbf4f25dfce7 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
22fde62510 |
FN-8001: open footer planning sessions in Planning view
Navigate Background Tasks footer planning rows into the embedded Planning view so resume actually loads planning mode.
- Call handleChangeTaskView("planning") when opening a background planning session
- Extend App tests for footer planning resume and unchanged non-planning session routes
- Update dashboard-guide planning resume entry-point docs
Files changed:
docs/dashboard-guide.md | 4 +-
packages/dashboard/app/App.tsx | 5 ++
.../app/components/__tests__/App.test.tsx | 94 +++++++++++++++++++---
3 files changed, 91 insertions(+), 12 deletions(-)
Fusion-Task-Id: FN-8001
Fusion-Task-Lineage: 402ece21-5f29-4304-a3aa-ef7004a30155
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
|
||
|
|
0b06026c74 |
FN-7992: open GitHub import issue/PR details in FloatingWindow
Show GitHub/GitLab import item details in a draggable FloatingWindow instead of an embedded two-pane preview, simplifying the import modal layout. - Replace inline list/preview split with FloatingWindow for issue and PR detail - Remove two-pane resize handle, mobile list/preview switch, and related CSS - Keep close confirmation when discarding detail-window changes - Update FloatingWindow styles and dashboard guide for floating import details - Slim GitHubImportModal tests while restoring core import-modal coverage Files changed: docs/dashboard-guide.md | 6 +- .../dashboard/app/components/FloatingWindow.css | 23 +- .../dashboard/app/components/GitHubImportModal.css | 359 +------- .../dashboard/app/components/GitHubImportModal.tsx | 347 ++------ .../components/__tests__/FloatingWindow.test.tsx | 2 +- .../__tests__/GitHubImportModal.test.tsx | 909 ++------------------- 6 files changed, 139 insertions(+), 1507 deletions(-) Fusion-Task-Id: FN-7992 Fusion-Task-Lineage: 0991e28c-d793-4a41-9312-6e250e8a09c4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
c0bef0bfbe |
FN-7969: deprecate unused builtin Coding (Ideas) workflow
Hide builtin:coding-ideas from new selection after occupancy preflight, while keeping it resolvable for any existing task selections. - Add builtin:coding-ideas to DEPRECATED_BUILTIN_WORKFLOW_IDS so it is excluded from defaultEnabledBuiltinWorkflowIds and listWorkflowDefinitions selection listings - Keep getBuiltinWorkflow / direct resolution working for pre-existing Coding (Ideas) task selections - Document deprecation and custom-workflow copy path in dashboard-guide and workflow-steps - Extend builtin-workflows and settings-sections tests for hide-from-selection + management/resolution retention - Add minor changeset for @runfusion/fusion Files changed: .changeset/fn-7969-deprecate-coding-ideas.md | 7 +++++++ docs/dashboard-guide.md | 2 +- docs/workflow-steps.md | 2 +- .../core/src/__tests__/builtin-workflows.test.ts | 28 ++++++++++++++-------- packages/core/src/builtin-workflows.ts | 9 +++---- packages/core/src/types.ts | 9 ++++--- .../app/__tests__/settings-sections.test.tsx | 2 ++ 7 files changed, 43 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-7969 Fusion-Task-Lineage: 578ae727-e1b6-4ff9-a3a2-d1228c50fba6 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f0485041c1 |
FN-8000: open mobile chat threads before streaming back-btn assertions
Fix ChatView mobile streaming tests that failed when chat-back-btn was missing after remount with an active session restored by useChat. - Open the session via sidebar click before asserting chat-back-btn in streaming mobile tests - Populate sessions/filteredSessions fixtures for the silent-request mobile case - Assert empty-state copy stays hidden once the thread is open - Document remount/sidebar restore requirement with FNXC comment Files changed: packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8000 Fusion-Task-Lineage: 8893884c-b5ed-4c5a-8962-31688b859b9a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |