**U9, PR4.** Docs-only correction to a document already on `main`
(#2511). No changeset.
## I got #2511 wrong, and it matters
#2511's table claimed all six merge safeguards were verified. **Two of
them were not**, and the error is the same family this program exists to
stamp out: I reported the **absolute** failure count under mutation,
with **no baseline**.
`merge-error-recovery.test.ts` (10 failures) and `self-healing.test.ts`
(1) are **already red on clean `main`**. The "11 failed" I credited to
the row 1 mutation *was that pre-existing red*. The mutation added
nothing. I even flagged the identical `11 failed` on rows 1 and 3 as "a
red flag" in my own notes and then did not chase it.
## Re-measured as deltas
Baseline fail-SET vs mutated fail-SET on the identical selection,
reporting only NEW failures, each named:
| # | Safeguard | Baseline | Mutated | **NEW** | Verdict |
|---|---|---|---|---|---|
| 1 | user pause | 11 | 11 | **0** | **NOT COVERED** |
| 2 | `autoMerge:false` | 0 | 9 | **9** | covered |
| 3 | dependency gating | 0 | 5 | **5** | covered |
| 4 | capacity single-flight | 10 | 10 | **0** | **NOT COVERED** |
| 5a | merge-proof (pre-enqueue) | 0 | 1 | **1** | covered, thin |
| 5b | file-scope | 0 | 6 | **6** | covered |
| 6 | at-most-once | 0 | 3 | **3** | covered |
**Four hold. Two do not.**
- **Safeguard 1** is the pause invariant re-ratified in #2486. Removing
`task.paused || task.userPaused` from the merge admission provider
admits a **user-paused card into the merge pump** — and nothing fails.
- **Safeguard 4** is the single-flight guard that serializes merge.
Removing it permits concurrent `drainMergeQueue` entry — and nothing
fails.
Both guards **work correctly today**. What is missing is any test that
would notice if they stopped. That is exactly the state U9 must not
convert on top of — and #2511 said the opposite.
## Also corrected: nothing here is defended by blocking CI
The one gate-admitted file (`merger-merge-lifecycle.test.ts`) is not the
file that proves any surviving row. Rows 2/5a/6 rest on
`project-engine.test.ts`, row 3 on core's `task-merge.test.ts` — neither
is in the gate (core's gate is two PG tests via `test:pg-gate`).
## Three distinct ways the first pass was wrong
All recorded in the doc, because each produced a confident wrong answer:
1. **Absolute counts with no baseline** — rows 1 and 4. A mutation run
must diff fail-sets and report only new failures.
2. **Too-narrow selection** — an earlier pass measured rows 1 and 3 at
zero and I nearly filed two false gaps. Widening fixed row 3 but is also
how the pre-existing red crept in. Both directions need the baseline
diff.
3. **A harness that silently matched nothing** — the delta harness's
regex required a `|project|` segment in vitest's `FAIL` line.
`@fusion/core` does not emit one, so it parsed **zero** failures at both
baseline and mutation and printed "NOT COVERED" for row 3, which is
covered by 5 tests. A verification tool that reports success without
checking anything is worse than no tool; it must be tested against a
known-failing case first.
The harness now aborts on a no-op patch, asserts a clean restore, and I
validated its parser against a known-failing run before trusting it.
## Next
**PR5 writes the missing tests for safeguards 1 and 4**, then gate
admission. Neither should wait for U8 — they guard code that exists
today, and the conversion needs them in place first. That is the
reversible call I'm making rather than asking.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
**U9, PR3.** Docs-only, one new file, zero production changes. No
changeset (internal docs). Independent of #2504.
This is the six-row safeguard table required as a U9 artifact —
delivered **verified**, replacing the partial, unearned version I put in
#2494.
## Every row proven by mutation
Break the guard in production, run the cited tests, confirm red,
restore. A cited test that does not fail is not evidence.
| # | Safeguard | Consulted at | Result under mutation |
|---|---|---|---|
| 1 | user pause | `project-engine.ts:645` (merge admission filter) |
**11 failed** / 1865 passed |
| 2 | `autoMerge:false` | `project-engine.ts:2797`
`allowsAutoMergeProcessing` | **9 failed** / 250 passed |
| 3 | dependency gating | `task-merge.ts:402` unresolved-dependency
reason | **5 failed** / 94 passed |
| 4 | capacity | `project-engine.ts:3178` single-flight `mergeRunning` |
**11 failed** / 1445 passed |
| 5a | merge-proof (pre-enqueue) | `project-engine.ts:2609`
`getTaskMergeBlocker` consult | **1 failed** / 272 passed |
| 5b | merge-proof (file scope) | `merger-file-scope.ts:200`
`FileScopeViolationError` | **6 failed** / 172 passed |
| 6 | at-most-once | `project-engine.ts:2730` `mergeActive` dedupe | **3
failed** / 263 passed |
**All six hold. Nothing is currently broken.** Per-row test attribution
is in the doc.
## Finding 1 — one of nine safeguard test files runs in blocking CI
Only `merger-merge-lifecycle.test.ts` is in the `engine-core`
allow-list. The core gate is two PG tests (`test:pg-gate`) and does not
include `task-merge.test.ts`.
AGENTS.md: CI blocks on Lint/Typecheck/Build/Gate, and "a red
non-blocking run is information, not a merge stopper."
So a change breaking **user pause on merge admission, dependency gating,
capacity single-flight, or the file-scope invariant** does not block a
PR today — it goes red in full-suite, after the merge.
Acceptable for a lane nobody is rewriting. Wrong for the lane U9
rewrites next. **Recommendation: admit the highest-value safeguard tests
to the gate before conversion begins, with the budget cost measured** —
engine-core is 5.36s against a ~60s ceiling, so there is room, but I
won't assume it. Proposed as PR4.
## Finding 2 — safeguard 5a rests on a single non-gate test
Removing the pre-enqueue merge-blocker consult fails exactly one test.
Thinnest of the six, on a destructive-risk gate. Its sibling 5b is well
covered (6 tests), so the invariant isn't unguarded — but the consult
that keeps a blocked task out of the queue very nearly is.
## Methodology note, because it cost an hour
**Rows 1 and 3 initially measured ZERO failures and looked like coverage
gaps. Both were wrong** — the test selection was too narrow. Widening
row 1 from three files to
`project-engine|merge|concurrency|self-healing` turned 0 failures into
11. Row 3's real coverage lives in `@fusion/core`'s suite, which `pnpm
--filter @fusion/engine` never runs, even though the engine config
aliases `@fusion/core` to source so the mutation *was* live.
I nearly reported two false gaps. Recorded as two rules: a narrow
mutation run cannot prove absence of coverage, and cross-package guards
need cross-package runs.
## Scope
New file only — deliberately **no** edit to
`docs/workflow-policy-ownership-map.md`, because #2504 already edits
that file at the same anchor and I want both PRs independently
revertable. Cross-link follows once both are on main.
Verified: no production diff, all mutations restored, `pnpm lint` clean.
## Not covered, stated rather than implied
Reviewer-lane safeguards; FN-7720 operator bypass; FN-8492
orphaned-pending-step rewrite; branch-group promotion sequencing. Each
needs its own verified row before the matching conversion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
**U9, PR2.** Docs-only, no changeset (AGENTS.md: internal docs).
## Why
All seven slices in `docs/plans/workflow-owned-merge-stack/` were marked
`draft-stack-handoff` — accurate when drafted 2026-06-09, wrong now. **A
worker picking up this stack cold would have re-implemented S04, which
has already landed.** I nearly did.
## Measured, against `main @ 46f35323c`
| Slice | Was | Now | Evidence |
|---|---|---|---|
| S02 projection | draft | `landed-unwired` |
`projectMergeRequestToWorkflowWorkItem` implemented, **0 production
callers** |
| S03 scheduler claim | draft | `landed-unwired` |
`claimDueWorkflowWorkItem` implemented; its only caller is S05's
processor, itself unwired |
| S04 IR regions | draft | **`landed`** | `merge-gate`, `merge-retry`,
`manual-merge-hold`, `merge-attempt`, `recovery-router` present in the
coding IR |
| S05 runtime driver | draft | `landed-unwired` | `runWorkItem` /
`processDueWorkflowWorkItem` implemented, exported from `index.ts`, **0
production callers** |
| S06/S07/S08 | draft | `not-started` | merge still runs through
`merger.ts` + the live `ProjectEngine.mergeQueue` pump |
## The finding that changes U9's sequencing
`WorkflowWorkItemKind` is `task | merge | retry | manual-hold |
recovery`. The only live pump —
`InProcessRuntime.drainWorkflowContinuations` — filters `kinds:
["task"]`. The generic processor that would claim the other four kinds
has **no production caller**.
So the entire merge-lane work-item vocabulary is dormant: **zero
writers, zero readers.**
**I checked whether this is a live bug and it is not.** Nothing in
production writes a non-`task` kind — the only two writers
(`plan-review-continuation.ts`, `workflow-column-boundary-hooks.ts`)
both go through `replaceActiveTaskWorkflowContinuation`. Nothing is
stranded today. I'd rather say that plainly than let a scary-sounding
finding stand unqualified.
But it produces a hard ordering constraint, now recorded in S07:
> **S07 must not land before S03/S05 are actually driven.** S07 is the
slice that starts writing `merge`-kind work items. If it lands first,
those items are created and never claimed — a card that reaches the
merge boundary and silently stops.
This also reframes U9's job on S02/S03/S05: **wire them, don't build
them.**
## Scope discipline
Docs-only — `git diff --stat` is 9 files, all under `docs/`. No
production code, no tests, no behavior. `pnpm lint` clean.
I also fixed the three parent-plan lines asserting the slices are "all
still `draft-stack-handoff`", and the four landed slices' Stack Role
paragraphs that would otherwise contradict their own new Measured State
block. Leaving those stale would recreate exactly the defect this PR
fixes.
Related: #2494 pins the S04 caveat — the IR regions are declared but
their config is read by nothing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Documentation**
- Updated workflow-owned merge planning documents with current
implementation and wiring statuses.
- Added measured wiring details showing which workflow capabilities are
active, implemented but unused, or not started.
- Clarified sequencing requirements to ensure merge processing is not
enabled before prerequisite workflow paths are operational.
- Corrected slice metadata and references to reflect the latest measured
state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docs only. Corrects a factual error in the program plan merged in #2463,
found during Phase A execution.
## What was wrong
KTD-6 listed three deletions as "provably dead." Two are. The third —
the flag-off inline move path in `task-store/moves.ts` — is the **live**
path:
- It is gated on `isWorkflowColumnsCompatibilityFlagEnabled`
(`store.ts:38`), which reads the **raw**
`experimentalFeatures.workflowColumns` key.
- That is a *different* function from the always-true public
`isWorkflowColumnsEnabled`, which is what the plan reasoned from.
- No production code writes that key, so the flag reads false — the
inline branch runs for essentially every project, and
`default-workflow-hooks.ts` is the dead one.
- `moves.ts:637` already said so: *"this 'flag-OFF' branch is the
DEFAULT move path for nearly every project — the strict compat flag
reads false because nothing sets it."*
Deleting it would have swapped every project onto an untravelled code
path and called it a cleanup.
## Changes
- **KTD-6** carries the correction inline, with the two consequences
that bind the rest of the program.
- **U2** rescoped to the two proven-dead deletions; `moves.ts` is out of
scope for it.
- **New U2b** converges the two implementations with a per-behavior
equivalence proof, and **blocks Phase B** — nothing downstream may
assume the trait-hook path runs until it lands.
- **U3's emit point must attach to the live inline path.** Wired into
the dead hooks path, the event seam would never fire — and because
subscribers are non-authoritative by design, *nothing would fail a
test*.
- Risk table gains: *a "dead" branch turns out to be live*.
## How it was caught
The Phase A worker escalated instead of following the instruction,
because U2 carried a delete-only Execution note: *any behavior change
found while removing a branch means the branch was not dead — stop and
treat it as a finding*. I verified the claim independently (flag
function, absent writers, live `settings.json`, source comment) before
accepting it.
That note stays on every deletion unit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Clarified terminology in the workflow lifecycle migration plan,
distinguishing the live inline move path from the compat-flag behavior.
* Expanded the convergence phase with explicit behavior-equivalence
requirements, pinned observable expectations, and removal of compat-flag
branching.
* Updated the migration diagram and phase ordering to reflect the new
gating structure, revised scope boundaries, and refreshed risk
mitigations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docs only — no code. Companion to #2462.
##
`docs/solutions/architecture-patterns/workflow-node-column-placement-and-graph-entry-contract.md`
Why a workflow node's `column` is a lifecycle contract rather than a
display choice: it decides **who can drive the node**, **whether the
card holds a WIP slot**, and **whether anything can move it onward**.
Contents:
- The graph **entry contract** (`resolveColumnResumeNode`, shipped in
#2462) with the resume table.
- The **plan-in-place chain** — triage → finalize → continuation seed →
drain → resume → capacity suspend → release — annotated with the check
each link performs. Notably `todo`, not `triage`: an intake column has
no releaser, so a card parked there waits for a human.
- Why the pre-release gate must be narrow (column match **and**
enablement).
- The measured failure table from three reverted placement attempts.
- Why removing a column is a lifecycle-vocabulary refactor, not a
workflow edit: **82 guards** that silently stop matching, **43 writes**
to a column that no longer exists, **59 dashboard literals**. A guard
that never fires doesn't fail a test — it disables a recovery path.
## `docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
The program that finishes the job, in four movements:
1. Resolve lifecycle columns from the workflow instead of ~207 string
literals.
2. Move every lane — planning, execution, review, merge — behind graph
nodes; lane services keep substrate only (storage, leases, timers,
supervision, capacity, recovery, audit).
3. A **post-commit event seam**: transitions commit transactionally,
*then* emit; subscribers react and may enqueue durable work items, but
no subscriber performs a transition. Enforced by test — dropping every
subscriber must change no lifecycle outcome.
4. Only then merge Todo into a single Planning column.
Phased so each phase lands green independently, with the IR change
deliberately **last** (KTD-7). Changing the workflow first makes the
suite green over dead guards — that's how the earlier attempts hid their
own breakage.
The merge lane **adopts** the existing design in
`docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md`
(slices S02–S08, still `draft-stack-handoff`) rather than authoring a
competing one, with a note to re-validate against current `main` since
it was drafted seven weeks ago.
Scale is stated honestly: ~48k lines across the four lane services, with
the executor unit explicitly landing across several commits rather than
one sweep.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
The Coding (Ideas) workflow now behaves like the board it presents:
Ideas stays inert, Todo owns planning and plan review, In progress owns
implementation, and In review owns code review and merge. The restored
preset is intentionally limited to that five-stage path, while the
existing Coding workflow remains unchanged.
Workflow execution now suspends at Todo→In progress instead of running
the implementation node early. A durable, single-owner continuation
records the exact resume node and survives process restarts; the
scheduler remains the only component allowed to admit the task into WIP.
Disabled optional review groups traverse the same boundary without
invoking a reviewer, avoiding the prior stuck-task behavior.
Workflow validation also rejects capacity holds with no reachable WIP
destination, so deterministic lifecycle deadlocks fail at authoring time
rather than after a task is running.
Session-settled decisions carried from planning: columns are execution
invariants, scheduler-owned WIP admission is preserved, the existing
Coding (Ideas) preset is restored and simplified, and invalid release
topology is rejected (user-approved).
## Validation
- `pnpm lint`
- `pnpm verify:fast`
- `pnpm test:gate` (296 engine, 128 PostgreSQL core, and 63 CI-shape
tests)
- Focused workflow lifecycle tests (106 assertions)
- PostgreSQL regression coverage proves atomic continuation replacement
and database rejection of a second active owner
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added durable, resumable workflow execution across capacity boundaries
(including explicit suspend/resume at the correct node).
* Introduced Todo “plan review” workflow continuations and automated
planning/capacity draining.
* Restored Coding (Ideas) as a selectable built-in and updated its lane
placement; improved optional-step group enablement support.
* **Bug Fixes**
* User moves back to Todo now cancels active workflow continuations.
* Rejected workflow boundary transitions now surface as errors (instead
of silently continuing).
* Workflows with undriveable capacity-hold configurations are now
rejected.
* **Tests / Data**
* Expanded coverage for workflow suspension, continuations, and
continuation replacement; updated database schema to persist
continuation metadata and enforce single active continuation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Fusion can now ship on two release tracks. Betas are cut from `main` as
`vX.Y.Z-beta.N` (npm dist-tag `beta`, GitHub prerelease), stable
releases are promoted to a long-lived `release` branch and published to
`latest`, and users pick their track with the new `updateChannel` global
setting — via **Settings → General → Release channel** or `fn update
--channel <stable|beta>`. Previously everything was single-track: every
publish landed on `latest` and every update surface could only see it.
| | beta | stable |
|---|---|---|
| Cut from | `main` | `release` branch |
| Version | `X.Y.Z-beta.N` (changesets pre-mode) | `X.Y.Z` |
| npm dist-tag | `beta` | `latest` |
| GitHub Release | prerelease | latest |
| Homebrew tap / X draft | skipped | bumped / printed |
## How releasing works now
`pnpm release` prompts for the channel and **defaults to beta**, so
day-to-day releases are betas; stable is always an explicit choice.
Choosing stable from `main` triggers assisted promotion: the script
proposes the newest beta tag reachable from HEAD, verifies `release`
fast-forwards to it, then runs the whole stable release inside a
temporary git worktree on `release` — the primary checkout never leaves
`main`. Changesets pre-mode preserves changeset files across betas, so
the promoted stable release aggregates every changeset since the last
stable into one clean changelog entry.
## Design decisions
- **Every publish path names an explicit `--tag`.** A beta accidentally
landing on `latest` is the one unrecoverable failure of a dual-track
scheme, so nothing relies on npm's implicit default (`release.mjs`,
`version.yml`).
- **Beta channel resolves to semver-max of `latest` and `beta`**, so
beta users are offered each promoted stable once it overtakes their
prerelease. Switching beta → stable never downgrades; `fn update
--channel stable --force` is the explicit escape hatch.
- **One comparator instead of three.** CLI, dashboard, and desktop each
had their own `isRemoteNewer` that ignored prerelease identifiers —
`0.73.0-beta.2`, `-beta.3`, and `0.73.0` all compared equal, which
breaks the moment any beta exists. They now share full SemVer-precedence
helpers (`compareVersions`, `resolveUpdateTargetVersion`) from
`@fusion/core`.
- **Installs pin exact versions** (`@runfusion/fusion@0.73.0-beta.2`),
never a dist-tag, so an install can't silently land on the wrong track.
- **Desktop channels via electron-updater manifests.** Beta tags build
desktop artifacts with `publish.channel=beta` (emitting `beta*.yml`);
the app sets `channel`/`allowPrerelease` from the shared setting,
re-read on every manual check.
- **Update caches are channel-stamped** — a cache written for one
channel is never served to the other, so switching tracks takes effect
on the next check instead of after TTL.
## Test plan
- New unit coverage: SemVer precedence + channel resolution in
`@fusion/core` (30), channel behavior of the dashboard update check (28,
incl. 9 new) and `fn update` (16, incl. 8 new: persist `--channel`,
no-downgrade, `--force`, cache channel mismatch).
- `pnpm verify:fast` green (scoped typecheck, builds, CLI build, boot
smoke); desktop + settings-section suites green.
- `release.mjs` dry-run matrix exercised by hand: channel prompt
(default/override/invalid), branch preflights per channel,
assisted-promotion target selection, fast-forward guard against a
diverged `release` branch, and bootstrap when no `release` branch
exists.
- Not exercised live: an end-to-end publish (needs TTY authorization +
real npm publish). First real run is the first `pnpm release --channel
beta`.
---
[](https://github.com/EveryInc/compound-engineering-plugin)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added beta and stable release channels across CLI, dashboard, and
desktop updates.
* Users can select a channel via Settings or `fn update --channel
<stable|beta>` (stored as a global default).
* Desktop beta releases now generate beta update manifests and publish
as prereleases.
* **Documentation**
* Expanded release-track, settings, and CLI references to explain
channel semantics and workflows.
* **Bug Fixes**
* Updates now pin the resolved version per channel, improve version
comparison, and prevent unintended cross-channel downgrades unless
`--force` is used.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Part **1 of 3** of the IR-driven lifecycle cutover (split from #2335 to
fit review-tool file limits; plan:
docs/plans/2026-07-18-001-refactor-ir-driven-lifecycle-cutover-plan.md,
included here).
**Scope (48 files, packages/core + docs/plans):** shared transition
policy + validator (KTD-5), IR validation hardening incl. the benchmark
capability floor, CAS review leases (KTD-4), pooled WIP capacity budgets
(KTD-9), lifecycle-trait helpers, durable IR pin/drift detection
(KTD-3), review-level creation-time preset, legacy adoption module +
census + migration 0026 + stale-binary guard (KTD-8), core-side builtin
workflow fixes (single default-IR authority, no-merge complete-column
support).
Note: `workflow-cutover.ts` (interpreter parity scaffolding) stays alive
in this PR — its last consumer dies in part 2/3, which retires it.
**Merge order:** this PR → #TBD-2 (engine) → #2335 (dashboard/top).
🤖 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 workflow-trait-driven task lifecycle transitions, including WIP
capacity pooling and workflow-aware recovery (IR pinning + drift
detection).
* Added legacy adoption/backfill for pre-cutover task states, with
unmappable rows safely parked.
* Added create-time `reviewLevel` presets to automatically configure
enabled workflow steps.
* **Bug Fixes**
* Fixed workflow moves when no workflow selection exists.
* Improved merge-blocker validation to be keyed to the workflow’s actual
review-lane identity, preventing invalid moves and misclassified
terminal states.
* **Tests**
* Added end-to-end and unit/integration coverage for workflow
validation, legacy adoption, migrations/schema guards, leases, review
presets, and transition rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Follow-up after **#2164** and main’s **FN-8103 / FN-8104**
(Postgres-only data access / SQLite retirement).
Main already routes Quality through `AsyncQualityStore` +
`getAsyncLayer()`. This PR keeps the **review hardening** that was still
missing:
- **Project binding** — reject request `projectId` mismatches vs bound
AsyncDataLayer; all SQL uses bound project
- **`createRunIfNoActive`** — advisory lock so concurrent starts cannot
double-queue
- **Cancel-safe runner** — cancel slot registered before the running
write; pre-spawn cancel skips process
- **`finalizeRun`** — never overwrites a `cancelled` terminal status
- **Detached execute** — catch only execution failures; prune fail-soft
in `finally`
- Guardrail tests + Quality v2 plan doc
## Test plan
- [ ] Task QA loads under PostgreSQL (no SQLite/backend-mode error)
- [ ] Concurrent start for same task → 409 second start
- [ ] Cancel during start does not leave a live orphan process
- [ ] Cancelled run stays cancelled after process exit
- [ ] `pnpm --filter @fusion-plugin-examples/quality test` (37 tests)
## Summary
Adds a bundled **Quality** plugin (`fusion-plugin-quality`) that makes
task QA easier and more visual:
- **Task QA tab** (action-first): preview/test server for the task
worktree, allowlisted test runs, report viewer, screenshots CTA,
suggested test cases, CI handoff
- **Quality hub** (left sidebar): project-wide run history and preset
launches
- Host **task-detail slot context** (`taskId`, worktree, `projectId`) so
plugin tabs can scope correctly
- `superviseSpawn` re-exported on the plugin packaging shim for
published plugins
- Plan: `docs/plans/2026-07-14-001-feat-quality-plugin-plan.md`
## Design constraints
- Does **not** replace the merge gate — advisory orchestration only
- Composes Dev Server process patterns and artifact registry (no second
browser stack)
- Never free-form shell; never port 4040
- Full-suite requires explicit confirm
## Test plan
- [x] `pnpm --filter @fusion-plugin-examples/quality test` (15 tests)
- [x] PluginSlot unit tests still pass
- [ ] Enable Quality plugin in dashboard Settings → Built-in Plugins
- [ ] Open Task Detail → **QA** tab with a worktree; start preview, run
verify:fast, generate suggestions
- [ ] Open left sidebar **Quality** hub and list runs
- [ ] Confirm merge gate / PR checks unchanged
## Residual / follow-up (same plan, later units)
- Deeper hub CI (host route)
- Full browser-verification toggle UX + agent QA sessions (U7/U9/U10)
- Richer screenshots gallery wiring to live artifacts API
- Test plans CRUD polish
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added the Quality plugin with a project Quality hub and task-focused
QA tab.
* Added test runs, reports, preview server controls, suggested test
cases, and run history.
* Added configurable test presets, cancellation, status tracking, and
safe command execution.
* Added experimental-feature controls for enabling Quality
functionality.
* Bundled Quality with the CLI and made it available through the plugin
manager.
* **Documentation**
* Added Quality plugin guidance, terminology, configuration details, and
implementation planning documentation.
* **Bug Fixes**
* Improved process supervision so command failures and shutdown timers
are handled safely.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Speeds up **time-to-HTTP-ready** for `fn dashboard` and `fn serve` after
the PostgreSQL cutover without reintroducing the historical 3s
cwd-engine race that degraded webhooks.
- **Dashboard store share (serve parity):** inject the factory-booted
`TaskStore` as `externalTaskStore` so cwd `ensureEngine` does not open a
second pool; share only when store root matches project working
directory (multi-project safe).
- **Serve multi-project:** stop awaiting `startAll()` before listen;
await only the primary engine; background the rest + reconciliation.
- **Defer non-route-critical engine work:** ordered OAuth (refresh →
monitor), automation schedule syncs, and auto-merge **enqueue** after
the engine handle is returnable.
- **Critical-path merge status clear:** still clear stale
`merging`/`merging-pr` before ready so manual merge is not blocked after
crash.
- **Serve `--paused`:** apply `enginePaused` before
`ensureEngine`/`startAll` (dashboard ordering).
- **Stop safety:** generation counter so deferred tails cannot resume
after `stop()` clears `shuttingDown`.
- **Phase timing:** shared `phaseTime` helper, factory substep logs,
serve time-to-listen.
Plan: `docs/plans/2026-07-14-001-feat-faster-startup-plan.md`
## Test plan
- [x] `packages/engine` — `project-engine-manager.test.ts` (path-matched
external store)
- [x] `packages/engine` — `project-engine-deferred-startup.test.ts`
(status clear, OAuth order, stop generation)
- [x] `packages/cli` — `startup-phase.test.ts`
- [x] `packages/cli` — `serve.test.ts` (60 tests, including `--paused`)
- [ ] Local: warm `fn dashboard` / `fn serve` and compare `startup phase
*` / `time-to-listen` logs
- [ ] `pnpm smoke:boot` (real serve `/api/health` on ephemeral port)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Performance**
* Improved dashboard and serve startup times, including faster
time-to-listen and time-to-ready.
* Moved non-essential background initialization off the critical startup
path.
* Parallelized dashboard service initialization where possible.
* **Reliability**
* Improved multi-project startup handling and project selection.
* Prevented cross-project task-store sharing.
* Added safer shutdown behavior for partially completed startup.
* **Diagnostics**
* Added startup phase timing logs to help identify performance
bottlenecks.
* **Tests**
* Expanded coverage for deferred startup, shutdown, project isolation,
and startup timing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Treat **shared PostgreSQL** (`DATABASE_URL`) as the multi-node durable
data plane; mesh HTTP is membership + optional auth, not task/settings
replication.
- **Peer exchange**: under Postgres backend mode, write queue is
**topology/auth-only**; non-topology pending rows fail rather than
replaying multi-leader task/settings payloads.
- **Mesh routes**: task-ID reserve/commit/abort always hit local shared
allocator rows (ignore remote `coordinatorNodeId`); mesh sync ignores
settings and only exchanges `authMaterial`.
- **Docs**: rewrite multi-project runbook, shared cluster protocol, and
architecture mesh sections for shared-Postgres + claims/leases.
## Context
Follows the SQLite→Postgres cutover. Multiple Fusion nodes can share one
external Postgres while keeping **per-node execution** (worktrees,
processes, claims via `central.task_claims`). Explicit non-goals remain:
scheduler failover and live process migration.
Plan:
`docs/plans/2026-07-15-001-refactor-mesh-shared-postgres-multinode-plan.md`
## Test plan
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/peer-exchange-service.test.ts`
- [x] `pnpm --filter @fusion/dashboard exec vitest run
src/__tests__/mesh-routes.test.ts`
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/shared-mesh-state.test.ts`
- [ ] CI gate (lint/typecheck/build/gate)
- [ ] Manual (optional): two processes, same `DATABASE_URL`, create task
on A visible on B; settings change without mesh settings sync; claim
exclusivity
## Operator note
Multi-node shared board requires **external** `DATABASE_URL` on every
node. Default embedded Postgres is still single-host.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved multi-node deployments using shared PostgreSQL as the durable
source of execution state.
* Task ID reservation/commit/abort now run locally (no remote
coordinator forwarding).
* Mesh syncing now prioritizes topology visibility and authentication
material; settings replication is disabled in shared-Postgres mode.
* **Bug Fixes**
* Prevented task/settings replication over mesh HTTP in shared-Postgres
deployments.
* Refined lease ownership, recovery, and reconciliation to converge via
shared-database primitives.
* **Documentation**
* Updated architecture and shared-mesh protocol guidance, including
multi-node setup and lease/task-ID allocation behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Migrate storage from SQLite to PostgreSQL — full dashboard cutover
Migrates Fusion's storage layer to the embedded PostgreSQL
`AsyncDataLayer` (the default backend) and **completes the
satellite-store + feature cutover** so every dashboard and Command
Center surface works in PG mode.
## Status — every surface works in embedded-PG mode
Verified live against a running embedded-Postgres dashboard (all
**200**, zero 5xx) and gate-tested (**23 files / 99 tests** on embedded
PG, plus engine-core 294 and ci-shape 63 in the blocking merge gate;
core/engine/cli/dashboard typecheck clean).
| Area | Surfaces | State |
|---|---|---|
| Satellite stores | workflows, todos, insights, research, missions,
goals, mailbox | ✅ |
| Views | artifacts, documents, evals | ✅ |
| Command Center | activity, productivity, team, tokens, tools,
**workflows**, **github**, **signals**, **plugin-activations**, **live**
(all 10) | ✅ |
| Run execution | insight generation, research run execution | ✅
(store-path; AI step needs a provider) |
| Live updates | SSE push for mission/research/insight events | ✅ |
| Workflow editing | create / update / delete / select (+ id counter) |
✅ |
| Engine | mission autopilot, incident-signal ingestion, regression
storm-guard, agent wake-on-message | ✅ |
| Core | tasks, agents, secrets, automations, memory, chat, usage, PRs,
git | ✅ |
## Approach
Each satellite store gets an `Async<Store>` wrapper exposing the sync
store's method names over the existing `async-*-store.ts` helpers;
`get<Store>Store()` returns a `Sync | Async` union; consumers `await`
(harmless on sync), and engine/CLI paths that can't convert use
`instanceof Sync` graceful fallback. Analytics aggregators branch on
`"ping" in dbOrLayer` to run schema-qualified raw SQL over `project.*`
(snake_case) in PG. Executors/orchestrators/autopilot are
await-converted to drive the union store; the async store wrappers
extend `EventEmitter` so SSE live-push fires in both backends.
Not-yet-ported capabilities degrade gracefully (never 500) and are
individually called out in commits.
## Sync with main
The branch is kept continuously merged with `main` (currently through
FN-7845, 2026-07-12); the earlier "final rebase deferred" note no longer
applies. Use **Create a merge commit** (or squash) to land it — GitHub's
rebase-merge cannot replay a merge-maintained branch.
## Residual Review Findings
Multi-agent code review of the PostgreSQL satellite-store ports (U1–U5)
applied 3 safe fixes (see `fix(review): apply autofix feedback`). The
following are **real but gated** — recorded here as follow-up work
rather than auto-applied. All are SQLite→PostgreSQL
**concurrency/atomicity regressions**: the sync stores were immune only
by SQLite's single-writer, single-threaded-handler execution; the async
ports open multi-await read-modify-write windows. **Reachability is low
today** because the execution engines that generate concurrent same-run
mutations (insight run executor, research orchestrator/dispatcher) are
`instanceof`-gated to sync mode in PG. No process-crash class survived
(all engine fallbacks correctly guard the sync store).
- **[P1] Research `appendResearchEvent` dual-write is non-atomic**
(`packages/core/src/async-research-store.ts`, corroborated: adversarial
+ reliability). The `research_run_events` insert (own transaction) and
the `run.events` jsonb update are separate writes — a crash between
them, or two concurrent appends, splits the table count from the jsonb
array. **Fix:** perform the seq-insert and the jsonb update in one
`layer.transactionImmediate`.
- **[P1] Research run terminal-reversion via stale full-row persist**
(`async-research-store.ts` `persistResearchRun`/`updateResearchStatus`).
Concurrent `PATCH /runs/:id/status` + `POST /runs/:id/events` can revert
a terminal run to `running` by overwriting the whole row, bypassing the
transition guard. **Fix:** scoped column `UPDATE`s with a `WHERE status
…` guard, or optimistic version column.
- **[P2] `updateResearchRun`/`updateInsightRun` read-then-write TOCTOU**
— concurrent PATCHes last-writer-wins on the lifecycle merge. **Fix:**
`SELECT … FOR UPDATE` / enclosing transaction.
- **[P2] `upsertRun`/`createRunOrThrowConflict` check-then-create race**
(`async-insight-store.ts`) — two callers can each create an "active"
run. **Fix:** partial unique index on `(projectId, trigger) WHERE status
IN ('pending','running')`.
- **[P3] `createResearchRetryRun` return-value divergence** — sync
returns the pre-update `queued` snapshot; async returns the reloaded
`retry_waiting` run (persisted state is identical). Pick one side for
cross-backend parity.
- **[P2/perf] Mission `getMissionWithHierarchy`/`getMissionHealth` N+1
fan-out** — O(milestones×slices) sequential round-trips hold one pool
slot per request; can starve the pool for large hierarchies. **Fix:**
batched/joined reads.
- **Testing gaps:** no PG-mode concurrency tests (interleaved
status/event mutations), no sync↔async parity assertion for the
lifecycle-error codes, and no mission status/health rollup parity test
vs the sync `MissionStore`.
~~Out of scope (deferred): AI run *execution* (insight/research) +
mission autopilot + live SSE mission events remain sync-gated/degraded
in PG mode.~~ **Since ported** — insight/research run execution, mission
autopilot, and SSE live push all run on the async layer now, which also
makes the concurrency findings above genuinely reachable; they remain
open follow-ups.
---
## Update — 2026-07-12: production-readiness hardening & live acceptance
Everything below landed on this branch since the description above was
written:
**Production blockers from review — fixed**
- `recoverStaleTransitionPending` ported to the async layer (backend
moves write + clear the crash-safe marker; startup/maintenance sweeps no
longer throw).
- Lost-update class fixed: `atomicWriteTaskJson`/`WithAudit` write
changed columns only (full-row upserts silently resurrected stale fields
across concurrent store instances — the "task stuck unplanned forever"
bug).
- First-boot **auto-migration**: booting the PG backend over a project
with a legacy `fusion.db` migrates it automatically (loud failure,
SQLite kept as backup), and the dashboard shows a one-time **"your data
was migrated" banner** with the backup paths and a Need-help Discord
link.
- `pg_dump`/`pg_restore` discovered from common install locations for
embedded-mode backups.
- The PG suite is part of the blocking merge gate (`test:pg-gate`).
**Multi-project isolation (PR #2007, merged into this branch)**
- `project_id` partition key on tasks / archived tasks / config,
`taskProjectScope` threaded through every scan/claim/count, per-project
config rows, layer bound to the project at startup.
- Review P1 follow-up: the shared cold-storage `archive.archived_tasks`
table is also partitioned and all archived-board reads/counts/searches
are scoped.
- Schema drift self-heal generalized to schema-qualified columns so
existing databases upgrade in place.
**Other changes**
- Node settings sync **removed** in PG mode (409
`settings-sync-disabled-postgres`) — nodes share state by connecting to
the same database; auth sync kept (per-machine file).
- Perf (review findings): `listTasks` pushes column filter + ORDER BY +
LIMIT/OFFSET into SQL; `getConversation` capped to the most recent 200
messages.
- Fixed a false "operator action required" pause-abort log fired on
every successfully auto-merged task.
**Live acceptance — PASSED (2026-07-12)**
A sandboxed instance (isolated HOME, embedded PG, real Opus executor)
ran a task through the complete cycle: create → triage (AI spec) →
execute → in-review → AI squash-merge landed on the project's `main` →
done. A write+read sweep of every data surface (settings, comments,
documents, attachments + artifact bridge + artifact edit, chat with real
generation, goals, missions, agent mail, secrets, workflows, memory, CC
analytics) was green on embedded PG.
**Known remaining work**
- The per-project `config` PK re-key has no upgrade path for
pre-isolation embedded-PG databases (needs a real `DROP
CONSTRAINT`/re-key migration; fresh databases are fine).
- `pg_dump`/`pg_restore` binaries are not yet bundled in release
artifacts (PATH/common-location discovery only).
- The satellite-store concurrency findings listed above.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Phil Larson <hello@phillarson.xyz>
Co-authored-by: fusion-merge <fusion-merge@local>
Rename the settings navigation entry so Remote Access is no longer conflated with Node Sync.
- Update the remote settings nav label to "Remote Access" while keeping the separate Node Sync section intact.
- Add coverage that rejects the old combined label and confirms both standalone entries render.
- Align the workflow settings plan and changeset with the clarified settings IA.
Files changed:
.changeset/fn-7062-remote-access-rename.md | 7 +++++++
...26-06-04-002-feat-workflow-settings-mechanism-plan.md | 2 +-
packages/dashboard/app/components/SettingsModal.tsx | 6 +++++-
.../__tests__/SettingsModal.scheduling-merge.test.tsx | 16 ++++++++++++++++
4 files changed, 29 insertions(+), 2 deletions(-)
Fusion-Task-Id: FN-7062
Fusion-Task-Lineage: d50d47eb-aa15-4bf6-8f26-edaa79c8de9e
Six-reviewer doc review reshaped two foundational decisions:
- Results model: graph writes the existing task.workflowStepResults field
keyed by node id (no new table) — avoids upgrade data-loss for in-review
tasks and collapses U2/U3. Sibling-table spike reverted.
- Migration: reconcile with the existing migration-109 fragment scheme
instead of infeasible node-injection into read-only built-in workflows.
Also folds in: store-fallback fail-closed (legacy execute calls not all dead),
watchdog re-entry contract, plugin-row migration from persisted fields,
full progress-bar render-state spec, and circular-dependency fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
optionalGroupIdSet falls back to builtin:coding (mirroring the executor's
unselected-task resolution) so a toggled built-in group id like
browser-verification is no longer downgraded to a legacy WS-xxx step row the
graph executor never matches. Create-time optional-step controls resolve
builtin:coding when no project default workflow is set so the toggles render.
First unit of the graph-native workflow-step refactor (see
docs/plans/2026-06-25-001-refactor-workflow-steps-graph-native-plan.md).
Fusion-Task-Id: FN-7039
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix 6 failing tests caused by intentional source changes that landed
without updating dependent test assertions:
- Core test-project: taskPrefix default changed from "FN" to undefined
(commit 800f845e1, derived from project name at runtime)
- Dashboard ScriptsModal.css: replace banned --text-primary with --text
- CLI package-config: update expected pi dep version ^0.79.1 -> ^0.79.9
- CLI skill-sync: document 4 new engine tools in engine-tools.md
- CLI version: update expected release:version script to include
run-ci-distill.mjs
- CLI bundled-plugin-freshness: rebuild stale dist directories
## Summary
Replaces dense, agent-authored technical changeset paragraphs with a
**structured, concise changeset schema** (end-user summary + category +
optional dev detail), enforced by a linter. Adds a **deterministic
distillation step** at version time that transforms a release's
collected changesets into clean, grouped, end-user-facing release notes.
Unifies both release paths (local `release.mjs` and CI `version.yml` /
`release.yml`) behind a single distilled artifact so the root
`CHANGELOG.md` and GitHub Release both carry the same user-facing notes.
## Changes
### Changeset format (`scripts/lib/changeset-schema.mjs`)
- Each changeset body now uses labeled fields: `summary` (required,
user-facing, max 120 chars), `category` (required:
feature/fix/breaking/security/performance/internal), `dev` (optional
developer detail)
- Legacy freeform changesets are detected and flagged for the transition
period
### Linter (`scripts/check-changeset-format.mjs`)
- Validates structured schema, summary length, category enum, and
frontmatter package scope
- Wired into `test:gate`, `pretest`, `pretest:full`, and `pr-checks.yml`
- Legacy changesets warn (exit 0) during transition; `--strict` flag
fails on them
### Distillation (`scripts/lib/distill-release-notes.mjs`)
- `distillDeterministic` builds grouped, end-user-facing release notes
by category (New, Fixed, Breaking, Security, Performance, Internal)
- `buildDistillationPrompt` and `DISTILLATION_SYSTEM_PROMPT` ready for
AI distillation via `createFnAgent` when model credentials are available
- Graceful fallback: deterministic bullet list when no model is
configured
### Release integration
- **Local path** (`scripts/release.mjs`): captures changeset entries
before `changeset version` deletes them, distills notes post-version,
replaces the version's CHANGELOG section with curated notes
- **CI path** (`scripts/ci-distill-release-notes.mjs`,
`scripts/run-ci-distill.mjs`): chained into `release:version` so both
flows get distilled notes
- **GitHub Release** (`release.yml`): uses curated CHANGELOG notes
instead of `generate_release_notes: true`
### Documentation
- `AGENTS.md`, `RELEASING.md`, `docs/contributing.md` updated with the
structured format guide
- `.changeset/README.md` template for `pnpm changeset` consumers
## Testing
- 54 new unit tests across changeset-schema, check-changeset-format,
distill-release-notes, and extract-version-notes
- `pnpm lint` clean
- `pnpm test:gate` green (371 tests)
- `pnpm check:changesets` passes (13 legacy warnings expected during
transition)
## Plan
Full implementation plan:
`docs/plans/2026-06-24-001-feat-better-changelog-plan.md`
<!-- stage-review-badge-begin -->
---
<a href="https://stagereview.app/Runfusion/Fusion/pull/1750">
<picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg">
<img src="https://stagereview.app/assets/gh-open-in-stage-light.svg"
alt="Open in Stage">
</picture>
</a>
<!-- stage-review-badge-end -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added structured release-note guidance and validation for changeset
entries.
* Release notes now use curated, grouped changelog content instead of
auto-generated text.
* Added a fallback process to keep release notes consistent when
automated distillation isn’t available.
* **Bug Fixes**
* Improved handling of legacy changesets and malformed entries.
* Ensured version-specific changelog sections are updated without
affecting older releases.
* **Tests**
* Added coverage for changeset validation, note grouping, and changelog
section replacement.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Update AGENTS.md, RELEASING.md, contributing.md with the structured
changeset format guide. Add .changeset/README.md template. Add changeset
for this change. Update distill-release-notes.mjs with final version.
Add the reviewed, completed plan for the dashboard App.tsx module-breakup
refactor (planning + two doc-review rounds). Status: completed — all 8
implementation units shipped on this branch.
- base-commit-capture: POSIX single-quote integration branch refs instead of
JSON.stringify (double quotes are subject to $-expansion in the shell)
- executor: add per-repo no_commits guard to the workspace verifyWorktreeInvariants
branch (parity with the singular path), gated by the same task-wide no-commit
eligibility
- executor: reviewWorkspacePerRepo failure message now states the per-repo verdict
list is partial (evaluation stops at first failure)
- worktree-acquisition: defensively wrap non-fatal/outer-catch logEntry/audit so a
logging throw cannot promote a non-fatal error to fatal or mask the original error
- docs/plans: add code-fence language tags and fix MD028 blank-line-in-blockquote
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>