Commit Graph

2665 Commits

Author SHA1 Message Date
gsxdsm
245086dad6 docs: the census total is a floor — 25 membership predicates it structurally cannot see, one a live defect (#2763)
Docs only, extending the entry #2748 landed. Opening it because the
fleet reads the census total as its completion bar, and that total
excludes a whole predicate class — a measurement that should not live in
a chat reply.

## Measured on `origin/main`

- **47** array/Set literals of two or more lifecycle ids, in 35 files.
- **25 are membership predicates against a task's column** —
`SET.has(task.column)` / `ARRAY.includes(task.column)` — in 19 files.
Two are documented fallbacks behind a resolved primary, so **~23 are
unconverted guards**.
- The census scans `===` / `!==` against a column. **None of these is a
comparison, so none is counted.**

| file | constant |
| --- | --- |
| `cli/src/commands/task.ts` (3) | `retryReviewColumns` |
| `dashboard/app/components/TaskCard.tsx` (2) | `TIME_INDICATOR_COLUMNS`
|
| `engine/src/eval-followups.ts` (2) | `OPEN_COLUMNS` |
| `engine/src/merger.ts` (2) | `sourceTerminal` |
| `engine/src/task-revert.ts` (2) | `REVERTABLE_COLUMNS` |
| `core/src/agent-role-policy.ts` (1) | `IMPLEMENTATION_TASK_COLUMNS` |

## One is a proven live defect

`isImplementationTask` is
`IMPLEMENTATION_TASK_COLUMNS.has(task.column)`, and
`evaluateImplementationTaskBind` short-circuits to `allowed: true` when
it returns false. **On a renamed board every agent is bind-compatible
with every task** — the role check that stops a liaison being handed
implementation work (the NEXT-871 loop FN-7851 fixed) does not apply.

It surfaced only because a reviewer questioned a coverage claim in one
of my dispatch tests (#2739). Passing an agent wasn't proof the
evaluator ran, so I asserted a `custom`-role agent must be *refused* —
and that test failed against production. Flagged at the site in #2739,
not fixed: `isImplementationTask` is a sync pure predicate with no
store, and making the routing policy async is a behaviour change to
agent admission.

## What this does and does not argue

The census is the right instrument — AST-based, honest about what it
measures, and it has caught real drift in both directions (it failed on
me in #2724 when merged conversions moved an inventory *down*). This is
not an argument against it.

It is an argument against reading **"backlog: N" as "N guards remain"**.
The same shape already appeared in the archived gate (#2724), where the
rule is additionally encoded in Drizzle predicates and raw `sql`
templates that no comparison scan can see. Two independent classes now,
found the same way — by looking at what the instrument's definition
excludes.

**Extending the census to count membership predicates is deliberately
left to you, not done here.** It would move every worker's number
mid-fleet, and deciding which sets are lifecycle guards versus
board-config definitions or type unions is exactly the judgement
`DELIBERATE-LITERAL` exists for — 47 collections would each need that
call.

## Verification

`pnpm lint` clean · census `--strict` exits 0 · no code changes.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 08:12:51 -07:00
gsxdsm
1322a1bb11 docs(solutions): the optional-flags seam kept four green suites blind to their own conversion — and why I did not ship a ratchet for it (#2748)
Docs only. No code, no census movement.

## The finding, measured

Four consecutive files in this program had **fully green suites at
conversion time** that could not have detected the conversion — correct
or broken:

| file | pre-existing cases blind to the change |
| --- | --- |
| `github-tracking-reconciler.ts` | **33** (fake store had no workflow
reader) |
| `TaskReviewTab.tsx` | **45** (`columnFlags` omitted everywhere) |
| `plan-approval-hold-invariant` drain | **25** (`opts.lifecycle`
omitted everywhere) |
| `task-age-staleness.ts` | **12** (`context.lifecycle` omitted
everywhere) |

The cause is structural. Every conversion here uses the same seam — the
caller passes resolved flags, the helper falls back to the legacy id
when they are absent — and every pre-existing test omits the flags. So
the suite passes **before** the conversion, **after a correct one**, and
**after a wrong one**, as long as the fallback is intact. "The suite is
green" carries no information about the change.

I reported this observation four times in PR bodies. Restating it a
fifth time is worth less than writing it where the next worker will
actually find it.

## It also corrects the obvious test

The natural property is "hold the traits fixed, change the id, behaviour
is identical". That is only half the invariant. It does not catch:

```ts
// Not a fallback — an OVERRIDE. The id wins even when traits disagree.
return column === "in-review" || flags?.mergeBlocker === true;
```

Renaming `in-review` → `checking` leaves that correct, because the trait
arm answers. The defect appears in the **converse** direction — a column
that still *carries* a lifecycle name while its traits say otherwise,
which is what you get by repurposing a default column rather than
renaming one. That is the direction that found a live **"Merge & Close"
offered on a mid-implementation card** in #2718.

## Why this is not a ratchet — a negative result, recorded

I tried to automate it, and I am shipping the reason it failed rather
than a guard I do not trust.

The **consumer scan is sound**: AST-based, 31 files, 66 role-helper call
sites. The **coverage half is not**. The renamed ids this program uses —
`building`, `checking`, `converted`, `published`, `backlog` — are
ordinary English words that appear in unrelated test prose, and a test
merely *importing* the module under test does not prove it exercises the
role path. My scan reported `TaskCard.tsx` as covered by
`Column.test.tsx` on a **filename coincidence**.

A guard built on that reports coverage that does not exist, which is
worse than no guard, so it is not shipped.

A sound alternative — pin the consumer set and make each new file
declare its status — was also rejected: a 31-entry status inventory
would conflict with every concurrent fleet PR that adds coverage. That
is the same churn already removed from the census baseline by dropping
its derived aggregates.

The attempt is written down so the next person does not repeat it from
scratch, and the requirement lives as a review criterion until someone
finds a sound signal.

## What it asks for

1. **A flags-supplying case** — if every case omits the new parameter,
the conversion is untested in both directions.
2. **Both directions where both are reachable** — renamed lane, and
repurposed column.
3. **A non-vacuous companion** — assert what the widened predicate must
still *exclude*, or a predicate matching every column satisfies your new
cases. (Both `TaskReviewTab` and the dispatch filters needed this.)
4. **Run the revert and record the failure text.** Twice in this program
a new case passed with the change reverted: once because the branch was
gated behind an unwired handler (`refine` needs `onOpenRefine`), once
because the hook was dispatched by trait and the test IR did not declare
that trait, so it never ran at all.

Cross-linked both ways with the adjacent `store-fake-defects` entry,
with the distinction stated so the two are not confused: **there** a
fake is missing a method so a branch never runs and production looks
wrong; **here** the fake is complete and the test is correct, but a
parameter is absent so production takes its documented fallback.

## Verification

`pnpm lint` clean · census `--strict` exits 0 (unmoved — this PR changes
no code).

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 07:11:22 -07:00
gsxdsm
11aba0394e docs(solutions): converting a column literal to a role makes it async — the four forms that ship green (#2710)
Four review rounds across `TaskCard.tsx` and `TaskDetailModal.tsx` each
found a **real defect**. None was in the conversion itself — every one
came from the same property change. The fleet has ~600 guards left to
convert against the same helpers, so this is written down rather than
left in four commit messages.

## The property that changes

```ts
task.column === "in-progress"   // stable for the lifetime of the render tree
isWipColumn                     // derived from fetched trait flags — CHANGES after first paint
```

Column trait flags arrive from a board-workflows fetch. Until it lands
they are `undefined` and every role helper falls back to the legacy id.
So a converted role is `false`, then `true`, within one mounted
component.

## The four forms

| | form | symptom |
|---|---|---|
| 1 | **stale memo** — deps still keyed only on `task.column` | timers,
labels, completion dates frozen at first-paint values (4 instances in
TaskCard) |
| 2 | **frozen `useState` initializer** | the section does not start
collapsed — it *appears later, already collapsed*, on a card nobody
touched |
| 3 | **eager action on a guess** — effect mutates state before flags
resolve | a tab opens and instantly bounces; the correction never lands
because the action destroyed the state it would have corrected |
| 4 | **stale identity** — flags resolved, but for the *previous* entity
| roles resolve from another task's workflow: confidently wrong rather
than merely stale |

**Form 4 defeats the obvious fix for form 3.** A `metadata === null`
guard asks whether data *loaded*, not whether it describes the entity
currently open — and it only appears in components that stay **mounted
across entity changes**, which is why TaskCard never showed it and the
modal did.

## Why a doc rather than four commit messages

All four ship **green**: types pass, existing tests pass, and the
**default board behaves identically** — because on the default lineage
the legacy fallback and the resolved role agree. They diverge only on a
**renamed board**, which is precisely the case the conversion exists to
support.

So the failure mode is: census count reaches zero, everything is green,
and the feature is broken exactly where the programme was meant to fix
it. A reviewer catching these one at a time is the expensive path, and
it has now cost four rounds on two files.

Also relevant: **this repo has no `react-hooks/exhaustive-deps` rule**,
so form 1 has no automated backstop at all.

## Contents

A checklist a converter can run against a component file, and the
concrete fix shape for each form — including tagging fetched metadata
with the id it describes, and applying that guard to the **role
bindings** rather than only the effects (reordering effects fixes the
call sites you noticed and leaves the bindings stale for everything
else).

Follows the convention already established by the engine-side scoping
note in `architecture-patterns/fleet-self-healing-cluster-scoping.md`,
which records the equivalent hazard for sync workflow reads.

## Verification

`pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm
check:lifecycle-columns` exits 0. `pnpm lint` clean.

Docs-only; no changeset.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:37:19 -07:00
gsxdsm
bb3bdab999 The ratchet follows the count down — a drop tightens instead of reddening the gate (coordinator item 2) (#2679)
Taken after asking twice for reassignment with no reply, and after the
same failure bit a **third** time. No open PR touches the census CLI, so
this is unowned in practice — **U12, say so if you have started and I
will close this in favour of yours.**

## What changed

A **drop** now tightens the baseline instead of failing. Failing hard
was defensible in isolation — a stale allowance is a hole, since those
guards can return up to the old count while the check stays green. What
it missed:

**The drop is almost never the failing author's to fix.** Eleven files
dropped during one merge wave, none of those PRs re-recorded, and none
of their authors did anything wrong. Measured three times since CI began
gating this: `columnRoles.ts` 0 → 1, then `executor.ts` twice.

A permanently-red gate is a bigger hole than a stale allowance, because
it gets ignored and then nothing is guarded at all. **The rise check —
the ratchet's actual purpose — is untouched and still fails hard.**

## The residual, named rather than glossed

In CI the write is discarded with the runner, so the committed baseline
stays stale until someone commits a tightened one. The exposure is
bounded (regrowth only up to the old count), printed on every run, and
strictly smaller than the exposure from a check people route around.
`--strict --exact` restores hard failure for the pinned end state.

**One writer:** the write is now a named `writeBaseline()` shared by the
tighten path and `--update-baseline`, rather than a second
`writeFileSync`. Two writers for one artifact is how they drift — a
lesson this file already learned once.

## Exercised end to end

| scenario | result |
|---|---|
| drop, `--strict` | exit **0**, `TIGHTENED`, allowance rewritten 9 → 6
|
| drop, `--strict --exact` | exit **1**, baseline untouched |
| rise, `--strict` | exit **1** |
| clean | exit **0** |

Pinned through the real CLI with an isolated baseline. Revert proof:
restoring the hard failure fails **1 of 32**.

## Two of my own mistakes, recorded

**A vacuous assertion, in the case that guards against vacuity.** I
first wrote `expect(allowedAfter).toBeLessThan(4 + allowedAfter)` — true
for every number. Replaced with a comparison against the inflated value
the fixture started from. This file documents that trap repeatedly and I
still walked into it, which is the argument for the mechanical revert
check over careful reading.

**The env override is `FUSION_CENSUS_BASELINE_PATH`**, not the
`FUSION_CENSUS_BASELINE` I used in the first draft — so the first
version of these cases silently ran against the **real** baseline and
passed for the wrong reason. A test whose fixture never took effect is
the same failure as a test whose fixture can't fail.

## Verification

32/32 census suites, `pnpm test:gate` **71/71**, `--strict` exits 0,
`pnpm lint` clean, `docs/testing.md` updated.

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

---

## Update — the base-ref ratchet (review round 2, commit `4895845579`)

The first version of this PR shipped a **named residual**: the
tightening write dies with the CI runner, so the committed allowance
stays high and a later PR can regrow guards up to it while `--strict`
prints green. I called the exposure bounded and moved on. Greptile
flagged it P1 and was right — naming a hole is not closing one.

`--strict` now stops trusting the committed number for files the branch
touched. It measures each **changed** file at the base commit
(`FUSION_CENSUS_BASE_REF`, else the PR base branch, else `origin/main`)
and fails if the file carries more guards than the base ref has. **The
enforced ceiling is what main has today**, so a stale, missing, or
long-unrecorded baseline no longer opens a window.

| decision | why |
|---|---|
| changed files only, `<ref>...HEAD` | untouched files have main's
counts by construction; censusing all ~400 at the base ref is ~400 `git
show` calls to re-derive numbers that cannot have moved. Three-dot also
stops charging this branch for guards that landed on main after the
fork. |
| a new file's base allowance is **0** | "absent at the base ref" as
unbounded would make a new file the cheapest place to hide a fresh guard
|
| fails **open** on an unresolvable ref, printing `SKIPPED` | a shallow
clone cannot produce an honest comparison; a degraded run must not read
as a clean one. The baseline comparison still applies. |
| merged into the existing `regressions` list | one failure per file,
and `--update-baseline` keeps working as the deliberate escape hatch. No
new exit path. |

**Revert proof, measured both ways.** With the base-ref block removed,
the regrowth fixture — base commit 2 guards, HEAD 5, baseline allowing 9
— exits **0** with `TIGHTENED`, which is precisely the reported
scenario. With it: exit **1**, `column-guard count ROSE`, `above its
count on the base ref`, baseline left at 9. **3 of the 4** end-to-end
cases go red on revert. The fourth passes without the fix by design — it
is the genuine-conversion case the auto-tighten exists to keep green,
and a case that reddens either way proves nothing.

The end-to-end suite builds a throwaway two-commit `git init` repo under
the temp dir, because this exploit is a property of the **plumbing**,
not of the comparison: resolving a ref, working out the changed set,
reading base source through `git show`. The comparator itself is pure
with the reader injected (`findRegrowthAgainstBase`), with its own cases
in `lifecycle-column-census-ast.test.ts` — including the one that would
silently pass everything, looking up the wrong key in
`summarize().byFile`.

**Rebased onto `origin/main` @ bc782d8d92** (the branch was forked
before the recent merge wave; its baseline read 746 against a tree of
722).

Verification on the rebased branch: census **722** / `--strict` exit 0 ·
**70/70** across both census suites · `pnpm test:gate` **71/71** · `pnpm
lint` clean.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:56:08 -07:00
gsxdsm
ae23be79f7 fleet: scheduler.ts 28 → triaged (NOT converted) + repo-wide reachability measurement — the work order sorts on a number that doesn't predict convertibility (#2687)
## Claim

`packages/engine/src/scheduler.ts` — the largest **unclaimed** cluster
(28). Triaged, **not converted**, for the reason below. Census
unchanged: **722 → 722**. No baseline movement is claimed, because
nothing was converted.

## Why not converted

Three of us independently hit the same wall on our first file — #2683
and #2684 (`self-healing.ts`), #2685 (helper coverage). This measures
the whole backlog **once** so the remaining workers don't each pay that
cost.

Two constraints gate conversion. Neither is visible in the per-file
counts the work order sorts on.

### 1. Location — the helpers aren't importable from 80% of the backlog

`isIntakeColumnRole` / `isPreImplementationColumnRole` /
`isHoldColumnRole` live in
`packages/dashboard/app/utils/columnRoles.ts`, a dashboard-**app**
module.

| Location | Guards | Share | Importable? |
|---|---:|---:|---|
| `packages/engine/**` | 316 | 43% | no |
| `packages/dashboard/app/**` | 150 | 20% | **yes** |
| `packages/core/**` | 148 | 20% | no |
| `packages/dashboard/src/**` | 78 | 10% | no |
| `packages/cli/**` | 24 | 3% | no |
| plugins | 6 | 1% | no |

**150 of 722 (20%)** can call them at all. Widening the helper *set*
(#2685, correctly) does not move this number — it's the module's
location, not its coverage. Core already exports `resolveColumnFlags`,
so a core-side predicate module would be *the same* abstraction made
reachable, not a new one. It is a prerequisite for the other 80% and
**not sufficient** — see below.

### 2. Flag scope — the binding constraint

A role predicate needs resolved trait flags. Most guards run in
functions handed a bare task row with no IR to resolve from. Threading
one in changes a signature and its call graph: a **behavior change, out
of scope**.

File-level proxy over the 572 non-dashboard guards: **339** in files
that reference an IR/flags resolver, **233** in files with none.

**That proxy overstates convertibility, and the overstatement is the
finding.** Reachability varies *within* one file, so a file-level
verdict is unusable. In my claimed cluster:

| Site | Context | Convertible? |
|---|---|---|
| `scheduler.ts:1690` | `resolveWorkflowIrById(...)` +
`resolveColumnFlags(c)` in the same block | **yes** |
| `scheduler.ts:231` | `isLegacyDependencySatisfied(dep: Task \|
undefined)` | no — task only |
| `scheduler.ts:341` | `shouldHoldActiveFileScopeLease(...)` | no — task
only |

So "convert the file" is not a unit of work that exists in this backlog,
and the rule *"the baseline must shrink by exactly your converted
count"* cannot be satisfied per-file until the count is per-site.

## A guard that must be skipped, not guessed

`packages/core/src/task-merge.ts:254`:

```ts
if (!options.skipColumnIdentityCheck && task.column !== "in-review") {
```

The parameter is `Pick<Task, "column" | "paused" | ...>` — no IR,
deliberately. The in-source FNXC comment records that callers who *have*
resolved the `merge-blocker` trait pass `skipColumnIdentityCheck` rather
than spoofing `{ ...task, column: "in-review" }`. The trait-aware path
already exists *beside* this literal.

Converting it wouldn't remove a legacy id — it would delete the fallback
the option was introduced to make explicit. **Flagged and skipped.**

## Suggested census upgrade (not done here)

Emit per-site whether trait flags are resolvable in the enclosing scope.
That turns the work order from "largest file" into "largest
**convertible** cluster" and makes baseline shrinkage predictable. I did
not touch `scripts/lifecycle-column-census.mjs` — it is the shared
instrument and changing it unannounced would invalidate everyone's
in-flight before/after numbers.

## Method correction worth propagating to every fleet worker

Claim-collision scans must compare a branch to its **merge base**, not
to `origin/main`. `git diff origin/main origin/<branch> -- <file>`
reports a difference when the branch is merely *stale* (the file didn't
exist at its base) — it flagged dashboard and CLI PRs as touching engine
E2E files. I nearly skipped a free cluster on that false signal.

`feature/code-organization-wave17` is excluded from collision checks:
**1556 files, 150 commits behind main, already `DIRTY`**. It must rebase
wholesale regardless, and counting it as a claim marks *every* cluster
in the backlog as taken.

Docs-only — no source, no test, no census change.
2026-07-30 02:53:18 -07:00
gsxdsm
bb30d37e59 fleet: self-healing.ts 110 → scoped (NOT converted) — sync workflow reads make this cluster unsafe to batch (#2683)
Claiming the largest unclaimed cluster per the work order, then
**handing it back sized rather than half-converted.** Docs only; census
unchanged (722 / triage 0).

## The cluster

`packages/engine/src/self-healing.ts` — **110 guards**, largest single
file in the order.

```
by column:   in-review 48 · in-progress 20 · done 17 · todo 13 · archived 12
by receiver: column 100 · to 7 · from 3
```

## Why the mechanical conversion is unsafe here

**The engine has no synchronous way to learn a task's workflow.**
`resolveTaskWorkflowIrSync` returns the DEFAULT IR for every task in
production — `getTaskWorkflowSelection` returns `undefined`
unconditionally (a PG-cutover stub), so the reader always takes its
`!workflowId` branch. It is typed non-optional, so **no caller can
detect the substitution.**

A conversion routed through it: compiles, reads better than the literal,
**counts as census progress**, and is wrong for every custom workflow,
silently. That is strictly worse than leaving the literal — the literal
is at least honest about being one. It is the "guard that cannot fire"
pattern wearing better clothes, and the ratchet would score it as a win.

The correct form uses `resolveTaskLifecycleColumns(store, taskId)`
(async, store-aware), which needs resolved lanes **in scope per
method**. Sampled sites (926, 932, 984) do sit in `async` methods so it
is reachable — but that is a per-sweep restructuring, not a per-line
substitution, and these sweeps iterate task lists, so a naive per-task
resolve turns one sweep into N store reads.

**In-tree precedent:** `triage.ts` `discoverReadyPlanningTasks` solved
this exact problem — store-free `couldBeCandidate` prefilter, bounded
(8) concurrent resolve over the survivors, decision stays synchronous
over a resolved map. Any batch here should follow that shape per sweep.

## Recommended split, by SWEEP not by column

110 sites cannot honour *"census before/after, baseline shrinks by
exactly the converted count"* while also restructuring six-plus sweeps
in one PR.

1. **the review/merge sweeps** (`in-review` 48) — largest, and the one
where a wrong lane silently changes **merge eligibility**. First and
alone.
2. **WIP/rebound sweeps** (`in-progress` 20, `todo` 13).
3. **terminal sweeps** (`done` 17, `archived` 12) — read
`complete`/`archived`; most mechanical of the three.
4. **the 10 `from`/`to` sites** — these are MOVE-transition arms, not
task-column reads. Different question (*"is this transition into a
review lane?"*), so they must not ride along with the `task.column`
work.

## Why I am not doing item 1 myself

I am near the end of a long session — this is the same context in which
I produced a confidently-wrong structural finding earlier today
(retracted in #2667, where I trusted a hand-rolled brace counter over a
comment in the file). A 48-site restructuring of the merge-eligibility
sweeps is exactly the work that should not be done by a worker in that
state, and the fleet rules' *flag-and-skip* discipline is the right call
over guessing.

**What a fresh worker gets from this PR:** the site census, the
async-scope survey, the hazard with its root cause, the in-tree pattern
to copy, and a four-way split with the risky piece isolated. That is the
expensive part of the job already done.

## Fleet rule this cluster proves, worth adding to the brief

**Never resolve a workflow synchronously in a converted guard.** Use
`resolveWorkflowIrForTaskWithProvenance` (branch on `source`) or
`resolveTaskLifecycleColumns`; if neither is reachable at the site, flag
and skip.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:44:27 -07:00
gsxdsm
13bf7e001d Closing-bar verification pass on origin/main — one tree, one report (+ the E2E red it found) (#2660)
**Closing-bar item 4, run on one clean tree at `origin/main`
(`be63e72f1`).** Nobody was assigned this and my own work is merged, so
I took it.

> **This PR is now REPORT-ONLY — net zero file changes.** I found the
planning-lane E2E red, fixed it, then discovered **#2658 (gsxdsm) makes
byte-for-byte the same change** to the same helper and was opened first.
I reverted mine rather than leave two identical edits to one function to
conflict. **The E2E result below depends on #2658 landing** — on
`origin/main` without it, that suite is 2 failed / 5 passed.
>
> The duplication is worth one note for the fleet: two workers
independently hit the same control-card failure and independently traced
it to FN-7648's unplanned-seed gate plus a fixture that never wrote a
spec. Independent confirmation of the diagnosis, but also ~an hour spent
twice — the census-style work order exists to stop exactly that, and E2E
fixture defects are not on it.

## Report — all four, one tree

| Check | Result |
|---|---|
| `pnpm test:gate` | **PASS** (132 + 10 + 487 + 71 tests) |
| `pnpm verify:fast` | **PASS** — 13 steps green in 89.9s, boot smoke
`GET /api/health 200`, clean shutdown |
| E2E families | **13 files / 109 tests PASS** — *after* the fix below;
**2 failed** before it |
| census | total **787**, triage **10** |

## Two corrections to the bar itself

**1. It is not "all-8 E2E" any more — there are 13 families.** The suite
grew while the bar was being written:

```
agent-count · agent-link · lease-rebound · lifecycle · merge-family · merge-rebound
merge-safeguards · merged-board · planner-lane · planner-lane-resolution
planning-lane · rebound-family · stranded-column
```

A verification pass scoped to 8 would have skipped 5 families —
including the one that was red. Worth fixing the number in the bar so
the final pass globs rather than counts.

**2. `DELIBERATE-LITERAL (reviewed)` reads 3, and I chased it —
RESOLVED, no gap.** I flagged the drop from an earlier "7" as a possible
fleet-safety hole. It is not one. Reconciled against `--json byFile`:

| File | markers | counted `deliberate` | counted `column` |
|---|---|---|---|
| `hold-release.ts` | 2 | **2** | **0** |
| `live-agent-count.ts` | 1 | **1** | 6 |
| `replan-target.ts` | 2 | 0 | 4 |

`deliberate: 3` = hold-release 2 + live-agent-count 1, which is exactly
the set of marker-covered **comparisons**. `replan-target.ts`'s two
markers sit above `return "triage"` **return-value** literals, not
comparisons — the census correctly does not count those as guards at
all, so they are neither `deliberate` nor `column`. The earlier "7" was
simply a different tree state before conversions landed; I was quoting a
stale number.

Worth noting the marker matcher is already hardened for the subtle case:
`hasDeliberateMarker` walks every **ancestor** rather than the enclosing
statement, because the real markers sit above the enclosing *function*
while the comparison is a `return` inside it — a statement-only lookup
"silently reclassified three reviewed literals as backlog". That is the
guard-cannot-fire pattern, already caught and fixed by whoever wrote the
AST version.

**Consequence for the fleet: the census's categories are trustworthy
as-is.** No pre-launch action needed on this.

## The red it found

`workflow-planning-lane-live-e2e.pg.test.ts` — **2 failed / 5 passed**,
including its own **control** case:

```
releases an ordinary held card on a default board (the control)
  → AssertionError: expected [] to include 'FN-OK'
```

`seedHeldTask` never wrote a `PROMPT.md`, so task creation's bootstrap
seed stood, and FN-7648's `isUnplannedForExecution` correctly refused to
release an unspecified card. **The sweep was right; the fixture was
asking it to release a card that had never been specified.**

**This is the second instance of the identical defect** — same cause and
same fix as `workflow-lifecycle-live-e2e`'s `seedTask` in #2634. This
suite was written after that fix and did not inherit it. The graph-entry
contract doc already states the rule: *"Scheduler/release test fixtures
must model a card that cleared the gate ... A held unreviewed card is
the gate working."*

Both failures had one cause — the mid-sweep approval-park case was
downstream of the control never releasing. **5 → 7 passed**, and cards
that are *supposed* to be held still are, held by their own
status/marker, which is what those cases assert.

Given it has now happened twice, a shared `seedPlannedTask` helper in
the E2E fixture module would prevent a third. I did not add one here: it
touches suites owned by U7 and U11 mid-consolidation, and this PR should
stay the verification pass plus its one finding.

## Bar status after this

- **gate / verify:fast / E2E** — green on one tree, with this commit.
- **triage → 0** — still **10**, all in U12's `moves.ts` (4) and
`register-task-workflow-routes.ts` (1) per file scan; flag resolution in
flight.
- **ratchet tightened (item 2)** — not done, U12's.
- Once triage hits 0 and the ratchet lands, re-running this exact pass
is a ~4-minute job and I can produce the final report.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:38:29 -07:00
gsxdsm
dca20496f4 consolidate/u7: plugins to zero + 8 executor rebound guards + resume lanes (supersedes #2607, #2635, #2640) (#2644)
Consolidation branch for U7, per the new one-branch working mode.
**Supersedes #2607, #2635, #2640** — the three of my PRs that were stuck
on review threads. My other seven (#2602, #2605, #2606, #2611, #2621,
#2628, #2633) are green with **zero unresolved threads** and are
deliberately left alone for the merge sweep.

## What is in here, file by file

| file | change | guards before → after |
|---|---|---|
| `plugins/…/glasses/src/agent-actions.ts` | gates, destinations and
degraded-resolution refusal all resolve from the task's own workflow | 2
→ 0 |
| `plugins/…/glasses/src/quick-capture.ts` | accepted capture columns
come from the board; default no longer names the deleted column | 1 → 0
|
| `plugins/…/glasses/src/settings.ts` | quick-capture default was
`triage`, the column #2515 removed | (assignment, uncounted) |
| `plugins/…/dependency-graph/src/GraphTaskNode.tsx` | redundant column
condition deleted | 1 → 0 |
| `packages/engine/src/executor.ts` | 8 rebound guards compare the
resolved column; 4 resume-eligibility literals share one resolver | 151
→ 143 (+4 off-bar) |
| `packages/engine/src/__tests__/` | 4 new suites, 26 cases | — |

`plugins/` reaches **zero** column guards with this branch.

## The three threads it closes

**#2607 — five findings, all mine, all the same rule.** I kept
*qualifying* a legacy-id fallback instead of removing it:

| attempt | rule | hole review found |
|---|---|---|
| 1 | fall back to `todo` when the role is missing | moved cards to
phantom columns |
| 2 | …only if the workflow **declares** `todo` | aliased **review**
lane named `todo` |
| 3 | …and only if no other role is assigned to it | **traitless**
parking column named `todo` |

The qualifications were the mistake. Once `resolveLanes` returns a lane
set the workflow *has* a column vocabulary, so "no column carries the
hold trait" is a complete answer — refuse. `destination()` is two lines
now, with no aliasing surface left to qualify.

Plus a sixth, which is a genuinely different state: **degraded
resolution is indistinguishable from the default board.**
`resolveWorkflowIrForTask` is total by design — a missing definition
silently returns the *default* coding IR — so a card on a custom board
whose definition could not be read resolved to `todo`/`in-progress`.
`undefined` lanes cannot express that (it means "no workflow at all",
where the legacy ids *are* the answer). The actions now refuse with 409.
#2618 would replace this check with resolver provenance; it is not
merged, so this does not depend on it.

**#2635 — "seven rebound sites remain untested."** Fair; my "same shape"
note was an assertion, not coverage. Seven of the eight need a live
graph run to reach, so the *shape* is pinned instead: a static check
that no guard in front of a rebound move compares against a column
literal, with a vacuity case (the same detection run against the
original shape) and a match-count floor (≥8), because a guard reporting
success on zero matches is worse than no guard.

**#2640 — duplicate workflow resolution.** Framed as I/O; it is also a
correctness bug. Eligibility and re-entry are two halves of one decision
and resolved the workflow separately, so a workflow edit landing between
them has the halves reading *different boards*. Now one caller-owned
memo per decision — caller-owned because a process-lifetime cache would
have to guess when a mid-flight workflow edit invalidates it.

## Behavioural findings, not tidying

- **The last-resort recovery for completed-but-stranded work did not
exist off the default lineage.** `promotedFromPlannerColumn` was false
on a renamed board, so finished work resting in planning was never
promoted; the code fell through to a review handoff that role adjacency
rejects, and the card stayed stuck with its work complete.
- **Rebound guards could not see the column their own move targeted.**
U5b converted the move target; the eight `column !== "todo"` checks in
front of it were left literal, so on a renamed board the engine moved a
card into the column it was already in — and `moveTaskInternal` runs
reset-on-entry on every real move, so at the `preserveProgress: false`
site it reset step progress a second time.
- **The FN-1404 `task:move` audit row was lying**, recording `to:
"todo"` while the move target was resolved. A run-audit trail that
disagrees with the move it describes is worse than none. Not a
comparison, so no census counts it.
- **A task interrupted by an engine pause never resumed on a renamed
board** (off-bar, `in-review`/`in-progress` literals): four comparisons
decided one question and had to agree; two of them disagreed on a
renamed board, so re-entry silently never fired.

## Revert proofs, isolated per site

| reverted | result |
|---|---|
| `destination()` back to attempt 3 | 3 of 38 fail |
| degraded-resolution refusals removed | 2 of 42 fail |
| capture set back to the legacy five | 2 of 3 fail (renamed-board
suite) |
| forward exclusions → literals | 1 of 14 fails |
| missing-wip refusal removed | 2 of 14 fail |
| `promotedFromPlannerColumn` → literals | 3 of 7 fail |
| promotion target → `"in-progress"` | 3 of 7 fail |
| one rebound guard → `!== "todo"` | 1 of 3 fails (static shape) |
| resume lanes → legacy trio | 1 of 5 fails |

Every conversion is paired with a negative — a forward move, a
not-a-planner-lane card, a default-lineage card, an unresolvable
workflow — so neither "always fire" nor "never fire" can pass for
"resolve the role".

## Commit discipline

Twelve commits, each one thing: the code move (`resolvePlannerLanes` out
of `triage.ts`) is separate from every behavior change, and each review
fix is its own commit with its own revert proof.

## Verification

- `pnpm test:gate` **71/71**
- 162/162 across the glasses plugin's 19 files; 26/26 across the four
new engine suites
- engine + glasses typecheck clean; `pnpm lint` clean

🤖 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**
* Engine recovery and retries now work correctly with renamed or
customized workflow columns.
  * Tasks in manual-intake columns are no longer automatically planned.
* Agent actions and quick capture now respect each board’s declared
columns and lifecycle stages.
* Awaiting-approval tasks are recognized regardless of their current
column.
* Command Center SDLC funnel stages now accurately reflect customized
workflows.

* **Documentation**
* Added guidance for safely changing workflow-column logic and
interpreting lifecycle-column 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 00:52:55 -07:00
gsxdsm
9dbc98f1b3 Audit: every sync workflow-IR read answers for the DEFAULT workflow (not a PG-only problem) (#2653)
Docs only. This came out of a #2593 review thread that reported the
problem as PostgreSQL-specific. **It is unconditional**, and it has
consequences well outside the guard I was fixing — including one that
looks like a live production break for custom workflows.

## The chain, each link checkable

1. `TaskStore.getTaskWorkflowSelection(taskId)` delegates straight to
`getTaskWorkflowSelectionImpl` — **no mode branch** (`store.ts:2545`).
2. `getTaskWorkflowSelectionImpl` **returns `undefined`
unconditionally** (`workflow-definitions.ts:505-512`). Its own comment:
*"sync selection reader is incomplete-PG; use
getTaskWorkflowSelectionAsync."* A PG-cutover stub that never got
finished.
3. So `resolveTaskWorkflowIrSyncImpl` always takes its `if
(!workflowId)` branch and returns `resolveDefaultWorkflowIr()`. Its
`isBuiltinWorkflowId` and `SELECT ir FROM workflows` branches are
**unreachable in production**.

`resolveTaskWorkflowIrSync` is typed `WorkflowIr`, non-optional — so
callers cannot detect the substitution. There is no `undefined` to check
and the IR that arrives looks valid.

**Why tests don't catch it:** test stores stub
`getTaskWorkflowSelection` with a real selection, so the reader works
under test and substitutes only in production. Any test written against
a stubbed store proves the caller's logic and never the reader's
behavior.

## Consequences, severity descending

1. **Custom fields appear to be rejected on custom workflows.**
`resolveTaskCustomFieldDefsSyncImpl` returns `ir.fields` — the DEFAULT
workflow's. `task-update.ts:128-136` validates against them, and its own
comment states the outcome: *"a write against a workflow with no fields
(the default) is rejected with a typed CustomFieldRejectionError."*
2. **Per-workflow capacity pools collapse** —
`resolveEffectiveWorkflowIdSyncImpl` reads the same selection, so every
task resolves to `resolveCapacityPoolId(undefined)`.
3. **Plugin transition hooks re-run against the wrong IR**
(`lifecycle-ops.ts:1052`, crash recovery).
4. **Terminal-node detection degrades** to `nodeId === "end"`
(`branch-and-pr-entities.ts:578`).
5. **A U7 guard was inert** — fixed in #2593. Its fail-closed arm was
`workflowIr ? … : true`, dead code against a non-optional return.

**#1 and #2 are REASONED FROM SOURCE, NOT OBSERVED.** I did not execute
those paths, and I am labelling them that way in the doc rather than
reporting them as confirmed. No test in `packages/core` covers
`CustomFieldRejectionError` or `resolveTaskCustomFieldDefsSync` —
consistent with the gap, but absence of a test is not proof of a break.
**Reproduce before fixing.** I would rather hand you a labelled
hypothesis than a confident claim I did not verify.

## Why this matters for the fleet, specifically

The census work replaces column literals with trait lookups. A
conversion that resolves its traits through a **sync** reader produces a
guard that reads the DEFAULT workflow's traits for every task —
plausible, wrong, and invisible. **It converts a visible literal into a
hidden bug**, and the ratchet counts it as progress.

Suggested addition to the fleet brief: conversions must resolve through
`resolveWorkflowIrForTaskWithProvenance` and branch on `source`;
`resolveTaskWorkflowIrSync` is never acceptable in a converted guard.

## Not fixed here

Each consequence needs its sync call path made async — a real slice per
site, not an end-of-turn edit. #2593 fixed only the one that was mine.
Census unchanged (781 / triage 5); this PR adds and converts no guards.

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

## Summary by CodeRabbit

* **Documentation**
* Added an architecture-pattern finding documenting a workflow-reading
limitation that can cause synchronous reads to use the default workflow.
* Described resulting effects on custom workflow updates, crash
recovery, capacity-pool handling, and terminal-node detection.
* Documented testing gaps and guidance to avoid synchronous task
workflow reads.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:37:59 -07:00
gsxdsm
2771408bba ci: enforce the lifecycle-column ratchet — it has never actually run (#2654)
**The ratchet was advisory.** `scripts/lifecycle-column-census.mjs`
existed only as `pnpm census:lifecycle-columns` — without `--strict` —
and **no workflow invoked it**. Nothing has ever compared the tree to
the baseline. Every "the baseline ratchet holds them" assumption in this
program rested on a check that does not run.

That explains both classes of hole:

**1. Three PRs lowered counts without re-recording,** leaving allowances
the deleted guards could return through while every check stayed green.
I've tightened them across #2593 and earlier PRs, but nothing stops the
next one.

**2. #2621 GREW the count while its own title claimed "count 0 → 0".**
It added `column === "triage"` and `column === "todo"` at
`register-task-workflow-routes.ts:2681`, taking that file to **23
against an allowance of 22**. It landed unchallenged. This is the
failure mode the ratchet exists to prevent, and it happened *inside this
program*, in a PR that asserted the opposite.

## The change

Adds `check:lifecycle-columns` (the census with `--strict`) to the
`pr-checks.yml` lint job, next to `check:changesets` and
`check:routes-modular` — the established pattern. **~1.8s over ~1950
files**, so this is not a slow-test addition.

## Proven to fail, in both directions

A guard that reports success without checking anything is worse than no
guard, so:

| injected defect | result |
|---|---|
| `const __probe = (c: string) => c === "triage"` added to `moves.ts` |
`count ROSE — moves.ts: 39 -> 40`, exit 1 |
| run against main's current baseline | exit 1 on
`mission-feature-sync.ts: allows 5, tree has 0` |

Both reverted; exit 0 restored. Note the second row: **this check is RED
on main right now**, which is the point.

## Merge order

**Stacked on #2593**, which carries the `DELIBERATE-LITERAL` marker for
the #2621 site (a v1 IR declares no roles, so no trait can answer that
question) plus the baseline re-record. Standalone on main this PR is red
— correctly. **Merge #2593 first**, then this.

I stacked rather than duplicating those two edits because I already
caused one conflict today by appending related content from two
branches, and #2651 merged a correction ahead of the section it
corrected. Same-content edits in two PRs is the same mistake.

## Census

Unchanged by this PR: **776 total, triage 5, reviewed 16** — it adds no
guards and converts none. It only makes the numbers enforceable.

## For the fleet

This should land before the 776-guard fleet launches. The brief says
"the baseline ratchet must shrink by exactly the converted count" —
until now nothing verified that claim, so a batch worker could report a
shrink that did not happen, or grow the count while converting, and CI
would agree.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:20:31 -07:00
gsxdsm
5481c27729 docs(solutions): finding 6 — read the implementation before claiming its output is wrong (#2649)
Completes `proving-a-code-path-actually-runs.md` (merged as #2642) with
the rule its own author broke three times while writing it. **Docs
only.**

## Why this belongs in that document rather than a new one

Findings 1-5 are about proving **your own** claim: does this path run,
can this test fail, is this negative result observable. Finding 6 is the
mirror image — the claims we make against **other people's** work — and
it is the same underlying error pointed outward. Splitting them would
let a reader take the first five as "be rigorous about my code" and miss
that the identical discipline applies when reviewing someone else's.

## The three cases, all mine, all in one day

| What I claimed | What was actually true |
|---|---|
| The census undercounts triage guards, 13 vs 10 | `summarize()` counts
`byColumnId` only for `kind === "column"`. My patched counter summed
`role`, `status` and `deliberate` too. The three "missing" ones were
exactly the ones it classifies correctly — and I reported this against
the instrument the program had just adopted as authoritative. |
| `resolvePlannerLanesForTask` silently disables two recovery paths for
legacy cards — escalated across four messages | The file's own header
had already reasoned it through and documented why that answer is
correct. And `TaskStore` implements `getTaskWorkflowSelectionAsync`,
which the resolver prefers — so real projects never take the path my `{
getTask }`-only probe forced. |
| `executor.ts` is clean of triage guards | A receiver-specific grep
missed three under `from` and `originColumn`. Same error one step
earlier: trusting a reconstruction of the thing instead of the thing. |

Every one was: reconstruct behaviour from outside → compare to actual
output → find a difference → report a defect, **without reading the
implementation.**

## The rules it adds

- Read the implementation and its header comment before reporting
anything as wrong. On this codebase the reasoning is usually already
written down, and the FNXC note frequently answers the exact objection —
twice today it answered mine verbatim.
- **A fixture is not a measurement of production.** When a probe and the
real system disagree, suspect the probe: ask what it had to stub, and
whether production ever supplies that shape.
- Retract precisely and immediately. A false defect report against
shared infrastructure costs more than the bug would have — it sends
people to verify something already correct, and spends the credibility
needed for the next report that is real.

Also updates the count in the intro (five → six) and adds an
`applies_when` entry so the doc surfaces for "about to report a tool as
defective", which is when it is needed and not when someone is already
debugging.

`pnpm lint` clean. No changeset — internal documentation.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:07:06 -07:00
gsxdsm
0c07584d51 U11 fallout: disprove the coding-ideas column collapse, and correct a U11 note that recorded the merge backwards (#2651)
Two findings, no behavior change. Both are about **recorded reasoning
that was wrong** — the kind that sends the next person the wrong way.

## 1. The coding-ideas column collapse does not work (IR change
reverted)

I implemented it — deleted `ideas`, moved its `intake`/`autoTriage:
false` onto Planning, repointed the `start` anchor, updated the IR
suites to the merged shape (they went green, 44/44). Then the wider
suites failed and showed why it cannot work.

**The manual gate IS the column boundary.** `replan-target.ts` names the
discriminator in its own comment: *"The real discriminator is which lane
the triage service SCANS, which depends on the intake column's
`autoTriage` config."* So `ideas` is unscanned, `todo` is scanned, and
"promote" means moving the card from one into the other. Merge them and
one column must be both:

| if… | consequence |
|---|---|
| `autoTriage: false` wins | never scanned → nothing is ever planned →
the capacity hold releases an **unplanned** card into `in-progress`,
violating FN-7648 |
| scanning wins | `autoTriage: false` is meaningless → the manual gate
is gone → the preset duplicates the default Coding workflow |

**8 tests fail, and they are not fixtures** — they encode the promotion
flow itself, e.g. `store-create-intake-column.test.ts` › *"promotes an
Ideas-parked task to todo without planning it (still bootstrap-stub
PROMPT.md)"*. Rewriting them would have meant inventing what "promote"
means with no destination column, which is how a broken flow gets
blessed by a green suite.

**What it would actually take:** a promoted flag the triage scan reads,
so one column can hold both "not yet promoted" and "being planned". That
is a new lifecycle signal, not a column merge — the same shape as the
deferred `needs-replan` follow-up. Happy to scope it.

**I also corrected my own earlier checklist** in this doc, which said to
delete the now-dead `isUnplannedStartCreate` arm. Wrong: `autoTriage` is
a general trait field (`builtin-traits.ts`), so any custom workflow can
declare a manual intake with `intake !== hold`. The arm is dead only for
this preset.

## 2. `replan-target.ts` recorded the U11 merge backwards

The note claimed U11 deletes `todo` and keeps `triage`. It is the
reverse — Shape B kept the id `todo` and deleted `triage`, precisely so
the ~120 `column === "todo"` guards kept their meaning and no data
migration shipped. The default lineage now declares `todo, in-progress,
in-review, done, archived`.

The lookups are correct today, but **for the opposite reason to the one
recorded**: the default lineage falls *through* the `triage` lookup and
lands on `todo`, its merged planning column. `triage` still matches the
workflows that genuinely declare it (Lead generation, PR review).

Also flagged without changing (it would be a behavior change): the
`return "triage"` fallbacks on the no-match and throw paths name a
column the default lineage no longer declares, so a workflow with
neither `triage` nor `todo` gets a nonexistent target.

## Census

**Unchanged: 781 total, triage 5.** This PR adds no guards and converts
none — `workflowHasColumn(ir, "triage")` is a call argument, not a
comparison, so it is outside what the census counts either way.

## Verification

41/41 engine replan-target suites (including the existing
`replan-target-merged-planning-column` suite that covers the corrected
behavior) · engine typecheck clean · the reverted IR restores the tree
to main's content for those three files, verified by `git checkout --`.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:26:49 -07:00
gsxdsm
7ab6506c0f docs(solutions): proving a code path actually runs — the five ways U8 shipped code that never executed (#2642)
Durable write-up of U8's verification findings. **Docs only — no code
change, no CI risk beyond lint.**

These currently exist only in PR bodies, which nobody greps.
`docs/solutions/` is where this project keeps exactly this kind of
thing, and every one of the five will recur: the handler-pair shape and
the resolved-vs-guessed fork both have more call sites than U8 touched.

## The five

1. **Two prompt-node handlers exist; only one runs.**
`createDefaultNodeHandlers` prefers the primitives handler whenever
`deps.primitives` is set, and `executeWorkflowGraph` always sets it — so
every seam entry in `createAuthoritativeWorkflowSeams` is unreachable
for prompt nodes. A lifecycle announcement sat there through two PRs. It
type-checked and its unit tests passed, because a seam-level test calls
the seam object directly and therefore always can.

2. **A negative instrumentation result is worthless without a control.**
No output from an instrumented seam is only evidence once you have shown
writes from that module are visible under the harness. One
`process.stderr.write` at module load separates "never ran" from "output
swallowed" — opposite conclusions.

3. **Source-string ratchets prove syntax, not behavior.** Three were
torn down in review. The sharpest guarded a never-executed-code bug with
a source search, reproducing the bug one level up; measured, the
behavioural version fails an inverted dispatch and the textual one
passes it. Includes the sub-rules paid for the hard way: use the AST not
regex (a brace in a string truncated an extraction to 13 lines and every
count read a *passing* zero), guard the guard, anchor by index rather
than a character window.

4. **A green test on first try, on a path with no prior coverage, is a
warning.** Two conversions were reverted in one day because their tests
passed with the change reverted. Negative assertions succeed trivially
when the method returns early — `recoverCompletedTask` has seven guards
before the converted line, and the fixture has to satisfy all of them.

5. **A named workflow selection is not a resolved one.** Provenance
cannot be inferred from the returned value, because a fallback IR and a
valid id-less IR are structurally identical — the resolver that knows
has to report it. This is the fork every remaining lifecycle-column
conversion hits.

## Why this rather than another conversion

Everything left in my area is now owned and further along than I could
take it: `executor.ts` → #2628 (which solved the `recoverCompletedTask`
fixture I could not), `self-healing.ts` → #2560 (independently hit all
three traps I catalogued), the dashboard cluster → #2625/#2626/#2636.
Duplicating that would be motion, not progress. Turning findings that
cost real cycles into something greppable is the useful thing I can
still add.

`pnpm lint` clean. No changeset — internal documentation.

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

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

## Summary by CodeRabbit

* **Documentation**
* Added a best-practices guide for verifying that workflow code paths
actually execute.
* Covers reliable behavioral assertions, instrumentation controls,
regression-proof tests, source validation, and detection of fallback
behavior.


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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:59:24 -07:00
gsxdsm
8e211d1870 TAKING scripts/: parse instead of grep — an AST classifier for the lifecycle-column bar, cross-checked by a second implementation (#2633)
The program's completion bar is "`column === "triage"` reaches zero".
This measures what that bar actually covers, and checks the measurement
in so it cannot drift.

## The number, measured by the checked-in tool

```
lifecycle-column-census: scanned 1956 source files

  COLUMN guards (the backlog):   1031
  ROLE comparisons (not guards):   10
  DELIBERATE-LITERAL (reviewed):    4

  by column id:
     313  done
     217  in-review
     201  in-progress
     177  archived
      83  todo
      40  triage

  top files:
     151  packages/engine/src/executor.ts
     136  packages/engine/src/self-healing.ts
      50  packages/dashboard/app/components/TaskCard.tsx
      44  packages/core/src/task-store/moves.ts
      34  packages/dashboard/app/components/TaskDetailModal.tsx
```

**`triage` is under 4% of the class.** Every one of those 1031 sites is
the same defect: a lifecycle decision made by column NAME, which stops
matching the moment a board renames a column. The bar can be met in full
while 991 identical guards remain — and two files hold a quarter of
them.

## The tracked count is wrong in three directions at once

Each of these cost real work this week, which is why this is a PR and
not a comment.

1. **Vocabulary.** It measures one of six legacy ids.
2. **Receiver.** It is anchored on locals named
`column`/`toColumn`/`fromColumn`, so it never saw the three real guards
in `executor.ts` written against `from` and `originColumn`. One of those
meant completed-but-stranded work was never recovered on a renamed
board, with nothing else owning that state (converted in #2628).
3. **Collision.** `role === "triage"`, `agentType === "triage"`,
`entry.agent === "triage"` compare an **AGENT ROLE**. The planner *lane*
is named `triage` and keeps that name — U11 removed the *column*. Ten
such sites were counted as backlog, and the "obvious" fix (renaming the
role) silently empties the planner's prompt template and mis-binds its
model markers.

A count that is too high and too low simultaneously sends work to the
wrong files while hiding the files that need it. So the census reports
**three separate numbers** and never nets them.

## Proven to fail on the original defect

Not asserted — exercised:

```
$ # reintroduce `task.column === "triage" || task.column === "todo"` into live-agent-count.ts
$ node scripts/lifecycle-column-census.mjs --strict; echo "exit=$?"
  packages/core/src/live-agent-count.ts: 10 -> 12
exit=1
$ # restore the file
$ node scripts/lifecycle-column-census.mjs --strict >/dev/null; echo "exit=$?"
exit=0
```

The CLI also exits 1 when its own file list comes back empty — a guard
that reports success without checking anything is worse than no guard.

## 12 regression cases, split by what they defend

Must catch: all six ids; a guard on a local named `from`/`originColumn`
(verbatim the executor.ts shape); single quotes; negation; several
comparisons on one line.

Must **not** catch: role comparisons; comment prose (two tracked
"guards" in `replan-target.ts` were prose about a filter that lives in
another file); a trailing `// … === "triage"` on a code line; sites
carrying a `DELIBERATE-LITERAL` marker.

Plus: **one marker cannot launder a distant guard in the same file** —
that is how allowlists rot.

## Report-only, deliberately

`--strict` compares per-file counts against
`scripts/lib/lifecycle-column-census-baseline.json` and fails when any
file's count **rises**. It is **not** wired into the merge gate: a
thousand-site backlog cannot be a blocking check the day it is first
measured, and a guard nobody can pass is a guard everyone disables.
Owners tightening their own area re-record the baseline in the PR that
lowers it. This is the ratchet shape the `DELIBERATE-LITERAL` markers
scattered through the program already anticipate.

## Stated limitation

Classification is by receiver **name**, so a future field named `agent`
that holds a column would be misclassified as a role comparison.
Recorded at the site, and it is precisely why the two classes are
reported separately instead of netted into one figure.

## Verification

- 12/12 new cases
(`packages/engine/src/__tests__/lifecycle-column-census.test.ts`)
- `pnpm test:gate` **71/71**; `pnpm lint` clean
- `pnpm census:lifecycle-columns`, `--json`, and `--strict` all
exercised end to end
- documented in `docs/testing.md`; no production code touched

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:52:18 -07:00
gsxdsm
6d10683dbd docs(solutions): store fakes that lie — six fixture defects that each looked like a production bug (#2534)
Six consecutive slices of U7 produced **six test-fixture defects, and
every one first presented as a bug in the code under test.** Not one was
real.

Each cost 15–60 minutes debugging the wrong file. **Two would have
shipped a false green** — a test passing while asserting nothing — if
the failure had happened to look plausible rather than implausible.

This is not a story about carelessness. Every one of these fakes was
modelled on an existing fixture in this repo, and the repo's fixtures
are inconsistent about exactly the things that matter.

## The catalogue

| # | Defect | How it presented | Real cause |
|---|---|---|---|
| 1 | `moveTaskIf` ignores its predicate | Test passed; in-txn guard
untested and indistinguishable from absent | Fake never invoked the
callback |
| 2 | `updateTaskAtomic: vi.fn()` never invokes its callback | *Every*
finalize bailed before the branch under test | Success is derived from
whether the callback ran |
| 3 | Harness default parameter swallows the input | "Task vanished"
case became a duplicate of the control | `harness(undefined)` triggers
the default |
| 4 | `logEntry: vi.fn()` returns `undefined` | Sweep appeared to match
only one column | `.catch` on a non-promise throws, aborting the loop
after item one |
| 5 | Harness lets `poll()` reach the real `specifyTask` | **exit 1 with
every test green** | Real agent path threw *asynchronously*, after
assertions passed |
| 6 | `updateTask: vi.fn()` returns `undefined` | Branch "did not run" |
Same as #4 |

**4 and 6 are the same shape, found a week apart, because nothing
prevented the second.** That is the argument for writing this down.

## The three rules

1. **Every store method a fake exposes returns what the real one
returns** — overwhelmingly a promise. Production writes `await
store.m(...).catch(h)` as a fail-soft idiom; `.catch` on `undefined`
throws a `TypeError` that unwinds into a broad *"never let housekeeping
break the poll"* handler and vanishes. Symptom is never "your fake is
wrong" — it is *"the loop only processed the first item"*.
2. **A fake handed a predicate or callback must invoke it.** Ignoring it
makes the guarded and unguarded implementations *indistinguishable*, so
a test named for the guard cannot detect the guard's removal. Includes
the `onLockedRead` hook, without which an in-transaction recheck stays
untestable even once the predicate is invoked.
3. **Stub the agent-dispatch boundary.** `poll()` ends in "start an
agent", which in a unit test throws *after* the test resolved — `17
passed`, exit code 1, which on CI reads as infrastructure noise.

> Never accept a non-zero exit on a green run. It is the only signal
that something escaped your assertions entirely.

## Also covered

- **How to spot a fixture defect fast** — the tell is *failing for the
wrong reason*. Three concrete checks before you open the production
file.
- **Why differential tests earn their keep** even when they feel
redundant: the default-vocabulary half doubles as a fixture self-check,
because it asserts behavior that is by definition already shipping. On
this program, "both halves failed" was the signal that found three of
the six.
- **The connection to guards that cannot fire** — six of those on this
program too, including a ratchet I wrote that matched only a
double-quoted literal (#2527). Same discipline either way: *prove the
check fails on the thing it claims to catch before trusting that it
passes.* Including the warning that one ratchet injection silently
failed to apply, leaving a green run that would have "proven" the
ratchet worked.

## The concrete next step, stated plainly

A shared `createTaskStoreFake({ tasks, workflowIr })` with
promise-resolving, callback-invoking defaults would remove this whole
class in one small PR. **It is not built here** because it is cross-unit
and needs adopters — building it inside U7 and hoping others find it is
how conventions die. The doc says: if you are about to hand-roll a
seventh store fake, build the helper instead and link it.

Docs-only; no changeset (AGENTS.md excludes internal docs).

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


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

## Summary by CodeRabbit

* **Documentation**
* Added guidance on six store-fake defect patterns that can resemble
production bugs during testing.
* Documented best practices for creating reliable store fakes, including
promise handling, callback invocation, and async dispatch isolation.
* Added diagnostic techniques for distinguishing fixture issues from
genuine application defects.
* Included guidance for validating production guards and links to
related documentation.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:42:36 -07:00
gsxdsm
73338502e5 fix(test) + E2E: re-green main's lifecycle release leg, and prove the MERGED board + REVISE rework (#2634)
**Second batch.** Three commits, no production code. `pnpm test:gate`
green, `pnpm lint` clean, all three E2E suites together **3 files / 41
tests, exit 0**.

## 1. main's lifecycle E2E is RED right now — this fixes it

Independently of my work, on a detached `origin/main`: **2 failed / 18
passed**. Scenarios 1 and 2 fail with `sweep.released` **empty**.

**Cause:** `seedTask` relied on task creation's PROMPT.md, which is a
bootstrap seed (`"# <id>\n\n<description>"`). FN-7648's
`isUnplannedForExecution` reads that file for any card resting in an
intake- **or** hold-trait column and refuses to move an unplanned card
into a processing column. The sweep reported `held: [{ reason:
"move-rejected-or-no-slot" }]`.

**That is the gate working.** The fixture was asking the scheduler to
release a card that had never been specified. The fix is the one the
graph-entry contract doc already prescribes: *"Scheduler/release test
fixtures must model a card that cleared the gate ... A held unreviewed
card is the gate working."* `seedTask` now writes a planned PROMPT.md.

**Verified it repairs main, not just this branch:** applying only that
file to a detached `origin/main` leaves scenarios 1 and 2 **passing**,
with the 4 residual failures being scenarios 3 and 6 — which need the
fixture-options commit main does not have.

### I was wrong in #2627 and this corrects it

In #2627 I named the in-transaction capacity gate (#2488/#2499) as the
likely cause. **It was not.** Two hypotheses died, both recorded in the
code comment so nobody re-runs them:

| Hypothesis | Result |
|---|---|
| E2E settings lack `maxConcurrent` → capacity gate rejects the move |
added `maxConcurrent`/`maxWorktrees` → **still 2 failed**. Not the
cause. |
| the move itself is refused | a direct `moveTask(id, wip)` →
**succeeded**. Never the blocker. |

Only then did probing the two release gates give
`isTaskBlockedOnApproval=false`, `isUnplannedForExecution=true`, and
dumping the file show the stub. I've flagged the wrong lead on #2627 too
— a plausible-sounding cause pointed at another worker's PR is worse
than no lead.

## 2. E2E evidence: the MERGED intake+hold board

U11's shape — one column carrying intake **and** hold — had no
end-to-end coverage; every prior E2E drove intake and hold as separate
columns.

- shared fixture gains opt-in `mergedIntakeAndHold`, plus `MERGED_VOCAB`
(legacy ids, so a failure is attributable to the **role** merge alone)
and `MERGED_RENAMED_VOCAB` (ids move too).
- lifecycle scenario 3 drives the full spine: planning runs **in place**
on the dual-role column, the real `runHoldReleaseSweep` releases
**from** it, the graph runs to complete.
- 4 merge-safeguard cases on the merged board (finalize, proofless
refusal with the same reason, merged+renamed landing no legacy id,
at-most-once).

## 3. E2E evidence: a REVISE routes back through rework

The plan's `InReview → InProgress: review requests changes` had **no**
live-engine evidence on any board — the fixture's review seam always
succeeded.

Two things the engine taught me, both corrected here:
- the **IR validator refused** my rework edge: it is only legal into a
node with `config.reworkRegion: true`. A real contract, and the
validator catching it is the system working. `exec` now declares it (the
shape the builtin uses on `merge-attempt`).
- my first assertion was wrong. A REVISE does **not** leave the card in
wip — rework re-enters `exec` within the same run, review approves on
its second call, and the card finishes at complete. The evidence is the
**seam sequence**
`["planning","execute","review","execute","review","merge"]`, not an
intermediate column the run has already passed. Asserting the final
column alone would have been satisfied by a graph that ignored the
REVISE entirely.

## Both families are mutation-attributed

| Scenario | Mutation | Result |
|---|---|---|
| 3 — merged intake+hold | `isHeldTask` treats intake/hold as exclusive
| **exactly its 2 tests** fail |
| 6 — REVISE → rework | disable rework re-entry in
`workflow-graph-executor` | **exactly its 2 tests** fail |

Both fixture options are opt-in; the two pre-existing suites are
behaviourally unchanged (27 → 29 → 41 passed across the additions, no
existing assertion touched).

## Still not shipped: safeguard 2's graph E2E

Attempted twice, deleted both times. Attempt 1 passed and then survived
mutating `merge-gate` to ignore `task.autoMerge` — the card parked on
the review column's `merge-blocker` trait, not the gate. Attempt 2
removed that trait to isolate the gate, and the **control** case parked
too. Isolating it needs a merge path mirroring the builtin (`merge-gate
→ merge node → end`) rather than a direct edge to `end` — a real
redesign, not a speculative edit. The enforcement that holds today is
`allowInReviewMergeProcessing` in `project-engine` (unit-mutation
verified, NEW=9; gated via #2526).

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:23:32 -07:00
gsxdsm
9a11e0b136 U2b reproduction: the live move path accepts the column U11 deleted (characterized, not patched) (#2601)
Found while proving U11's caveat 2. **Characterization plus guard-rails
— no production change, deliberately.**

## The defect

A default-workflow card in Planning can be moved **into `triage`** — a
column its workflow no longer declares — re-creating exactly the
stranded state `reconcileUndeclaredTaskColumns` exists to repair.

Measured on a fresh store:

```
experimentalFeatures.workflowColumns   null            ← no production writer
createTask(...)                        column = "todo"
moveTask("todo" → "triage")            ACCEPTED
moveTask("todo" → "bogus-column")      REJECTED: "Valid targets: in-progress, triage, archived"
```

The second rejection is the tell. Validation is real — but it is the
**legacy `VALID_TRANSITIONS`** table talking, and that table does not
know the card's workflow. Its `todo` row still lists `triage`.

## Why the workflow-aware check does not run

`moves.ts` gates its adjacency block — including
`workflowHasColumn(workflowIr, toColumn)` — on
`isWorkflowColumnsCompatibilityFlagEnabled`, which reads the raw
`experimentalFeatures.workflowColumns` key. Nothing writes it, so the
block is dead on the path every real project takes.

**Corollary, already reported:** U11's undeclared-source escape hatch in
`resolveAllowedColumns` also does not run in production. It was added
with #2515 so a stranded card would have a legal move instead of `Valid
targets: none`; on the live path that rescue comes from the legacy table
instead. Mutation-verified — stubbing the hatch back to `[]` leaves the
operator-move test green.

## Why I did not fix it

PR #2499 un-gated the capacity check and **explicitly scoped validation
out**:

> SCOPE, deliberately narrow: only the CAPACITY check is un-gated.
`workflowIr` stays flag-gated so transition VALIDATION keeps its current
behavior — the inline path's bare-Error/"Valid targets:" contract is
unchanged, and none of the Phase A2 divergences are flipped here.

That is a considered decision by the owner of this function, and several
suites pin the contract it protects. Overriding it from outside would
flip an error shape I do not own.

**What has changed since that decision is U11:** the legacy table now
offers a target the default workflow does not declare, which it never
did before. That is new input to the scoping call, not licence to ignore
it — so this lands as a reproduction for U2b rather than a patch.

U2b's branch (`feature/workflow-move-path-convergence`) is stale — HEAD
predates several merged PRs, clean tree — so nothing is being raced.

## What ships

The defect is **characterized, not asserted-as-correct**: the test pins
today's behaviour so it is visible and measurable, and an `it.todo`
states the intended behaviour. Writing it as a passing "refuses" test
would have required the fix; writing it as a failing test would redden
CI; asserting the current behaviour as *correct* would be a lie.
Characterization plus `it.todo` is the honest third option.

Four guard-rails pin what a fix must **not** break:

- every declared lifecycle move (`todo → in-progress → in-review →
done`)
- archiving
- a `recoveryRehome` deliberately reaching an undeclared column — the
path that rescues already-stranded cards, and the one a careless fix
would break
- a premise test asserting the compatibility flag really is unset, so
the suite fails loudly if that ever changes rather than silently testing
a different code path

## Exposure

Narrow but real. U10 already fixed the dashboard move menu to offer only
workflow-declared targets, so the board does not present this. The
**write path** does — REST API, CLI, plugins, any stale client — which
is why the guard belongs in `moves.ts` rather than only in the UI.

5 passed + 1 todo; lint clean.

🤖 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 coverage for task moves involving workflow-declared and
undeclared columns.
* Documented a known issue where tasks can currently be moved into the
deleted `triage` column.
  * Preserved valid moves, archiving, and recovery re-homing behavior.

* **Documentation**
* Added reproduction steps, affected move paths, and guardrails for
addressing the issue.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:12:57 -07:00
gsxdsm
30e0a8f291 U11 P0 audit: no hard stall in the recovery block — and one obvious fix is wrong (#2570)
Docs only. Answers the P0 question per site: **does it still fire, what
silently stops happening, is there a backup?**

## Headline: no hard stall

The alarming reading — *"the orphaned-planning-status sweeps stop
finding default cards, so a card whose planner died sits with
`status:"planning"` forever, invisible to discovery"* — **does not
hold.**

`triage.ts`'s `sweepStalePlanningStatuses` is the **periodic primary**
for that repair and already tests `column !== "triage" && column !==
"todo"`. It covers the merged column. The two self-healing sweeps
perform the same repair and are **redundant nets**, not the sole rescue.

That is the difference between a P0 and a cleanup, and it is only
visible by reading the **backup** path rather than the broken guard.
Recorded so nobody re-derives the panic.

## Self-healing block, by blast radius

| site | fires? | what stops | backup | verdict |
|---|---|---|---|---|
| `:12106`, `:12427` | no | clearing a stale `planning` status |
`triage.sweepStalePlanningStatuses` | redundant net lost — **cleanup** |
| `:2961/2981/3016` `recoverAdvancedTriageTasks` | no | re-homing a card
with a worktree + durable IR pin to its **pinned** resume column |
hold-release still releases it on capacity (real spec ⇒
`isUnplannedForExecution` false) | **degraded, not stuck** — fix first |
| `:12254` | no | a bounded priority nudge | none needed; the doc says
nudge, not rescue | **low** |
| `:12151`, `:9151` | **yes** | — | already OR-paired | **safe** |

**Second-order trap at `:3016`.** It skips when `resumeColumn ===
"triage"`, guarding against resuming a card into the column it already
occupies. Post-merge the pinned column is `todo`, which is **not**
skipped — so pairing the literal at `:2961` *without* also pairing
`:3016` produces a `todo → todo` move. **Repair the three together.**

## Two sites in the ownership split are already handled

- **`usage-limit-detector.ts:126`** (assigned to u8) — already fixed in
**PR #2567**. Real breakage: the planning lane stopped being recognised,
so a card being planned was neither parked when its provider hit a usage
limit nor resumed when it recovered.
- **`spec-staleness.ts:95`** (assigned to u7) — already proven safe
as-is, merged with #2515. **Its obvious fix is wrong.** I tried `||
task.column === "todo"` and it turned an existing test red: it breaks
the parked-preserved-progress path.

## The generalisation, which is the most useful thing here

**On the merged column, `todo` answers two different questions.**

After the merge `todo` is both the planner column *and* the
capacity-hold column. So any site that used `triage` to mean *"is being
planned"* **cannot simply be paired with `todo`**, because `todo` also
means *"is parked waiting for capacity"*. Those sites need **status or a
trait**, not a wider literal.

That is precisely the mistake a bulk conversion makes, and
`spec-staleness.ts` is the worked example: the guard was already asking
status, and widening the column would have destroyed the distinction.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 19:03:23 -07:00
gsxdsm
67904f8a2c U11: merge Todo into Planning on the default lineage (+ the migration mechanism, and a measured safety audit that cuts the work list 32%) (#2515)
**Merges Todo into Planning on the operator's real default workflow.**
Held from merge pending the `triage` literal audit below — see *Gating*.

## The board change

`builtin:coding` → `BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR` →
clones `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`. That IR now declares
**five** columns, and `plan`, `plan-review`, `plan-replan` and `start`
all live in the merged Planning column:

```
columns: todo="Planning", in-progress, in-review, done, archived
  start -> todo      plan       -> todo
  plan-review -> todo  plan-replan -> todo
  parse -> in-progress            (first implementation node)
```

The id stays `todo`, the display name becomes "Planning". That is the
cheaper half: `todo` was already the hold column, so every trait lookup,
task row, stored selection and the 121 `column === "todo"` guards keep
their meaning, and **no stored row needs re-homing**. Promoting `triage`
instead would have produced the same board while making those guards
workflow-*dependent* — live for Coding (Ideas), silently dead for
Coding.

`builtin:legacy-coding` keeps its six-column shape, per the operator's
decision. It exists to be the old thing.

## Entry contract, before and after each IR edit

| | result |
|---|---|
| before the default-lineage edit | **15 passed** |
| after the edit | **13 passed, 2 failed** |
| after reading both | **15 passed** |

Neither failure was routed around. One was a genuine expectation change
(two planning entry points became one); the other was my own
`mergeTodoIntoPlanning` helper throwing *"source IR is not the
split-column shape this merge transforms"* — because production **is**
the merged shape now. I **deleted** the helper rather than making it
tolerant: a transform that has silently become a no-op asserts nothing.

## The safety argument, proven not asserted

Entering at `start` is exactly what dragged cards backward in the three
earlier reverted attempts. `merged-planning-start-node-no-move.test.ts`
proves against the **real** boundary controller and **real** default IR
that entering `start` performs no move (`moveTask` is never *called*),
reaches no hold→wip capacity seam, and **still moves on a genuine
crossing** so the no-op is same-column rather than a disabled boundary.
Removing the controller's same-column short-circuit turns exactly the
two no-move tests red.

## The migration mechanism

A card can outlive its column. `resolveAllowedColumns` derives targets
from graph adjacency, and an undeclared source has none — so it returned
`[]` and **every** move was rejected with "Valid targets: none",
including the one that would rescue the card. An undeclared source now
resolves to the workflow's rebound target. Escape hatch, not relaxation:
declared columns are untouched, and it offers the rebound target *only*,
so a stranded card gets back **into** the lifecycle rather than a free
jump past review.

## A real regression this surfaced

`isDefaultWorkflowColumns` matched the legacy **six** ids as a set. The
merged default declares five, so the match stopped firing and the
default board fell through to neighbor-only adjacency, which **drops
legal moves and invents an illegal one**:

| edge | effect |
|---|---|
| `in-progress → done` | **dropped** — the mission-validation cross edge
|
| `in-review → todo` | **dropped** — review work back to planning |
| `todo/done → archived` | **dropped** — the FN-4892 direct-archival
edges |
| `done → in-review` | **invented** — a backward edge no rule allows |

Adjacency now derives from lifecycle **roles**. The load-bearing
assertion: the legacy six still reproduce `VALID_TRANSITIONS`
**verbatim**. Applied only when a workflow declares the full role set,
so custom boards keep neighbor adjacency.

## Failure accounting (core package, vs a 49-failure baseline)

| stage | failed | new |
|---|---:|---:|
| after the merge | 65 | 18 |
| after the escape hatch | 52 | 5 |
| after role-derived adjacency | 53 | 4 |

The 4 remaining are 3 `builtin-workflows` expectations encoding the
pre-merge shape and 1 create-intake expectation naming `triage` on
`builtin:coding`.

Two `schema-applier` and two `workflow-reconciliation-production-shape`
failures appeared in intermediate runs and are **not mine** — both files
pass in isolation (75/75 and 7/7). I re-ran each before attributing
them, which is why the earlier "priority" flag on the reconciliation
pair was withdrawn.

Gate: **309/309**. Lint clean.

## Gating: the `triage` audit
(`docs/solutions/architecture-patterns/u11-triage-literal-safety-audit.md`)

Program tracking cited **58** `triage` comparisons. Measured with the
same pattern:

| | count |
|---|---:|
| raw comparisons | 87 |
| inside comments | 1 |
| **not a lifecycle column at all** | **15** |
| column comparisons | 71 |
| OR-paired with `"todo"` in the same expression | 32 |
| **exclusive `triage` — the real work list** | **39** |

**15 do not compare a column.** `role === "triage"`, `surface ===
"triage"`, `sessionPurpose === "triage"`, `entry.agent === "triage"`
name the planning **agent**. Converting them would be actively wrong,
and the failure — a planning agent that can't resolve its prompt
template — would look nothing like a column bug.

**One site changes an operator-visible affordance**, which is why
per-site review beat a sweep:

`TaskCard.tsx:1927` — `taskColumnFlags?.intake === true && task.column
!== "triage"`. The literal is a **narrowing**, not a match. After the
merge a Planning card has `intake === true` and `column === "todo"`, so
the narrowing stops applying and **Start begins rendering on default
Planning cards where it previously did not.** A sweep would have
"converted" the literal and shipped the new affordance silently.

These guards do not go **dead**, they go **workflow-dependent** —
`triage` stays live for legacy-coding, Ideas, every linear built-in and
any user workflow (R11) — which is harder to detect than dead.

Work list and ownership are in the audit doc.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:39:20 -07:00
Phil Larson
b85a5d4531 fix(core): bound compound engineering review remediation (#2532)
## Summary

- cap Compound Engineering Code Review remediation at two Execute→Review
repair passes
- enable no-progress detection for the built-in CE workflow
- preserve explicit project/workflow overrides while making the authored
CE default visible in settings and docs
- update stale IR/changeset language that still described Code Review as
unbounded when unset

## Why

The previous CE default was effectively unbounded. A reviewer that
repeatedly returned `REVISE` could consume thousands of remediation
cycles without terminally parking the task. The built-in workflow should
fail closed after a small, explicit budget while still allowing
operators to author a different numeric cap.

## Verification

- `FUSION_PG_TEST_SKIP=1 corepack pnpm@10.33.0 --filter @fusion/core
exec vitest run src/__tests__/builtin-workflows.test.ts` — 46 passed, 17
skipped
- `corepack pnpm@10.33.0 --filter @fusion/core typecheck`
- `corepack pnpm@10.33.0 --filter @fusion/dashboard exec vitest run
app/components/__tests__/WorkflowSettingsPanel.test.tsx
app/components/__tests__/workflow-setting-display.test.ts` — 33 passed
- `corepack pnpm@10.33.0 --filter @fusion/dashboard typecheck`
- `corepack pnpm@10.33.0 changeset status --since=origin/main`
- `git diff --check origin/main...HEAD`


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

- **Improvements**
- Compound Engineering Code Review now caps remediation attempts at 2;
after two unsuccessful attempts, the process parks instead of retrying
indefinitely.
- Post-restart review recovery now completes in a single maintenance
cycle to reduce delays.
  - Default post-review fix budget increased from 3 to 10.
- Review revision limits now consistently honor workflow-authored
defaults when settings are left empty, and `0` disables automatic
remediation.

- **Documentation**
- Updated the workflow editor, settings reference, workflow steps, and
operator panel text to clarify cap/default/disable semantics (including
CE: 2).

- **Tests**
- Added/updated unit tests to validate the new bounded remediation
behavior and messaging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 00:05:29 -07:00
gsxdsm
99be8e6153 docs(U9): correct the safeguard baseline — safeguards 1 and 4 are NOT covered (#2520)
**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>
2026-07-28 22:19:35 -07:00
gsxdsm
aeef592187 docs(U9): safeguard baseline — six merge safeguards verified by mutation (#2511)
**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>
2026-07-28 17:15:50 -07:00
gsxdsm
6ee20d9817 docs(U9): correct merge-stack slice statuses to measured wiring state (#2504)
**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>
2026-07-28 17:15:16 -07:00
gsxdsm
d873ee8160 docs: correct KTD-6 — the "flag-off" move path is live, the hooks path is dead (#2466)
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>
2026-07-27 13:28:26 -07:00
gsxdsm
3c5d07ab01 docs: workflow-owned lifecycle program plan + column-placement contract (#2463)
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>
2026-07-26 23:43:45 -07:00
gsxdsm
0e3d2a2265 refactor: delete meta-task auto-archive and automated recovery follow-ups (#2461)
Deletes two pieces of automated "meta" machinery that filed and
garbage-collected cards restating state already on the task that failed.
Net **-1015 lines**.

## Why

**Automated recovery follow-ups.** `createAutomatedFollowup` and its
dedup engine (289 lines of signature matching, 1h recurrence
rate-limiting, 24h supersedes windows) existed to file recovery cards
for verification-cap and merge-conflict give-ups. In both cases the
parent is *already* parked `failed` with a descriptive `error` and a log
entry carrying the failing command, branch, and output — the card was a
second copy of that.

**Meta-task auto-archive.** The sweeps that garbage-collected those
cards were worse than redundant: the regex classifier matched ordinary
feature work, and its positional fallback bound cards to unrelated
tasks, so **live work could be archived**.

They are removed together, because the auto-archive sweeps only existed
to clean up after the follow-up engine.

## What changed

### Deleted
- `packages/engine/src/verification-followup-dedup.ts` in full —
`createAutomatedFollowup`, `decideAutomatedFollowup`,
`AutomatedFollowupKind`, `computeVerificationFailureSignature`,
`extractFailingTestFiles`.
- `findActiveRecoveryFollowUp` — dead code, defined and never called
(`tsc` independently flagged it `6133 declared but its value is never
read`).
- The meta-task auto-archive sweeps `autoArchiveResolvedMetaTasks` /
`autoArchiveStalledMetaTasks` and helpers `classifyMetaTask` /
`resolveMetaTargetTaskId` / `computeMetaChainDepth` / `archiveMetaTask`
/ `evaluateMetaAutoArchiveGuards`, plus settings
`metaTaskStallAutoCloseMs` and `metaTaskActiveExecutionGraceMs`.
- Run-audit types `task:auto-archived-meta-resolved`,
`task:auto-archived-meta-stalled`,
`task:auto-archive-meta-resolved-skipped`,
`task:auto-archive-meta-stalled-skipped`,
`verification:followup-created`, `verification:followup-deduped`.

The two signature helpers were **deleted rather than relocated** — once
the three call sites went they were provably unreachable:
`buildVerificationFailureSignature` had exactly one caller, and it was
the only caller of `extractFailingTestFiles`.

### Call sites 1 and 2 — park kept, card dropped
Verification-cap and merge-conflict give-ups keep their park, audit
event, operator comment, and log entry. Site 1's `error` string was
reworded off `"See follow-up task for investigation."` (no follow-up
will exist) to carry the guidance itself. `autoResolveDisabled` was
**kept** — it still drives the outer park guard and the `reason` string;
only the inner branch that guarded card creation is gone.

### Call site 3 — autostash orphan, replaced not deleted
This one is a genuine data-loss guard, so it keeps a durable trail. A
`live`-classified orphan is a merger stash holding **real uncommitted
work**, and unlike sites 1–2 there is no parked parent — the parent may
already be `done` and merged, so nothing else on the board would ever
mention the stash.

The card is replaced by a `logEntry` **and** an `addTaskComment` on the
parent, preserving every fact the old description carried: the sha,
`record.label` (the handle `git stash` recovery needs),
`record.detectedByTaskId`, and `sourcePhase`. New truthful run-audit
event `task:autostash-orphan-live-detected` replaces the borrowed
`verification:followup-*` name, with ids/outcomes-only metadata per
AGENTS.md.

### Kept unchanged: the two real product features
Eval follow-ups (`eval-followups.ts`) and PR-comment follow-ups
(`pr-comment-handler.ts`) only borrowed the shared engine for its dedup
pass. Both keep their exact behavior, column, priority, `sourceType`,
and log lines, with dedup inlined as a `listTasks` scan on
`suggestionId` / `prNumber` respectively. Both fail open (create) if the
listing throws, matching the old engine.

## Test changes — read this one

Two tests asserted the *deleted* engine's rate-limited `"[verification
recurrence]"` logEntry. Those assertions were removed, **not loosened**:
both tests still assert no duplicate card is created, and the eval test
still asserts the existing id is reported back. No coverage of surviving
behavior was weakened. The three `meta-*` test files were deleted along
with the sweeps they covered.

## Verification

```
$ pnpm test:gate
 Test Files  2 passed (2)     Tests   10 passed (10)    # core
 Test Files  16 passed (16)   Tests  299 passed (299)   # engine-core
 Test Files  1 passed (1)     Tests   70 passed (70)    # ci-shape
GATE_EXIT=0

$ pnpm --filter @fusion/engine --filter @fusion/core exec tsc --noEmit -p tsconfig.json
TSC_EXIT=0   (no output)
```

Plus a file-scoped run over the touched surfaces (`eval-followups`,
`pr-comment-handler`, `merger-autostash-orphan-surface`,
`merger-autostash-cleanup`, `run-audit`, `run-audit-secret-taxonomy`,
`project-engine`, `project-engine-manager`): **213/213 passed**.

A repo-wide grep confirms no surviving references to any deleted symbol,
module, or audit event.

🤖 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**
* Failed tasks now retain recovery and verification details directly on
the original task instead of generating separate follow-up cards.
* Live autostash issues now preserve stash information in task comments
and activity logs.
* Existing evaluation and pull-request follow-ups continue to be reused
when appropriate.

* **Changes**
  * Removed automatic archival of meta-tasks.
  * Removed obsolete meta-task timing settings.

* **Documentation**
* Updated architecture and settings documentation to reflect these
workflow changes.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:38:58 -07:00
Victor Canô
d6c917d726 feat(dashboard): add view and settings-section enumeration API (#2453)
## Summary

Gives external integrations (command palettes, plugin launchers,
alternate dashboard shells) a supported way to **discover the host UI**
— instead of hardcoding the dashboard's view ids, labels and settings
search terms and hand-syncing them on every release. This is the
read-only metadata slice of the "constrained by a stable host context
and API client" idea in
`docs/proposals/2026-07-01-dashboard-theme-plugin-system.md`, and the
follow-on to #2415 (theme tokens + overlay layering).

Two additions, both inert unless called:

| Endpoint | Returns |
|---|---|
| `GET /api/views` | Every registered built-in view id, in dashboard
order — `id`, English `label`, plus optional i18n `labelKey`, legacy
`aliases` and `internal` flag. |
| `GET /api/settings/sections` | Selectable Settings sections — `id`,
`label`, `labelKey`, `scope`, `group`, `keywords`, `searchableKeys`,
`advanced`. |

Both are read-only, return static project-independent metadata, take no
project id, and are mounted inside `createApiRoutes` so they sit behind
exactly the same `/api` authentication as every other dashboard route —
no more, no less.

## What actually changed — one source of truth

The endpoints are the small part. The core of the diff is **collapsing
duplicated UI metadata into two shared registries that now drive both
the dashboard UI and the API**:

- `packages/dashboard/src/shared/dashboard-views.ts` — canonical view
ids + English labels + i18n keys + legacy aliases.
- `packages/dashboard/src/shared/settings-sections.ts` — canonical
settings sections + scope/group/search metadata, with `group` and
`advanced` derived from the list's own structure.

`LeftSidebarNav`, `SettingsModal` and `useViewState` were rewritten to
consume those registries instead of carrying their own copies (net
**−230 lines** in `SettingsModal` alone). Edit the registry and the
rendered UI and the API move together.

## Drift protection

Being precise about what each test can and cannot catch, because
"no-drift" claims are easy to overstate:

- `left-sidebar-nav-registry-parity.test.tsx` — the one test that
catches drift the registry does not already determine. It **renders**
the sidebar with a recording `t()` spy and pins each entry's translation
key and English fallback to the registry (the sidebar still hardcodes
its keys). It also asserts the rendered destination count equals the
enrolled id list, so a newly added sidebar view fails until it is
enrolled.
- `ui-metadata-sync.test.ts` — pins the Settings navigation list,
advanced-visibility set, persisted view list, reset-key registry and
both endpoint payloads to the registries. Since those consumers are now
*derived* from the registries, these assertions mainly guard against a
future consumer **re-hardcoding** its own copy. Two of them do stand on
their own: each section's served `group` is pinned to the group header
it actually renders under, and no published `labelKey` may resolve to a
non-leaf i18n node.
- `register-ui-metadata-routes.test.ts` — drives the real Express router
and asserts each endpoint serves the registry payload verbatim, with no
filtering or reshaping.
- Exactly two **existing** tests are updated, both for the same reason:
they asserted that `SettingsModal.tsx`'s *source text* contains a
section literal that now lives in the registry.
`VoiceInputSection.modal-visibility.test.tsx` now asserts Voice Input's
Basic-mode contract against `SETTINGS_SECTION_METADATA`, and
`mcp-documentation.test.ts` reads the registry for the two MCP section
ids. No other existing test in the package changes.

## Design notes / decisions for review

- **`GET /api/views` returns the full registry, not the live menu.** It
includes flag-gated / experimental ids and `internal` (non-navigable)
destinations; reachability depends on flags and plugins this endpoint
does not evaluate. Documented as "known view ids", not "visible nav
entries".
- **`labelKey` is optional and best-effort; `label` is the guarantee.**
A `labelKey` is published only where the dashboard itself renders that
view's title through it. `graph` (labelled from a plugin manifest) and
the internal `task-detail` carry none rather than advertise a key that
resolves to nothing — and `task-detail` in particular must not point at
`taskDetail.title`, which is an occupied i18n *namespace* whose lookup
returns an object rather than falling through to a default. A guard test
now enforces that. Separately, a few published keys (`nav.ideation`,
`nav.importTasks`, `nav.automations`, `pr.view.title`) are the
dashboard's real keys but aren't in the shipped catalogs yet because the
host supplies their English inline; the docs say plainly that consumers
must fall back to `label`.
- **`keywords` / `searchableKeys` are explicitly non-contractual.**
`searchableKeys` exposes the raw i18n translation-key strings backing a
section's searchable copy; values, ordering and presence may change
between releases. Documented as best-effort search hints, never stable
identifiers.
- **Migration is deliberately partial.** The desktop sidebar, Settings
navigation and persisted view list now come from the registries;
`Header.tsx` and the mobile More sheet still hardcode a few of the same
labels. They can still drift from what `GET /api/views` reports;
converting them is left to a follow-up so this diff stays reviewable.
- **No project scoping, deliberately.** The proposal doc rightly pushes
plugin traffic through a project-scoped client — these two endpoints are
the exception that proves the rule: they return static registry metadata
that is identical for every project, so threading a `projectId` would
imply a scoping guarantee that does not exist here. They never touch
`getScopedStore` / `TaskStore`.
- **Two endpoints rather than one `/api/ui-metadata` envelope.** Views
and Settings sections are independent registries with different
consumers, and `/settings/sections` sits naturally beside the existing
`/settings/*` routes. A consumer that only needs navigation doesn't pay
for settings metadata.
- **The registry extraction ships with the endpoints rather than as a
separate PR.** The registries *are* the mechanism that keeps the API
honest — split apart, the first half is a refactor with no observable
effect and the second can't land without it.
- **Placement:** `packages/dashboard/src/shared/` is a new directory,
and these are the first *production* `app/ → src/` imports in the
package (today the only one is in `ProviderIcon.test.tsx`). They sit
under `src/` because `src/`'s tsconfig cannot import `app/`, so a module
both sides consume has nowhere else to go; both registries are
dependency-free data leaves, and `vite build` plus
`check-no-node-only-core-imports-in-dashboard` confirm the client bundle
is unaffected. The considered alternative was `packages/core/src` behind
the `dashboard-browser-safe-core-modules.json` allowlist, where
`mobile-nav-primary-items.ts` keeps a destination→labelKey table — these
stayed out of `core` because they are dashboard-owned UI ids, and
because the two tables describe different surfaces (core mirrors the
mobile nav's `nav.skills`/`nav.settings`; this registry mirrors the
desktop sidebar's `header.skillsView`/`header.settings`).
- Ships a `@runfusion/fusion` **minor** changeset (`category: feature`).

Happy to adjust any of the above — shape, placement, or dropping
`searchableKeys` — if you'd rather it landed differently.

## Verification

- Rebased onto `main@26dcccb7c`. Two conflicts, both resolved by
absorbing upstream's work rather than reverting it:
- `SettingsModal.tsx` — upstream's `voice-input` section (and the
`FNXC:VoiceInput` decision comment explaining it stays out of the
advanced-only set) moved into the registry. The registry's section list
is byte-identical to `main`'s `SETTINGS_SECTIONS` (45/45 entries, all
fields), and the registry-derived `ADVANCED_SETTINGS_SECTION_IDS` is
identical to `main`'s hardcoded set (19/19, same order) — both verified
mechanically, not by eye. Upstream's `RUNTIME_*`
hide-uninstalled-runtimes sets are untouched.
- `routes/README.md` — the `mount-sequence` list regenerated from
`CREATE_API_ROUTES_REGISTRAR_MOUNT_SEQUENCE`, so `registerVoiceRoutes`
and `registerUiMetadataRoutes` are both in place and the contract test
passes.
- `DASHBOARD_VIEWS` covers exactly `main`'s `BuiltInTaskView` union,
aliases included, and `BUILT_IN_TASK_VIEWS` reproduces `main`'s 27-entry
array in order (`devserver` still preceding `dev-server` for the
migration path).
- Every one of the 20 sidebar labels the refactor rewrote was checked to
be byte-identical to `main`'s hardcoded fallback, and every `FNXC:`
decision comment displaced by the move was accounted for — all 75 in
`SettingsModal.tsx` and all 11 in `useViewState.ts` survive, relocated
onto the registry entries they document.
- The full `dashboard-app` + `dashboard-api` suites were run at this
commit (**20,706 passing**) and again on unmodified `main@26dcccb7c`,
and the failing-file sets compared: **every file that fails here also
fails on `main`** — nothing regresses. The overlap is environment-driven
(Postgres-backed `*.pg.test.ts`, tests needing built `dist` artifacts,
and `SettingsModalNodeRouting.test.tsx`'s `No "fetchSystemInfo" export
is defined on the "../../api" mock`), none of it touched by this change.
- `tsc --noEmit` clean for both dashboard projects, `eslint` clean on
every changed file, and `vite build` of the client bundle succeeds (the
two pre-existing `@fusion-plugin-examples/claude-runtime` /
`playwright-core` module-resolution errors reproduce on unmodified
`main`).
- Repo gate scripts pass: `check-changeset-format`,
`check-routes-modular`, `check-no-node-only-core-imports-in-dashboard`,
`check-no-cwd-relative-dashboard-test-reads`, `check-mock-completeness`.
- The three new assertions were mutation-tested rather than assumed
load-bearing: breaking the registry's `group` derivation, dropping an
enrolled sidebar id, and re-pointing `task-detail` at the
`taskDetail.title` namespace each make their test fail.
- Local CodeRabbit review over two passes: 3 minor findings, all
addressed (parity projection missing `group`; route tests asserting
partial instead of exact payloads; the `labelKey` guard not covering the
settings registry).


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

## Summary by CodeRabbit

* **New Features**
* Added authenticated, read-only APIs for discovering dashboard views
and selectable Settings sections.
* Added dashboard view metadata, including labels, aliases, internal
status, and translation keys.
* Added Settings metadata with grouping, scope, advanced status, and
search-related information.
  * Updated navigation and Settings UI labels to use shared metadata.

* **Documentation**
  * Documented the new metadata endpoints and integration guidance.

* **Bug Fixes**
* Added safeguards and automated checks to keep UI navigation and API
metadata synchronized.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-26 22:34:18 -07:00
gsxdsm
e4cb957b57 FN-8619: migrate modal geometry to FloatingWindow
Unify migrated modal geometry and responsive behavior under FloatingWindow.

- Cover Agent Detail and GitHub Import floating-window interaction and persistence contracts.
- Preserve unique Agent Detail labels, mouse-only backdrop dismissal, and tablet touch layouts.
- Update the dashboard modal migration inventory.

Files changed:
 docs/dashboard-modal-inventory.md                  |   6 +-
 .../dashboard/app/components/AgentDetailView.css   |  49 +++++-----
 .../dashboard/app/components/AgentDetailView.tsx   |   6 +-
 .../dashboard/app/components/FloatingWindow.css    |  10 +-
 .../dashboard/app/components/TaskDetailModal.css   |  15 +++
 .../AgentDetailView.floating-window.test.tsx       | 102 +++++++++++++++++++++
 .../__tests__/GitHubImportModal.test.tsx           |  58 ++++++++++++
 ...etailModal.responsive-and-dependencies.test.tsx |  24 +++++
 8 files changed, 241 insertions(+), 29 deletions(-)

Fusion-Task-Id: FN-8619

Fusion-Task-Lineage: efa95e41-b12e-4e5d-aeb2-65b43c2dd8e0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 20:04:33 -07:00
gsxdsm
43160a7aae FN-8621: migrate complex modals to FloatingWindow
Unify complex dashboard modal presentation under persisted FloatingWindow geometry.

- Migrate Create Room, Task Detail, Agent Detail, and GitHub Import modal presentations.
- Preserve documented embedded and docked exceptions, dismissal behavior, and nested scrolling.
- Add presentation-contract coverage and publish dashboard guidance and changesets.

Files changed:
 ...n-8619-resize-persist-modals-floating-window.md |   7 ++
 .changeset/fn-8621-create-room-floating-window.md  |   7 ++
 docs/dashboard-guide.md                            |  16 +++-
 docs/dashboard-modal-inventory.md                  |  12 +--
 .../dashboard/app/components/AgentDetailView.css   |  16 +---
 .../dashboard/app/components/AgentDetailView.tsx   | 102 ++++++++++++++++-----
 .../dashboard/app/components/CreateRoomModal.css   |  19 +++-
 .../dashboard/app/components/CreateRoomModal.tsx   |  57 ++++++++----
 .../dashboard/app/components/FloatingWindow.css    |  13 ++-
 .../dashboard/app/components/FloatingWindow.tsx    |  15 +++
 .../dashboard/app/components/GitHubImportModal.css |  11 +--
 .../dashboard/app/components/GitHubImportModal.tsx |  40 ++++++--
 .../dashboard/app/components/TaskDetailModal.css   |  52 +----------
 .../dashboard/app/components/TaskDetailModal.tsx   |  58 ++++++------
 .../__tests__/AgentDetailView.core.test.tsx        |   2 +-
 .../AgentDetailView.mobile-scroll.test.tsx         |   6 +-
 .../components/__tests__/CreateRoomModal.test.tsx  |  62 +++++++++++--
 .../components/__tests__/FloatingWindow.test.tsx   |   1 +
 .../__tests__/GitHubImportModal.test.tsx           |   8 +-
 ...etailModal.responsive-and-dependencies.test.tsx |  77 +++++++---------
 .../__tests__/modal-presentation-contract.test.tsx |  74 +++++++++++++++
 .../dashboard/app/hooks/useEmbeddedPresentation.ts |   2 +-
 .../dashboard/app/hooks/useModalResizePersist.ts   |   5 +
 23 files changed, 441 insertions(+), 221 deletions(-)

Fusion-Task-Id: FN-8621

Fusion-Task-Lineage: 04b6f3fe-d527-4a21-a0cb-489eb20f5e91

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 19:52:32 -07:00
gsxdsm
89b4621a93 FN-8607: migrate shared modals to FloatingWindow
Standardize agent, onboarding, and utility modal behavior on the shared FloatingWindow contract.

- Migrate modal hosts to shared geometry, sheet, dismissal, and focus behavior.
- Make FloatingWindow the sole owner of modal ARIA semantics and protect portal-safe dismissal surfaces.
- Cover migrated modal contracts and document the responsive sheet requirements.

Files changed:
 docs/dashboard-guide.md                            |   6 +-
 packages/dashboard/app/components/AgentGenerationModal.tsx        |   8 +-
 packages/dashboard/app/components/AgentImportModal.tsx  |   4 +-
 packages/dashboard/app/components/AgentListModal.tsx    |   4 +-
 packages/dashboard/app/components/AgentOnboardingModal.tsx        |   4 +-
 packages/dashboard/app/components/DockerNodeOnboardingModal.tsx   |   4 +-
 packages/dashboard/app/components/ExperimentalAgentOnboardingModal.tsx |   4 +-
 packages/dashboard/app/components/FloatingWindow.css    |  36 ++++--
 packages/dashboard/app/components/FloatingWindow.tsx    |  12 ++
 packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx |   2 +-
 packages/dashboard/app/components/NativeShellOnboardingModal.tsx  |   2 +-
 packages/dashboard/app/components/SetupWizardModal.tsx  |   3 +-
 packages/dashboard/app/components/SubtaskBreakdownModal.tsx       |   2 +-
 packages/dashboard/app/components/__tests__/AgentModals.floatingWindow.test.tsx | 134 ++++++++++++++++++---
 packages/dashboard/app/components/__tests__/OnboardingModals.floatingWindow.test.tsx | 128 +++++++++++++++++---
 packages/dashboard/app/components/__tests__/UtilityModals.floatingWindow.test.tsx | 124 +++++++++++++++++--
 packages/dashboard/app/components/__tests__/migratedModalFixtures.tsx |  59 ++++++---
 packages/dashboard/app/components/__tests__/modalFloatingWindowContract.test.tsx |  20 ++-
 18 files changed, 458 insertions(+), 98 deletions(-)

Fusion-Task-Id: FN-8607

Fusion-Task-Lineage: 11ba6a9c-ce89-4358-83c4-a15ef15b6128

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 18:57:02 -07:00
gsxdsm
ba24a530ef FN-8620: migrate dashboard modals to FloatingWindow
Standardize dashboard modal geometry and interaction behavior through the shared FloatingWindow contract.

- Migrate New Task, right-dock expansion, terminal, onboarding, and utility modals to FloatingWindow.
- Expand shared window resizing, positioning, accessibility, and contract coverage.
- Document the modal migration inventory and publish changesets.

Files changed:
 .changeset/fn-8607-floating-modal-contract.md      |   7 +
 .changeset/fn-8620-bespoke-geometry-modals.md      |   7 +
 docs/dashboard-guide.md                            |  47 ++
 docs/dashboard-modal-inventory.md                  |   6 +-
 .../app/components/AgentGenerationModal.tsx        |  14 +-
 .../dashboard/app/components/AgentImportModal.tsx  |   7 +-
 .../dashboard/app/components/AgentListModal.tsx    |   8 +-
 .../app/components/AgentOnboardingModal.tsx        |   6 +-
 .../app/components/DockerNodeOnboardingModal.tsx   |  14 +-
 .../ExperimentalAgentOnboardingModal.tsx           |   6 +-
 .../dashboard/app/components/FloatingWindow.css    |  21 +
 .../dashboard/app/components/FloatingWindow.tsx    |  62 ++-
 packages/dashboard/app/components/MailboxModal.tsx |  14 +-
 .../components/MilestoneSliceInterviewModal.tsx    |  12 +-
 .../app/components/NativeShellOnboardingModal.tsx  |   6 +-
 packages/dashboard/app/components/NewTaskModal.css | 196 +------
 packages/dashboard/app/components/NewTaskModal.tsx | 593 +++++++--------------
 packages/dashboard/app/components/RightDock.css    |  84 +--
 .../app/components/RightDockExpandModal.tsx        | 333 ++----------
 .../dashboard/app/components/SetupWizardModal.tsx  |  20 +-
 .../app/components/SubtaskBreakdownModal.tsx       |   6 +-
 .../dashboard/app/components/TerminalModal.css     | 120 +----
 .../dashboard/app/components/TerminalModal.tsx     | 321 ++---------
 .../__tests__/AgentModals.floatingWindow.test.tsx  |  26 +
 .../components/__tests__/FloatingWindow.test.tsx   |  27 +-
 .../app/components/__tests__/NewTaskModal.test.tsx |  66 +--
 .../OnboardingModals.floatingWindow.test.tsx       |  20 +
 .../app/components/__tests__/RightDock.test.tsx    |  18 +-
 .../components/__tests__/TerminalModal.test.tsx    |  61 +--
 .../UtilityModals.floatingWindow.test.tsx          |  18 +
 .../components/__tests__/migratedModalFixtures.tsx |  25 +
 .../__tests__/modalFloatingWindowContract.test.tsx |  30 ++
 32 files changed, 726 insertions(+), 1475 deletions(-)

Fusion-Task-Id: FN-8620
Fusion-Task-Lineage: 2a912260-3292-4b13-bcf0-9de5a3df8ccd
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 18:35:10 -07:00
gsxdsm
034827f251 FN-8623: restore CDP touch geometry test lane
Restore a dedicated Chromium CDP lane for dashboard touch-geometry coverage.

- Add an opt-in touch-geometry test command and isolated Vitest project.
- Keep the browser-dependent spec out of deep and quality backfill collection.
- Document browser discovery, port, and single-collection requirements.

Files changed:
 docs/testing.md                                    | 10 ++-
 packages/dashboard/package.json                    |  1 +
 .../__tests__/dashboard-test-config-guard.test.ts  | 71 +++++++++++++++++++++-
 .../task-modal-touch-resize-browser.test.ts        |  5 ++
 packages/dashboard/vitest.config.ts                | 28 ++++++++-
 scripts/lib/test-inventory-spec.json               |  3 +-
 6 files changed, 114 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8623

Fusion-Task-Lineage: eafd7497-9302-49a4-8e9e-aa93c9f56a6f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 18:04:28 -07:00
gsxdsm
62e13f484b docs: index dashboard-modal-inventory.md in Audit Reports
FN-8617 committed docs/dashboard-modal-inventory.md as the canonical
classification of all 45 dashboard modal surfaces (classes A-D) with
file:line evidence and FloatingWindow migration targets, but the docs
index was not updated. Add the Audit Reports entry so the doc is
discoverable from the docs hub.

Verified: orphan scan clean (only known intentional orphans remain);
pnpm --filter @runfusion/fusion test:docs-index passes (2/2).
2026-07-26 17:20:56 -07:00
gsxdsm
2bb8537352 FN-8616: make agent tool-output limits configurable
Expose the shared agent tool-output budget as a scoped operator setting with an explicit no-limit option.

- Resolve global and project output caps with a safe finite default and zero sentinel.
- Propagate configured budgets through PI and plugin runtime tool wrappers.
- Add settings controls, localized labels, documentation, and regression coverage.

Files changed:
 .changeset/fn-8616-tool-output-budget-setting.md   |  7 ++++
 docs/agents.md                                     |  4 +-
 docs/settings-reference.md                         |  1 +
 .../core/src/__tests__/tool-output-budget.test.ts  | 23 ++++++++---
 packages/core/src/index.gate.ts                    |  2 +
 packages/core/src/index.ts                         |  2 +
 packages/core/src/settings-schema.ts               | 12 ++++++
 packages/core/src/tool-output-budget.ts            | 31 +++++++++++++--
 packages/core/src/types/settings-scope.ts          |  8 ++++
 .../app/components/settings/save-split.ts          |  1 +
 .../sections/GlobalGeneralSection.search.ts        | 20 ++++++++++
 .../settings/sections/GlobalGeneralSection.tsx     | 26 ++++++++++++
 ...lobalGeneralSection.tool-output-budget.test.tsx | 46 ++++++++++++++++++++++
 .../settings-default-descriptions.test.tsx         |  1 +
 .../src/__tests__/agent-session-helpers.test.ts    | 20 ++++++++++
 .../src/__tests__/runtime-resolution.test.ts       | 15 +++++++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 45 ++++++++++++++++-----
 packages/engine/src/agent-runtime.ts               |  2 +
 packages/engine/src/agent-session-helpers.ts       | 18 +++++++--
 packages/engine/src/pi.ts                          | 29 ++++++++++----
 packages/engine/src/runtime-resolution.ts          | 10 ++++-
 packages/i18n/locales/en/app.json                  |  4 ++
 packages/i18n/locales/es/app.json                  |  6 ++-
 packages/i18n/locales/fr/app.json                  |  6 ++-
 packages/i18n/locales/ko/app.json                  |  6 ++-
 packages/i18n/locales/zh-CN/app.json               |  6 ++-
 packages/i18n/locales/zh-TW/app.json               |  6 ++-
 packages/i18n/src/resources.d.ts                   |  4 ++
 28 files changed, 323 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8616

Fusion-Task-Lineage: 3ca99a61-d6ae-48ff-98d2-f14a153aa2b7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 16:16:27 -07:00
gsxdsm
d3002d1453 FN-8617: restore authoritative dashboard modal inventory
Restore the committed source of truth for dashboard modal migration coverage.

- Add the evidence-backed inventory of dashboard modal classifications and migration ownership.
- Link the dashboard guide to the canonical modal inventory.

Files changed:
 docs/dashboard-guide.md           |   4 ++
 docs/dashboard-modal-inventory.md | 103 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 107 insertions(+)

Fusion-Task-Id: FN-8617
Fusion-Task-Lineage: 87159726-d574-4f71-bd6f-66cf9f2f9a9e
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 16:05:08 -07:00
gsxdsm
743dc5f46e FN-8612: remove tablet task modal padding
Keep task modals dense on tablets without reducing their touch resize targets.

- Move the Task Detail drag target out of layout flow while preserving its 44px hit area.
- Restore desktop-density New Task header and body padding on tablet resize surfaces.
- Add CSS, unit, and browser coverage for tablet geometry and generic floating windows.

Files changed:
 .changeset/fn-8612-tablet-modal-padding.md         |   7 +
 docs/dashboard-guide.md                            |   8 +-
 .../task-detail-modal-tablet-width.test.ts         |   8 +
 .../dashboard/app/components/FloatingWindow.css    |  40 ++++-
 packages/dashboard/app/components/NewTaskModal.css |  14 ++
 .../FloatingWindow.touch-geometry.test.tsx         |  14 ++
 .../app/components/__tests__/NewTaskModal.test.tsx |   8 +
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  54 ++++++-
 .../task-modal-touch-resize-browser.test.ts        | 177 ++++++++++++++++++++-
 packages/dashboard/vitest.config.ts                |   6 +
 10 files changed, 322 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-8612

Fusion-Task-Lineage: fecd7c48-7b6c-434e-9002-f8f21241120c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 15:42:53 -07:00
gsxdsm
07c8c95b10 FN-8614: cap agent tool output
Bound every engine-injected tool result to preserve agent context capacity.

- Add shared 16,000-character total text budgets with deterministic truncation markers and validated overrides.
- Apply outermost output clamps to Pi and non-Pi plugin tool paths, with semantic caps for high-volume reads.
- Cover budget behavior and document the operator-facing configuration contract.

Files changed:
 .changeset/fn-8614-tool-output-budget.md           |  7 ++
 docs/agents.md                                     |  8 ++
 .../core/src/__tests__/tool-output-budget.test.ts  | 58 +++++++++++++
 packages/core/src/index.gate.ts                    |  7 ++
 packages/core/src/index.ts                         |  7 ++
 packages/core/src/tool-output-budget.ts            | 97 ++++++++++++++++++++++
 .../src/__tests__/agent-artifact-tools.test.ts     | 10 +++
 .../src/__tests__/agent-document-tools.test.ts     | 10 +++
 .../__tests__/agent-task-logs-read-tools.test.ts   |  8 ++
 .../__tests__/tool-output-budget-wrapper.test.ts   | 67 +++++++++++++++
 packages/engine/src/agent-session-helpers.ts       |  7 +-
 packages/engine/src/agent-tools.ts                 | 43 ++++++++--
 packages/engine/src/pi.ts                          | 54 +++++++++++-
 13 files changed, 374 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8614

Fusion-Task-Lineage: b6a76ccd-d7b4-4b43-af7e-cfd16ffb7fc8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:42:06 -07:00
gsxdsm
a6885b73f2 FN-8606: migrate core workflow modals to shared floating windows
Migrate dashboard dialogs to the shared movable and resizable FloatingWindow contract.

- Move core, workflow, Git, planning, and automation modal surfaces to stable floating-window identities with persisted geometry.
- Suspend geometry and floating controls for phone and short-viewport sheets, including Quick Chat.
- Add accessibility wiring, tablet touch targets, migration tests, and operator documentation.

Files changed:
 .changeset/fn-8606-floating-window-core-modals.md  |   7 +
 docs/dashboard-guide.md                            |   4 +
 packages/dashboard/app/App.tsx                     |   8 +-
 .../dashboard/app/components/ActivityLogModal.tsx  |  30 ++--
 packages/dashboard/app/components/AddNodeModal.tsx |   8 +-
 .../dashboard/app/components/ChangesDiffModal.tsx  |  35 +++--
 .../dashboard/app/components/ConnectNodeModal.tsx  |   9 +-
 .../dashboard/app/components/FloatingWindow.css    |  71 ++++++++-
 .../dashboard/app/components/FloatingWindow.tsx    |  27 +++-
 .../dashboard/app/components/GitManagerModal.tsx   |  30 +++-
 .../dashboard/app/components/GroupTaskModal.tsx    |   8 +-
 .../app/components/ModelOnboardingModal.tsx        |  28 ++--
 .../dashboard/app/components/NodeDetailModal.tsx   |   9 +-
 .../dashboard/app/components/PlanningModeModal.css |   1 -
 .../dashboard/app/components/PlanningModeModal.tsx |  57 +++----
 .../app/components/ScheduledTasksModal.tsx         |   9 +-
 packages/dashboard/app/components/ScriptsModal.css |   1 -
 packages/dashboard/app/components/ScriptsModal.tsx |  32 ++--
 .../dashboard/app/components/SettingsModal.css     |   1 -
 .../dashboard/app/components/SettingsModal.tsx     |  58 ++++---
 .../app/components/WorkflowAddStepModal.css        |  15 --
 .../app/components/WorkflowAddStepModal.tsx        |  46 +++---
 .../components/__tests__/ActivityLogModal.test.tsx |  33 ++--
 .../app/components/__tests__/AddNodeModal.test.tsx |  12 +-
 .../components/__tests__/ChangesDiffModal.test.tsx |  78 +++++-----
 .../components/__tests__/ConnectNodeModal.test.tsx |   7 +
 .../components/__tests__/FloatingWindow.test.tsx   | 173 ++++++++++++++++++++-
 .../components/__tests__/GitManagerModal.test.tsx  |  30 ++--
 .../components/__tests__/GroupTaskModal.test.tsx   |   9 ++
 .../__tests__/ModelOnboardingModal.test.tsx        |  18 ++-
 .../components/__tests__/NodeDetailModal.test.tsx  |   9 ++
 .../__tests__/PlanningModeModal.autosize.test.tsx  |  30 ++--
 .../__tests__/ScheduledTasksModal.test.tsx         |  21 ++-
 .../app/components/__tests__/ScriptsModal.test.tsx |  15 +-
 .../__tests__/SettingsModal.mobileClose.test.tsx   |  25 ++-
 .../__tests__/WorkflowAddStepModal.test.tsx        |   7 +
 .../floatingWindowMigration.test-helpers.ts        | 126 +++++++++++++++
 37 files changed, 828 insertions(+), 259 deletions(-)

Fusion-Task-Id: FN-8606
Fusion-Task-Lineage: dab0df2d-73f4-4b0f-bec3-a45016310c91
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 14:37:35 -07:00
gsxdsm
827b145fa1 FN-8611: persist manual import translations
Persist manual GitHub and GitLab import translations across preview sessions.

- Add durable translation cache reads and identity-aware translation requests.
- Route manual translations through the translation budget with actionable API errors.
- Restore cached previews automatically and cover API/UI behavior with tests.

Files changed:
 .changeset/fn-8611-manual-import-translations.md   |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 packages/dashboard/app/api/ai-text.ts              |  52 ++++++--
 packages/dashboard/app/api/legacy.ts               |   2 +
 .../dashboard/app/components/GitHubImportModal.tsx |  14 ++-
 .../components/GitHubImportTranslateControls.tsx   |  39 +++++-
 .../__tests__/GitHubImportModal.test.tsx           |  42 +++++++
 packages/dashboard/src/ai-translate.ts             |  23 ++++
 .../register-ai-text-assistant-routes.test.ts      | 135 +++++++++++++++++----
 .../routes/register-ai-text-assistant-routes.ts    | 131 ++++++++++++++------
 10 files changed, 370 insertions(+), 77 deletions(-)

Fusion-Task-Id: FN-8611

Fusion-Task-Lineage: 00a84f43-4469-4bc5-bbb6-8245d8d79781

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 13:05:23 -07:00
gsxdsm
47087342fc FN-8605: harden tablet touch window controls
Make shared floating dashboard windows reliably movable and resizable on tablet touch viewports.

- Apply a tablet-aware 44px drag and resize hit-target contract to FloatingWindow.
- Preserve phone full-screen sheets below 768px while retaining desktop-hybrid geometry.
- Add geometry, browser touch, and viewport boundary coverage with captured visual evidence.
- Document the shared modal touch contract and release the dashboard fix.

Files changed:
 .changeset/fn-8605-floating-window-touch.md        |   7 ++
 docs/dashboard-guide.md                            |   5 +
 docs/testing.md                                    |   2 +-
 .../dashboard/app/components/FloatingWindow.css    | 116 ++++++++++++++----
 .../dashboard/app/components/FloatingWindow.tsx    |  56 ++++++++-
 .../components/__tests__/FloatingWindow.test.tsx   |  80 +++---------
 .../FloatingWindow.touch-geometry.test.tsx         | 136 +++++++++++++++++++++
 .../FloatingWindowStack.cross-type.test.tsx        |   2 +-
 .../app/hooks/__tests__/useViewportMode.test.ts    |  18 ++-
 packages/dashboard/app/hooks/useViewportMode.ts    |   7 +-
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  39 +++++-
 .../__screenshots__/fn-8605/phone-fullscreen.png   | Bin 0 -> 11393 bytes
 .../e2e/__screenshots__/fn-8605/tablet-after.png   | Bin 0 -> 14275 bytes
 .../e2e/__screenshots__/fn-8605/tablet-before.png  | Bin 0 -> 14308 bytes
 .../task-modal-touch-resize-browser.test.ts        |  83 +++++++++++++
 15 files changed, 453 insertions(+), 98 deletions(-)

Fusion-Task-Id: FN-8605

Fusion-Task-Lineage: 993c7ed8-c3e9-4673-b0b7-5c50746991a7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:51:39 -07:00
gsxdsm
fde3b76a8c FN-8602: enable tablet touch resizing for task modals
Enable touch-driven resizing for task modals on tablet viewports.

- Add tablet touch resize handles and pointer interactions for task and new-task modals.
- Preserve responsive modal sizing while keeping phone layouts fullscreen.
- Add unit, browser, and visual regression coverage with updated documentation.

Files changed:
 .changeset/fn-8602-tablet-touch-resize.md          |   7 +
 docs/dashboard-guide.md                            |   6 +-
 docs/testing.md                                    |   4 +
 packages/dashboard/app/components/NewTaskModal.css |  70 ++++++++++
 packages/dashboard/app/components/NewTaskModal.tsx |  19 ++-
 .../dashboard/app/components/TaskDetailModal.css   |  12 ++
 .../dashboard/app/components/TaskDetailModal.tsx   |   7 +-
 .../app/components/__tests__/NewTaskModal.test.tsx |   4 +
 ...etailModal.responsive-and-dependencies.test.tsx |   1 +
 .../hooks/__tests__/useModalResizePersist.test.tsx |  10 +-
 .../app/hooks/__tests__/useViewportMode.test.ts    |  26 +++-
 .../dashboard/app/hooks/useModalResizePersist.ts   |  26 +++-
 packages/dashboard/app/hooks/useViewportMode.ts    |  13 ++
 packages/dashboard/app/styles.css                  |  16 +++
 .../app/task-modal-touch-resize-e2e-fixture.html   |   5 +
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  60 ++++++++
 .../__screenshots__/fn-8602/phone-fullscreen.png   | Bin 0 -> 43987 bytes
 .../e2e/__screenshots__/fn-8602/tablet-after.png   | Bin 0 -> 52549 bytes
 .../e2e/__screenshots__/fn-8602/tablet-before.png  | Bin 0 -> 11500 bytes
 .../task-modal-touch-resize-browser.test.ts        | 154 +++++++++++++++++++++
 20 files changed, 424 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-8602

Fusion-Task-Lineage: f07ce22f-f529-4be3-ba1b-04063154a1b0

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:12:35 -07:00
gsxdsm
cca13737b6 FN-8603: reduce steady-state diagnostic log noise
Route routine core, engine, and dashboard diagnostics through debug-gated shared loggers.

- Demote steady-state diagnostic sites while preserving warnings and errors for actionable failures.
- Add cross-package severity contracts and manifest coverage for demoted log sites.
- Document logging severity guidance and add a patch changeset.

Files changed:
 .changeset/fn-8603-log-severity.md                 |  7 ++
 docs/diagnostics.md                                | 20 ++++--
 .../__tests__/log-severity-spam-contract.test.ts   | 71 ++++++++++++++++++
 packages/core/src/activity-analytics.ts            |  5 +-
 packages/core/src/ai-summarize.ts                  | 61 +++++++---------
 packages/core/src/async-mission-store.ts           |  5 +-
 packages/core/src/async-secrets-store.ts           |  7 +-
 packages/core/src/central-core.ts                  | 17 ++---
 packages/core/src/docker-provisioning.ts           | 13 ++--
 packages/core/src/index.ts                         |  1 +
 packages/core/src/master-key.ts                    |  9 ++-
 packages/core/src/memory-compaction.ts             | 29 ++++----
 packages/core/src/memory-insights.ts               |  7 +-
 packages/core/src/migration-orchestrator.ts        |  7 +-
 packages/core/src/mission-store.ts                 |  5 +-
 packages/core/src/node-discovery.ts                |  7 +-
 packages/core/src/notification/dispatcher.ts       |  9 ++-
 .../core/src/plugins/bundled-plugin-install.ts     | 11 +--
 packages/core/src/reflection-store.ts              |  5 +-
 packages/core/src/secrets-store.ts                 |  7 +-
 packages/core/src/task-store/agent-logs.ts         | 21 +++---
 packages/core/src/task-store/async-events.ts       |  5 +-
 packages/core/src/task-store/async-maintenance.ts  |  7 +-
 packages/core/src/task-store/comments-ops.ts       |  7 +-
 packages/core/src/task-store/task-mutation-ops.ts  | 11 +--
 packages/core/src/task-store/workflow-integrity.ts |  9 ++-
 packages/core/src/types/merge-policy.ts            |  5 +-
 packages/core/src/usage-events.ts                  |  5 +-
 .../__tests__/log-severity-spam-contract.test.ts   | 48 +++++++++++++
 packages/dashboard/src/ai-refine.ts                |  5 +-
 packages/dashboard/src/ai-session-diagnostics.ts   | 10 +--
 packages/dashboard/src/chat.ts                     |  8 ++-
 packages/dashboard/src/devserver-manager.ts        |  9 ++-
 packages/dashboard/src/file-service.ts             |  5 +-
 packages/dashboard/src/github-tracking-comments.ts |  7 +-
 .../dashboard/src/github-tracking-reconciler.ts    |  5 +-
 packages/dashboard/src/github-tracking-state.ts    |  5 +-
 packages/dashboard/src/gitlab-lifecycle.ts         |  5 +-
 packages/dashboard/src/insights-routes.ts          |  9 ++-
 packages/dashboard/src/issue-image-attachments.ts  |  5 +-
 packages/dashboard/src/knowledge-index.ts          |  5 +-
 packages/dashboard/src/plugin-routes.ts            |  7 +-
 packages/dashboard/src/routes/board-workflows.ts   |  5 +-
 packages/dashboard/src/routes/context.ts           |  5 +-
 .../dashboard/src/routes/register-auth-routes.ts   | 13 ++--
 .../routes/register-docker-provisioning-routes.ts  |  7 +-
 .../dashboard/src/routes/register-git-github.ts    | 21 +++---
 packages/dashboard/src/routes/register-gitlab.ts   |  7 +-
 .../src/routes/register-session-diff-routes.ts     |  9 ++-
 .../src/routes/register-settings-memory-routes.ts  |  7 +-
 .../src/routes/register-setup-activity-routes.ts   |  7 +-
 .../dashboard/src/routes/register-signal-routes.ts |  5 +-
 .../src/routes/register-task-workflow-routes.ts    | 11 +--
 packages/dashboard/src/runtime-logger.ts           | 11 +--
 packages/dashboard/src/server.ts                   |  7 +-
 packages/dashboard/src/sse.ts                      |  8 ++-
 packages/dashboard/src/terminal-service.ts         | 34 ++++-----
 packages/dashboard/src/view-chunk-manifest.ts      |  5 +-
 .../engine/src/__tests__/log-severity-manifest.ts  | 83 ++++++++++++++++++++++
 .../__tests__/log-severity-spam-contract.test.ts   | 40 ++++++++++-
 .../src/__tests__/logger-debug-gating.test.ts      |  7 +-
 packages/engine/src/goal-anchoring-audit.ts        |  5 +-
 packages/engine/src/plugin-runner.ts               | 44 ++++++------
 packages/engine/src/pty-native.ts                  |  9 ++-
 .../engine/src/runtimes/child-process-worker.ts    |  4 +-
 packages/engine/src/self-healing.ts                | 12 ++--
 packages/engine/src/worktree-hooks.ts              | 10 ++-
 67 files changed, 632 insertions(+), 250 deletions(-)

Fusion-Task-Id: FN-8603

Fusion-Task-Lineage: 53901db6-1af2-4bd7-b5ea-49507e048ef2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:01:19 -07:00
gsxdsm
a516c8b409 docs(FN-8600): capture the live-planning-worktree reclaim incident
Documents why self-healing force-removed a worktree a planning session was
using and parked the card branch-conflict-unrecoverable: planning gained a task
worktree but never took an active-session lease, so the reclaim sweep's liveness
guard had nothing to see, and a zero-commit branch classifies as
tip-already-merged by construction.

Captures the investigation's dead ends too — including reading maxConcurrent
from a multi-tenant config table without filtering by project_id, which produced
a confidently wrong root cause — and the three ways the first version of the fix
was itself wrong.

CONCEPTS.md: adds planning to the Active-session lease kinds (the entry had gone
stale), states the converse invariant that an unheld path reads as proof nothing
is running, and defines Top-level agent slot — the capacity concept whose
conflation with the worktree limit derailed the first hour of diagnosis.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 09:32:22 -07:00
gsxdsm
47d030215c feat(workflow): run pre-merge review gates in the In review column
Code Review and Browser Verification now run with the card in `in-review`
instead of `in-progress`, so the board shows the card under review with the
running step as a badge (matching the Coding (Ideas) preset). Their paired
remediation nodes stay in `in-progress`, so a changes-requested verdict
visibly sends the card back to implementation.

The column move IS the badge switch: the dashboard badge was already
lane-gated on `column === "in-review"`. Applied to the shared stepwise
coding IR, so it is inherited by builtin:coding (the default),
builtin:stepwise-coding, builtin:brainstorming and builtin:coding-ideas;
builtin:legacy-coding keeps its historical placement.

Two consequences handled:

- Capacity: `in-review` has no `wip` trait, so the slot is released during
  review and the remediation crossing back into `in-progress` can hit the
  non-bypassable in-transaction capacity check. The column boundary now
  PARKS the run on a `capacity-exhausted` rejection instead of failing it,
  preserving the failed gate result and worktree so the next graph run
  retries once a slot frees. Non-capacity rejections still propagate.

- Reopen clears: `applyReopenFieldClears` wiped `workflowStepResults` on
  every in-review -> in-progress move, which the remediation crossing now
  performs routinely. That destroyed the remediation input, made
  `routeRetryableRemediationGraphFailureToPreMergeFix` and
  `recoverFailedPreMergeWorkflowStep` silently no-op, and — worse — made
  both `getTaskMergeBlocker` branches vacuously false, so a card could
  return to `in-review` and be mergeable with its gate never re-run. Now
  exempted for graph-owned in-review -> in-progress crossings only;
  operator reopens, merge bounces and every -> todo/triage rebound still
  clear, so the executor's documented bounce invariant is unchanged.

Adds regression coverage for both (there was previously none for the
reopen clear in either direction), and annotates the unreachable legacy
scheduler dispatch block rather than mirroring the fix into dead code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 02:04:40 -07:00
gsxdsm
fd073e287f FN-8592: self-heal stranded hold continuations
Restore graph-owned plan-review continuations for eligible hold-column cards stranded after planning cancellation.

- Detect real-spec hold cards with no active workflow continuation and re-seed Plan Review safely.
- Serialize workflow continuation seeding, review-result writes, and lease claims to prevent duplicate recovery.
- Add recovery diagnostics, release warnings, regression coverage, and a patch changeset.

Files changed:
 .changeset/fn-8592-stranded-hold-continuation.md   |   7 +
 AGENTS.md                                          |   1 +
 docs/architecture.md                               |   4 +
 .../workflow-task-serialization-protocol.test.ts   | 119 +++++++++++++
 .../workflow-work-items-conditional-seed.test.ts   | 191 +++++++++++++++++++++
 packages/core/src/store.ts                         |   5 +-
 .../src/task-store/async-workflow-workitems.ts     | 123 +++++++++----
 packages/core/src/task-store/project-store-ops.ts  |  14 ++
 .../src/task-store/workflow-task-create-ops.ts     |  16 +-
 .../src/task-store/workflow-workitems-ops-2.ts     |  91 ++++++----
 .../src/__tests__/pre-release-plan-review.test.ts  |  17 ++
 ...self-healing-stranded-hold-continuation.test.ts | 171 ++++++++++++++++++
 packages/engine/src/hold-release.ts                |  57 +++++-
 packages/engine/src/plan-review-continuation.ts    |  94 ++++++++++
 packages/engine/src/runtimes/in-process-runtime.ts |  30 +---
 packages/engine/src/self-healing.ts                | 100 ++++++++++-
 16 files changed, 945 insertions(+), 95 deletions(-)

Fusion-Task-Id: FN-8592

Fusion-Task-Lineage: fe7ffd34-96e4-4418-a879-7418e6293d30

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 00:46:07 -07:00
gsxdsm
106c61e6ee fix(agent-tools): close the fn_delegate_task Deny bypass and the store's window clamp
Follow-up to 13a2b2a9d, from a multi-agent review of that commit. Three of its
claims did not hold.

1. fn_delegate_task bypassed the gate entirely (P0). It reaches the same
   createAgentTask primitive, was registered unconditionally in both session
   lanes, and validated only that the TARGET agent is non-ephemeral — never the
   caller. Under Deny an ephemeral worker could enumerate agents and delegate
   unlimited tasks. It is now withheld under Deny, and also under
   upon_validation: delegation has no proposal channel, so leaving it available
   would launder a create past the operator review that policy requires.

2. The widened dedupe window was capped at 5 minutes. The store query in
   branch-and-pr-entities.ts carried its own independent `?? 60_000` /
   `min(300_000, …)` pair, so widening only duplicate-guard.ts under-delivered
   and made the new ceiling unreachable. Both sites now share
   FINGERPRINT_WINDOW_DEFAULT_MS / FINGERPRINT_WINDOW_MAX_MS.

3. The pi-extension gate does not fire at all. pi's ExtensionContext carries no
   agentId — the read is a speculative cast and only tests supply one, so every
   real call short-circuits as a human caller. The fail-closed direction is kept
   for the day an identity signal exists, but the limitation is now documented
   instead of implied to be enforcement.

Also: the session prompt now states when creation is disabled and names
fn_task_log as the fallback (the base prompt still taught fn_task_create, which
is the same instruction/capability mismatch that fed the retry storm);
suppression emits an `agent:task-create-withheld` run-audit event; and the two
source-text ratchet tests are replaced with behavioral assertions on the tool
list the executor actually hands the model — verified to fail when the guard is
broken, which the string assertions did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:44:56 -07:00
gsxdsm
0c85613313 fix(engine): address code-review findings on the planner/worktree recovery fixes
Review of 2dbfe3d31 + 05b704dc6 surfaced real defects in both fixes:

- The unusable-worktree probe composed two helpers across an unnecessary
  self-healing -> step-runner import edge, and the directory check added no
  discriminating power over the `.git` probe. Replaced with one canonical
  hasUsableWorktreeShape beside classifyTaskWorktree, which also applies the
  repo-root gate (FN-6861) when a rootDir is available; both call sites pass one.
  Its narrower guarantee vs the canonical classifier is now documented and
  pinned by tests, including the de-registered shape it cannot see.
- REPLAN_PARK_STATUSES is derived from PLANNING_STAGE_STATUSES instead of
  re-listed, so a new durable park status cannot be added to one set only.
- The preserve/clear decision no longer pretends to steer `worktree`: the rebound
  is a reopen move, which clears it regardless. Documented, and the test now
  asserts the durable row rather than only the updateTask argument.
- `branch` is cleared only when it is the re-derivable canonical fusion/<id>;
  a non-canonical branch survives so a card's only commit pointer is not dropped.
- The recovery log named the recorded worktree even when the session had targeted
  an AI-merge clean room. It now names the refused path and says whether the
  recorded worktree was gone too.
- Added task:auto-recover-worktree-session-metadata so the decision is legible to
  agents, not only in human log prose.
- isTaskStillInPlanningStage's parameter type now includes the execution stamps
  its implementation reads.
- Test hygiene: real-fs fixtures wrapped in try/finally; changeset dev note
  corrected; FN-8361 asserted at the discovery surface, not only in the guard
  table.

Also captures the shared bug class in docs/solutions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:24:10 -07:00
gsxdsm
351fba1d33 FN-8594: improve project overview mobile reflow
Make the project overview usable at small viewport widths while hiding its non-functional mobile task navigation.

- Reflow overview filters, stats, cards, and skeletons for 480px and 380px breakpoints.
- Hide the mobile navigation bar and clear its reserved height outside an active project task view.
- Add regression coverage and document responsive dashboard behavior.

Files changed:
 docs/dashboard-guide.md                            |  5 +-
 packages/dashboard/app/App.tsx                     |  8 +-
 .../mobile-feature-access-regression.test.tsx      | 22 ++++-
 .../project-overview-small-screen.test.ts          | 91 ++++++++++++++++++++
 packages/dashboard/app/components/MobileNavBar.tsx | 19 ++++-
 packages/dashboard/app/components/ProjectCard.css  | 58 +++++++++++++
 .../dashboard/app/components/ProjectOverview.css   | 99 ++++++++++++++++++++++
 7 files changed, 294 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8594

Fusion-Task-Lineage: 0999f6e4-ffe9-4ed6-a471-8e5ba3413029

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 23:12:41 -07:00
gsxdsm
99b80ad748 feat(dashboard): add opt-in auto-update and harden restart supervision
Add the `autoUpdateAndRestart` global setting (default off, Settings ->
General next to Release channel). When enabled, the dashboard host installs
available updates on the selected channel by itself and requests the
supervised in-place restart. Supervised hosts only: without a parent to
respawn, installing would leave a running process whose code no longer
matches its own install.

Fix two ways the restart affordance could silently do nothing:

- The supervisor now stamps FUSION_SUPERVISOR_PID and supervision is only
  counted when that pid is the real parent. FUSION_RESTART_SUPERVISED is
  inherited by every process Fusion spawns, so `fn dashboard` launched from
  an agent terminal skipped its own supervisor while still advertising
  restart support -- a restart request then killed it for good.
- Settings and the update banner probe /system/info on mount and treat
  capability as advisory: the button always issues the request and shows the
  server's actual refusal instead of sitting disabled after a failed probe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 22:52:53 -07:00
gsxdsm
3597d06380 FN-8578: pin verified Parakeet v3 model asset
Enable secure downloads of the upstream Parakeet v3 voice model.

- Pin the archive URL, filename, SHA-256, expected files, and nested extraction layout.
- Accept the archive's BSD tar timestamp format while preserving safe extraction checks.
- Cover download gates and installation behavior, and document the verified model asset.

Files changed:
 .changeset/fn-8578-parakeet-v3-asset-pin.md        |  7 ++++
 docs/dashboard-guide.md                            |  2 +-
 docs/settings-reference.md                         |  8 ++--
 .../src/stt/__tests__/model-manager.test.ts        | 46 ++++++++++++++++++++++
 .../dashboard/src/stt/__tests__/voice-stt.test.ts  | 33 +++++++++++++++-
 packages/dashboard/src/stt/model-manager.ts        | 13 +++---
 packages/dashboard/src/stt/types.ts                | 18 +++++++--
 7 files changed, 114 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-8578

Fusion-Task-Lineage: ab397190-fa0c-491e-b0f8-0ab6641be95a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-25 08:21:00 -07:00