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>
## 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>
`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>
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>
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>
## 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 -->
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>
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>
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>
Measured wall times: gate 14.0s, boot smoke 20.4s (-6.3s), changed-only
pnpm test 17.5s; quarantine ledger 0. Shard-timing snapshot rebuilt
from today's CI shard artifacts (run 31929730933) plus a locally
measured full dashboard suite so dashboard lane weighting keeps its
per-file data. The trim shows: the former top-6 core PG offenders are
gone from the slowest-20 (sqlite-migrator 2m29s serial -> 20.4s;
SettingsModal.general off the table entirely). Caveat: the dashboard
rows come from the pre-trim analysis measurement, so entries like
SettingsModal.scheduling-merge (30.2s, now ~13s) are pessimistic until
the next dashboard re-measure.
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`
Confirm the W33 spike as cold-start variance and preserve a repeatable diagnosis protocol.
- Record five sequential phase-timed samples and the 20.5-second median threshold.
- Link the controlled remeasurement protocol from the testing guide.
- Explain the timing snapshot handoff and why incomplete CI artifacts were not published.
- Preserve the no-appeasement requirement beside boot-smoke phase timing.
Files changed:
.../boot-smoke-w33-walltime-anomaly.md | 100 +++++++++++++++++++++
docs/testing.md | 2 +-
scripts/boot-smoke.mjs | 7 ++
3 files changed, 108 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-9105
Fusion-Task-Lineage: 6956b037-db0f-4560-bb04-136080a975b5
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Full-suite repair, engine source-scan cluster. The package code
organization waves moved ~30 engine modules into subdirectories
(plugins/, execution/, scheduling/, healing/, worktree/, executor/
peels); the log-severity manifest, prompt carve-out, emit-surface,
failure-lane, and worktree-invariant scanners now read the moved
locations, verified per file via git log --follow. Two scans caught
real drift rather than moves: the lifecycle census had 12 unexamined
column guards (resolved with DELIBERATE-LITERAL markers for the mailbox
archived tab, the FN-9059 lease-owner terminality check, and the FN-9056
legacy done fallback — baseline re-recorded with zero absorbed debt),
and planning-claim gained a genuine second writer in self-healing's
FN-8998 transport-failure recovery, admitted to the allowlist with its
CAS-guarded justification. 9 files / 119 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
check:lane-wiring runs in CI's Lint gate and has been failing on main since FN-9037 landed
`listTaskRecommendations`, so it blocks every PR in the repo, not just the one that hit it.
Recorded rather than rewired, because this is the false-positive shape the escape hatch exists
for. The guard catches a callee silently falling back to a LEGACY COLUMN LITERAL when a caller
omits the lane; `listTaskRecommendationsImpl` falls back to
`resolveProjectColumnsForRoles(store, ["complete"])`, which reads the board's own lanes. Real
callers already pass a resolved set — the dashboard route resolves `completeColumns` before
calling — so the fallback only serves the pass-through wrapper. Resolving again in the wrapper
would duplicate that query on every call for no behavioural difference.
The reason lives at the call site as well as in the baseline, since a bare count in a JSON file
is exactly the kind of entry that later reads as unexplained debt.
Baseline diff verified to be a single added entry (store.ts: 1) — nothing else raised or lowered.
Verified: check:lane-wiring exits 0, core typecheck clean, eslint clean, FNXC date check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The check-workspace-package-graph validator globs the filesystem for package
manifests but only excluded node_modules/dist. On any machine that had run a
desktop packaging build (pnpm deploy), the gitignored electron-builder staging
dir packages/desktop/deploy/ carries a copy of desktop's package.json and
tripped the unglobbed-package violation, breaking pretest/gate locally while CI
(clean checkout, no deploy dir) stayed green. Exclude the staging path alongside
node_modules/dist since a gitignored dir is absent from the isolated worktree
this validator guards.
## Summary
Starts **U5** of the package code-organization program after wave 18
(executor peels) landed.
Peels pure free-function clusters out of `self-healing.ts` into
`packages/engine/src/self-healing/` without behavior changes. Public
imports from `./self-healing.js` remain stable via re-exports.
### Peels
| Symbol | New home |
|--------|----------|
| `autoRecoverWorktreeSessionStartFailure` |
`self-healing/auto-recover-worktree-session.ts` |
| `archiveAsGhostBug` | `self-healing/archive-ghost-bug.ts` |
| `hasStepProgress` / work-complete helpers |
`self-healing/step-progress.ts` |
### Line count
- `self-healing.ts`: ~15456 → ~15231 (baseline ratcheted to post-peel
live; main had already drifted past the prior grandfathered ceiling via
organic growth)
- New modules each well under 2,000 lines
## Test plan
- [x] `pnpm --filter @fusion/engine exec tsc --noEmit`
- [x] `self-healing-trait-rekey.test.ts` (autoRecover requeue)
- [x] `self-healing-paused-abort-recovery.test.ts`
- [x] `self-healing-model-unavailable-recovery.test.ts`
- [ ] CI gate
## Follow-ups
U5 Slice B: domain method clusters (startup, in-review, merge-status,
workspace, surfacing) into additional `self-healing/*.ts` modules.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved automatic recovery when worktree sessions fail to start,
including stale or incomplete session data.
* Tasks can be safely requeued while preserving progress, or escalated
after retry limits are reached.
* Improved handling of completed work and failures where task completion
was not recorded.
* Preserved valid task branches during recovery and provided more
reliable fallback requeue behavior.
* Ghost bugs are automatically archived with recovery details and
activity history.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Classify workflow work-item `workflowRole` comparisons as role
vocabulary in the lifecycle-column census.
- Add a regression test so triage role comparisons cannot raise a
phantom lifecycle-column guard.
## Test Plan
- `node --test scripts/__tests__/lifecycle-census*.test.mjs`
- `corepack pnpm check:lifecycle-columns`
- `corepack pnpm lint`
- `corepack pnpm check:changesets --strict`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved classification of workflow role comparisons, including
`workflowRole === "triage"`, so they are recognized separately from
lifecycle-column comparisons.
* Ensured workflow role values are correctly identified as role
vocabulary rather than lifecycle-column values.
* **Tests**
* Added automated coverage to verify accurate workflow role and column
identification across comparison patterns.
<!-- end of auto-generated comment: release notes by coderabbit.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 -->
verify:fast ran every step serially, so its wall clock was the sum of steps with no
ordering relationship between them. Static checks and per-package typechecks are
each independent, so they now run as bounded-concurrency groups.
static checks ~6.0s -> ~1.6s (11 validators, mostly node startup)
typecheck 11.0s -> 7.4s (engine + dashboard)
no-change run 28.1s -> 22.3s
Ordering that matters is untouched: bootstrap, builds, and boot smoke stay serial
and in plan order, and each group is a barrier. A failing group awaits its in-flight
siblings before throwing rather than abandoning partial tsbuildinfo/dist state, and
reports the first failure in plan order so the message does not depend on which
sibling lost the race. FUSION_VERIFY_FAST_SERIAL=1 restores the old behavior when
interleaved child output makes a failure hard to read.
Boot smoke is now 84% of a no-change run (18.8s); it re-runs initdb into a throwaway
HOME every time. Left alone -- caching that would change what the gate proves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## Problem
Dashboard typecheck fails with **TS2416** in `@fusion/core`'s
`TaskStore`:
```
Property 'emit' in type 'TaskStore' is not assignable to the same property in base type 'EventEmitter<TaskStoreEvents>'.
```
The `override emit<E extends string | symbol>(event, ...args)` generic
conflicts with the base class's generic `emit<K>(eventName: keyof
TaskStoreEvents | K, ...)`. This breaks the dashboard typecheck / CI
merge gate.
## Fix
Change the override to:
```ts
override emit(event: unknown, ...args: any[]): boolean {
return EventEmitter.prototype.emit.call(this, event as string, ...args);
}
```
`event: unknown` remains assignable to the base's generic signature
while still forwarding non-typed runtime keys (`agent:log`,
`settings:updated`, …). Internal `EventEmitter.prototype.emit` calls
cast `event as string`. Behavior-preserving.
## Verification
- `@fusion/dashboard` `tsc --noEmit` → **PASS** (previously failed with
TS2416)
- `eslint` on touched file → clean
- Single-file change (`packages/core/src/store.ts`, +6/−3)
## Scope
No behavior change, no changesets required.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved task event handling to support a broader range of event
identifiers.
* Preserved cached-lane information for single-argument task update
events.
* Maintained support for custom and arbitrary event names without
disrupting existing behavior.
* Improved classification of workflow roles, session purposes, and
outcome-related status checks in lifecycle analysis, producing more
accurate findings and reducing misleading results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->