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>
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>
`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>
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>
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>
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>
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>
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>
`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>
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>
The Cursor provider card's Enable action only flips useCursorCli in
settings and never registers fusion-plugin-cursor-runtime, so
getRuntimeById("cursor") missed and every cursor-cli selection hit the
runtime-routed fail-fast error even with an authenticated cursor-agent.
Mirror the FN-7761 Grok eager bootstrap in serve, dashboard, and daemon,
guarded by the same source-scan regression test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
- replaces newly added future-dated FNXC metadata with the actual UTC
change time
- tightens the FNXC future-date baseline to zero known exceptions
## Test plan
- `pnpm check:fnxc-future-dates`
- `pnpm check:lifecycle-columns`
- `pnpm check:changesets`
- `pnpm --filter @runfusion/fusion exec vitest run
src/__tests__/staged-plugin-core-imports.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm lint`
The task-created hook (GitHub tracking issue creation) was deferred behind
the fire-and-forget auto-title-summarize chain whenever autoSummarizeTitles
is on and the task has a long description and no title — i.e. for every
'fn task create'. The short-lived CLI process closed the store and exited
before the deferred chain ran, so tasks ended up with
githubTracking.enabled=true but no issue (observed on FN-9045..FN-9061).
fn task create now suppresses the deferred hook and calls
createTrackingIssueForTask directly after create, printing the linked
issue; exports that helper from @fusion/dashboard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fn_task_create gains github_tracking/github_repo params and fn task create
gains --github/--no-github/--github-repo flags, resolved through
resolveTaskGithubTracking (task > project > global). CLI create now also
honors the project/global tracking-enabled default it previously ignored;
explicit disables persist enabled:false so later default flips cannot
re-enable a task.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fusion-Task-Id: automation
FN-8932 seeds the durable Memory Keeper alongside the four routed workflow principals, so fn_list_agents and fn_agent_org_chart now enumerate five built-in agents on a fresh project. Update the two extension.test.ts assertions from count 4 to 5 and assert Memory Keeper is present.
Serialize computer snapshots and actions through a durable app-scoped fence.
- Hold the fence from snapshot acquisition or element resolution through OS action consumption.
- Report snapshot consumption only when the expected latest pointer was invalidated.
- Heartbeat and token-check directory locks for safe crash recovery and add command-fence coverage.
Files changed:
.../commands/__tests__/computer-commands.test.ts | 26 +++++++++-
.../__tests__/computer-snapshot-index.test.ts | 2 +-
packages/cli/src/commands/computer.ts | 37 ++++++++-----
.../cli/src/commands/computer/snapshot-store.ts | 60 ++++++++++++++++------
4 files changed, 93 insertions(+), 32 deletions(-)
Fusion-Task-Id: FN-9031
Fusion-Task-Lineage: f196b7ca-ce3f-4a50-98d2-d6ac8bf184ac
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
- Complete the isolated `@fusion/core` mock used by the
experiment-finalize extension suite
- Classify three intentional physical/synthetic lifecycle literals
introduced on current main
- Re-record the strict lifecycle census baseline with zero unexamined
guards
## Test plan
- `pnpm --filter @runfusion/fusion exec vitest run
src/__tests__/extension-experiment-finalize.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/core exec vitest run
src/__tests__/task-intake-owner-resolver.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine exec vitest run --project engine-default
src/__tests__/mission-feature-sync-lanes.test.ts --silent=passed-only
--reporter=dot`
- `pnpm check:lifecycle-columns`
- `node scripts/check-mock-completeness.mjs`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Improved mission reconciliation previews for task links, specification
alignment, and lifecycle updates.
- Prevented stale or superseded validation runs from overwriting current
feature status or ownership.
- Improved blocked-feature diagnostics and archived-task handling across
workflow configurations.
- **Documentation**
- Clarified validation, assignment checks, and mission synchronization
behavior.
- **Tests**
- Expanded coverage for reconciliation previews and validator ownership
scenarios.
- **Chores**
- Updated lifecycle baseline data for known archived-task cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->