Commit Graph

13659 Commits

Author SHA1 Message Date
gsxdsm
161edaa694 FN-9148: add PostgreSQL loaded-failure census
Add retained-log census tooling and evidence for PostgreSQL loaded-lane timeout investigations.

- Parse complete Vitest logs and optional diagnostics without opening PostgreSQL or running tests.
- Classify failing files, lifecycle positions, failure shapes, backend headroom, waits, and watchdog data.
- Cover high-failure, healthy, malformed-diagnostics, and truncated-log cases with fixtures.
- Document the reproduced population, unsupported remedies, and successor measurement requirements.

Files changed:
 ...res-loaded-lane-unrelated-failure-population.md |  80 ++++++++++
 docs/testing.md                                    |   8 +
 .../fixtures/pg-loaded-failure-census/high-run.txt |  77 +++++++++
 .../fixtures/pg-loaded-failure-census/high.jsonl   |   4 +
 .../fixtures/pg-loaded-failure-census/low-run.txt  |   4 +
 .../pg-loaded-failure-census/truncated-run.txt     |   2 +
 .../__tests__/pg-loaded-failure-census.test.mjs    |  65 ++++++++
 scripts/pg-loaded-failure-census.mjs               | 172 +++++++++++++++++++++
 8 files changed, 412 insertions(+)

Fusion-Task-Id: FN-9148

Fusion-Task-Lineage: c632a9d0-b823-4416-ab46-0d834e850007

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-19 06:24:01 -07:00
gsxdsm
687990c0a6 FN-9146: document PostgreSQL flake campaign evidence
Document the bounded investigation results for active core PostgreSQL suite-only flakes.

- Record per-lane outcomes and sampled cluster-capacity evidence for entries 1, 2, and 7.
- Clarify reproduced, unreproduced, and unattributed statuses without changing quarantine policy.
- Ratchet campaign completeness, ownership, and documentation-link coverage in the register tests.

Files changed:
 .../suite-only-flakes-observed-register.md         | 71 +++++++++++++++++-
 docs/testing.md                                    |  4 +
 scripts/__tests__/observed-flake-register.test.mjs | 85 ++++++++++++++++++++--
 3 files changed, 148 insertions(+), 12 deletions(-)

Fusion-Task-Id: FN-9146

Fusion-Task-Lineage: d5de3fac-48d6-4ee7-ae09-173007d234e5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-19 05:38:54 -07:00
gsxdsm
91daa37162 FN-9147: synchronize script test baselines
Align script-test expectations and documentation with the current validator and dashboard test inventory.

- Add the pre-JSON anchor validator to the verify-fast baseline.
- Ratchet documented pretest validators against canonical order and membership.
- Remove deleted dashboard tests from timing data.

Files changed:
 docs/testing.md                        | 22 +++++++++-
 scripts/__tests__/verify-fast.test.mjs | 76 +++++++++++++++++++++++++++++++++-
 scripts/test-timings.json              |  2 -
 3 files changed, 95 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-9147

Fusion-Task-Lineage: fdb33c60-e019-402b-b91b-ba0f0f971350

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-19 05:23:41 -07:00
gsxdsm
ed2cbd08a1 FN-9145: clarify active flake register states
Reorganize the suite-only flake register around its current decision state.

- Separate active observations, gate evictions, and archived closed records while preserving entry anchors.
- Mark entry 1 as an escalated second sighting and restore FN-9126/FN-9131, FN-9128, and FN-9127 ownership.
- Add regression checks for active counts, exact states and owners, and testing-guide anchors.

Files changed:
 .../suite-only-flakes-observed-register.md         | 189 +++++++++++++--------
 scripts/__tests__/observed-flake-register.test.mjs |  65 +++++++
 2 files changed, 181 insertions(+), 73 deletions(-)

Fusion-Task-Id: FN-9145

