Commit Graph

169 Commits

Author SHA1 Message Date
gsxdsm
72f5f8e51a fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed (#2995)
`check-fnxc-future-dates.mjs` validates the **date** portion of a stamp
and never looks at the clock time:

```js
const STAMP = /FNXC:[A-Za-z0-9_-]+\s+(\d{4}-\d{2}-\d{2})/g;
…
for (const match of source.matchAll(STAMP)) if (match[1] > today) hits += 1;
```

The capture stops before the hour, so a stamp may carry **any** `hh:mm`
and pass. Found while pre-flighting #2992, whose new comments read
`2026-07-30-25:30`.

## It is not one typo

Four stamps **already on `main`** carry a clock time that cannot exist:

```
packages/cli/src/__tests__/task-list-board-columns.test.ts:2     -24:40
packages/cli/src/commands/task.ts:29                             -24:40
packages/cli/src/commands/task.ts:636                            -24:40
scripts/check-lane-wiring.mjs:18                                 -24:00
```

Three separate authors, so this is the gate's blind spot rather than one
person's slip — and #2992 adds two more, which is how I noticed.

AGENTS.md specifies `yyyy-MM-dd-hh:mm`. The stamp's whole purpose is to
make the FNXC record a readable chronology of *why* code exists; a
timestamp that cannot exist quietly costs it that, and nothing was going
to catch it.

## The fix

Hours `00-23`, minutes `00-59`, counted per file **alongside** the
future-dated population rather than as a separate gate — same defect
class (a stamp that does not describe a real moment), and one ratchet is
cheaper to keep honest than two.

**Mutations, both directions:**

| stamp | result |
|---|---|
| `2026-07-30-25:00` | **flagged** |
| `2026-07-30-23:75` | **flagged** |
| clean tree | `475 known future-dated stamp(s), none added`, exit 0 |

## On the four existing stamps

Normalized by clamping the impossible hour to `23`, minutes preserved,
so relative ordering within each file survives. **That is a
normalization with a stated rule, not a claim about the true minute** —
`-24:40` most plausibly meant "just past midnight", but writing
`2026-07-31-00:40` would be future-dated against today's local calendar
and fail the very gate this PR extends. Clamping keeps every stamp real,
ordered, and non-future; the exact minute was already unrecoverable.

**Verified:** FNXC gate exit 0, lane-wiring gate exit 0,
`task-list-board-columns` 5/5, lint clean.

Comment-only changes to the CLI files (stamp text inside FNXC blocks),
so no behaviour change and no changeset.

Noted separately on #2992 so its two new stamps get corrected there
rather than landing and immediately failing this gate.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:03:02 -07:00
gsxdsm
5adf0d955a fix(scripts): the soft-delete reconciler wrote a literal archived into boards that do not have one (#2999)
## A repair script that wrote a column the board does not have

Under `--apply`, against an operator's live database:

```js
await tx.execute(sql`UPDATE project."tasks" SET "column" = 'archived' WHERE id = ${row.id}`);
```

On a board that does not declare `archived`, that is not a mislabel — it
parks the row in a column the workflow does not have, **manufacturing
exactly the undeclared-column state this migration keeps repairing
elsewhere**.

The selection was wrong in the same direction, which made the write far
worse. "Leaked" meant `column !== "archived"`, so on a renamed board
**every** soft-deleted row looked leaked — including the ones resting
correctly in that board's own archived lane. The repair then rewrote
them. The tool's fix *was* the damage.

## Three changes, because fixing one would have left the others deciding

**The SQL pre-filter carried the same literal** (`AND "column" !=
'archived'`), so the query and the planner each imposed the legacy
vocabulary independently. Dropped it — soft-deleted rows are a small
set, so selecting them all and filtering in the pure planner costs
nothing and leaves **one** place that decides what "archived" means.

**The filter takes the set**; a row resting in *any* of the board's
archived lanes is not leaked.

**The write resolves per task**, because the destination must be that
card's own lane, not a board-wide pick. A row whose archived lane cannot
be resolved is **skipped and reported**, never written with a guessed
id. A recovery script that declines to act on rows it does not
understand is recoverable; one that writes a plausible wrong value is
not.

Verified rather than assumed — a store that answers nothing resolves to
the default lifecycle:

```
lifecycle from unanswering store: {"intake":"todo",…,"archived":"archived"}
```

so a legacy board repairs exactly as before.

## Correcting myself

On #2994 I wrote that this follow-up "needs the same `importCore` seam".
It doesn't: `openBackend` already returns `{ core, store, … }` and this
script already destructures `core`. No new plumbing was required. I
posted that correction on #2994 too, since acting on it would have
wasted someone's time.

## Revert proof

```
✖ a soft-deleted row already in the board's RENAMED archived lane is not leaked
✖ a board with several archived lanes treats all of them as resting places
ℹ pass 5   ℹ fail 2
```

The other two new cases pass both ways by design — they guard the legacy
meaning and the still-catches-a-real-leak direction — so I am not
counting them as coverage of the defect.

## Verification (measured)

- `node --test` across all three script suites — **17 passed / 0
failed**
- `node --check`, `eslint` — clean
- `check-sql-column-literals`, `lifecycle-column-census --strict`,
`check-lane-wiring`, `check-fnxc-future-dates` — green

No changeset: root `scripts/` is repo tooling, not part of the published
package.

## Gate blind spot found while verifying this, NOT fixed here

I removed a raw-SQL lane literal and expected
`check-sql-column-literals` to drop from 22 — its own header says *"a
LOWER count fails too so the baseline is ratcheted down"*. It stayed at
**22 and green**, because it walks `PACKAGES` only and **never scans
`scripts/`**.

That is the same shape as the lane-wiring gap #2978 closed (it scanned
neither `plugins` nor `dashboard/app`).
`scripts/audit-branch-cross-contamination.mjs:185` still holds `WHERE …
"column" IN ('triage','todo','in-progress','in-review')`, invisible to
the gate. Left as a separate follow-up rather than bundled into a
product fix.
2026-07-30 23:59:54 -07:00
gsxdsm
ac67b8d585 fix(scripts): the FN-4000 consistency reconciler failed in BOTH directions on a renamed board (#2994)
## The FN-4000 consistency reconciler failed in *both* directions

`findTaskStateInconsistencies` keyed both checks on legacy lane
literals, and they break in opposite ways:

```js
const hasDoneTransient = task.column === "done" && (status failed || error || worktree || blockedBy || …);
if (task.status === "failed" && task.column !== "in-review") { … }
```

| check | on a renamed board | effect |
| --- | --- | --- |
| `hasDoneTransient` | **never fires** | a finished card still holding
`status:"failed"`, a worktree, a blockedBy or live recovery counters is
never reported and never normalized — precisely the stale state FN-4000
exists to clear |
| `failed-status-outside-in-review` | **fires for every failed card** |
no column equals the literal, so the report lists the whole board |

The second is the more dangerous of the two: a tool that reports nothing
looks broken, but a tool that reports everything looks like it is
working.

## Wiring, and why the resolver is injected rather than built inline

Lanes are resolved **per task** (a board can span workflows) and passed
in. Resolving inside the loop would drag `importCore()` — and therefore
a built `packages/core/dist` — into every unit test of a pure
reconciliation loop.

`main` wires the real resolver whenever it opened a real backend, so
this is **not** the inert optional-parameter shape this migration keeps
finding. A caller injecting its own store (tests) has no staged dist and
falls back to the documented legacy literals, which is exactly today's
behaviour.

`importCore` is now exported from `scripts/lib/backend-db.mjs` so
operator scripts reach core helpers through the **same staged-dist seam
`openBackend` already uses**, rather than each growing its own dist path
— `@fusion/core` is not resolvable from repo-root `scripts/`, which is
what made the obvious import fail.

The normalization move now targets the card's **own** column: naming
`"done"` was only ever a way of spelling *"where it already is"*, since
the move exists to trigger the store's done-normalization.

## One of my test expectations was wrong before the code was

My first version asserted that a card in a renamed complete lane with
`status:"failed"` yields only the transient-state finding. It yields
**both** — and that is correct, because a failed card outside the review
lane genuinely is flagged. I isolated the case (dropping
`status:"failed"`, keeping the worktree) so it pins one behaviour
instead of blurring two, rather than "fixing" the expectation to match
whatever came out.

## Revert proof

Restoring the four literals:

```
✖ reports stale transient state in a RENAMED complete lane
✖ does NOT flag a failed card that is sitting in the board's own review lane
✖ runReconciliation normalizes a renamed complete lane by moving the card to its OWN column
ℹ pass 5   ℹ fail 3
```

The remaining two new cases pass both ways by design — "still flags a
failed card outside the resolved review lane" and "unresolved lanes keep
exactly the legacy behaviour" guard against over-correction, so I am not
counting them as coverage of the defect.

## Verification (measured)

- `node --test` — **8 passed / 0 failed** (3 pre-existing + 5 new)
- sibling script suites (`recover-stale-blocked-by`,
`reconcile-leaked-soft-deletes`) — **7 passed**, unaffected by the
shared-lib export
- `node --check`, `eslint` — clean
- `lifecycle-column-census --strict`, `check-sql-column-literals`,
`check-lane-wiring`, `check-fnxc-future-dates` — green

No changeset: root `scripts/` is repo tooling, not part of the published
package.

## Still not addressed in this territory

`reconcile-leaked-soft-deletes.mjs` carries a raw `UPDATE
project."tasks" SET "column" = 'archived'` — on a renamed board that
writes a column the workflow does not declare, creating the
undeclared-column state this migration keeps repairing elsewhere. It
holds a raw backend rather than a store, so it needs the same
`importCore` seam this PR exports; left for a follow-up rather than
bundled here.
2026-07-30 23:49:05 -07:00
gsxdsm
bb6c08d9d6 fix(scripts): the blocked-by recovery reported "Repairs: 0" on a board it never examined (#2992)
## A recovery tool that reports "Repairs: 0" without having examined
anything

Every lane test in `recover-stale-blocked-by.mjs` is a legacy id:

```js
function isTerminalColumn(column) { return column === "done" || column === "archived"; }
const isActive = row.column === "in-progress" || (row.column === "in-review" && row.worktree && !row.paused);
if (row.column !== "todo" || !row.blockedBy) continue;          // ← the candidate gate
```

On a board whose lanes are named anything else, that gate matches
**nothing**. The planner returns no findings and the script prints
`Repairs: 0`.

An operator running a recovery reads that as *"the board is fine"* when
the tool never examined a single card. **A silently empty answer from a
recovery tool is the worst shape available** — indistinguishable from
success, and consulted precisely during an incident.

This is not dead code: `docs/soft-delete-verification-matrix.md` cites
it as the GREEN backstop for FN-5528, and it has its own test file.

## Detection only — and why I did not "fix" the classification

Correct classification needs the board's resolved trait vocabulary. This
script holds a **raw backend** (`openBackend` → `asyncLayer` + `sql`),
not a `TaskStore`, so resolving lanes here would mean reimplementing IR
trait resolution inside a `.mjs` script — a worse bug than the one it
fixes, and precisely the kind of second, drifting copy this migration
keeps deleting.

So the assumptions are not repaired; they are made **loud**. That is the
same principle the lane-wiring gate applies to itself:

> a gate whose errors land on "nothing to report" is the one failure
mode a ratchet must not have

The unknown-lane list rides on the returned array as a
**non-enumerable** property rather than widening the return type —
`recoverBlockedBy` is consumed as `findings[]` by the entry point and by
tests, and an operator may be scripting around that shape.

## The first test pins the gap rather than papering over it

```js
assert.deepEqual(unrecognisedLanes(rows), ["backlog", "checking"]);
// The gap this warns about, pinned rather than claimed fixed: the planner still sees nothing.
assert.deepEqual(planRecoverBlockedBy({ rows, tasksDir }), []);
```

I would rather the next reader find that assertion than discover it
themselves during an incident.

## Revert proof

With `unrecognisedLanes` returning `[]` (the pre-fix behaviour):

```
✖ names lanes the planner does not understand, so an empty result cannot read as healthy
✔ stays quiet on a legacy board, so the warning means something when it appears
✖ reports each unknown lane once, ignoring rows with no column at all
ℹ pass 5   ℹ fail 2
```

The legacy-board case passes **both ways by design** — it guards against
the warning firing spuriously, so I am not counting it as coverage of
the defect.

## Verification (measured)

- `node --test` — **7 passed** (4 pre-existing + 3 new), 0 failed
- `node --check`, `eslint` — clean
- `check-sql-column-literals`, `lifecycle-column-census --strict`,
`check-lane-wiring`, `check-fnxc-future-dates` — green

No changeset: root `scripts/` is repo tooling, not part of the published
package.

## How this was found, since the method matters more than the fix

My batch is "cli + plugins + anything left", and I had been reading
*"anything left"* as nothing. Eight packages and all of `scripts/` sit
outside the four named batches. This is the first thing I found there;
sibling one-shot scripts (`reconcile-task-state-consistency.mjs`,
`reconcile-leaked-soft-deletes.mjs` — which contains a raw `UPDATE … SET
"column" = 'archived'`) carry the same hardcoded assumptions and are
**not** addressed here.
2026-07-30 23:41:03 -07:00
gsxdsm
684c324084 fix(gate): the lane-wiring census counted { reviewColumns: undefined } as wired (#2984)
## What

Follow-up to the finding @gsxdsm left on #2981, taking the direction
offered there.

Both arms of this census asked whether the lane argument was
**present**, not whether it carried anything:

```ts
isThing(task, { reviewColumns: undefined });   // property present -> counted as wired
isThing(task, undefined);                      // arity satisfied  -> counted as wired
```

The callee receives exactly what it received before: nothing. The seam
is still inert, the board still reads the legacy vocabulary — the census
just stops saying so, which is the one failure mode a ratchet must not
have.

Same defect as the positional one #2981 fixes in
`check-inert-flag-seams`, one level in. The two gates are complementary
by design — this one owns the options-object and default-valued shapes
the other is structurally blind to — so the hole had to be closed in
**both**. Neither covered it, confirmed by probing each with a control
shape.

## The direction I took, since the review raised it as a contract
question

> *tightening just relocates the dishonesty into whichever spelling
survives... especially as I have already spent three attempts learning
that heuristic tightening here trades false positives for worse false
negatives.*

Agreed, which is why this is the narrowest possible reading rather than
a heuristic:

**Only a literal `undefined` / `void 0` counts as empty.** Shorthand `{
reviewColumns }` forwards a variable whose value is not knowable from
syntax, and treating it as unwired would flag every correct forwarding
wrapper in the tree — exactly the false-positive wave that trains
readers to skip a gate. Same for a call expression, a conditional, or
anything else with a value at runtime.

That keeps the rule provable from syntax alone. It doesn't relocate the
dishonesty so much as remove the one spelling that is *demonstrably*
empty; anything ambiguous still counts as wired, so the gate stays
conservative in the direction that matters.

## No tests existed for this census

`check-lane-wiring` and `lane-wiring-census.mjs` had no unit coverage on
`main`, so both rules ship with tests rather than resting on the probe
that found them.

## Measured

| check | result |
|---|---|
| clean `main` | exit 0, unchanged — all five gates green |
| now caught | property spelled `undefined` · property spelled `void 0`
|
| correctly **not** flagged | a real value · shorthand forwarding · a
call-expression value · a middle `undefined` with a real argument after
it |
| new suite | **8 tests**; reverting both rules fails **exactly** the 3
positives, negatives hold |

## Not done here, deliberately

The second finding on #2981 — `computeBlockerFanoutMap`'s dashboard
wrapper dropping all four lane options, so the fanout display reads
legacy literals on a renamed board — is **not** in this PR. Confirming
the diagnosis: `useBlockerFanout.ts`'s `UseBlockerFanoutOptions`
declares only `staleHighFanoutAgeThresholdMs` and forwards only that,
and all three dashboard call sites (`Board`, `TaskDetailModal`,
`ExecutorStatusBar`) have the same gap.

One correction to how it's framed, though: core already has the right
seam for it. `classify` and `escalationClassify` are documented there as
*"the only correct option on a multi-workflow board"*, precisely because
the set-shaped options assume a column id means the same thing
everywhere. So the fix should thread **per-task classifiers**, not
resolved column-flag sets — otherwise it reproduces the union read that
this program's own learnings doc lists as the fourth failure shape.

What's genuinely undecided is where a per-task role answer comes from in
a sync render path: `Board` holds `columnDef.flags` for the *selected*
workflow only, and the dashboard has no per-task resolver hook. That's
the design call, and it's dashboard-batch work rather than a mechanical
edit — so I've left it for whoever owns that batch rather than guessing
at it inside a gate PR.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:30:07 -07:00
gsxdsm
2411699756 fix(gate): passing undefined for the lane answer read as supplying it (#2981)
## What

Continuing the #2979 discipline — probe a ratchet with shapes its author
did *not* have in mind — applied to my own inert-seam gate. Four probes,
three got through. Two turned out to be the sibling gate's job. This is
the one that's nobody's:

```ts
resolveSomething("KB-1", undefined)
```

The seam is a trailing optional parameter, so the gate asked how many
**arguments** a call site passes. Spelling the omission out satisfies
that count while the callee receives exactly what it received before:
nothing. The parameter is still inert, the board still reads the legacy
vocabulary — the gate just stops saying so.

Not an exotic spelling. It's what a partial wiring-up produces when
flags are threaded through an intermediate that has none to pass, and
what a mechanical positional edit produces when it fills argument slots.

## Missed by both gates — checked before touching anything

`check-lane-wiring` (#2966) covers the default-valued and options-object
shapes this gate is structurally blind to. I probed it first, and it
caught **both**, so the two remain genuinely complementary rather than
overlapping. But it counts arguments the same way here, so this shape
was uncovered by either.

| probe | inert-seam (before) | lane-wiring |
|---|---|---|
| omitted entirely | caught | — |
| default-valued param | missed | **caught** |
| options-object flags | missed | **caught** |
| explicit `undefined` | missed | **missed** ← this PR |

## The trim is trailing-only

A **middle** `undefined` still positions the arguments after it, so
those are real answers. That's the case that keeps the trim honest, and
it's pinned as a test.

## Measured

| check | result |
|---|---|
| clean `main` | exit 0, unchanged |
| now caught | explicit `undefined` · `void 0` · several trailing
undefineds |
| correctly **not** flagged | a real trailing value · a middle
`undefined` with a real value after it |
| gate's own suite | **12 → 18 tests**, all green |
| reverting to the raw argument count | fails **exactly** the 3
positives; the negatives hold |

## One note on the fourth gate

`check-fnxc-future-dates` went red on this branch — on my own comments.
I'd stamped them `2026-07-31`, which is tomorrow. Fixed by correcting
the stamps to today, not by re-recording the baseline; the baseline
already tolerates some pre-existing future stamps and adding mine to it
would have been appeasement. Worth noting that the gate earned its keep
against the person who has been writing the other gates.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:08:08 -07:00
gsxdsm
634d487c3e fix(gate): a lane id hoisted into a const evaded the SQL column-literal gate (#2980)
## What

In #2979 I argued a ratchet should be mutation-probed with shapes its
author did **not** have in mind, on the day it ships. Applying that to
my own gate: two of three probes walked straight through.

```ts
const LANE = "done";
sql`... WHERE "column" = ${LANE}`                       // MISSED

const LANES = ["in-progress", "in-review"];
sql`... WHERE "column" IN (${sql.join(LANES)})`         // MISSED
```

Both bind the query to the legacy vocabulary exactly as an inline
`'done'` does. An interpolation that wasn't a column reference collapsed
to the NUL sentinel, so the predicate dissolved before the matcher ever
ran.

**This is the shape a cleanup produces.** Hoisting a repeated string to
a named const reads as tidying, which makes it the likeliest way one of
these gets rewritten — and the gate would have gone quiet on a file that
changed only in punctuation. Third time this scanner has had that
failure (static-span join, element-access column ref, now this). The
array form isn't hypothetical: `IN ('in-progress','in-review')` was the
live workflow-analytics defect.

## The first version of this fix was wrong, and that's the useful part

Resolving *any* string-valued const double-counted the analytics files,
which build queries as:

```ts
const completedClauses = [`t."column" = 'done'`, "t.columnMovedAt IS NOT NULL"];
```

Those elements are SQL fragments **already counted where they're
written**. Resolving the const re-injected each into the outer template.
The three analytics files went `3/3/1` → `6/5/2` — and it read exactly
like a genuine find. Only **bare lane ids** are resolved now; the
fragment-array case is pinned as a test.

I also nearly shipped that version on a bad probe: `node gate | tail`
then `echo $?` reads *tail's* exit status, not the gate's. Every probe
reported "caught" while the gate was actually failing on main for an
unrelated reason. Worth repeating because the harness looked fine and
agreed with what I expected.

## Measured

| check | result |
|---|---|
| clean `main` | **22 sites, exit 0, unchanged** — no false positives
introduced |
| now caught | const string · const array via `sql.join` · as-const via
`inArray` |
| correctly **not** flagged | resolver-produced lanes · non-legacy ids ·
SQL-fragment array |
| gate's own suite | **26 → 32 tests**, all green |
| blinding the resolution | fails **exactly** the 3 new positive tests;
the 3 negatives still pass |

The negatives outnumber what feels necessary on purpose: eager
resolution is how this went wrong the first time, and the fragment-array
test is the one that would have caught it.

## Scope

Same-file `const` declarations only. Cross-file imports need a type
checker and a program-wide pass — a constant imported from another
module is **still invisible**, and `--list` output is where that gets
audited. Stating the boundary rather than half-resolving it and calling
the gate complete.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:59:56 -07:00
gsxdsm
ee8ae1eb23 fix(census): the header claimed 0 trait-fallback branches while sites of that shape existed (#2874)
The census header has been printing `of the column guards, 0 are
trait-fallback branches (already converted)` while sites of exactly that
shape exist. I flagged this on #2842 as a suspected classifier gap; this
confirms and fixes it.

## The miss

Only `cond ? trait : literal` was recognised. The other spelling — a
**negative** test with the literal on the **true** branch — is what a
caller writes once it hoists its resolved lanes:

```ts
complete: completeLanes === undefined ? columnId === "done" : completeLanes.includes(columnId)
```

That is `github-tracking-state.ts:245-246` — a fully converted resolver
whose two degraded arms were reported as unconverted debt. **The backlog
read higher than the remaining work**, and a reader chasing it was sent
to lines that are already correct.

Second half of the miss: `completeLanes` matches no hint. Adding `Lanes`
to the hint list does **not** work, and the reason is itself a prior fix
— hints are word-bounded because the unbounded form once let `hold`
match `threshold` and `household`. `\bLanes\b` cannot match inside
`completeLanes`, where the boundary does not exist. So resolved-lane
identifiers get an explicit suffix rule.

## Both guards on the new rule exist because I broke them while writing
it

Worth stating, because each failure ran in the **dangerous direction** —
marking a *live* line "already converted", which removes a real guard
from a backlog people trust:

| mistake | what it excused |
|---|---|
| widened the shared `testsTraitData` | fed the ancestor-walking rules
too, which marked `step.status === "done" \|\| step.status ===
"in-progress"` at `register-task-workflow-routes.ts:941` — a
step-**status** comparison, not a column guard — as converted |
| let the new rule walk ancestors | excused any literal inside a block
governed by a negative lane test |

Measured: the count went to **6 with two of them wrong** before I caught
it. The rule is now immediate-parent-only with its widened identifier
match local to it, and reports exactly the **2 real sites**.

## Verification

- Census: **176 guards, 2 trait-fallback** (was 176 / 0). The total is
unchanged — this sub-count is diagnostic and does not move the ratchet,
so `--strict` exits 0 with no baseline re-record.
- 5 cases in
`scripts/__tests__/lifecycle-census-inverted-fallback.test.mjs`,
including both negatives that pin the mistakes above plus one for the
suffix rule not over-reaching (`airplanes` is not a lane test).
- `pnpm lint` clean; gate green (161/487/13/71).

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved lifecycle analysis accuracy for trait fallback logic,
including inverted conditions, legacy fallback syntax, and null or
undefined checks.
* Added safeguards to avoid misclassifying complex conditions, unrelated
identifiers, and nested expressions.
  * Improved handling of lifecycle lane and column naming patterns.

* **Tests**
* Expanded coverage for valid and invalid fallback scenarios, identifier
boundaries, parent-expression restrictions, and property-path checks.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:43:31 -07:00
gsxdsm
ef50244234 feat(gate): freeze the SQL column-literal surface — 30 sites, none may be added (#2841)
Instruments a surface no existing check can see. Follows #2839, and
**corrects the count I reported there** (12 → 14).

## Why it was invisible

The lifecycle census parses TypeScript **comparisons**; a legacy id
inside a SQL string is string data. The inert-seam gate reasons about
parameters and call sites. Neither has ever looked here.

**What it cost:** `cleanupStaleMergeQueueRowsImpl` filtered on `t.column
!= 'in-review'`, so on a renamed board every queued card looked stale,
its `merge_queue` row was deleted, and the card became **unleaseable**.
The operator found it reviewing #2819 — in SQL I had already read past
during that same work.

The quieter half is analytics: five sites count `"column" = 'done'`, so
throughput, cycle time, and team dashboards report **zero completed
work** on a renamed board. Nothing errors, which is why nobody files it.

## What this does, and does not do

It does **not** fix the sites. `resolveProjectColumnsForRoles` is the
mechanism and its migration has an owner (#2839). This freezes the
population so the surface cannot grow underneath that migration: a new
file or a higher count fails, **and a lower count fails too** — so the
baseline ratchets down as sites migrate rather than leaving slots to
silently regrow into. That is the same rot as an allow-list entry for a
deleted function, which this repo already hit once.

AST-based, deliberately: a line grep for the same pattern reports **37**
hits, **25 of them prose** quoting `column === "done"` in explanatory
notes. A guard that is 68% false positives trains its readers to skip it
— a lesson this program has already paid for.

## Two corrections found by mutation-testing my own gate

**1. Clause fragments were missed.** Requiring a SQL keyword *in the
same literal* skipped `team-analytics.ts`, which builds
`["assignedAgentId IS NOT NULL", `"column" = 'done'`, ...]` and joins
them into a `WHERE` later. That fragment is as vocabulary-bound as any
full query but contains no keyword. Fixing it took the population **12 →
14**, so the number I put on #2839 was low.

**2. My first mutation test proved a direction it had not.** I replaced
the first textual occurrence in a file — which was inside a **comment**
— and read the unchanged count as the scanner being broken. The scanner
was right; my test was wrong. All three directions are now driven
against real SQL:

| mutation | result |
|---|---|
| add a full query with a legacy comparison | `3 SQL column literal(s),
baseline allows 2` |
| add a bare clause **fragment** (no keyword) | caught — same failure |
| migrate one away (count drops) | `1 site(s) now, baseline still allows
2 — re-record it` |
| restore | exit 0 |

I am flagging that second one because it is the exact failure mode this
program keeps finding: a green result read as evidence when the
experiment was invalid.

## Verification

`pnpm test:gate` green with the new check in it · lint 0 · single AST
pass. Wired into `test:gate` and both `pretest` hooks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

## Summary by CodeRabbit

* **Bug Fixes**
* Added automated checks to detect increases in legacy SQL column
literals.
* Added baseline tracking to ensure known SQL literal counts do not
regress.

* **Tests**
* Expanded pre-test and gated verification steps with SQL literal and
mock completeness checks.
  * Updated test validation workflows to enforce the new safeguards.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:06:24 -07:00
gsxdsm
5a3541315c fix(gate): pnpm test is red on main — stale exemptions, plus two false positives they were masking (#2851)
**`pnpm test` and `pnpm test:full` fail on `main` right now** (commit
`51934931e1`). Found by running the gates against `main` after my
earlier PRs landed, not by CI telling me.

> **Correction to this PR's first version.** I originally wrote that
`pnpm test:gate` was failing. It is not: `check-inert-flag-seams` runs
in the `pretest` and `pretest:full` hooks, not in `test:gate`. So the
blocking merge gate (Lint / Typecheck / Build / Gate) is unaffected and
PRs are not blocked — what is broken is every local `pnpm test` run,
which fails before a single test executes. Lower urgency than I claimed,
still worth fixing promptly, and I would rather correct the scope than
leave an overstated one standing.

Three causes, each surfaced by fixing the one before it.

## 1. Stale exemptions — the mechanism working

#2819 and #2823 merged, so the two `ALLOWED_OMISSIONS` entries covering
those call sites became stale and the staleness check failed them.
Removed. This is my cleanup: the entries were designed so they could not
outlive their fixes, and they didn t.

## 2. Renamed imports were not resolved

Removing the first entry surfaced:

```
enqueueMergeQueue() — best call passes 2 of 5
```

Its only production caller passes all five — through `import {
enqueueMergeQueue as enqueueMergeQueueAsync }`. Call sites were recorded
under the **local** name, so an aliased supplier was invisible and the
seam read as unsupplied. The local name is now mapped back to the
exported one.

## 3. Method calls were conflated with module functions

That fix then surfaced two engine sites as omitting — but
`store.enqueueMergeQueue(taskId, opts)` is a **2-arg `TaskStore`
method** that resolves the review columns internally (#2819), not the
5-arg module function sharing its name. Property-access calls are no
longer attributed to module-level seams.

**Tradeoff, stated at the site:** a genuine `namespace.fn(...)` call is
now skipped. This codebase calls module functions as bare identifiers,
and aliases are resolved by fix 2, so that shape does not currently
occur. Recorded rather than left for someone to discover.

## Both directions re-verified

A fix that quietly disarms the gate would be worse than the red, so I
re-ran the defects it exists to catch:

| mutation | result |
|---|---|
| drop `Column.tsx`'s flags argument (partial supply) | `supplied by
10/11 call sites; omitted at .../Column.tsx:1 (of 2)` |
| drop the aliased 5-arg supplier (wholly unsupplied) | `best call
passes 3 of 5` |
| restored | exit 0 |

My first attempt at the second row grepped for the wrong message shape
and printed nothing. **I re-ran it rather than reading silence as
success** — which is the failure this gate exists to prevent, and one I
have made in this same file before.

## Verification

`pnpm test:gate` green (it was never affected) · `node
scripts/check-inert-flag-seams.mjs` exit 0 · lint 0 · gate reports `21
lane/flag seams, all supplied at every production call site`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:10:45 -07:00
gsxdsm
8b039a543e fix(desktop): advance Pi runtime pin to 0.82.1 for packaging PR lane (#2465)
## Summary
- Advance the matched Pi runtime pin (`pi-ai`, `pi-coding-agent`,
`pi-agent-core`, `pi-tui`) from **0.82.0 → 0.82.1** so
electron-builder's production-dependency walk accepts `pi-agent-core`'s
`pi-ai@^0.82.1` requirement.
- Fixes the Desktop packaging PR-lane failure:
`Production dependency @earendil-works/pi-ai not found for package
@earendil-works/pi-agent-core` (required `^0.82.1`).
- Keep the workspace override guard; update pin-policy fixtures and CLI
package-config expectations.
- Tighten the advisory packaging step-order test so it asserts against
the real `electron-builder --dir` step (not a missing release-only step
name that previously passed via `indexOf === -1`).
- Run `pnpm dedupe` so the packaging lane's lockfile dedupe
early-warning is clean.

## Context
#2439 pinned the full Pi closure at 0.82.0 and made recent main-based
packaging runs green. This advances to the current upstream patch so
deploy + electron-builder stay aligned with `pi-agent-core@0.82.1`'s
declared dependency range.

## Test plan
- [x] `node scripts/check-pi-versions-pinned.mjs`
- [x] `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs`
- [x] `pnpm --filter @runfusion/fusion exec vitest run
src/__tests__/package-config.test.ts`
- [x] `pnpm --filter @fusion/desktop exec vitest run
src/__tests__/release-workflow.test.ts`
- [x] `pnpm dedupe --check`
- [ ] GitHub: Desktop packaging (should run full packaging walk —
lockfile/package.json touched)
- [ ] GitHub: PR Checks (Lint, Typecheck, Build, Gate)
2026-07-26 23:47:49 -07:00
gsxdsm
f1a2d9ae1f FN-8626: validate committed test timing snapshot
Add an automated guard that keeps CI test-sharding timings usable.

- Validate snapshot structure, freshness, and recorded test-file paths.
- Confirm planning loads the snapshot and shard dry-runs use it without stale warnings.

Files changed:
 scripts/__tests__/ci-test-shard-timings.test.mjs | 50 +++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8626

Fusion-Task-Lineage: ada6525f-8dcc-4494-8586-3aa2e41618f3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 20:42:30 -07:00
Phil Larson
0643a64f0d fix(desktop): pin complete Pi runtime closure (#2439)
## Summary
- pin `pi-agent-core`, `pi-ai`, `pi-coding-agent`, and `pi-tui` to one
exact 0.82.0 workspace override set
- extend the Pi version policy guard to reject missing, ranged, or
mismatched desktop runtime closure overrides
- add a patch changeset for the legacy desktop packaging fix

## Test plan
- `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs` (5
passed)
- `node scripts/check-pi-versions-pinned.mjs`
- `corepack pnpm check:changesets --strict`
- focused engine fixtures: 4 files / 47 tests passed
- GitHub: Desktop packaging, Lint, Typecheck, Build, Gate, and Greptile
Review passed
2026-07-26 07:34:50 -07:00
gsxdsm
084dd76d64 feat(release): write release copy with opus and draft tweets for betas too
- distillation runs on opus (env-overridable) with a 4-minute budget
- highlights must name the surface and outcome; vague filler is banned
- tweets target 200-280 chars with concrete changes and varied structure
- betas get their own tester-facing draft carrying `fn update --channel beta`
- prerelease openers read as "Fusion 0.74 beta:" instead of "Fusion 0.74-beta.0"
2026-07-24 23:15:36 -07:00
gsxdsm
330e4970f0 refactor(release): move the version-anchor package.json rewrite into the shared lib
Makes the re-anchor file mutation unit-testable alongside the anchor decision.
2026-07-24 23:01:11 -07:00
gsxdsm
dba9746287 fix(release): base the next beta on the shipped stable version
After a stable release, main stayed inside the old pre-mode cycle, so the next
beta numbered below the published stable (v0.73.0-beta.7 after v0.73.0) and the
dev checkout kept reporting the last beta.

- beta releases re-anchor a stale pre-mode cycle on the newest stable tag
- both channels refuse a version at or below the newest published stable
- stable promotion now back-merges release into main automatically (fail-soft
  on conflict) so the local dev version is the stable version
2026-07-24 22:59:19 -07:00
gsxdsm
e3dba364d1 FN-8564: update bundled Pi runtime to 0.82.0
Update Fusion's matched Pi dependencies and compatibility coverage for version 0.82.0.

- Pin Pi AI and coding-agent packages to the exact 0.82.0 release pair.
- Refresh provider catalog, supplemental model, auth storage, and Droid thinking coverage.
- Add the published CLI patch changeset.

Files changed:
 .changeset/fn-8564-pi-082.md                       |   7 +
 packages/cli/package.json                          |   4 +-
 packages/cli/src/__tests__/package-config.test.ts  |   2 +-
 packages/core/package.json                         |   2 +-
 packages/dashboard/package.json                    |   2 +-
 ...ister-model-routes-kimi-k3-supplemental.test.ts |   6 +-
 packages/engine/package.json                       |   4 +-
 .../src/__tests__/provider-registration.test.ts    |   4 +-
 packages/engine/src/auth-storage.ts                |  11 +-
 packages/engine/src/pi.ts                          |   6 +
 packages/pi-claude-cli/package.json                |   8 +-
 .../src/thinking-config.ts                         |  10 +-
 pnpm-lock.yaml                                     | 176 +++++++++++----------
 pnpm-workspace.yaml                                |   6 +-
 .../__tests__/check-pi-versions-pinned.test.mjs    |   8 +-
 15 files changed, 142 insertions(+), 114 deletions(-)

Fusion-Task-Id: FN-8564

Fusion-Task-Lineage: 543c5e17-4cb2-446f-9a1c-ec7ec8b8117a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-24 19:06:14 -07:00
gsxdsm
6166b496e4 FN-8559: refresh test timing snapshots
Refresh timing data and keep velocity reports aligned with the current snapshot.

- Attribute CI timing reports from absolute checkout paths
- Render report-only slowest tests from the latest timing snapshot
- Update timing snapshot data and regression coverage

Files changed:
 scripts/__tests__/ci-test-shard-timings.test.mjs  |    9 +
 scripts/__tests__/test-velocity-baseline.test.mjs |   38 +
 scripts/ci-test-shard.mjs                         |   13 +
 scripts/test-timings.json                         | 2522 +++++++++++++--------
 scripts/test-velocity-baseline.mjs                |   33 +-
 5 files changed, 1616 insertions(+), 999 deletions(-)

Fusion-Task-Id: FN-8559

Fusion-Task-Lineage: 09ed6b78-f82f-4732-a5e3-1066efe385b3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-24 06:21:53 -07:00
gsxdsm
c5a8421533 FN-8560: forward desktop test telemetry flags
Desktop test execution now preserves CI reporter and JSON output flags.

- Forward caller-selected Vitest reporters and output files through the desktop test wrapper.
- Add coverage for reporter syntax and shard timing command forwarding.
- Document desktop timing artifact requirements.

Files changed:
 docs/testing.md                                    |  7 ++++-
 packages/desktop/scripts/__tests__/test-args.test.ts | 36 ++++++++++++++++++++++
 packages/desktop/scripts/test-args.ts              | 13 ++++++++
 packages/desktop/scripts/test.ts                   |  3 +-
 scripts/__tests__/ci-test-shard.test.mjs           |  9 ++++++
 5 files changed, 66 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8560

Fusion-Task-Lineage: 2a89b28d-39f6-4949-aec2-1123c36126cf

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-24 05:30:45 -07:00
gsxdsm
c5e9a7956a fix(ci): stop watchdog false-kills of the grown core slice; actually upload timing artifacts
- Shard watchdog floor 25min (was 15): the July PG-cutover test growth pushed
  @fusion/core past 900s on contended CI runners; run 30075604930 killed a
  healthy core run at exactly the floor because the 27-day-old (still "fresh")
  undercounting timings snapshot tightened the budget to it — the same
  false-kill class as the 5->15min raise. Floor pin + in-band example updated.
- full-suite.yml timing upload: include-hidden-files — the .timings/ dot-dirs
  were silently excluded by upload-artifact@v4, so the step has uploaded
  nothing since it was added and the snapshot could never be refreshed from CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 00:55:38 -07:00
gsxdsm
ff165ecb5a fix: scope beta release notes to that beta's changesets; stable keeps full-cycle rollup
Pre-mode preserves consumed changeset .md files, so every beta's distilled notes and GitHub prerelease body aggregated the entire cycle since the last stable (v0.73.0-beta.4 shipped the full 0.72.0→0.73.0 aggregate). Betas now distill only changesets not yet recorded in pre.json's consumed ledger, and fail loudly when a beta would ship nothing new. Stable promotion still feeds the full preserved set, keeping its notes an explicit rollup of every beta in the cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:18:24 -07:00
gsxdsm
28e8c0abc9 FN-8524: cover workspace manifests in Docker builds
Ensure the builder install layer includes every selected workspace manifest.

- Copy the five omitted plugin manifests before frozen installation.
- Validate pre-install Dockerfile coverage against pnpm workspace entries.
- Document the manifest coverage requirement and focused test command.

Files changed:
 Dockerfile                                         |   8 +-
 docs/docker.md                                     |   3 +-
 .../dockerfile-workspace-manifests.test.mjs        | 111 +++++++++++++--------
 3 files changed, 78 insertions(+), 44 deletions(-)

Fusion-Task-Id: FN-8524

Fusion-Task-Lineage: 16a4d9df-ff08-4051-9e53-0da414ef6a85

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 22:04:32 -07:00
gsxdsm
f120e6c879 FN-8506: run static checks in verify:fast
Run canonical pretest validators before test-free verification work.

- Derive read-only static check steps from the root pretest script.
- Test fail-fast static-check planning and execution.
- Document the expanded verify:fast gate and correct changeset metadata.

Files changed:
 .changeset/mobile-board-pointercancel-settle.md |   2 +-
 docs/testing.md                                 |   5 +-
 scripts/__tests__/verify-fast.test.mjs          | 105 ++++++++++++++++++++++--
 scripts/verify-fast.mjs                         |  86 ++++++++++++++++---
 4 files changed, 174 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-8506

Fusion-Task-Lineage: 87f74fda-1fd0-4e08-9bfe-51e0c4c9a31d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 18:26:33 -07:00
gsxdsm
9002fca9de FN-8497: reduce merge gate wall time
Keep merge-gate coverage focused while running its independent test lanes concurrently.

- Limit PostgreSQL gate coverage to lifecycle and transactional-handoff canaries.
- Run engine and PostgreSQL gate lanes concurrently while preserving failure propagation.
- Enforce canary coverage policy and refresh velocity documentation and history.

Files changed:
 docs/test-velocity-baseline.md                     |  16 +--
 docs/testing.md                                    |   5 +-
 package.json                                       |   2 +-
 packages/core/package.json                         |   2 +-
 .../__tests__/engine-vitest-gate-policy.test.mjs   |  79 +++++++++++++-
 scripts/test-velocity-history.json                 | 115 +++++++++++++++++++++
 6 files changed, 204 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-8497
Fusion-Task-Lineage: 8777959c-6d8c-4686-a975-d91af2c169ea
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 15:47:24 -07:00
gsxdsm
241a5c94ea chore: bump @earendil-works/pi to 0.81.1 (#2399)
## Summary
- Bump `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent`
from **0.80.10 → 0.81.1** (exact matched pins).
- Update `pnpm-workspace.yaml` overrides so floating `*` consumers
(`droid-cli`, `pi-llama-cpp`, runtime plugins) stay on the same
ModelRuntime surface.
- Refresh pin-guard tests, package-config assertions, and FNXC notes for
the new pin.

## What's new in pi 0.81.x
- Qwen Token Plan providers
- Expanded usage accounting (tools/compaction/branch summaries)
- Resilient compaction retries + retry lifecycle events
- Full provider-extension registration API
- Built-in llama.cpp router management
- Provider/catalog fixes (Bedrock env credentials, OpenAI Responses
early-stream retry, Codex 272K defaults, extension stream-fallback
restore)

## Test plan
- [x] `scripts/check-pi-versions-pinned` (4/4)
- [x] Typecheck: core, engine, dashboard, cli, pi-claude-cli
- [x] `package-config.test.ts` (35)
- [x] `provider-registration.test.ts` (14)
- [x] `auth-storage-concurrency` + `model-registry-refresh` (15)
- [x] `register-model-routes-kimi-k3-supplemental` (1)
- [ ] CI gate green
- [ ] Spot-check Anthropic OAuth + API key session
- [ ] Spot-check openai-codex model picker / supplemental models

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

## Summary by CodeRabbit

* **Updates**
  * Updated the bundled Pi runtime to version 0.81.1.
* Added support for newer models and providers, including Qwen Token
Plan.
  * Improved usage accounting and session reliability.
* Strengthened compaction retry handling and provider catalog accuracy.
  * Added support for the expanded maximum thinking level.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-22 01:32:14 -07:00
gsxdsm
7911fdb9b1 fix(release): preserve distilled changelog summaries across releases
syncRootChangelog rewrote every prior release from raw package notes, so only
the latest distilled Highlights view survived. Re-emit already-distilled bodies
on sync, keep the archive pointer outside version sections, and restore wiped
summaries from release history.
2026-07-21 20:29:46 -07:00
Phil Larson
fe9269b57b fix(i18n): restore Chinese roadmap duplicate labels (#2358)
## Summary
- restores the missing Simplified Chinese duplicate-roadmap report label
- restores the missing Traditional Chinese duplicate-roadmap report
label
- adds a patch changeset for the catalog correction

## Test plan
- `pnpm --filter @fusion/i18n test` (5 files, 29 tests)
- `pnpm build`


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

* **Bug Fixes**
* Restored Simplified and Traditional Chinese translations for duplicate
roadmap report titles.
* Updated the roadmap reporting UI text to clarify when a report is
already in the roadmap and ask whether to add the user’s data point.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 00:18:53 -07:00
gsxdsm
e0e395a715 FN-8365: enforce dashboard route registrar mount order
Keep dashboard API registration modular while preserving Express route precedence.

- Route all top-level dashboard registrars through a runtime-checked canonical mount sequence
- Add mount-order and inline-route-ratchet coverage with CI enforcement
- Document registrar ownership and mount-order conventions

Files changed:
.github/workflows/pr-checks.yml                    |   3 +
AGENTS.md                                          |   2 +
package.json                                       |   5 +-
packages/dashboard/src/routes.ts                   | 136 +++++-----
packages/dashboard/src/routes/README.md            | 276 ++++++++++-----------
packages/dashboard/src/routes/__tests__/create-api-routes-mount-order.test.ts |  66 +++++
packages/dashboard/src/routes/create-api-routes-mount-sequence.ts |  54 ++++
scripts/__tests__/check-routes-modular.test.mjs    |  28 +++
scripts/check-routes-modular.mjs                   |  65 +++++
scripts/lib/routes-modular-baseline.json           |   3 +
10 files changed, 433 insertions(+), 205 deletions(-)

Fusion-Task-Id: FN-8365

Fusion-Task-Lineage: 9c36a263-ed5e-4524-8ea5-71ed3f3e34d9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 15:53:07 -07:00
gsxdsm
965f15f5ca FN-8368: enforce browser-safe dashboard core imports
Prevent dashboard code from bypassing Vite's browser-safe core boundary.

- Add an allowlist-backed scanner for dashboard core value imports, including dynamic template imports.
- Run the scanner in test and merge-gate prechecks, with regression coverage and import guidance.
- Document reviewed browser-safe core leaves and Vite alias requirements.

Files changed:
 docs/dashboard-guide.md                            |   6 +
 package.json                                       |   6 +-
 packages/dashboard/vite.config.ts                  |   5 +
 ...no-node-only-core-imports-in-dashboard.test.mjs |  80 ++++++++++
 ...heck-no-node-only-core-imports-in-dashboard.mjs | 167 +++++++++++++++++++++
 .../lib/dashboard-browser-safe-core-modules.json   |  59 ++++++++
 6 files changed, 320 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8368

Fusion-Task-Lineage: 13e70672-d1da-430c-a360-0a714ad33d9f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 14:08:51 -07:00
gsxdsm
ecffdbb14b fix: isolate test-mode database access
Prevent automated tests from inheriting production PostgreSQL URLs and route global test-mode startups to a dedicated external or embedded test database.
2026-07-19 00:10:02 -07:00
gsxdsm
e445b3e367 FN-8201: pin pi dependency versions
Pin the pi runtime packages to a single exact version so global npm installs resolve a compatible set.

- Pin pi-ai and pi-coding-agent declarations across workspace manifests
- Add a guard and tests that reject ranged or mismatched pi versions
- Document the source-install fallback and add a patch changeset

Files changed:
 .changeset/fn-8201-pin-pi-versions.md              |   7 ++
 docs/getting-started.md                            |   3 +
 package.json                                       |   6 +-
 packages/cli/package.json                          |   4 +-
 packages/cli/src/__tests__/package-config.test.ts  |  18 +++-
 packages/core/package.json                         |   2 +-
 packages/dashboard/package.json                    |   2 +-
 packages/engine/package.json                       |   4 +-
 packages/pi-claude-cli/package.json                |   8 +-
 .../__tests__/check-pi-versions-pinned.test.mjs    |  45 ++++++++
 scripts/check-pi-versions-pinned.mjs               | 120 +++++++++++++++++++++
 11 files changed, 205 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-8201

Fusion-Task-Lineage: bf0ac363-df5f-4445-835b-cfd2d4909659

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-17 09:12:01 -07:00
gsxdsm
06d03d4e1f FN-8103: enforce PostgreSQL-only production data access
Require production paths to use PostgreSQL-aware stores and prevent new unrestricted database access.

- Add a checked allowlist that bans production getDatabase() calls by default.
- Route Quality plugin persistence through an async PostgreSQL-aware store and add Drizzle ORM.
- Document backend-safe plugin storage patterns and cover guarded access behavior.

Files changed:
 docs/PLUGIN_AUTHORING.md                           |  20 +++
 package.json                                       |   6 +-
 .../src/__tests__/agent-logs-backend-mode.test.ts  |   7 +
 packages/core/src/store.ts                         |   7 +-
 packages/core/src/task-store/remaining-ops-5.ts    |   8 +-
 plugins/fusion-plugin-quality/package.json         |   1 +
 .../src/__tests__/async-quality-store.pg.test.ts   |  36 +++++
 .../src/__tests__/cancel-and-plans.test.ts         |   8 +-
 .../src/__tests__/experimental-gate.test.ts        |   1 +
 .../src/routes/create-routes.ts                    |  50 +++----
 .../src/runner/command-runner.ts                   |  17 ++-
 .../src/store/async-quality-store.ts               |  34 +++++
 pnpm-lock.yaml                                     |   3 +
 scripts/__tests__/check-no-getdatabase.test.mjs    |  90 ++++++++++++
 scripts/check-no-getdatabase.mjs                   | 159 +++++++++++++++++++++
 scripts/lib/getdatabase-allowlist.json             |  18 +++
 16 files changed, 422 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-8103
Fusion-Task-Lineage: ff17bcb2-5341-4c6c-a5c4-993580539676
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 10:13:49 -07:00
gsxdsm
e9f14bf024 perf: speed up local pnpm build and cap stacked verifications (#2134)
## Summary

- Extend the workspace content-hash skip cache to **all** packages (not
just plugins), with `--force` / `--full` flags
- Default local CLI packaging to a **fast mode** (bin/extension +
migrations only); full desktop/plugin/DTS staging runs on CI or `pnpm
build:full`
- Enable TypeScript `incremental` builds for warm recompiles
- Add `maxConcurrentVerifications` (default **1**) so concurrent tasks
cannot stack monorepo typecheck/build and peg CPU

Warm `pnpm build` measured ~**126s → ~0.8s** when nothing changed.

## Test plan

- [x] `node --test scripts/__tests__/build-workspace.test.mjs` (12 pass)
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/verification-concurrency.test.ts`
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/settings-parity.test.ts`
- [x] Local: first `pnpm build` rebuilds as needed; second warm `pnpm
build` skips all packages (~0.8s)
- [x] Fast CLI packaging logs skip of desktop/plugin staging without
`FUSION_CLI_FULL_PACKAGE`
- [ ] CI: `pnpm build` still full-packages under `CI=true` (plugin
staging / release surfaces)

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

* **New Features**
* Added a Scheduling setting to limit concurrent verification tasks from
1–8, with a default of 1.
* Verification tasks now support cancellation while waiting or running.
  * Added options for forced and full workspace builds.

* **Performance**
* Local builds can skip unchanged packages and use incremental
compilation for faster rebuilds.
* Local CLI packaging is faster by default, while full packaging remains
available when needed.

* **Documentation**
* Updated the settings reference with the new verification concurrency
option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 08:44:11 -07:00
gsxdsm
97172fdcf2 fix(FN-7952): require PostgreSQL in CLI and desktop (#2110)
## Summary

CLI commands, daemon/dashboard startup, packaged desktop startup, and
live-data maintenance scripts now share the mandatory PostgreSQL
lifecycle. Operators no longer risk a command silently reading or
writing a disconnected SQLite shadow when PostgreSQL setup fails.

## Design decisions

- Every startup owner retains and awaits its PostgreSQL shutdown
callback, including partial-startup failure paths.
- CLI project context and lock-retry flows resolve through asynchronous
project stores.
- Maintenance scripts use the shared backend helper; explicit database
migration/inspection remains the only CLI surface allowed to read legacy
SQLite sources.

## Validation

- CLI and Desktop typechecks pass on the stacked branch.
- `pnpm test:gate` passes all 478 gate tests.
- This PR changes 54 files.

## Stack

- Depends on #2109, which depends on #2108.
- Bundled plugins and docs/release follow in later PRs.

Related: #2105


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

## Summary by CodeRabbit

* **New Features**
* PostgreSQL is now the authoritative store for structured project and
task metadata.
* Projects can be recognized and initialized using
`.fusion/project.json`, without creating a legacy SQLite database.
  * CLI commands now retry transient PostgreSQL contention errors.

* **Bug Fixes**
* Improved cleanup when commands complete, fail, or run in the
background, preventing lingering resources.
  * Improved desktop, server, and session shutdown reliability.

* **Documentation**
* Updated storage and standalone binary guidance to reflect PostgreSQL
and legacy SQLite compatibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 23:18:55 -07:00
gsxdsm
c25f8b796d Harden PostgreSQL migration foundation (#2088)
## Summary

- make SQLite-to-PostgreSQL cutover retryable, fail-closed, versioned,
and transactionally serialized
- isolate migration sessions from runtime traffic and apply schema
upgrades through `0002`
- enforce tenant ownership across automations, analytics, activity,
usage, agent runs, evals, and todos
- replace expired SQLite-only coverage with PostgreSQL parity and
concurrency coverage

This is PR 1 of 2. The stacked follow-up restores PostgreSQL parity for
CLI, engine, dashboard, and bundled integrations.

## Verification

- `pnpm check:changesets --strict`
- `pnpm --filter @fusion/core typecheck`
- migration schema, connection, and SQLite cutover suite: 57 tests
passed
- `pnpm test:gate`: 463 tests passed

## Post-Deploy Monitoring & Validation

- take a restorable PostgreSQL backup before deploy
- confirm `fusion_schema_migrations` contains `0002`
- confirm each expected project has a complete
`fusion_sqlite_migrations` row
- verify no null or empty tenant ownership in automations, activity
logs, agent runs, and usage events
- monitor for ownership inference failures, cutover verification
failures, and migration session errors
- restore the backup for data rollback; do not downgrade the
tenant-isolation schema in place

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

* **New Features**
* PostgreSQL-backed analytics and live dashboard metrics are now
project-scoped (activity, tools, monitor, signals, and live snapshots).
* Evaluation runs and scheduled eval batches received lifecycle
improvements (ordering, updates, and execution flow).
* Todo list changes now emit events; WhatsApp persistence and
project-scoped roadmap data are supported.

* **Bug Fixes**
* SQLite-to-PostgreSQL cutovers now fail safely with stronger
verification, serialized cutover handling, and safer project ownership.
* PostgreSQL backend writes and reads are now strictly project-isolated
and fail closed when project context is missing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 08:16:42 -07:00
gsxdsm
f7d29dd1da chore: archive pre-0.60 changelog notes and distill corrupted 0.47–0.59 entries
Raise the durable archive cutoff to 0.60.0, keep only the current release in CHANGELOG.md, and rewrite labeled summary/category/dev package aggregates for 0.47–0.59 into operator-facing Highlights/New/Fixed notes.
2026-07-13 23:00:25 -07:00
gsxdsm
c15c78feeb feat: migrate storage from SQLite to PostgreSQL (#1793)
# 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>
2026-07-13 19:07:58 -07:00
gsxdsm
edfe57c8e0 feat: AI Highlights and engagement X draft on release
Claude-author Highlights plus an engagement-oriented tweet (≤280) during changelog distillation, with scheme-free links, Fusion version openers that drop .0 patch, and a soft deterministic fallback when Claude is offline.
2026-07-13 01:21:33 -07:00
gsxdsm
b85a6b8663 FN-7912: add quarantine-ledger deadline visibility check
Add a report-only script that surfaces flaky-test quarantine entries approaching their 14-day deletion clock, so maintainers can make deliberate rescue-or-expire decisions before entries silently expire.

- Add scripts/check-quarantine-ledger.mjs: reads scripts/lib/test-quarantine.json, computes days-remaining against the existing 14-day deletion clock (shared DELETION_CLOCK_DAYS from scripts/test-velocity-baseline.mjs), and buckets each entry as expired/near/healthy/unknown
- Support --warn-within=<days> (default 5) to tune the near-deadline window, --json for machine-readable output, and --strict as an opt-in local/CI gate (exits 1 on expired/near entries) while default mode stays exit-0 and non-blocking
- Wire pnpm check:quarantine-ledger script in package.json
- Add scripts/__tests__/check-quarantine-ledger.test.mjs covering deadline bucketing/sorting, empty/missing ledger handling, --strict behavior, and --json output shape
- Document the new command and its flags in docs/testing.md under the quarantine ledger/deletion ratchet section

Files changed:
 docs/testing.md                                    |  10 +
 package.json                                       |   1 +
 scripts/__tests__/check-quarantine-ledger.test.mjs | 159 ++++++++++++++++
 scripts/check-quarantine-ledger.mjs                | 202 +++++++++++++++++++++
 4 files changed, 372 insertions(+)

Fusion-Task-Id: FN-7912

Fusion-Task-Lineage: c08e2e09-473a-4ad0-8c27-43cbc3355168

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-12 21:21:10 -07:00
gsxdsm
2c7df1bd78 FN-7875: split archived pre-0.50 release notes out of root CHANGELOG.md
Adds a deterministic changelog-archive split so scripts/release.mjs stops regenerating one ever-growing root CHANGELOG.md and instead keeps only current release notes at the root while durably archiving pre-0.50.0 history.

- Add scripts/lib/changelog-archive.mjs with partitionVersionsByCutoff (splits a version-ordered list at the 0.50.0 cutoff, preserving order and treating non-parseable keys as archived) and archivePointerLine (renders the "older releases" pointer appended to the current changelog).
- Rework scripts/release.mjs's syncRootChangelog to build CHANGELOG.md (current versions + archive pointer) and a new CHANGELOG-archive.md (versions before 0.50.0) via a shared buildRootChangelogLines/normalizeChangelogLines helper instead of one monolithic file.
- Add scripts/__tests__/changelog-archive.test.mjs covering cutoff partitioning, boundary/patch handling, non-parseable keys, custom cutoffs, and the archive pointer text.
- Regenerate CHANGELOG.md (now only 0.50.0+) and add CHANGELOG-archive.md containing the pre-0.50.0 history moved out of the root file.

Files changed:
 CHANGELOG-archive.md                         | 10882 +++++++++++++++++++++++
 CHANGELOG.md                                 | 11717 ++-----------------------
 scripts/__tests__/changelog-archive.test.mjs |    58 +
 scripts/lib/changelog-archive.mjs            |    56 +
 scripts/release.mjs                          |    49 +-
 5 files changed, 11710 insertions(+), 11052 deletions(-)

Fusion-Task-Id: FN-7875
Fusion-Task-Lineage: 220e6aa1-54fb-4800-a86e-6d8d21f6bf18
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-12 13:10:52 -07:00
gsxdsm
8f7089ecc9 fix(FN-7779): rebuild changed plugins on pnpm dev + warn on stale plugin dist
The Grok stale-dist bug was possible because the dev/build path never
refreshed plugin dist:

- The `client` prebuild (default `pnpm dev dashboard`) rebuilt only
  @fusion/core + @fusion/engine + @fusion/dashboard, never plugins.
- The FN-6638 stale-dist startup warning only scanned packages/, never
  plugins/, so a source-ahead plugin dist ran phantom-old with no warning.

Changes:
- build-workspace.mjs: add `--plugins-only` to plan/build just the plugins
  that changed, reusing the existing content-hash skip cache (cheap no-op when
  unchanged).
- scripts/dev-prebuild-client.mjs: new orchestrator — fast core/engine/
  dashboard build, then incremental changed-plugin rebuild. The `client`
  prebuild now runs this single cross-platform command.
- dist-freshness.mjs: scan plugin roots (plugins/, plugins/examples/) so a
  stale plugin dist is warned like a stale package dist; the warning names the
  plugin dir.

Verified: --plugins-only plans only plugins, skips unchanged on the second
run, and re-plans exactly the one plugin whose source changed. All script and
CLI lib tests pass.

Fusion-Task-Id: FN-7779
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:26:23 -07:00
gsxdsm
01be51bf35 fix: require typed operator authorization for real releases
Remove the self-grantable FUSION_RELEASE_AUTHORIZED env signal and replace
it with an interactive prompt: a real release now requires a live human to
type "authorized" at a TTY. Releases can no longer run non-interactively
(no TTY is blocked outright), and --yes does not bypass the typed phrase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 09:00:47 -07:00
gsxdsm
aae6a6eca2 FN-7662: harden boot-smoke temp dir cleanup against macOS ENOTEMPTY races
Boot-smoke's post-verdict temp-dir cleanup could throw ENOTEMPTY on macOS when async writers (fsevents/Spotlight, the just-killed child) still touched the throwaway HOME/project dirs, turning an already-decided PASS into a pnpm verify:fast failure.

- Add removeTempDir() helper in scripts/boot-smoke.mjs: wraps rmSync with maxRetries/retryDelay and swallows any residual error (never throws) since cleanup always runs after the smoke verdict is decided/printed
- Replace both isolatedHome/isolatedProject rmSync call sites (exit handler and EADDRINUSE retry-port branch) with removeTempDir()
- Guard main() behind an import.meta.url === pathToFileURL(process.argv[1]) check so requiring boot-smoke.mjs from tests doesn't spawn a real server
- Add scripts/__tests__/boot-smoke.test.mjs covering ENOTEMPTY tolerance, always-failing rm, retry/backoff params, and the no-boot-on-import guard

Files changed:
 scripts/__tests__/boot-smoke.test.mjs | 77 +++++++++++++++++++++++++++++++++++
 scripts/boot-smoke.mjs                | 44 +++++++++++++++++---
 2 files changed, 115 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-7662

Fusion-Task-Lineage: 3288d055-4f70-4487-8462-933563214ce5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-08 00:37:07 -07:00
gsxdsm
20184acdfd FN-7486: fix no-diff merge recovery ownership checks
Fix no-op task branch recovery by recognizing canonical branches with no unique diff before rejecting inherited foreign trailers.

- Add no-diff ownership classification for already-merged detection when canonical task branches inherit another task's landed commit.
- Teach self-healing and branch-misbound recovery to ignore foreign branch-tip trailers only for branches proven to have no unique task diff.
- Skip synthetic verify:fast typechecks for JavaScript alias packages without tsconfig files and cover the behavior with tests.
- Add regression coverage and a patch changeset for the recovery fix.

Files changed:
 .changeset/fn-7486-merge-recovery-noop-ownership.md       |  7 ++
 .../already-merged-detector.real-git.test.ts       | 68 +++++++++++++++++
 .../self-healing-already-merged.real-git.test.ts   | 89 ++++++++++++++++++++--
 packages/engine/src/already-merged-detector.ts     | 88 +++++++++++++++------
 packages/engine/src/self-healing.ts                | 61 +++++++++++++--
 scripts/__tests__/verify-fast.test.mjs             | 16 +++-
 scripts/verify-fast.mjs                            | 15 +++-
 7 files changed, 303 insertions(+), 41 deletions(-)

Fusion-Task-Id: FN-7486

Fusion-Task-Lineage: 4185c6ed-9731-4ffc-b033-34bf0c3a83ad

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-03 22:04:45 -07:00
gsxdsm
4be9275747 FN-7463: refresh test feedback baseline docs
Refresh the weekly test feedback-loop baseline and carry operator notes through the reporting helper.

- Add the 2026-W27 baseline snapshot with updated gate, pnpm test, quarantine, and slowest-test data.
- Refresh the published Markdown summary and trend table for #leads.
- Preserve operator notes in Markdown and printed leads summaries, with regression coverage for notes and empty live quarantine ledgers.

Files changed:
 docs/test-feedback-loop-baseline.md               |  52 +++++-----
 docs/test-feedback-loop-baselines.json            | 113 ++++++++++++++++++++++
 scripts/__tests__/test-feedback-baseline.test.mjs |  72 +++++++++++++-
 scripts/test-feedback-baseline.mjs                |  10 +-
 4 files changed, 219 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-7463

Fusion-Task-Lineage: 2279c412-5371-41dd-81d1-7fc397bb349a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-03 07:47:03 -07:00
gsxdsm
9d89cb8d32 FN-7420: fix test velocity quarantine reporting
Refresh the test velocity baseline and coverage so quarantine counts come from the live ledger.\n\n- Update the weekly baseline report to show zero active quarantines and a neutral delta.\n- Strengthen the report-only regression test to cover stale report and history quarantine values.\n- Document the FN-7420 requirement near the regression scenario.\n\nFiles changed:\n docs/test-velocity-baseline.md                    | 10 +++++-----\n scripts/__tests__/test-velocity-baseline.test.mjs | 10 +++++++---\n 2 files changed, 12 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-7420

Fusion-Task-Lineage: 9144ddb5-3a72-4a37-a1d1-471871f3cbc4

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:32 -07:00
gsxdsm
4178e7d325 FN-7415: Delete stale executor pause quarantine
Delete the expired executor pause quarantine and its stale direct-dispatch coverage.

- Remove the obsolete executor-pause test suite after the graph runtime cutover.
- Clear the matching Vitest exclude and quarantine-ledger entry.
- Refresh test audit, timing, line-count, and planning references for the deleted suite.

Files changed:
 ...7-001-refactor-workflow-runtime-cutover-plan.md |    2 +-
 docs/test-value-audit.json                         |  102 -
 .../engine/src/__tests__/executor-pause.test.ts    | 3061 --------------------
 packages/engine/vitest.config.ts                   |    5 -
 scripts/__tests__/test-velocity-baseline.test.mjs  |    2 +-
 scripts/lib/test-quarantine.json                   |    8 +-
 scripts/line-count-baseline.json                   |    1 -
 scripts/test-timings.json                          |    1 -
 8 files changed, 3 insertions(+), 3179 deletions(-)

Fusion-Task-Id: FN-7415
Fusion-Task-Lineage: 3d1551c8-1353-47cb-a70f-d88e733a6652
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:31 -07:00
gsxdsm
00ce5d6a27 FN-7414: refresh test velocity quarantine baseline
Refresh the velocity baseline artifacts so live quarantine counts are reflected consistently.

- Add regression coverage for live quarantine ledger counts overriding stale report and history values.
- Update the weekly velocity baseline report to show the current quarantine count and deltas.
- Append the latest measured velocity history row with quarantine count one.

Files changed:
 docs/test-velocity-baseline.md                    |  22 ++---
 scripts/__tests__/test-velocity-baseline.test.mjs |  75 +++++++++++++++
 scripts/test-velocity-history.json                | 112 ++++++++++++++++++++++
 3 files changed, 198 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7414

Fusion-Task-Lineage: 7c6cd47c-482d-426c-a714-79da33d4591d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:31 -07:00
gsxdsm
627c5fb0bd FN-7371: clarify CI mode regression guard
Clarifies the test-infrastructure regression guard for CI mode selection.\n\n- Update the FNXC rationale for the CI=true --print-mode subprocess test.\n- Document that full-suite mode remains an explicit opt-in via --full or FUSION_TEST_FULL.\n\nFiles changed:\n scripts/__tests__/test-changed.test.mjs | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-7371

Fusion-Task-Lineage: b7482ef1-4db7-4003-85f7-3466470d3dff

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-01 10:04:55 -07:00
gsxdsm
70d7dcaa6c FN-7290: skip unchanged plugin builds
Add a git-backed plugin build cache to make root workspace builds skip safe unchanged plugin packages.

- Route root pnpm build through a workspace build planner that always builds non-plugin packages and selectively skips cached plugin packages.
- Hash plugin inputs with declared local workspace dependencies plus root build tooling/config and require dist outputs before skipping.
- Cover cache planning and invalidation behavior with script tests and document the operator-facing build behavior.

Files changed:
 .changeset/fn-7290-plugin-build-cache.md   |   7 +
 docs/testing.md                            |   5 +-
 package.json                               |   2 +-
 scripts/__tests__/build-workspace.test.mjs | 253 +++++++++++++++++
 scripts/build-workspace.mjs                | 427 +++++++++++++++++++++++++++++
 5 files changed, 692 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-7290

Fusion-Task-Lineage: a91861c7-385d-4e9c-bc43-11ed0ff188c8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-30 11:08:55 -07:00