Opt-in `pnpm smoke:pipeline` lane replaying 19 declared scenarios across
builtin:coding-ideas and the builtin:coding non-regression floor, driving the
real engine: disposable local Git repositories, throwaway PostgreSQL store,
production graph dispatch, ProjectEngine merge admission, real worktree
acquisition, and deterministic mock-provider scripts under testMode.
Each scenario declares one closed terminal state (merged-done, inert-intake,
parked, manual-hold, no-op-merge); an undeclared terminal fails the run, and
five wedge detectors (W1-W5) reject contradictory parks, finalization loops,
severed sessions, unreachable waits, and quiescence without progress.
Differential proof: on the pre-FN-180 tree (95ea06b48) exactly S05, S06, S09,
S10 and S16 fail across both workflows with behavioral assertions, and pass
after FN-180 — the FN-175/FN-177 incident classes are reproduced mechanically.
The declared duration budget is re-baselined 70s -> 90s at landing. The harness
did not degrade: the identical branch measured 61.8-64.1s against the
pre-integration main and 73.2-80.2s against the same main after 65 upstream
commits, with growth in transform, import and test phases the lane does not own.
docs/testing.md records the measurements, the cause, and the file-consolidation
lever to reach for before the budget is touched again.
Excluded from engine-default and engine-core; the merge gate is unchanged and
CI runs the lane non-blocking after merge.
Fusion-Task-Id: FN-182
Core 32 failures -> 0 (5,981 passing); CLI 46 -> 0 (2,021 passing). Three
agents per package, root-cause fixes only.
One product defect, same class as FN-9059 and found the same way — by a test
that leaked state between runs:
`projectTableNames` was missing SEVENTEEN tables the schema declares
(current_plan_evidence, spec_locks, spec_drift_reports, symbol_locks,
configuration_revisions, chat_tags, chat_session_tags, mission_lineage_stops,
task_verification_requests, unplanned_execution_blocks,
workflow_agent_capacity_leases and the six task_lifecycle_* tables). That list
drives BOTH the PG test-harness per-test reset and production health
compaction, so those tables were never truncated between tests (a plan-evidence
version counter carried forward, making whole-file runs disagree with isolated
ones) and never VACUUM/ANALYZEd in production. Registered, with
project-table-registry.test.ts as a ratchet — verified it fails on an
unregistered new table naming the offender.
Everything else was drift behind deliberate changes: branch-write provenance,
FN-073 dependency validation, the FN-9191 pre-merge merge gate, U11's triage/
planning lane merge, refinement workflow coming from the ORIGIN selection,
async-converted provider registration, a barrel mock missing exports a guard
added, and several source-pinned inventories broken by module moves. Tests for
removed features were deleted with their removing commit cited.
Also fixes a vitest config gap where @fusion/core/mcp-builtin-servers resolved
only to dist/, which was breaking test COLLECTION in unrelated CLI files and had
been misread as transient cross-agent noise.
Quarantines mission-store.pg's concurrent-claim race (second sighting): it holds
a transaction open, sleeps 250ms and asserts the rival has not settled, which
fails under parallel load. An A/B against the registry change above looked
causal on one run and did not reproduce on three — that coincidence is the flake
itself, and rescue needs a real lock-wait probe rather than a longer sleep.
Core's config now inlines its exclude array, because check-quarantine-ledger.mjs
cannot resolve a variable reference and silently reported the ledger unpaired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Full engine suite at a97aa84a20: 3 failures out of 12,414. All three diagnosed:
- self-healing-pending-wedge-notification's marker-selection case fails ONLY in
a full-suite run (expects 1 elapsed marker, sees 2) and passes deterministically
alone. This is its SECOND sighting, so per AGENTS.md it is an on-sight
quarantine with no further discretion: ledger entry + matching vitest exclude,
same commit, 2026-09-06 deletion deadline.
- spec-drift-reconciler's exponential-backoff case shows the same shape on a
FIRST sighting, so it is recorded in the observed register instead of evicting
that file's other passing coverage. Both are timer-driven reconciler tests that
only fail alongside other suites, pointing at cross-file fake-timer state.
- merge-orphan-durable-write-inventory drift was pure lineHint movement (19
changed, zero newly unclassified entries) after product edits shifted lines.
Regenerated.
Also fixes check-quarantine-ledger.mjs, which could not see the exclude I added:
its comment stripper treated the `/**` inside glob literals like "node_modules/**"
and "src/**/*.slow.test.ts" as a block-comment opener and deleted through to the
next "*/", swallowing whole array literals and every entry after them. It now
scans string-aware, so the lockstep check actually holds. Nothing was appeased:
no timeout widened, no retry added, no assertion relaxed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#3514 removed the last unwired task-merge review-lane call site but left
the ratchet allowance at 1, so every PR against main failed
check:lane-wiring on a drop.
Mark isAtOrPastReviewLane as DELIBERATE-LITERAL so the lifecycle-column
census ratchet stops treating FN-158's physical pre-cutover lane IDs as
new unexamined guards. Refresh the pinned baseline to match.
## Summary
- update workspace worktree test stores for callback-based entry
mutations
- preserve validation, existing-entry, and singular-routing behavior in
the fakes
- canonicalize the macOS worktree path fixture before comparing
persisted state
- remove the stale inert-seam exception left after the review-column
callback became fully supplied
## Test plan
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/node-worktree-isolation.test.ts
src/__tests__/workspace-root-worktree-routing.test.ts
src/__tests__/worktree-acquisition.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/engine exec vitest run
src/__tests__/worktree-acquisition-workspace.test.ts
--silent=passed-only --reporter=dot`
- Directly changed engine workspace suite with PostgreSQL-dependent
blocks skipped: 338 passed, 4 skipped
- `pnpm --filter @fusion/engine typecheck`
- `node scripts/check-inert-flag-seams.mjs`
- `node --test scripts/__tests__/check-inert-flag-seams.test.mjs`
- `pnpm lint`
- `pnpm check:changesets`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved recovery of orphaned worktrees by consistently using
canonical paths.
* Strengthened worktree assignment and updates to preserve existing
entries and prevent stale task state.
* Improved synchronization and validation during concurrent workspace
updates.
* Refined self-healing recovery so branch metadata changes only when
necessary.
* Improved handling of stalled tasks, retry exhaustion, and clearing
obsolete worktree or branch information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Fixes the `session.subscribe is not a function` crash that breaks every
engine workflow step (Plan Review, Code Review) executed by an ACP agent
(Hermes ACP, Prime, Grok).
Introduced by the ACP custom-tools bridge (#3476) in the sense that it
made the gap reachable: ACP sessions stream through the bridging client
handler onto `callbacks` and never exposed the pi-style `subscribe()`
that the engine's AgentSession contract promises. Two production call
sites call it unconditionally:
- `packages/engine/src/executor/execute-workflow-step.ts` (workflow
steps — Plan Review / Code Review)
- `packages/engine/src/pi.ts` fallback wiring (`wireFallbackHooks`,
`promptableSession.subscribe`)
(`reviewer.ts` guards with `typeof session.subscribe === "function"`;
the other paths do not.)
Real-world symptom (verified on 0.77.0-beta.6 with a Hermes ACP agent):
every Plan Review attempt fails before producing a verdict:
```
[pre-merge] Workflow step failed: Plan Review
outcome: Plan Review failed before producing a verdict: session.subscribe is not a function
Plan Review provider retry budget exhausted
```
## Fix
Fix at the seam rather than guarding every call site forever:
- `AcpRuntimeAdapter.createSession` wraps the raw callbacks so each
forwarded text/thinking/tool event is **also** replayed to subscribers
as the pi-shaped event consumers parse (`message_update` +
`assistantMessageEvent.{text_delta,thinking_delta}`,
`tool_execution_start/end`)
- exposes `session.subscribe(handler)` returning an unsubscribe
function; dispose clears subscribers
- merges engine `taskEnv` into the subprocess env behind the existing
allow-list trust boundary (KTD6b preserved — only allow-listed keys
forward, task values win)
- original callback delivery unchanged; subscriber exceptions are
isolated so a faulty consumer can't break the streaming bridge
## Testing
- New regression tests in `runtime-adapter.test.ts` against the real
echo-agent fixture (`ACP_FIXTURE_RICH_PROMPT=1`):
- subscribe replays text/thinking/tool events as pi-shaped events
- unsubscribe stops delivery
- dual delivery: original `onText` callback AND subscriber both fire
- Full plugin suite: 255 passed (21 files)
- `tsc --noEmit` clean for the plugin
- changeset for `@runfusion/fusion` (patch, bugfix)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added session event subscriptions with unsubscribe support.
* Streamed text, thinking, and tool updates are replayed in a consistent
event format.
* Added support for task-specific environment values when launching
subprocesses.
* **Bug Fixes**
* Ensured streamed events reach both existing callbacks and subscribers.
* Isolated subscriber errors so they do not interrupt other handlers.
* Session cleanup now stops further subscription notifications.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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>
`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
- 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`
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>
## 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 -->
## 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 -->
Stabilize live Proceed-action handoffs and re-admit the Planning Mode flow suite.
- Settle hydration and re-query the Proceed action before direct-create test clicks.
- Remove the Planning Mode test quarantine and record its rescue in the testing ledger.
Files changed:
.../suite-only-flakes-observed-register.md | 4 ++++
docs/testing.md | 3 +++
.../PlanningModeModal.planning-flow.test.tsx | 20 ++++++++++++++++----
packages/dashboard/vitest.config.ts | 5 -----
scripts/lib/test-quarantine.json | 5 -----
5 files changed, 23 insertions(+), 14 deletions(-)
Fusion-Task-Id: FN-8936
Fusion-Task-Lineage: ed869b67-9394-458b-879c-54da0d7d327e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Operator directed deletion of tests that test pre-refactor behavior no
longer in the codebase (removed APIs, mock shape drift, stale assertions
from the 2026-08-05 full-suite quarantine wave, run 30982276306).
All 27 entries were permanently red — not flaky — testing APIs removed
during the PG cutover and workflow peel refactors (getBuiltinWorkflow,
resolveWorkflowIrForTaskWithProvenance, layer.db.select mock shapes,
vi.mock hoist errors, stale serialization/count literals).
Kept 3 actionable entries that catch real issues:
- register-model-routes-kimi-k3-supplemental (real CI flake, rescue feature ready)
- project-engine.test.ts (catches real 60s→120s assertion drift)
- PlanningModeModal.planning-flow (second-sighting real race)
Vitest config exclusions and quarantine ledger updated in lockstep.
## Summary
Restores the non-blocking full suite on `main` after consistent shard
failures (latest red: [run
30982276306](https://github.com/Runfusion/Fusion/actions/runs/30982276306);
all four shards failed on `@fusion/core`, `@fusion/engine`, and
`@fusion/plugin-sdk`).
### Fixes
- **Path / import drift** after code-organization peels: update
static-guard and integration tests to new module locations (`central/`,
`board/`, `execution/`, `merge/`, `worktree/`, `plugins/`, `types/*`
barrels, etc.).
- **Inventory re-pins**:
- SQLite production `DatabaseSync` allowlist
(`central/project-identity.ts`, `db/sqlite-validation.ts`)
- Engine blocking-shellout allowlist regenerated from live source (33
audited sites)
- Core log-severity manifest paths for peeled modules
- **Partial protocol assert update** for `isPlanReviewSatisfied` (file
also quarantined until full rescue)
### Quarantine (deletion ratchet)
Remaining behavioral reds quarantined on sight — no
timeout/retry/assertion appeasement:
- **14 core** files (incomplete unit fakes for `layer.db.select`,
ledger/census drift, 15s wedge timeout, serialization protocol drift)
- **13 engine** files (mock-hoist errors, fake-store/census/behavior
drift under suite)
Paired updates: `scripts/lib/test-quarantine.json` + package vitest
excludes. Deletion clock starts `2026-08-05`.
### Local verification
- Path-fixed core scanners: 173 passed
- Path-fixed engine scanners: 58 passed
- `@fusion/plugin-sdk` full: 16 passed
- PG smokes: mission-autopilot, research-execution, satellite,
transition-pending, workflow-sync
## Test plan
- [ ] CI PR checks green (lint/typecheck/build/gate)
- [ ] Full suite on merge to main: all 4 shards green or only
intentional non-blocking signal
- [ ] Confirm quarantined files appear in ledger + vitest excludes and
are not executed
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Updated test coverage to reflect reorganized source locations and
module paths.
* Refreshed static checks, allowlists, and source-based assertions
without changing tested behavior.
* **Chores**
* Quarantined failing core and engine test suites with documented
tracking details.
* Updated test configuration and quarantine records to improve suite
stability and reporting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Add an opt-in source-development loop that restarts the dashboard and
engine when runtime TypeScript or JSON changes. Use `pnpm dev:watch`;
`pnpm dev:hmr` now combines Vite UI HMR with the same supervised
API/engine restart path.
The watcher filters tests, fixtures, generated declarations, build
output, and task state. It coalesces bursts with a two-second maximum
wait, waits for the child to acknowledge its IPC listener, and rebuilds
runtime dist artifacts before a source-triggered respawn.
## Safety model
- Close scheduler, triage, heartbeat, mission, routine, self-healing,
and merge admission before checking for active work.
- Let already-running agents reach a safe boundary; do not mutate
durable pause settings.
- Enter the existing graceful exit-code-86 shutdown and supervised
respawn path.
- Retry failed liveness reads and declined restart requests instead of
dropping the pending change.
- Keep ordinary `pnpm dev` behavior unchanged; inherited watch state
does not break nested non-dashboard development commands.
A development restart intentionally replaces the dashboard process, so
transient dashboard connections and project dev-server children
reconnect or restart with it. Agent work is the protected boundary.
## Validation
- `pnpm lint`
- `pnpm test:gate` (753 tests passed across engine, core, PostgreSQL
gate, and CI-shape suites)
- Focused CLI watcher/restart/supervision suites: 40 tests passed
- Focused engine drain/manager suites: 52 tests passed
- `pnpm --filter @runfusion/fusion typecheck`
- `pnpm --filter @fusion/engine typecheck`
- `pnpm verify:fast` (13 steps passed, including CLI build and real
health boot smoke)
- Manual unsupported-command probe confirms explicit `--watch` fails
clearly outside the dashboard command
## Post-Deploy Monitoring & Validation
- Watch for `[fusion:dev] source changed`, `source restart deferred`,
`active work drained`, and `restart requested` logs during the first
watched development session.
- Healthy behavior is one exit-86 respawn per edit batch, no interrupted
active agents, refreshed dist artifacts, and a healthy dashboard after
respawn.
- Investigate repeated restart loops, watcher attachment warnings,
declined restart retries, or liveness-read failures.
- Immediate mitigation is to use ordinary `pnpm dev` without `--watch`;
no production runtime behavior or durable setting needs rollback.
- Validation owner: Fusion maintainers during the first source edit
after merge.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added `pnpm dev:watch` to automatically restart development runtime
processes when source files change.
* Development restarts now wait for active work to finish, preventing
new work from starting during the transition.
* Enhanced `pnpm dev:hmr` with graceful runtime source restarts while
keeping the dashboard available.
* Rapid source changes are grouped to avoid unnecessary restarts.
* **Documentation**
* Updated development setup and contribution guides with the new watch
workflow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Keep the Kimi K3 dashboard route test quarantined until the mandated deletion date.
- Preserve the /api/models supplemental test and paired Vitest exclusion through 2026-08-15.
- Record the explicit retention deadline in the quarantine ledger.
Files changed:
packages/dashboard/vitest.config.ts | 5 +++++
scripts/lib/test-quarantine.json | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-8774
Fusion-Task-Lineage: 8ef704e4-f682-4a97-af1a-2070ca43d8a1
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Remove the typed authorization phrase and the --yes/-y auto-confirm path so
every real release must confirm y/N in an interactive terminal. Reject --yes
with a clear error so old muscle memory cannot skip the proceed prompt.
Make the useTasks hydration freshness coverage deterministic and restore it to the dashboard suite.
- Control the system clock for hydration fixtures and flush async updates without advancing time.
- Remove the rescued test from the dashboard exclusion list and quarantine ledger.
Files changed:
.../__tests__/useTasks-hydration-freshness.test.ts | 30 ++++++++++++++--------
packages/dashboard/vitest.config.ts | 8 ------
scripts/lib/test-quarantine.json | 5 ----
3 files changed, 19 insertions(+), 24 deletions(-)
Fusion-Task-Id: FN-8724
Fusion-Task-Lineage: 1d764e2c-0975-4d26-92c6-187a6a94caee
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
Quarantine the timing-sensitive Kimi K3 SDK catalog test without changing timeout budgets.
- Reuse the native model registry once per test file.
- Add the observed CI timeout to the dashboard quarantine ledger and config.
- Document validation and timeout-budget preservation requirements.
Files changed:
docs/testing.md | 8 ++++++++
...ister-model-routes-kimi-k3-supplemental.test.ts | 23 ++++++++++++++++++++--
packages/dashboard/vitest.config.ts | 8 ++++++++
scripts/lib/test-quarantine.json | 5 +++++
4 files changed, 42 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-8647
Fusion-Task-Lineage: 31e79677-d923-4003-a8e8-082159334e65
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
The active-worktree slot-accounting fix removed two deliberate scheduler
literals (done/archived: 3 -> 2); re-record so the ratchet follows the count
down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
**The date gate passes on `main` — but not because this was fixed.**
## What actually happened
UTC rolled over to `2026-08-01`. The gate compares against the later of
local and UTC, so two `2026-08-01` stamps in `scheduler.ts` became valid
on their own. That is the ratchet's normal drop path and is fine.
`#3278` then re-recorded the baseline "after the UTC rollover", which
set `scheduler.ts` to **allow 1** — exactly enough to absorb the one
stamp that did *not* age out:
```
FNXC:ConcurrencyAdmission 2026-08-06-09:00 ← six days out, wrong on any calendar
```
So the gate reports `123 known future-dated stamp(s), none added` and
exits 0, with a stamp inside it that will not be valid until next week.
## Why this is the failure the gate exists to catch
A blanket re-record cannot distinguish **aged out** from **still
wrong**, so it launders the second past the first. The sibling ratchet
states the rule outright:
> Do NOT re-record the baseline to clear this — that is the same false
green one layer up.
This is that, one layer up again: not a guard cleared by a baseline, but
a *baseline refresh* clearing a guard as a side effect.
## The fix
- stamp repointed to `2026-08-01` — today in UTC, which is the calendar
the gate actually compares against
- **allowance removed**, not left at 1, so the entry cannot be regrown
into
**Mutation-verified**: with the allowance gone, restoring `2026-08-06`
exits **1**. Before this change the same stamp exited **0**. That is the
whole point — the ratchet can now see it.
## One thing worth carrying forward
A six-days-out stamp is not a timezone slip. Neither the old `date -u`
guidance nor the current local-date guidance in AGENTS.md would have
prevented it, and CI-only checking cannot catch it before merge. This is
the concrete case for running the date check at author time, which I
have flagged but not landed since it changes the gate's contract.
## Verification
- `check-fnxc-future-dates` — exit 0, allowance removed
- `scheduler` suites — **148 pass**
- `tsc --noEmit` (engine) — 0 errors
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What
Re-records the future-dates baseline after the UTC rollover. **92 → 64
file entries.** Tooling only, no source changes.
The gate's own instruction is *"If a count went DOWN, re-record the
baseline in the same commit."* UTC is now `2026-08-01`, so every stamp
dated `2026-08-01` is dated **today** rather than after it, and **28
files no longer carry any future-dated stamp at all.**
## Why pruning matters more than the number
Those 28 files kept a non-zero allowance they no longer need, and **an
allowance is a hole a new violation can hide in.** Four separate commits
landed a future-dated stamp last evening — in that environment, a stale
allowance on a file is exactly where the fifth would go unnoticed. With
the entries pruned, the next one in any of those files is caught on the
first run instead of being absorbed silently.
This is the same direction as #3168 (*"tightens the allowance 1 → 0"*)
and #3211, just triggered by the clock rather than by a fix.
## What this is not
- **Not a correction to anyone's stamp** — no source file is touched.
- **Not a loosening** — no entry increases and no file is added.
- The **123 stamps still dated beyond today** (e.g. `2026-08-06`) keep
their existing allowances untouched.
```
baseline file entries 92 -> 64
check-fnxc-future-dates rc=0
lint clean
```
## Related, deliberately not included
`scheduler.ts:2258` carries `FNXC:WorkflowScheduling 2026-08-01-01:05` —
dated today, one hour ahead of the current clock. I repointed it while
preparing this change and then reverted: after the rollover it is no
longer a gate violation, and mixing a cosmetic timestamp edit into a
baseline re-record would make both harder to review. Noting it so the
residual I flagged when closing #3270 does not get lost — it is now an
accuracy nit rather than a gate concern.
`9094d1640e` (globalPause gates every graph node entry) reddened **two**
lifecycle gates on main. Both are fixed here, in separate commits.
## 1. The census ratchet went 0 → 2
`isTerminalColumnTask` in `scheduler.ts`:
```ts
const flags = columnFlagsForTask(task);
if (flags) return flags.complete === true || flags.archived === true;
return task.column === "done" || task.column === "archived"; // ← counted
```
**The code is correct.** It resolves traits first and falls back only
when the workflow is unreadable. The census counts fallback literals on
purpose — *"a fallback literal is still a literal and should go when the
trait path becomes unconditional"* — and reports them beside the backlog
as already-converted. Its own remedy for a legitimate one is a
`DELIBERATE-LITERAL` marker at the site.
Recorded rather than converted because **there is nothing to convert
to**: a task whose workflow cannot be read has no resolved lane, and
treating it as non-terminal would count a finished card's retained
worktree against live capacity — the opposite of what the surrounding
fix does.
Marker sits in the declaration's **leading** comments; an inline one
attaches to the wrong node and is silently ignored, which cost a
miscount once before. Baseline re-recorded in the same commit, since the
census tracks deliberate counts and reports a marker addition as
`RECLASSIFIED`.
## 2. The stamp gate was red as well
Six files stamped `2026-08-01-00:2x` while UTC was `2026-07-31`:
```
workflow-column-boundary.ts 2 workflow-graph-task-runner.ts 1
workflow-column-boundary-hooks.ts 1 in-process-runtime.ts 5 (allows 4)
workflow-column-boundary-capacity.test 1
```
This checkout is UTC-7, so "just after midnight local" is tomorrow in
UTC — the case AGENTS.md documents, which passes `pnpm lint` locally
*because* the local clock agrees with what was written. Second
occurrence today; I fixed the same shape on #3208 for another worker.
Repointed to `2026-07-31-22:2x`, preserving relative order. **Zero
non-comment lines changed** — 8 lines across 6 files, verified by
diffing out FNXC lines.
## Measured
| check | before | after |
|---|---|---|
| `census --strict` | **1** | **0** |
| backlog | **2** | **0** (DELIBERATE-LITERAL 148 → 150) |
| `check-fnxc-future-dates` | **1** | **0** |
| `pnpm test:gate` | 0 | 0 |
| `census-reclassification-message` | 2 failed | **1 failed** |
That last row is deliberate: the remaining failure is the
expired-premise case #3260 fixes, and I have not touched it. The
capacity test from `9094d1640e` still passes 9/9.
## Why this landed at all
Both gates run in `pr-checks.yml`, so a PR carrying either would have
gone red. Worth someone checking how it merged — a stale merge base
would explain it, and if so the same hole is open for the next merge.
Closes the gap I flagged when re-measuring #3150: that population is at
**0**, and nothing was holding it there.
## Why this surface has no gate today
The lifecycle census parses **comparisons**. A move destination is a
call **argument**:
```ts
await store.moveTask(id, "in-review"); // never counted by anything
```
#3150 measured 31 of these across four files. They are now 0 — I
verified that on current main before writing this — but the comparison
backlog drifted **787 → 854** during the window its own ratchet was
unwired, and this population never had one.
## The failure mode is louder than the guards'
A wrong lane **guard** silently answers "no". A wrong move **target** is
rejected by `moveTaskInternal` with `TransitionRejectionError:
unknown-column` — so on a board that renamed its review lane, every task
finishing implementation **threw** instead of reaching review. Loud at
runtime, invisible to any test on the default board.
## AST, not grep — and that is measured, not stylistic
| scan | result |
|---|---|
| comment-naive grep of `self-healing.ts` | 1 hit — **JSDoc prose**: `*
could call moveTask("in-review")` |
| #3150's own SQL survey by grep | 37 hits against **12** real sites (25
comments) |
Comments are not AST nodes, so that false-positive class cannot occur
here in either direction.
## Controls — all four run, because a gate that only reports 0 proves
nothing
| probe | expected | got |
|---|---|---|
| real `moveTask(id, "in-review")` injected | fail | **exit 1**, names
the file |
| identical call as JSDoc prose | pass | **exit 0** (AST ignores
comments) |
| legacy target + leading `DELIBERATE-LITERAL` | pass | **exit 0**
(marker honored) |
| probe removed | pass | **exit 0** |
The third is the #1411 `recoveryRehome` safe-landing path, where the
legacy id genuinely *is* the target. Marker must be **leading** — the
census already learned that an inline marker attaches to the wrong node
and is silently ignored.
## Ratchet semantics match the census
Fails on a **drop** as well as a rise. A stale allowance is a hole a
re-added target can return through while the gate stays green — exactly
what let the comparison baseline drift.
## Measured
| check | result |
|---|---|
| this gate | scans **1816** files, reports **0** |
| `check:lifecycle-columns` / `check:sql-column-literals` | 0 / 0 |
| `check:fnxc-future-dates` / `check:lane-wiring` | 0 / 0 |
| eslint / `pnpm test:gate` | clean / exit 0 |
Wired into `pr-checks.yml` beside the sibling ratchets, named to match
("Move-target ratchet").
## Scope
Gate only — **no production code touched**, and no conversions in this
PR. The population was already empty; this makes "31 → 0" an invariant
instead of a snapshot, which is the caveat I attached when recommending
#3150 for closure.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Quality Improvements**
* Added automated validation for task-movement configuration values.
* Pull request checks now detect unexpected changes in tracked values.
* Added baseline tracking with strict validation to identify both
additions and removals.
* Added support for explicitly documenting intentional exceptions.
* Improved reporting for file-discovery and source-reading failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What
The census prints **"a new guard cannot land silently"** next to a zero.
That claim was true only for the guard form it happened to parse. This
closes the two it could not see. No product change.
The comparison walk visits `BinaryExpression` only, so neither of these
was visible:
```ts
["done", "archived"].includes(task.column)
switch (task.column) { case "todo": ... }
```
Both are lifecycle-column guards by any reading.
## How I found it
By applying this program's own rule — **break the guard on purpose** —
to the guard itself. I staged a probe file with five guard forms and
measured which moved the count:
| form | counted before |
|---|---|
| `t.column === "todo"` | ✅ |
| `t.column !== "in-review"` | ✅ |
| `["done","archived"].includes(t.column)` | ❌ |
| `switch (t.column) { case "triage": }` | ❌ |
| SQL string `"column" = 'done'` | ❌ (separate gate owns this) |
A worker converting a `===` chain into an array membership would have
scored the conversion **and kept the guard**.
*(The first probe run was itself invalid — the file was untracked and
the census enumerates git-tracked files, so the scanned count stayed at
1961 and nothing was measured. Staging it moved the scan to 1962.
Checking the scanned count is what caught that.)*
## The near-miss worth reading
My first implementation counted **unless** the receiver looked like a
role or status — mirroring the `===` walk. On the real tree it reported
**7 column guards**, and I nearly published that as a hidden backlog.
Six were false: `switch (eventName)`, `switch (state)`, `switch (event)`
— event and state enums routinely carry `case "done"` / `case
"archived"`. Landing it would have injected six phantom guards into a
backlog the ratchet treats as zero, and `--strict` would then have
**failed every other worker's PR**.
So the new walks require a **positive** column signal instead. That
regression is pinned by a test asserting all three receivers stay
uncounted.
## Measured
```
real repo, before and after: COLUMN guards 0, STATUS 185 (no false positives)
staged probe: 2 detected before -> 4 after
new tests: 6/6 pass; 3 FAIL with the extension reverted
existing lifecycle-census test: 9/9 still green
lint clean; census --strict passes; fnxc-future-dates: none added
```
## Known limit, stated rather than left to be discovered
The positive signal is the receiver **name**, so `switch (column.id)` —
a `Column` object rather than a task's column — is **not** counted. That
is a real guard shape and it is deliberately out of scope: widening to
reach it is exactly what produced the six false positives, so it needs
its own discrimination rather than a looser regex. Flagged here so the
next person extends it deliberately instead of assuming coverage.
## Why this and not another conversion PR
The conversion queue has been genuinely empty for several cycles —
census 0, 116 resolver sites unchanged across four commits, every site
blinded and pinned. The remaining risk in this program was never another
literal; it was that **the instrument defining "done" could not see two
of the shapes it claims to protect against**. A zero from a detector
with blind spots is the exact failure this phase has spent its time
documenting.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added lifecycle-column guard detection for array membership checks and
`switch` cases.
* Recognizes supported column receiver names and classifies findings
consistently with existing guards.
* Ignores status, event, and state receivers, and avoids duplicate trait
fallback findings.
* **Tests**
* Added coverage for membership checks, `indexOf`, `switch` guards, and
deliberate-literal suppression.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Two problems, both caused by the backlog actually shrinking.
## 1. Four failing tests on main
**Pre-existing, not introduced here** — running this file on clean
`origin/main` gives `49 passed / 4 failed` with identical messages. I
checked that before touching anything, because the failures surfaced
while I was editing the same file.
The ratchet cases build their fixture like this:
```ts
Object.entries(baseline.byFile).find(([, c]) => c > 1) // needs a file with MORE THAN ONE guard
```
After the tail reclassification no such entry exists. `find` returns
undefined → `byFile[undefined] = NaN` → the baseline is corrupt → every
case fails with `expected … to contain 'TIGHTENED'`, a message that
points squarely at the CLI when the **fixture** is at fault. That
misdirection is why this sat red.
The ratchet doesn't care *which* file it tightens, only that an
allowance exceeds the measured count. So `inflate` now takes any entry,
and synthesises one against a real scanned file when the backlog is
empty.
`deflate` is the harder half: a RISE needs an allowance **below** the
real count, and once every measured count is 0 the only value below is
negative. The empty case uses `-1`. That is not a realistic baseline
value and the comment says so — it is the sole way to exercise the
`measured > allowed` comparison against a tree with nothing left to
count, which is the tree this suite now runs on.
Same class as the unbounded-slice rot in #3207: **census self-tests
coupled to the size of a shrinking backlog.** That is now twice, so it
is a pattern rather than an accident.
## 2. The report went silent at the finish line
The verdict was two inline branches and neither fired at zero —
`CONVERSION QUEUE EMPTY` required `totals.column > 0`. So the one state
the entire fleet phase was working toward printed **nothing**, which
reads as a broken scan rather than the protected end state.
Extracted to a pure `describeBacklogState({ columnGuards,
unexaminedGuards })` returning lines, so the caller stays a dumb
printer:
```
BACKLOG ZERO: no lifecycle-column guard remains.
This is the protected end state, not an empty scan — `--strict` fails on any RISE, so a new
guard cannot land silently. Use the role helpers (resolveLifecycleColumns / columnHasRole).
```
Pure **specifically** so the zero state is testable before the tree
reaches zero. While it was inline, only the *current* backlog state was
observable — and a message nobody can test before they need it is the
one that is wrong when they do.
## Evidence
| check | result |
|---|---|
| census test file | **53 passed** (was 49 passed / 4 failed) |
| behaviour on today's tree | **unchanged** — identical `CONVERSION
QUEUE EMPTY` block |
| empty-baseline probe | exits 1, `column-guard count ROSE` |
| forced zero verdict | prints `BACKLOG ZERO … not an empty scan` |
| `--strict` / `check-fnxc-future-dates` / eslint | 0 / 0 / clean |
| `pnpm test:gate` | exit 0 (744 tests) |
Four new tests pin all three states, including that the unexamined
branch must **not** claim the queue is empty while real work is
outstanding.
## Census
No guard converted — this is tooling and test repair. Backlog unchanged
at 1, which #3215 takes to 0.
## The defect
The enrich-mode task picker filtered with `task.column !== "archived"`.
On a board whose archive lane is renamed, that matched nothing — so
filed-away tasks stayed in the picker and an operator could attach
research findings to work they had deliberately archived.
## Census before / after
| | before | after |
|---|---|---|
| COLUMN guards (backlog) | 10 | **9** |
| `ResearchTaskActionModal.tsx` | 1 | **0 — converted** |
Baseline re-recorded in the same commit; `--strict` green.
## This site was declined twice, and I wrote the second wrong estimate
#3213 left it counted, correctly, on the note that was here — which was
mine. Both prior cost estimates were wrong, so this corrects my own
work:
1. **"Needs a data-fetch change"** — reasoned about
`columnFlagsByTaskId`, a per-**task** map built from board-resident
rows. Right that such a map can't help (archived rows are exactly what a
board map omits), but this guard asks a per-**column** question, so it
never needed one.
2. **"Needs prop threading, MainContent → ResearchView → here"** — right
that the answer is column-keyed, wrong about where it lives. `ListView`
builds `columnFlagsById` *inline*, which made it look like the owner.
The data is `useBoardWorkflows`, a hook already called from `App`,
`Board`, and `HeaderWorkflowSwitcherSlot`.
**Measured cost: one file.** The modal already takes `projectId`, and
`ResearchView` renders it only when a finding is open (`open` hardcoded
beside `if (!finding) return null`) — so the hook cannot fetch for a
closed modal, which was the one real objection to calling it here.
Union across workflows keyed by column id, first declaration wins — the
same convention `ListView` uses, so the two cannot disagree about a
shared id. `isArchivedColumnRole` fail-softs to the legacy id when a
column has no flags, so an unresolved workflow behaves exactly as the
literal did.
## Tests — the invariant, not the repro
Per the surface-enumeration rule, four cases: renamed archive lane,
legacy id, unresolved workflow (fail-soft), and a second workflow's
archive lane through the cross-workflow union. A repro-only test would
pass on the legacy board and prove nothing about the case the guard
exists for.
**Anti-vacuity control:**
| | renamed lane | union | legacy id | fail-soft |
|---|---|---|---|---|
| pre-fix literal | **FAIL** | **FAIL** | pass | pass |
| converted | pass | pass | pass | pass |
The legacy and fail-soft cases hold in both directions **on purpose** —
they pin that this conversion did not change the pre-resolution answer.
Flagging that so 4/4 isn't read as four independent proofs.
## Measured
| check | result |
|---|---|
| `census --strict` / `check-fnxc-future-dates` | exit 0 / exit 0 |
| `eslint` | clean |
| `tsc -p tsconfig.app.json` (the config that actually covers `app/`) |
exit 0 |
| new tests | 4/4 |
| `pnpm test:gate` | exit 0 (744 tests) |
## Note on process
My first attempt at the control silently did nothing — the revert script
threw a `SyntaxError`, so the "pre-fix" run was the fixed code and
reported 4/4. Caught it because the error printed. The table above is
from the re-run.