Fusion-Task-Lineage: 39b3292a-db48-4be4-878c-7d2b026abae3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-19 04:16:48 -07:00
gsxdsm
f57661a013 chore(release): v0.77.0-beta.3
Version bump via changesets.
2026-08-18 22:16:44 -07:00
dependabot[bot]
8b0b88ae95 Bump tar from 7.5.13 to 7.5.22 (#3464)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.13 to 7.5.22.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2a22bfc5d3"><code>2a22bfc</code></a>
7.5.22</li>
<li><a
href="df1cd8dc09"><code>df1cd8d</code></a>
Allow transform to be falsey</li>
<li><a
href="0cd9cc3c58"><code>0cd9cc3</code></a>
7.5.21</li>
<li><a
href="631ae59121"><code>631ae59</code></a>
list: prevent unbounded recursion</li>
<li><a
href="ebbb720941"><code>ebbb720</code></a>
7.5.20</li>
<li><a
href="2f271963a7"><code>2f27196</code></a>
fix: fully disable and dispose of unzip when aborting parser</li>
<li><a
href="be440da64e"><code>be440da</code></a>
7.5.19</li>
<li><a
href="2812e93386"><code>2812e93</code></a>
add maxDecompressionRatio guard against explosive decompression</li>
<li><a
href="9ecd4d2956"><code>9ecd4d2</code></a>
7.5.18</li>
<li><a
href="9e78bf058b"><code>9e78bf0</code></a>
refuse to let header size be less than 0</li>
<li>Additional commits viewable in <a
href="https://github.com/isaacs/node-tar/compare/v7.5.13...v7.5.22">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar&package-manager=npm_and_yarn&previous-version=7.5.13&new-version=7.5.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 22:12:17 -07:00
dependabot[bot]
392b780979 Bump @capacitor/push-notifications from 7.0.6 to 8.1.2 (#3465)
Bumps
[@capacitor/push-notifications](https://github.com/ionic-team/capacitor-plugins)
from 7.0.6 to 8.1.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/capacitor-plugins/releases">@​capacitor/push-notifications's
releases</a>.</em></p>
<blockquote>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.1.2</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.1.1...@capacitor/push-notifications@8.1.2">8.1.2</a>
(2026-07-15)</h2>
<p><strong>Note:</strong> Version bump only for package
<code>@​capacitor/push-notifications</code></p>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.1.1</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.1.0...@capacitor/push-notifications@8.1.1">8.1.1</a>
(2026-05-15)</h2>
<p><strong>Note:</strong> Version bump only for package
<code>@​capacitor/push-notifications</code></p>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.1.0</h2>
<h1><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.4...@capacitor/push-notifications@8.1.0">8.1.0</a>
(2026-05-15)</h1>
<h3>Features</h3>
<ul>
<li><strong>push-notifications:</strong> add banner and list
presentation options for iOS (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2529">#2529</a>)
(<a
href="f266d2e86b">f266d2e</a>)</li>
</ul>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.0.4</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.3...@capacitor/push-notifications@8.0.4">8.0.4</a>
(2026-05-07)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>add Importance 0 for notification channels (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2507">#2507</a>)
(<a
href="b98c4f9ee3">b98c4f9</a>)</li>
</ul>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.0.3</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.2...@capacitor/push-notifications@8.0.3">8.0.3</a>
(2026-03-25)</h2>
<p><strong>Note:</strong> Version bump only for package
<code>@​capacitor/push-notifications</code></p>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.0.2</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.1...@capacitor/push-notifications@8.0.2">8.0.2</a>
(2026-03-06)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> notification Bundles with non-string
values (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2445">#2445</a>)
(<a
href="2788f81d5d">2788f81</a>)</li>
</ul>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/8"><code>@​8</code></a>.0.1</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.0...@capacitor/push-notifications@8.0.1">8.0.1</a>
(2026-02-12)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>AGP 9.0 no longer supporting <code>proguard-android.txt</code> (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2468">#2468</a>)
(<a
href="a8760a989f">a8760a9</a>)</li>
</ul>
<h2><code>@​capacitor/push-notifications</code><a
href="https://github.com/7"><code>@​7</code></a>.0.7</h2>
<h2><a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@7.0.6...@capacitor/push-notifications@7.0.7">7.0.7</a>
(2026-07-03)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0bfde98313"><code>0bfde98</code></a>
chore(release): publish [skip ci]</li>
<li><a
href="2be8127d58"><code>2be8127</code></a>
docs(motion): add orientation listener example to README (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2558">#2558</a>)</li>
<li><a
href="060e41c5fb"><code>060e41c</code></a>
chore: add dist to root gitignore (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2560">#2560</a>)</li>
<li><a
href="5b652926fe"><code>5b65292</code></a>
docs(browser): remove Web/iOS specific warning for close() (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2519">#2519</a>)</li>
<li><a
href="ee3475836b"><code>ee34758</code></a>
chore(android): format code for latest prettier (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2553">#2553</a>)</li>
<li><a
href="e84bf954b3"><code>e84bf95</code></a>
chore(status-bar): format Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2547">#2547</a>)</li>
<li><a
href="0b4e735cfd"><code>0b4e735</code></a>
docs(README): remove watch plugin and add local-llm (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2544">#2544</a>)</li>
<li><a
href="d32b1c6608"><code>d32b1c6</code></a>
chore: format java code (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2536">#2536</a>)</li>
<li><a
href="265ca0b1b5"><code>265ca0b</code></a>
chore: format java code (<a
href="https://redirect.github.com/ionic-team/capacitor-plugins/issues/2535">#2535</a>)</li>
<li><a
href="63c874c253"><code>63c874c</code></a>
chore(release): publish [skip ci]</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@7.0.6...@capacitor/push-notifications@8.1.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@capacitor/push-notifications&package-manager=npm_and_yarn&previous-version=7.0.6&new-version=8.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 22:12:05 -07:00
ischindl
72877c8cf9 fix(RUFU-074): idle backoff + jitter for the task-deleted outbox consumer (#3471)
**Problem:** Each dashboard/engine project consumer polled
`task_deleted` outbox on a fixed 5s setInterval, so ~44 per-project
consumers thundered together on the same cadence — an idle DB query
storm and CPU hot-spot even when projects were paused/idle.

**Fix:** The outbox consumer reschedules itself from each poll outcome:
an idle poll (zero events) grows the next delay by
`TASK_DELETED_OUTBOX_BACKOFF_STEP_MS` toward `MAX_POLL_MS`, with ±20%
jitter so the consumers de-synchronize; a poll that delivered events
resets to the fast base. A paused/idle project drains its outbox and
backoff alone drops the DB load.

**Includes:** regression test (bounded jitter + idle growth),
performance changeset, solution doc, deploy handoff script.

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

- **Performance**
- Reduced unnecessary idle polling by gradually increasing the polling
interval, up to 60 seconds, with bounded timing variation.
- Restored the faster 5-second polling cadence when new events, waits,
or transient errors occur.
- Preserved event ordering, delivery guarantees, acknowledgements, and
independent behavior across concurrent consumers.

- **Documentation**
- Added guidance on polling behavior, deployment verification, and
monitoring targets.

- **Tests**
- Added coverage for backoff growth, jitter limits, event bursts,
concurrent consumers, error handling, retries, and clean shutdown.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-18 22:11:47 -07:00
ischindl
f195ff5b3d feat(RUFU-081): add Prometheus-format /metrics system observability endpoint (#3475)
**Problem:** Fusion had no Prometheus-scrapeable metrics surface, so
production CPU/health/DB/fleet observability had to be inferred from
logs and ad-hoc monitoring.

**Fix:** Expose a Prometheus-text `/metrics` HTTP endpoint on the
dashboard with runtime + domain samplers: request latency quantiles
(p50/p95/max), last-request-age freeze indicator, CPU user/system,
RSS/heap, child-process spawn rates by kind, git children count, PG tps,
active/idle/total projects, running agents, and board task counts.
Scraped at 5s by the existing Prometheus/Grafana stack
(fusion-observability).

**Includes:** full test coverage for the Prometheus text parser and each
sampler.

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

* **New Features**
* Added a public, unauthenticated `/metrics` endpoint with
Prometheus-formatted dashboard, runtime, process, database, and domain
metrics.
* Added request latency, resource usage, child-process, project
activity, agent, and task metrics.
* Metrics remain available in headless mode and provide stable snapshots
without triggering additional data writes or on-demand I/O.

* **Documentation**
* Documented metric families, sampling behavior, and degraded-operation
handling.

* **Tests**
* Added comprehensive coverage for collection, formatting, parsing,
endpoint behavior, and sampler lifecycle.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-18 22:11:22 -07:00
gsxdsm
027faaa09f ci(release): fail PRs that regress the beta cycle in .changeset/pre.json (#3486)
## Problem

PR #3472 resolved a `.changeset/pre.json` rebase conflict against a copy
predating the v0.76.0 stable:

| | at `v0.77.0-beta.1` | after #3472 |
|---|---|---|
| `initialVersions["@runfusion/fusion"]` | `0.76.0` | `0.75.1` |
| consumed ledger | 67 entries | 158 (the pre-0.76.0 cycle's) |

Nothing failed at PR time. Days later `pnpm release` saw the cycle
anchored below the shipped `v0.76.0`, fired its stale-cycle re-anchor
(`pre exit` → rewrite all 36 `package.json` → `pre enter`), and proposed
**`0.77.0-beta.0`** — below the already-published `0.77.0-beta.1`. The
re-anchor guard exists to stop a beta numbering under a stable; fed a
stale anchor it caused exactly that.

`pre.json` is generated by changesets, hand-edited by nobody, and
conflicts in nearly every long-lived branch — so a wrong resolution is
invisible until release day. This moves the failure to the PR that
causes it.

## The check

`scripts/check-pre-json-anchor.mjs`, three invariants:

- **`anchor-below-stable`** — `initialVersions` must not sit below the
newest `v*` stable tag. This is the exact predicate
`evaluateBetaCycleAnchor` keys on in `release.mjs`, so green here means
the release will *not* re-anchor.
- **`ledger-regression`** — the consumed ledger must stay a **superset**
of the last `chore(release):` commit's. Deliberately not a count test:
#3472's ledger *grew* 67 → 158 while dropping all 67 real entries, so a
size comparison would have passed it.
- **`dangling-ledger-entry`** — every consumed entry keeps its
`.changeset/*.md`, which pre-mode needs to aggregate notes into the
eventual stable release.

Skips cleanly outside pre-mode (the stable track deletes `pre.json`).

## Wiring

Added to the **Lint** job and `pretest`. Job names are unchanged, so no
branch-protection update is needed. The Lint checkout takes
`fetch-depth: 200` + `fetch-tags` rather than a full 486MB clone —
releases land every few days, so that always reaches a baseline; out of
range the ledger rule reports `SKIPPED` rather than passing vacuously,
and the two local rules still run.

## Verification

- Reproducing #3472's exact `pre.json` in the tree → **exit 1** on all
three rules, with the 67 dropped entries named.
- Clean `main` → exit 0.
- 11 unit tests (`scripts/__tests__/check-pre-json-anchor.test.mjs`),
including an explicit assertion that the ledger *grew* in the regression
case.
- `eslint` clean; workflow YAML parses; job names still `Lint,
Typecheck, Build, Gate`.

No changeset: CI config only, no `@runfusion/fusion` behavior change.

🤖 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 automated validation for beta release-cycle metadata.
* Checks anchor versions, consumed changeset records, and corresponding
changeset files.
* Provides clear success or error messages and skips checks when release
history is unavailable or not applicable.

* **Chores**
  * Pull request checks now run the beta-cycle validation automatically.
* Added comprehensive coverage for valid, invalid, and skipped
validation scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 22:10:59 -07:00
Phil Larson
4d54cfea77 test(engine): repair extracted orphan-resume fixture (#3487)
## Summary
- observe the injected executor dispatch seam in the orphan-resume lane
test
- make the lane guard non-vacuous and cover every renamed non-WIP lane

## Test plan
- `corepack pnpm --filter @fusion/engine exec vitest run
src/__tests__/executor-resume-lanes-resolved.test.ts
--silent=passed-only --reporter=dot` (12 passed)
- `corepack pnpm --filter @fusion/engine typecheck`
2026-08-18 22:10:31 -07:00
Phil Larson
86656db91a test: align terminal scrollback callback assertions (#3488)
## Summary
- updates `useTerminal` callback assertions for the new `(data, reset)`
scrollback contract
- covers both an explicit reset frame and the default non-reset path

## Test plan
- `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest
run --project dashboard-app --silent=passed-only --reporter=default
app/hooks/__tests__/useTerminal.test.ts`
- `pnpm --filter @fusion/dashboard typecheck`
- `pnpm check:changesets`


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

## Summary by CodeRabbit

* **Tests**
* Updated terminal scrollback coverage to validate the new reset-state
information for live updates and buffered replay.
* Expanded checks across message delivery, buffering, initial rendering,
duplicate output, and reconnection scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-18 22:10:05 -07:00
gsxdsm
ad24d9a0eb fix: TUI Enter crash, and show any running tunnel in the System panel
Enter on the System panel killed the TUI. It opens the dashboard URL with a
detached spawn, and a MISSING opener — xdg-open on any slim Linux container,
exactly where Fusion runs headless — is reported asynchronously as an 'error'
event rather than a synchronous throw. The try/catch around spawn never saw
it, so Node re-threw an 'error' with no listener and took the dashboard down.

The tunnel row now covers any tunnel, not just `pnpm dev --tunnel`:
SystemInfo.devTunnelUrl becomes tunnelUrl, fed by the dev wrapper's IPC
hand-off AND by the engine's TunnelProcessManager status subscription. An
operator's remote tunnel URL previously lived only in the Settings UI and
/remote/status, so a headless terminal could not read the address its own
Fusion was reachable at. A dev tunnel wins when both exist, and a stopped
tunnel clears the row rather than stranding a dead URL.

And the panel measures the row it draws. estimateSystemContentRows accounted
for URL and Token but not Tunnel, so a wrapping trycloudflare hostname was
squeezed out of the panel it had just been added to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:57:57 -07:00
gsxdsm
5422063fb9 test: pin the dev tunnel row in the TUI system panel
Covers the render both ways: the Tunnel row appears with its URL when a
tunnel is published, and is absent for an ordinary `fn dashboard` run.
Verified by mutation — deleting the row from app.tsx fails the first test.

Driving this through a real TTY proved impractical from a non-interactive
shell (isTTYAvailable needs stdin AND stdout to be TTYs, which neither
`docker exec -t` nor a detached `script` provides), so the render is pinned
here instead of by observation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:30:47 -07:00
gsxdsm
204772bcc3 fix: show the dev tunnel URL in the TUI instead of painting over it
`pnpm dev --tunnel` printed its banner to stdout, and a TTY run then handed
the screen to DashboardTUI, which repainted over it. The public URL — the
entire output of the flag — was unreadable.

The wrapper forwards the URL to the dev child over the IPC channel it already
uses for the listening report, and the dashboard renders a Tunnel row in the
system panel beside URL and Token.

Capture is order-independent. cloudflared can publish before or after the TUI
is constructed, and an IPC message that arrives with no listener attached is
lost, so the URL is stored at run scope on arrival and applied by whichever
half completes second.

Watch-mode restarts reuse the tunnel (a fresh quick tunnel would hand out a
new hostname on every reload), but the new child knows nothing about it, so
the wrapper re-announces on each listening report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:20:34 -07:00
gsxdsm
2556083b68 test: restore the four workspace-merge tests broken by a stale mock
project-engine.test.ts hand-writes a factory for ../merge/merger-ai.js, and
it had drifted from the module: WorkspaceMergeDispatchSupersededError was
missing. Production imports that class, so the merge-queue drain threw "No
<export> is defined on the mock" before the code under test ran — the four
Phase C hardening cases then failed on a resolved promise and an absent
updateTask call, for a reason unrelated to what they assert.

No product change. All 114 tests in the file pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:14:26 -07:00
gsxdsm
16e63462cc fix: audit fallout — tunnel port, orphaned dev processes, scrollback clear
Auditing for repeats of the `pnpm dev --tunnel` bugs turned up the same
mistakes elsewhere.

Remote tunnels assumed 4040. ProjectEngine's Cloudflare quick tunnel
hardcoded http://localhost:4040, so a dashboard on an explicit --port, a PORT
override, or runDashboard's EADDRINUSE rebind published a PUBLIC tunnel to
whatever else held 4040. The dashboard now records its bound port
(setLocalDashboardPort, from both runDashboard and headless serve) and the
tunnel reads it, keeping 4040 only as the pre-report default.
register-discovery-routes already derived its port from req.socket.localPort
and is untouched.

Stopping the dev wrapper orphaned everything it started. It installed no
signal handlers, so teardown only ran from the child's close handler:
signalling the wrapper left the dev server AND its cloudflared alive —
observed twice, four surviving processes each time, including a public
trycloudflare URL still serving a dev server believed to be down. Ctrl-C hid
it by signalling the whole process group.

SessionTerminal appended scrollback instead of clearing first, though the
server sends it as a separate frame precisely so the client can clear. Latent
today because every reattach builds a fresh xterm; a duplicated-history bug
the moment an in-place reconnect appears.

And BackupManager's centralDbPath is gone: written, never read, and a
leftover of the removed SQLite backup — the same class of stale artifact that
onboarding was using as evidence about a Postgres install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:09:03 -07:00
gsxdsm
b67e3aa8bc fix: stop onboarding ambushing a working install with questions
Two defects, both visible as "why is it asking me about AI provider setup
when I just started a dev server?".

The auto-launch gate probed ~/.fusion/fusion-central.db to decide whether the
install was initialized. SQLite central was removed, so a Postgres install
never creates that file and the probe was permanently false: onboarding
auto-launched on every interactive start of a completely working Fusion,
until something happened to stamp the completion marker. The probe now also
accepts the embedded Postgres data directory.

And auto-launched onboarding ran the full interactive flow. It fires while
the operator is starting something else, so its questions interrupt work
nobody asked to interrupt — and a dev server stopped on a prompt never
listens, which is why `pnpm dev --tunnel` produced no dev server and so no
tunnel link. Auto-launch is now non-interactive: create the central database,
stamp the marker, point at the dashboard, ask nothing. `fn onboard` still
runs every step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:55:18 -07:00
gsxdsm
8d76af3e5f docs: correct a stale dev-tunnel comment describing the removed fallback
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:40:41 -07:00
gsxdsm
ce69558c7a fix: always create the central DB during onboarding, never ask
The step was a skippable prompt, but declining left an install Fusion cannot
run on — reported by a line that described the breakage without fixing it.
There is no useful negative answer.

It also blocked non-interactive startup: `pnpm dev --tunnel` sat on "Run
central db now? (Y/n)", never reached listening, and so served nothing at all
while the tunnel had no dev server to point at.

The database is now created unconditionally when absent; the already-exists
path is untouched. Scripted onboarding tests lose their leading central-DB
answer, and the skip-every-step case now asserts the database is still
created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:40:34 -07:00
gsxdsm
f12b9f8463 fix: wait for the dev server rather than tunnelling a guessed port
When the dev child had not reported a bound port within 60s, the wrapper fell
back to the configured port and published a tunnel to it. In the case the
port fix exists for — a container whose own Fusion owns 4040 — that hands out
a dev-looking URL serving a completely different instance, with only a
passing "which may not be it" warning to show for it.

Observed with a dev server stopped on the interactive `Run central db now?
(Y/n)` prompt: it never listens, so it never reports, so the tunnel published
the container's Fusion instead.

A missing tunnel is a visible problem that explains itself; a tunnel to the
wrong app is a silent one. The wrapper now waits, unbounded, printing a
notice once a minute that names the interactive-prompt case. An explicit
--tunnel=PORT still publishes immediately, since it names a target the dev
child knows nothing about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:38:48 -07:00
gsxdsm
6f461a4744 fix: take the dev tunnel's token from the dev server, not a guessed file
The banner re-derived the token from ~/.fusion/settings.json. That is simply
the wrong source: on a real run the file contained no daemonToken while the
dashboard's own banner, two lines above, printed a working one — so the
tunnel claimed no token existed next to a token that plainly did.

The dashboard already holds the resolved token at the point where it reports
its bound port, so it now reports both over the same IPC message and the
wrapper prefers that over anything it could derive. The env/settings lookup
survives only for targets that report nothing, such as an explicit
--tunnel=PORT aimed at a server the dev child knows nothing about. The token
crosses the existing parent/child channel only; it is never logged or sent
onward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:16:44 -07:00
gsxdsm
e4a53b6f47 fix: stop terminal history duplicating on reconnect, agree one size across viewers
Driving a shared PTY with two real WebSocket viewers against a live instance
surfaced two defects.

Duplicated history: the server replayed the entire scrollback on every
attach, and the client appended it into an xterm that still displayed that
history. Every reconnect — backgrounded tab, laptop sleep, heartbeat timeout
— therefore added a second copy, seen as the last prompt appearing twice.
TerminalService now tracks cumulative output and serves a resume: a client
reports the offset it has rendered and receives only the gap, or a full
replay flagged reset so it clears first.

Wrong size: resize was last-writer-wins. Measured — viewer A at 80x24 had
its shell report 200x50 the moment viewer B attached on a bigger screen,
while A still drew 80 columns, so wrapped lines and full-screen programs
broke for A. TerminalViewportRegistry sizes the PTY to the per-dimension
minimum across attached viewers, the rule terminal multiplexers settled on,
and gives room back when a viewer leaves. Viewers that have not yet measured
themselves do not constrain the size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:52:36 -07:00
gsxdsm
9f10767254 feat: share terminal sessions across browsers
Terminal PTYs already lived in a server-side registry whose WebSocket attach
supports several viewers, but the tab list is per-browser localStorage and a
browser with no stored tabs skipped the session listing entirely (FN-7686's
cold-open optimization) and spawned its own PTY. Two people on one Fusion —
or one person in a second browser — therefore never saw each other's
terminals and quietly accumulated parallel sessions.

- A zero-tab client adopts the server's sessions, oldest first, so every
  client converges on the same set. A client with stored tabs still only
  validates them; adopting there would resurrect tabs the user closed here.
- FN-7686's guarantee weakens from "never waits" to "waits at most
  ADOPT_LIST_TIMEOUT_MS (1.5s), then behaves exactly as before": auto-create
  fires on a 0ms timer, so a background list can never win that race. Its
  regression test and comment now state the bounded contract.
- Closing a tab asks which close was meant — detach here (PTY keeps running
  for other viewers) or end the session for everyone. alwaysAsk is set: this
  gates an informed choice and both wrong answers are destructive. Desktop
  and mobile close controls both route through it.
- A footer Reopen control lists sessions the server still runs that this
  browser is not showing and reattaches to them, so detaching is not a
  one-way door.

Also fixes a multi-viewer data bug: the attach called
getScrollbackAndClearPending(), discarding queued output and deleting a slice
of every already-attached viewer's live stream. It now flushes pending output
to existing viewers first, then reads scrollback for the newcomer, so each
viewer receives it exactly once.

Drive-by: closeTab's dependency array was empty while its body reads
projectId, freezing it at the first render's project.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:13:53 -07:00
gsxdsm
0289d26b13 fix: tunnel the dev server's real port, not whatever holds 4040
The tunnel target was resolved from PORT/4040 before anything bound, but an
occupied port makes the dashboard silently rebind to an ephemeral one
(server.listen(0) on EADDRINUSE). With a normal Fusion already running on
4040, `pnpm dev --tunnel` therefore published THAT instance under a
dev-looking URL while the dev server sat unreachable on a random port.

The dashboard now reports the port it actually bound to the dev supervisor
over IPC, and the wrapper tunnels that:

- IPC is enabled whenever --tunnel is set, not only in watch mode; a plain
  tunnel run previously had no channel at all.
- The tunnel waits for the report (60s cap, then falls back to the configured
  port with a warning), so it also cannot come up against a port nothing is
  serving yet.
- A mismatch is logged instead of silent.
- A reported port is treated as the dashboard whatever its number, so the
  banner keeps printing the bearer token; without that, the ephemeral case
  would classify the dev dashboard as a foreign port and drop it.
- An explicit --tunnel=PORT names a target the dev child knows nothing about,
  so it never waits and is still compared against the configured port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:49:42 -07:00
gsxdsm
ee57f8a3b9 fix: print the dashboard token with the dev tunnel URL
`pnpm dev --tunnel` published a bare URL under a "public, unauthenticated"
header. That label was wrong for the flag's own default target: --tunnel with
no port aims at the dashboard, which is bearer-token gated, so the recipient
of a shared link got a 401 with no token to supply.

resolveDevTunnelAuth() now classifies the target and the banner says what is
actually true of it:

  token         dashboard with auth on — prints the token and a ?token= link,
                resolved from FUSION_DASHBOARD_TOKEN, FUSION_DAEMON_TOKEN,
                then ~/.fusion/settings.json
  token-pending first run, token not minted yet — defers to the dashboard's
                own startup banner
  no-auth       --no-auth is on; the dashboard really is open
  foreign       a non-dashboard port; Fusion has no auth to lend it, the only
                genuinely ungated case

Auth resolves at banner time rather than flag-parse time so a token the dev
child mints on a first authenticated run is already readable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:21:50 -07:00
gsxdsm
4c545676b8 fix(security): persistent remote links keep working, short-lived ones still expire
The remote-login session fix capped EVERY session at shortLived.ttlMs, so opening
a persistent link gave a 15-minute session. That is wrong for the link type an
operator uses for their own devices — persistent means the link keeps working.

Session lifetime now follows the token type:
  short-lived -> never outlives the token that authorised it (falls back to the
                 configured TTL when there is no usable expiry, and an already
                 expired token does not fall through to the long one)
  persistent  -> a long session (30 days), because the link itself never expires

It is still a SESSION rather than the daemon token — opaque, revocable, and gone
on restart — so the leak this all started from stays fixed: a recipient never
receives the dashboard's permanent credential.

Moved the rule out of server.ts into remote-session.ts so it is unit-tested
rather than living inline in a request handler. 15 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:05:15 -07:00
gsxdsm
0e7c353f2b fix(security): remote login no longer hands over the dashboard token
Sharing a remote link gave the recipient the dashboard's real credential.
`/remote-login?rt=…` validated the remote token and then redirected to
`/?token=<daemonToken>` — so the daemon token landed in their URL bar, their
history, and anything that logs URLs. It also made the separate remote token
pointless: revoking it left the recipient permanently authenticated, because
they were holding the daemon token itself, not the remote one.

A validated remote token now mints an opaque, expiring, revocable session
(createRemoteSessionStore) returned as an HttpOnly, SameSite=Lax cookie (Secure
over https, which a quick tunnel always is), and the redirect carries nothing
sensitive. The auth middleware gains a third credential source, checked only
after the daemon token and only when a validator is installed, so the existing
header and fn_token paths and their constant-time comparison are untouched.

Session TTL is capped by the remote token's own remaining life when it is
short-lived — a 15-minute share link must not buy a longer stay through the back
door — and otherwise uses the configured shortLived.ttlMs (default 15m). Sessions
are in-memory on purpose: a restart invalidating them fails in the safe
direction, and persisting would write a credential to disk for no benefit.

A source-level ratchet asserts the handler never puts the daemon token in a
redirect again; verified it fails when the old line is reinstated, since the leak
was one line and far easier to reintroduce than to notice.

Also: POST /api/remote/tunnel/start answered {state:"starting"} when no engine
was attached, so the UI showed a tunnel coming up that never would, settling to
stopped with lastError:null and no way to tell it from a broken one (hit live in
a container whose launch dir is not the registered project — unscoped requests
fall back to a store with no engine). It stays 200 and idempotent, because a
dashboard legitimately runs --no-engine, but now reports the truth: stopped, with
REMOTE_TUNNEL_ENGINE_UNAVAILABLE naming the ?projectId= fix.

63 dashboard remote/auth tests pass, including 11 new ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:55:31 -07:00
gsxdsm
7423555c46 feat(dev): pnpm dev --tunnel publishes the dev server over a quick tunnel
Operator case: someone works inside a remote Fusion (a container, a shared box),
starts a dev server there, and needs to view it from their own browser. The dev
server binds inside that machine, so without a tunnel the only options are port
publishing or a VPN — both needing cooperation from whoever owns the host.

  pnpm dev --tunnel            # tunnels the dashboard port (PORT, default 4040)
  pnpm dev --tunnel=5173       # tunnels a Vite dev server instead
  pnpm dev --tunnel dashboard  # tunnel the default port AND run the dashboard
  FUSION_DEV_TUNNEL=1 pnpm dev

Cloudflare QUICK tunnels are usable here precisely because a dev server is HTTP:
no account, no domain, no card. The TCP endpoints that SSH would have needed
require a card (ngrok) or a domain plus Zero Trust (Cloudflare) — that asymmetry
is why this exists for HTTP only, and it is recorded in the module header so the
next person does not retry the SSH variant.

Design decisions:
- Tunnel failure is NON-FATAL. A missing cloudflared or a tunnel that never
  publishes a URL logs and is skipped; losing a preview URL must never cost the
  operator their dev loop.
- Watch-mode restarts reuse the existing tunnel. A fresh quick tunnel hands out a
  different hostname each time, which would invalidate an already-shared link.
- `--tunnel` consumes a following token only when it is numeric, so
  `--tunnel dashboard` forwards `dashboard` to the dev command rather than
  tunnelling port NaN. That is the bug this flag shape invites, so it is tested.

Verified end to end in a container: a dev server bound to 127.0.0.1 inside it was
fetched from the public internet through the tunnel (200, correct body). Also
confirmed that tunnelling the DASHBOARD port does not weaken auth — unauthenticated
requests through the tunnel return 401 for /api/tasks, /api/settings and
/api/artifacts, with only /api/health open by design.

Adding two fields to parseDevWrapperArgs' return broke two existing strict toEqual
assertions; those were updated rather than loosened to toMatchObject. 27 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 17:18:56 -07:00
gsxdsm
b18c9d7594 FN-9144: Preserve test velocity measurement verdicts
Make test-velocity investigation notes durable across report regeneration and concurrent history updates.

- add idempotent note targeting for historical measurement entries
- serialize history mutations with bounded stale-lock recovery
- render all annotated cycles and record the W33 gate variance verdict
- document the generated-report workflow and cover retention/concurrency behavior

Files changed:
 .../merge-gate-w33-walltime-regression.md          |  15 ++
 docs/test-velocity-baseline.md                     |  12 ++
 docs/testing.md                                    |   2 +
 scripts/__tests__/test-velocity-baseline.test.mjs  | 153 +++++++++++++-
 scripts/test-velocity-baseline.mjs                 | 221 +++++++++++++++------
 scripts/test-velocity-history.json                 |   6 +
 6 files changed, 349 insertions(+), 60 deletions(-)

Fusion-Task-Id: FN-9144

Fusion-Task-Lineage: e3869e40-2cbc-4e5b-844e-9091da96b652

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-18 08:08:24 -07:00
gsxdsm
86950400c9 chore(release): v0.77.0-beta.2
Version bump via changesets.
2026-08-18 00:22:21 -07:00
gsxdsm
682082b558 fix(release): restore beta cycle anchor clobbered by #3472 rebase
PR #3472 resolved a .changeset/pre.json rebase conflict against a stale
copy, reverting initialVersions from 0.76.0 back to 0.75.1 and the
consumed-changeset ledger from 67 back to the pre-0.76.0 cycle's 158.

release.mjs then saw the cycle anchored below the shipped v0.76.0 stable,
treated it as a stale cycle, and did pre exit -> rewrite versions -> pre
enter. That reset the anchor and made `changeset version` propose
0.77.0-beta.0 -- a downgrade from the already-published 0.77.0-beta.1.

Restores pre.json to its v0.77.0-beta.1 state. All 67 ledger entries
still have their .md files; 30 changesets remain unconsumed for beta.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:17:39 -07:00
Timoteo
725b0a3330 feat: expose Fusion custom tools to ACP agents via loopback MCP bridge (#3476)
## Summary

- ACP runtimes can expose Fusion custom tools (`fn_*`) to external
agents such as Hermes ACP and Prime.
- When the engine passes `customTools`, `AcpRuntimeAdapter` starts a
per-session loopback tool bridge and registers it as a stdio MCP server
in `session/new.mcpServers`.
- The bridge uses a per-session bearer token, binds only to loopback,
preserves the MCP request ID as `toolCallId`, propagates tool `isError`
results, and exposes only runnable non-built-in tools.
- The MCP shim supports `initialize`, `ping`, `tools/list`, and
`tools/call`, reporting transport, authentication, malformed-request,
and unknown-tool failures correctly.
- Bridge startup failures degrade gracefully: the ACP session remains
usable without custom tools and exposes a fixed `fusionToolBridgeError`
reason code for engine auditing.
- Startup, request, and teardown paths clean temporary schemas and
listeners; disposal aborts cooperative tools, bounds non-cooperative
drains, prevents post-dispose execution, and preserves the existing
synchronous session contract through an awaitable `disposePromise`.
- The packaged CLI stages `mcp-schema-server.cjs` beside the ACP runtime
distribution.

## Verification

Validated at source commit `2fa6fd2e26923ac6241d1b9379b3819f1832c079`:

- `pnpm --filter @fusion-plugin-examples/acp-runtime test` — 21 files /
251 tests passed
- `pnpm --filter @fusion-plugin-examples/acp-runtime typecheck` — passed
- Plugin build with `.cjs` asset copy — passed
- `pnpm check:changesets` — passed
- `git diff --check` — passed
- MCP shim smoke test — passed
- Greptile Review — passed
- CodeRabbit — passed
- Devin review — 0 bugs, 6 analyses, `errored_tasks=[]`, quality `4/10`

## Scope

ACP runtime plugin only. The Hermes CLI runtime remains untouched;
Hermes ACP is enabled by selecting the generic ACP runtime with
`acpBinaryPath: hermes` and `acpArgs: ["acp"]`.

---------

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-08-18 00:11:06 -07:00
ischindl
0540686599 fix(RUFU-073): thread a per-tick task workflow-selection cache through scheduler reads (#3470)
**Problem:** Scheduler was re-reading each task's
`task_workflow_selection` once per park-resolution (sweep, hold-release,
moved, unpause/wake), causing a nonstop PostgreSQL query storm (~232
idx_scan/s) on idle polling — a major engine CPU hot-spot.

**Fix:** Memoize the workflow selection per scheduler tick/event —
thread a shared, per-event selection cache through
`resolveWorkflowIrForTask` and all park-resolution handlers, then throw
it away. Each task resolves its parked columns with at most one read of
`task_workflow_selection` per tick. A selection write is always observed
on the next event's fresh cache (never a global/infinite LRU).

**Includes:** regression test asserting the once-per-tick read
invariant, performance changeset + per-tick-cache solution doc,
deploy+verify handoff script, and the parallel quarantine-ledger merge
(origin FN-9125 + RUFU-072 OOM entries both retained).

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

* **Performance Improvements**
* Reduced repeated workflow-selection reads during scheduler ticks and
related event processing.
* Improved scheduler and health API responsiveness through per-operation
caching and read deduplication.
* Preserved existing behavior, including retry handling for failed reads
and synchronous data-store support.

* **Documentation**
* Added architectural guidance covering workflow-selection performance,
caching behavior, and verification criteria.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-18 00:10:46 -07:00
Phil Larson
95466b7811 fix(dashboard): remove stale taskStuck aliases (#3483)
## Summary
- removes the deleted taskStuck helper from dashboard package exports
and local build/test aliases
- removes the dependency-graph plugin TypeScript path for the deleted
dashboard module
- adds a script regression so the removed module cannot be reintroduced
as a stale alias

## Test Plan
- pnpm test:scripts --
scripts/__tests__/dashboard-stuck-task-removal.test.mjs
- pnpm check:changesets
- pnpm exec eslint
scripts/__tests__/dashboard-stuck-task-removal.test.mjs
packages/dashboard/vite.config.ts packages/dashboard/vitest.config.ts
- pnpm --filter @fusion/dashboard typecheck
- pnpm --filter @fusion-plugin-examples/dependency-graph build
- pnpm --filter @fusion/dashboard build

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

## Summary by CodeRabbit

* **Bug Fixes**
* Removed stale stuck-task references from dashboard package and build
configurations.
* Prevented unavailable task-stuck utilities from being exposed or
resolved.

* **Tests**
* Added validation to ensure removed task-stuck references do not
reappear in dashboard or plugin configuration.

* **Documentation**
  * Recorded the cleanup in the project’s release notes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-18 00:09:53 -07:00
gsxdsm
1da6375c68 fix(git): Fusion sets its own commit identity, per agent
Auto-merge did nothing on a fresh container: tasks reached in-review, the merge
began, and FN-001 sat at status:merging forever with no error in the UI. The
cause was that git could not commit at all — `git commit` in the container dies
with "Author identity unknown ... Please tell me who you are", because a
container has no git identity and Fusion was borrowing the environment's.

Only workspace-fence-ref.ts ever passed an explicit identity. The merge commits,
the merger's --amend, and the experiment git-ops all inherited whatever
user.name/user.email the host happened to have. The existing commitAuthor*
settings only added a Co-authored-by TRAILER; they never set the author.

resolveCommitIdentity (packages/engine/src/git-identity.ts) now resolves:
operator commitAuthor* settings > the acting agent > Fusion. An agent-derived
identity is `<Agent Name> (Fusion) <slug@agents.fusion.local>`, so history
attributes a change to the agent that made it instead of one anonymous bot, per
the operator's request.

Applied at mergerCommitEnv — the single env all eight merger commit sites share
— and via `-c user.name/-c user.email` for merger-ai and experiment git-ops,
which build their own argv and bypass that env. Author AND committer are pinned:
git fails on a missing committer just as hard as on a missing author.

`commitAuthorEnabled: false` returns undefined and restores ambient git config,
so an operator who wants commits authored as themselves keeps that.

Verified: 9 new helper tests, 95 engine auth+identity tests, pnpm test:gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:49:33 -07:00
gsxdsm
889728bd20 fix(onboarding): offer a default model as soon as a provider connects
Operator on a fresh install was never prompted to pick a default model, and the
dashboard later advertised "Continue Setup" at the AI Setup step even though
they had finished the flow.

availableModels was loaded once at mount and re-fetched only when a CUSTOM
provider was added. On a fresh install nothing is connected at mount, so the
list starts empty and the Default Model section renders its empty state — and
after an OAuth login or an API-key save it was never refreshed, so it kept
saying "No models available yet. Connect a provider above to see model options."
while a provider sat connected right above it.

Both connect paths now refresh the catalogue. Once a provider is connected and
no model is chosen, the section retitles from "Default Model (Optional)" to
"Choose your default model" and scrolls itself into view once — the moment it
becomes actionable is exactly the moment it is off screen, below every provider
card. It stays skippable.

scrollIntoView is called only when it exists: JSDOM does not implement it, and a
cosmetic nudge must never throw out of an effect and take the modal down (it did
— 24 suites failed until guarded).

Completion is additionally marked in a `finally`, so a failed default-model
settings write cannot leave onboarding recorded as unfinished. That part is
hardening, NOT a proven repro: a test written against it passed with and without
the change, so it was removed rather than kept as false coverage, and the reason
is recorded in the suite.

Also folds in the container's default git identity, which the merge investigation
needed (see the follow-up commit for the per-agent identity work).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:45:40 -07:00
gsxdsm
189087adf8 feat(docker): ship gh, tailscale and cloudflared in the image
Operator asked for cloudflared, tailscale, rg, git and gh available by default
in the container. git/ca-certificates/ripgrep already landed; this adds the
remaining three.

Each comes from its vendor's own signed apt repository rather than a
curl-to-shell installer, so signature checking and upgrades follow the normal
apt path:
  gh          https://cli.github.com/packages
  tailscale   https://pkgs.tailscale.com/stable/debian
  cloudflared https://pkg.cloudflare.com/cloudflared

Why each belongs in the image: gh backs Fusion's gh-cli GitHub auth mode (the
auth route instructs operators to run `gh auth login`, impossible without the
binary), cloudflared backs the dashboard's remote-access feature whose in-app
installer cannot bootstrap itself reliably in a slim container, and tailscale is
the private-network option for the same box.

Installing tailscale does NOT make tailscaled runnable by itself: the daemon
also needs --cap-add NET_ADMIN --device /dev/net/tun at docker run. Shipping the
binary is the image's part; granting kernel capabilities stays an explicit
operator decision.

Commands were validated live in a running container before being written here;
the guard test asserts both the repo wiring and the package names so half a
change cannot silently ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:48:49 -07:00
gsxdsm
aedee4b823 feat(docker): ship ripgrep in the image
The coding agents Fusion drives reach for `rg` as their primary search tool. It
was absent from the image, so inside a container they silently fall back to
slower or partial search while working fine on a developer machine that has it
installed. Operator asked for it by default.

Installed alongside git and ca-certificates in the runner stage, and covered by
the same runner-stage guard so it cannot quietly drop out again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:43:59 -07:00
gsxdsm
3105b06102 fix(docker): install ca-certificates so git can clone over HTTPS
Operator hit "Git clone failed: ... server certificate verification failed.
CAfile: none CRLfile: none" the moment they tried to add a project in the
container.

The runner stage installed git but not ca-certificates, and the slim base ships
zero CA certificates (/etc/ssl/certs was empty). git verifies TLS against the
SYSTEM trust store, so every HTTPS remote failed and project setup — the first
thing anyone does after logging in — was impossible in Docker.

It hid because Node carries its OWN bundled CA store: the dashboard, model API
calls, and the OAuth token exchanges against platform.claude.com and OpenAI all
worked fine, so the image looked healthy right up until the first clone. Nothing
else in the image exercises the system trust store, so a guard is added rather
than trusting someone to notice next time.

Verified in the running container: installing ca-certificates took it from 0 to
301 certs and `git clone https://github.com/Runfusion/Fusion.git` then succeeded
as the node user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:21:13 -07:00
gsxdsm
9eae6b9bc5 fix(auth): a provider's first-ever login silently saved nothing
Operator could not log in to Anthropic or Codex on a fresh container: every
attempt ended "Login did not complete. Please try again.", while the same
providers worked flawlessly on their long-lived native install.

FusionAuthStorage.modify() is the seam pi persists a COMPLETED LOGIN through
(Models.login -> credentials.modify(provider.id, ...) in pi-ai models.js:198).
It resolved its write target with `creating: false` and returned before invoking
the callback whenever the provider had no credential row yet:

    const target = this.resolveWriteTarget(provider, current, false);
    if (!target || !this.credential(target, current)) return { changed: false };

So a first login completed its browser flow, exchanged the code, took and
released the lock file, wrote NOTHING, and resolved as success — leaving the
dashboard poll to see authenticated:false and report the generic failure.

It reproduces only on a store with no existing row, which is why it looked
environment-specific: an install that has logged in before takes the same path
as a refresh over an existing row and is fine, while every new container, new
machine, or wiped ~/.fusion can never complete a first login for ANY provider.

Evidence from the operator's container: flow ended with err=None (pi resolved,
no error), nothing logged, auth.json still {}, the agent directory's mtime
bumped when the lock was taken and released while auth.json itself never
changed, and an API-key write — which goes through set(), not modify() — landed
immediately.

modify() now creates when absent and updates when present; a callback returning
undefined still writes nothing, so pi's refresh-bails-out behaviour is unchanged.

auth-storage-instances.test.ts asserted the old behaviour, grouping modify() with
remove/logout/removeInstance as "non-creating". The removal guarantees are kept;
the modify() assertion is inverted, because it encoded the defect.

Also surfaces the server's own loginError through a new describeLoginFailure()
helper instead of the generic sentence, so an OAuth state mismatch reads as the
stale-tab instruction it is. Writing its test caught a bad regex of mine:
`code.*expired` matched "OpenAI Codex ... token_expired", a different failure.

Verified: the new first-login test fails against the old `creating: false` and
passes with the fix; 86 engine auth tests, 238 dashboard auth/dialog tests, and
pnpm test:gate all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:39:06 -07:00
gsxdsm
7c1d06237c feat(dashboard): use the persistent sign-in dialog in Settings authentication too
Settings had its own copy of the flow onboarding just replaced: instructions and
the paste field rendered inline in the provider row of a scrolling list, with no
single place showing what the login was waiting on. Same dialog now serves both,
so an operator who learns the flow at first run sees it again when adding a
provider later.

Settings differs in one way that matters: every flow is keyed by `stateKey`
(`providerId`, or `providerId[instance]` for a named credential instance),
because one provider can hold several accounts. `loginDialog` therefore carries
{ stateKey, providerId, instanceId, providerName } and threads instanceId back
to handleSubmitManualCode / handleCancelLogin, and the row suppresses its own
instructions + paste field ONLY for the key the dialog owns — a sibling account
keeps its inline field. (An early draft keyed on `provider:default`, which is
not the real format and broke exactly that case; caught by the new tests.)

The dialog renders outside renderModalShell: the modal presentation is a
FloatingWindow, and a portaled dialog inside a window's React subtree lifts that
window above itself on first click. The embedded presentation is unaffected.

Verified in a container build against the real Settings UI: dialog opens on
Continue to login, the row's inline paste field disappears (0 present), exactly
one paste field exists, and the dialog is not a descendant of the window.
756 dashboard tests pass, including 3 new handoff tests; typecheck and eslint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:58:15 -07:00
gsxdsm
0a50e2142d FN-9143: inherit selected workflow in New Task dialog
Make every New Task entry point seed the dialog from the active Board or List workflow.

- Resolve implicit workflow selection through the modal manager while preserving explicit choices and All workflows behavior.
- Forward selected workflow IDs from List view and navigation-backed dialog entry points.
- Add cross-surface regression coverage, operator documentation, and a patch changeset.

Files changed:
 .changeset/fn-9143-new-task-selected-workflow.md   |  7 +++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/App.tsx                     |  4 +-
 packages/dashboard/app/components/AppModals.tsx    |  4 +-
 packages/dashboard/app/components/Column.tsx       |  4 +-
 .../dashboard/app/components/LeftSidebarNav.tsx    |  4 +-
 packages/dashboard/app/components/ListView.tsx     |  7 ++-
 .../components/__tests__/LeftSidebarNav.test.tsx   |  1 +
 .../app/components/__tests__/ListView.test.tsx     | 13 +++--
 .../new-task-dialog-selected-workflow.test.tsx     | 46 ++++++++++++++++++
 .../dashboard/app/components/dashboard/types.ts    |  2 +-
 .../app/hooks/__tests__/useModalManager.test.ts    | 55 ++++++++++++++++++++++
 packages/dashboard/app/hooks/useModalManager.ts    | 19 ++++++--
 13 files changed, 148 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-9143

Fusion-Task-Lineage: 43951c5d-24d2-4243-826a-c6f416607882

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-17 20:54:43 -07:00
gsxdsm
83a33be353 feat(dashboard): persistent sign-in dialog for paste-back provider logins
Operator report: during a container login there was nowhere obvious to paste the
redirect URL and no sign of what the app was waiting on. The flow was split
across a pre-flight confirm that warned about paste-back and vanished, a card
that shrank to a disabled "Waiting for login…" chip, and the paste field
rendered inline in that card below the fold of a scrolling modal.

ProviderLoginDialog now opens with the flow and stays until it ends: a two-step
progress list, a button to re-open a lost sign-in tab, the paste field, and the
terminal outcome inline instead of a toast that disappears while the operator is
in another browser tab.

Three defects found and fixed while verifying it in a real container:

- It sank behind the onboarding modal and clicks landed on the modal instead.
  createPortal relocates the DOM node but NOT the React tree, so pointer events
  bubbled to the host FloatingWindow, which raises itself to a fresh
  nextFloatingZ() on every pointerdown — each click in the dialog lifted the
  window above it. Fixed by rendering the dialog as a sibling of the window,
  claiming z once on open (it was calling nextFloatingZ() inline on every render
  of a modal that re-renders on a 2s poll), and stopping propagation on the
  overlay. Ratcheted for every portaled .modal-overlay.
- Spacing did not match any other dialog: it hand-rolled header/action padding
  instead of using .modal-header/.modal-actions, and padded each child
  separately. Every row now shares var(--modal-padding) — verified at a uniform
  17px inset across header, steps, paste prompt, field, Submit, and actions.
- The paste field was invisible (.form-input fills with var(--surface), and so
  does .modal — measured #0c0c0e on #0c0c0e), Submit was a 25px row-density
  btn-sm, and both could scroll out of reach. The field now sinks to var(--bg)
  with var(--border-strong), Submit takes standard control padding, and the
  paste region is pinned outside the scroll area.

Dialog anatomy rules (spacing primitives, portal/stacking) documented in
docs/dashboard-guide.md.

Verified: 441 dashboard tests including 4 new dialog tests, eslint, dashboard
typecheck, and the rendered dialog measured in a container build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:47:28 -07:00
gsxdsm
bb11e493f7 fix(auth): restore Codex login and promote outboard resize targets
Operator report from a containerized dashboard: OpenAI Codex login never opened
a browser window at all, and floating windows still needed the FN-8015 follow-up.

- pi's `AuthPrompt` is a discriminated union — text, secret, select, manual_code —
  and FusionAuthStorage.login's interaction shim flattened every variant into
  `onPrompt({message, placeholder})`, discarding `type` and a select's `options`.
  pi's Codex `login()` OPENS with `prompt({type:"select"})` (Browser vs Device
  code) before emitting any auth URL, so the dashboard answered the method picker
  with the promise that waits for a pasted code — input the UI never solicits,
  because nothing had been surfaced yet. The flow hung until the route's 30s
  kickoff timeout: "Login initiation timed out", no window. The route's
  onSelect/selectOauthOption has had the right answer since FN-5917, but the
  callback was dead code from the moment login moved to pi's ModelRuntime.
  Verified against a real container: the login endpoint now returns Codex's
  auth.openai.com URL in 0.03s instead of timing out after 30s.
- Promote FN-8766's outboard east/NE/SE resize targets from Task Detail to every
  desktop window. With FN-8015's body gutter deleted, a hosted scrollbar sits
  flush against the painted edge where those hit zones used to cover it (issue
  #2140); moving the targets outside the shell keeps it grabbable without
  insetting anything. That needs the host to stop clipping, so the body and its
  direct child inherit the corner radius — only 8 of ~30 callers set that
  themselves — and phones re-assert clipping since they hide every handle.
- Document the fixed OAuth callback ports (Anthropic 53692, Codex 1455) and
  PI_OAUTH_CALLBACK_HOST for Docker: without them the browser callback cannot
  reach the container's loopback listener, which is why subscription logins
  appeared to fail there.

Verified: 14989 dashboard tests, 58 engine auth-storage tests (4 new, covering
each prompt type), pnpm test:gate, eslint, and both typechecks all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:38:26 -07:00
gsxdsm
9db2565e99 fix(dashboard): remove the shared floating-window gutter and fix browser onboarding
Operator report: the first-run "Set Up AI" modal had extra space along its right
edge, and it asked a browser visitor to connect a remote server and mentioned a
native shell they do not have.

Three defects, verified in a real container build at desktop and mobile widths:

- FN-8015 reserved `margin-inline-end: var(--space-lg)` on the shared
  `.floating-window__body` so a hosted scrollbar cleared the east resize hot
  zones. One shared reservation every caller had to know about produced a
  recurring class of asymmetric-right-inset bugs instead: it was zeroed
  piecemeal five times (FN-8766, mobile task detail, FN-8722, FN-8702, every
  tablet window) while two callers leaned on it for their right inset and had to
  restore it whenever one of those predicates fired. Delete the gutter, its five
  overrides, and GitHub Import's borrowed-inset compensation. Where a scrollbar
  and a resize target actually collide, use FN-8766's outboard east targets.
  Trade-off accepted by the operator; the ratchet test now forbids the gutter in
  any stylesheet at any breakpoint.
- The hosted Set Up AI modal did not fill its window: its standalone
  `height: min(85vh, ...)` rule ties on specificity with FloatingWindow's
  `height: 100%` and won on source order, leaving ~60px of dead window surface
  under the footer alongside the 16px gutter strip. Right gap 17px -> 1px,
  bottom gap 62px -> 1px.
- The "Connect remote Fusion server" card keyed only on
  `desktopMode !== "local"`, and `desktopMode` is undefined on web, so every
  browser first-run led with a native-shell hand-off form. Now also requires
  `host !== "web"`.

Verified: 165 dashboard test files / 5829 tests pass, eslint and
`tsc -p tsconfig.app.json` clean, and a container built from this tree shows the
panel flush at 1280x800 and as a symmetric full-screen sheet at 390x844 with no
remote-server card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:56:48 -07:00
gsxdsm
37bd6ee859 chore: add changeset for docker build and volume ownership fixes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:26:30 -07:00
gsxdsm
2fa6ca28d8 fix(docker): make a default docker build + documented run actually work
Three defects found while bringing up a container from a clean checkout:

- The dashboard's vite build (~5.7k modules) exceeded V8's default old-space on a
  stock Docker Desktop VM and aborted the image build with "Ineffective
  mark-compacts near heap limit" (exit 134). Raise the ceiling for that RUN only.
- The documented `-v fusion-home:/home/node/.fusion` mount seeded a root-owned
  named volume over a path absent from the image, so embedded Postgres initdb hit
  "Permission denied", the supervisor burned its 4 restarts, and the container went
  unhealthy on first run. Pre-create the directory node-owned so a fresh named
  volume inherits it; document that bind mounts still need a host-side chown.
- Drop the dependency-graph plugin's tsconfig path mapping for the taskStuck module
  deleted in 2eae0b2507 / 29d94e0fa3.

Verified: full `docker build` from a clean export of this tree succeeds unpatched,
and a run against brand-new named volumes with no manual chown reaches health=healthy
with /api/health 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:15 -07:00
gsxdsm
29d94e0fa3 fix: drop dependency-graph plugin's use of removed stuck-task tagging
The dependency-graph plugin imported the deleted taskStuck util through its
dashboard interop shim, breaking the dashboard vite build. Remove the isStuck
gate from graph nodes, the taskStuckTimeoutMs prop threading, the taskStuck
module declaration from dashboard-interop.d.ts, and the stalled-card-as-stuck
test coverage. Covered by the existing remove-stuck-task-tagging changeset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 15:58:58 -07:00
gsxdsm
2eae0b2507 feat: remove stuck-task tagging from the dashboard; fix liveness-ratchet scan path
Removes the dashboard's stuck-task tagging per operator request: the Stuck
card/status badges, stuck row styling, the footer Stuck segment and
stuckTaskCount stat, utils/taskStuck.ts, the isStuck agent-activity gate,
and the taskStuckTimeoutMs prop plumbing (App -> Board/Lane/Column/
WorktreeGroup/MainContent -> TaskCard/ListView/ExecutorStatusBar). Stuck-task
tests are deleted or reconciled. The taskStuckTimeoutMs setting and the
engine's recovery sweeps (including the stuck-killed status) are unchanged —
the setting is engine-side only now.

Also repoints the FN-6756 liveness-gate ratchet's facade scans at
executor/task-executor-session-facades.ts, where the wave20 extraction moved
hasLiveSessionSurface/clearPhantomExecutorBinding (the two pre-existing red
tests on main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 15:47:43 -07:00