Commit Graph

196 Commits

Author SHA1 Message Date
gsxdsm
3577cb6adf fleet: project-engine.ts 12 → 5 — auto-merge silently declined every card on a renamed board (#2706)
**Claim announced before the work** (on #2689, alongside
`register-task-workflow-routes.ts`):
`packages/engine/src/project-engine.ts`.

**12 → 5.** Repo backlog → **685**.

## The failure mode here has no error signature

Every merge guard in this file spelled the lane `in-review`. On a
renamed board nothing throws, nothing logs a warning — **auto-merge
simply declines every card**:

| guard | what a renamed board gets |
|---|---|
| `requestInterpreterMerge` | returns `noOp: true` — *"parked cleanly in
review, awaiting human merge"* — for a card that was in review and fully
eligible |
| the merge-queue snapshot | returns an **empty list** for a queue full
of review cards, so the coordinator sees nothing to admit |
| the `taskMoved` auto-merge handoff | never fires, so nothing reaches
auto-merge in the first place |
| the pause-interruption tracker | drops every card from its
paused-review set on the next update, so a merge paused mid-flight is
never interrupted |

The operator sees cards resting in review with auto-merge **on**, and
every log line says the system did the right thing. There is no string
to search for — which is the argument for the census being a parse
rather than a grep over error messages.

## Implementation notes

- **Core's `resolveTaskLifecycleColumns` directly** — the canonical
helper, so no new abstraction and no fourth local resolver in a file
that had none.
- **The merge-queue snapshot resolves per task through a shared
`irCache`**, because a merge queue can hold cards from *different*
workflows. Per-workflow, not per-card: one IR read each.
- **The handoff and its post-grace recheck share one snapshot.** They
are halves of one decision — "did this card just enter the merge lane,
and is it still there?" — and that is exactly the split that produced
the defects in `executor.ts`.

## Revert proof

**1 of 3 cases reddens** with the literal restored.

The suite invokes the real `requestInterpreterMerge` via `.call()` on a
minimal `this` (`runtime.getTaskStore`, `allowInReviewMergeProcessing`,
`onMerge`) instead of standing up a whole `ProjectEngine` runtime. The
body under test is the shipped one, and *reaching* `onMerge` is the
assertion. I would rather explain that seam than either skip the proof
or spend the test budget booting a runtime. The default-board case is
labelled in the file as no-change evidence, not counted as coverage.

## The remaining 5, flagged not guessed

All five are `column === "done"` **merge-confirmation reads** — "did the
merge land?". That is a different question from any lane role, and it
shares its answer with the dependency guards I flagged in `executor.ts`
(`12325`) and `register-task-workflow-routes.ts` (`3995`). Three files,
one open question: **what does "landed / satisfied" mean on a board
whose terminal column is not named `done`, and is it the complete column
or the terminal union?** Deciding it once and applying it to all three
is right; swapping it three times independently is how the resolver
choice ends up inconsistent — which already happened once inside
`executor.ts`, where the correct resolver inverts between two guards a
few lines apart.

## Verification

`pnpm test:gate` **158 / 10 / 487 / 71** · **56/56** across the six
auto-merge / project-engine suites · 3/3 in the new suite · `tsc -p
packages/engine` clean · `pnpm lint` clean · census `--strict` exit 0.

No changeset: `@fusion/engine` is private, and the behaviour change is
confined to renamed boards.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:25:19 -07:00
gsxdsm
a59c6aea50 fleet: register-task-workflow-routes.ts 20 -> 5 (#2713)
## Census before / after

| | before | after |
|---|---:|---:|
| `register-task-workflow-routes.ts` | **20** | **5** |

I handed this cluster off in an earlier turn with an analysis rather
than a conversion. Coming back to it with the analysis already done made
it tractable.

## Converted with the file's own idiom

This file already had `resolveIntakeColumnForTask` /
`resolveWipColumnForTask` / `resolveReboundColumnForTask`: resolve the
column **id** from the task's workflow, fall back to the legacy id when
the IR cannot be read. I added the three roles it was missing — review,
complete, archived — in that same shape.

**Deliberately not the `columnRoles` predicate helpers** used in
`packages/dashboard/app`. Those take resolved trait *flags*, which a
route handler does not have — it has a store and a task id, and must do
an async lookup. Importing them here would mean fetching flags per
request to answer a question this file already answers a simpler way.
One idiom per layer.

Every converted handler **resolves once and reuses**, so two checks in
the same request cannot disagree about which column is the review lane.
The retry handler had three separate review checks and now shares one
resolution.

## Also fixes an inversion

`isArchived` ORed the legacy id with the resolved trait
**unconditionally**, so a column merely *named* `archived` counted as
archived even when its own workflow said otherwise. Same pattern
previously found in `TaskContextMenu` and `isPreExecutionHoldColumn`.
Now flags-first, id as fallback.

## The five survivors, each with a reason

| count | line | why |
|---:|---|---|
| 1 | 1193 | `tasks.filter(t => t.column === "todo")` — a **list** path.
Per-task IR resolution is N store reads on board load; the site already
carries a note measuring that cost. Needs the hold column resolved per
*workflow* from the board payload — a real change, not a rename. |
| 1 | 1926 | **Already flags-first**: `moveTargetIr && declaresColumns ?
columnHasFlag(...) : column === "in-progress"`. The literal *is* the
documented no-IR fallback. |
| 1 | 4872 | The fallback arm of the `isArchived` fix above — same
shape, deliberately kept. |
| 2 | 4024 | `depTask.column` — a **dependency's** column, not the
task's. Resolving it means fetching that task's IR per dependency; out
of scope. |

So three of the five are correct as they stand, and two are genuinely
deferred.

## Verification

`plan-approval-intake-column` + `stranded-refinements-routes` 13/13.
`pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm
check:lifecycle-columns` exits 0 with the baseline re-recorded here.
`tsc -p packages/dashboard/tsconfig.json` clean. `pnpm lint` clean.

Typecheck was run after **every** batch rather than once at the end —
with 15 edits across async handlers in a 6000-line file, a single late
typecheck would not tell me which batch broke it.

No changeset: no user-visible change.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:17:03 -07:00
gsxdsm
f6e460acdf fleet: moves.ts 15 → 2 (hot path; one hoisted resolution, net one FEWER than before) (#2705)
Claiming **`packages/core/src/task-store/moves.ts`** — 15 guards,
largest unclaimed. This is the core move path, every task move in the
system, so the conversion is built to add **no work** to it.

## Census before/after

| | before | after |
|---|---:|---:|
| `moves.ts` column guards | **15** | **2** |
| repo backlog (this branch vs `origin/main`) | 693 | **679** |

Baseline re-recorded; `--strict` exits 0.

*(Backlog figures don't compose across my open fleet PRs — each branch
carries only its own reductions. 693 → 679 is this branch against main
as measured, not a running total.)*

## Zero added cost, and actually one fewer resolution than before

`moveTaskInternal` **already** resolves the workflow IR unconditionally
at line 400 — the `useWorkflow` gate is gone — and already derived a
lifecycle from it ~400 lines later for the trait hooks. So one hoisted
`moveLifecycle` immediately after the IR resolution serves all 14
guards, and the later local now **aliases** it instead of resolving a
second time.

Net effect on the hot path: **one fewer `resolveLifecycleColumns` call
than before this PR.**

## Converted: 14

6× `toColumn === "done"` → complete · 4× `fromColumn === "in-review"` →
review · 1× `toColumn === "in-review"` → review · 2× `toColumn ===
"todo"` → hold · 1× `toColumn === "in-progress"` → wip

Every site keeps its legacy id as the fallback. `undefined` here means
no IR on this path or a v1 column-less IR, and a move must behave
**exactly** as before when there is no basis to resolve from — this is
the transaction that arbitrates capacity, so "unchanged when
unresolvable" is the requirement, not a nicety.

## Flagged, not converted: 1

**Line 309** — `task.column === "archived"` in the handoff-invariant
check. It sits in a different function that runs **before** any IR
resolution, so converting it would mean *adding* a resolution to a path
that currently has none. That is a cost on the handoff path rather than
the free reuse everything else here gets, so it wants a deliberate
decision rather than my inclusion.

## Verification — the paths that matter, not just typecheck

Because this is the move transaction, `tsc` + lint is not sufficient
evidence:

- **`pnpm test:gate` GREEN** — 158 + 10 + 487 + 71
- `workflow-capacity-invariant` + `move-path-equivalence` **7/7** (the
in-transaction capacity gate lives in this file)
- `handoff-to-review-atomicity` **4/4**
- `store-movement` + `move-task-preserve-status` +
`task-move-hard-cancel-ordering` + `transition-pending-and-status-clear`
**16/16**
- `pnpm lint` clean · core `tsc` clean · `--strict` exits 0

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:06:02 -07:00
gsxdsm
0c7dc8c8ae feat(census): report MIXED-VOCABULARY files — the shape behind four half-conversion findings in one day (#2704)
## The pattern

Four review findings dispatched to me in a single day were the **same
defect**: a guard converted to role resolution while the function it
*feeds* still filters on the literal. The resolved guard admits a custom
column, the literal collaborator rejects it, and **nothing errors** —
the endpoint returns `repaired: 0` and reads as converted.

| PR | resolved side | literal collaborator |
|---|---|---|
| #2700 | review guard | `reconcileInReviewBranchRebind` filters `===
"in-review"` |
| #2700 | retry guard | `isInReviewMissingWorktreeSessionStartFailure`
likewise |
| #2698 | role-aware tabs | reconciliation effects still compare
`"done"` / `"in-review"` |
| #2688 | role-derived flags | memos and a `useState` capture keyed on
the stale value |

Since opening this I have been handed **two more** of the identical
shape (#2701, #2702). It is not a coincidence; it is what a conversion
phase produces by default.

## What this adds

A file where **both vocabularies are live** is where that can happen, so
the census now names those files.

**Measured: 23 of 134 guard-bearing files, holding 311 of 686 guards** —
and the top of the list is exactly where the findings landed:

```
MIXED-VOCABULARY files (a role resolver AND legacy literals): 23, holding 311 guards
   110  packages/engine/src/self-healing.ts
    57  packages/engine/src/executor.ts
    26  packages/engine/src/scheduler.ts
    20  packages/dashboard/src/routes/register-task-workflow-routes.ts
```

## Report-only, deliberately

A partially converted file is the **expected** state during a conversion
phase. Gating this would punish correct in-progress work and would be
routed around within a day. What it buys is that a reviewer of a listed
file knows to check the collaborators of anything converted — which is
what this repo's **Surface Enumeration** rule already requires, and what
each of those PRs missed.

The rule exists. The fleet work order does not mention it, so reviewers
are catching these one site at a time.

## Verification

Five tests, both directions: flags a mixed file; does **not** flag a
fully literal one (or the entire backlog lights up and the signal
carries no information); does **not** flag a fully converted one; does
not match a resolver name inside a longer identifier (the
`hold`-inside-`threshold` trap from #2677); survives an unreadable file.
**Mutation: dropping the resolver condition fails 2 of 38.**

The helper lives in the **lib**, not the CLI — importing the CLI
executes it and calls `process.exit`, so nothing defined there is
reachable from a test. I found that by trying.

38 census tests green · `--strict` and `--compare` exit 0 · lint clean ·
gate green (487 + 158 + 10 + 71). **No census numbers change.**
2026-07-30 04:02:19 -07:00
gsxdsm
339f6e7830 fix(census): stop the baseline serialising the fleet — every fleet PR conflicted with every other one (#2699)
## The problem

Every fleet PR conflicts with every other fleet PR in
`lifecycle-column-census-baseline.json` — **even when they convert
entirely different files**. I have rebased **six** of my own branches
for nothing but this file, and the resolution was *always* "take main's,
re-run `--update-baseline`". Never once a real merge.

That makes a generated artifact the serialisation point for the whole
fleet phase.

## The cause

`totals`, `byColumnId`, `properties` and `queryByColumnId` are
**derived** — recomputable from the per-file maps — and **`--strict`
never reads any of them**. It compares `byFile`, `deliberateByFile` and
`queryByFile`, and nothing else.

But every conversion changes at least one aggregate line. So those lines
were a **shared write on a file whose real content is per-file and
disjoint**. Removing them, two PRs converting different files touch no
common lines.

## Trade-off, stated because it undoes a deliberate choice

An earlier note kept the totals in the pin *"so the new number lands in
the diff where a reviewer sees it"*. That was a good reason. The signal
survives elsewhere:

- the CLI prints the totals on every run;
- `--update-baseline` prints each tightened entry by name;
- the fleet rules already require a census before/after **in the PR
body**.

Reversible if the diff-visible number proves to matter more than the
conflicts.

## Cost, stated too

Merging this makes every in-flight fleet PR re-record once. That is one
more instance of an operation they are already performing on every
rebase — a one-time cost against a recurring one.

## Verification

The end-to-end test that asserted the write via `totals.column` now
asserts the same claim via the per-file entry: the stale pin says 1, the
rewritten pin must carry the tree's real higher count for that file.
**Mutation: suppressing the `--update-baseline` write still fails it**,
so the assertion did not weaken.

71 census tests green · `--strict` and `--strict --exact` exit 0 · lint
clean · gate green (487 + 158 + 10 + 71).

## Not done

A merge driver. `.gitattributes` can name one, but registering it needs
`git config` per clone and this repo has no `postinstall`/`prepare` hook
to do that — so it would silently not apply for most people. Removing
the shared lines fixes the conflicts without needing any local setup.
2026-07-30 03:56:12 -07:00
gsxdsm
78b6b5ba37 fleet: packages/engine/src/executor.ts 85 → 57 (in progress; 4 batches, plus the structural measurement this cluster needs) (#2689)
**Claiming `packages/engine/src/executor.ts`** — the largest unclaimed
cluster (self-healing.ts and scheduler.ts are taken).

## Census

| | before | after |
|---|---:|---:|
| `executor.ts` | 85 | **75** |
| repo total | 722 | **712** |
| `done` | 195 | 190 |
| `archived` | 147 | 142 |

Baseline re-recorded in the same commit; it shrinks by exactly the
converted count (10 literals across 5 sites).

## Batch 1 — terminal-lane guards

Five identical *"this card is already finished, refuse"* guards, all the
literal pair `live.column === "done" || live.column === "archived"`. On
a renamed board neither matches, so the refusal falls through — the same
inert-guard shape as #2670.

Converted to `resolveTerminalColumnsFor`, **the helper this file already
established** at line 4509 — no new abstraction. It unions the resolved
terminal columns with the legacy pair, so each converted guard is a
strict **superset** of the literal: it can refuse in more cases, never
fewer. That is what makes this batch safe without per-site behavior
review.

## The structural measurement this cluster needs

#2683 found self-healing.ts unsafe to batch because of **sync** workflow
reads — a converted guard there would resolve through a sync path that
cannot resolve a selection in production, silently falling back to
defaults. I measured whether executor.ts has the same problem, per guard
(not per line):

| context | guards |
|---|---:|
| **async** — safe, can `await resolveWorkflowIrForTask` | **71** |
| **sync** — needs threading or is not convertible in place | **14** |
| module scope | 0 |

The 14 sync-context guards are at lines 3455, 3479, 3530, 3540, 4611,
5501, 5502, 5504, 5777, 10213, 12306 (×3), 15782 — `in-progress` 4,
`in-review` 4, `archived` 3, `done` 2, `todo` 1. **I am not converting
those in place**, and I will flag rather than guess if threading
resolved data changes behavior.

So: unlike self-healing, this cluster is **83% safely convertible**,
which is why it is worth working as a batch.

## Note on #2685

Engine code converts through core's resolvers
(`resolveLifecycleColumns`, `resolveTerminalColumns`), not the dashboard
`columnRoles` helpers. So the 680-guard helper gap #2685 fixes is
**dashboard-side** — this cluster is not blocked on it.

## Verification

engine `tsc` clean · lint clean · gate green (487 + 158 + 10 + 71).

**Pre-existing failures, not caused by this change:** five tests in
`src/__tests__/reliability-interactions` fail, all in
`SelfHealingManager.recoverStarvedRefinementTriageTasks`. I confirmed by
stashing this change and re-running on a clean tree — they fail there
too. This change touches only `executor.ts` and does not go near that
path. Flagging rather than fixing: it is someone's cluster and not mine
to alter mid-flight.

## Not done

Batches 2+ (the remaining 75). I will keep working this file in this PR
with small commits, per the fleet rules.
2026-07-30 03:25:21 -07:00
gsxdsm
e9e63d8e0f consolidate/capacity: --strict was red on main (my #2621), 14 stale baselines, routines seeding a deleted column, worktrees-off audit (#2652)
Capacity unit consolidation. Three coherent themes, small commits
inside.

## Census before/after (`node scripts/lifecycle-column-census.mjs`)

| | before | after |
|---|---:|---:|
| triage column guards (the bar) | 10 | **10** |
| `--strict` on main | ❌ **RED** | ✅ green |
| baseline staleness | 14 files stale | **0** |

This branch does **not** move the triage bar — its remaining 10 are
moves.ts (dies with the flag), the dashboard cluster, and one deliberate
site. It fixes the instrument that measures the bar, plus a live defect
the comparison count cannot see.

---

## 1. `--strict` was RED on clean `origin/main`, and it was my fault

```
packages/dashboard/src/routes/register-task-workflow-routes.ts: 22 -> 23
```

My merged #2621 added a v1-IR pre-WIP fallback answering a greptile P1
and shipped no marker or baseline update, so the program's measuring
instrument has been failing on main since it landed.

Fixed **at the site** with a `DELIBERATE-LITERAL` marker, not by bumping
the baseline. That branch runs only when the IR declares no columns and
no nodes, so there is no role to resolve — `resolveLifecycleColumns`
returns nothing and the legacy pre-implementation ids are the only
pre-WIP signal that exists there. It is *unconvertible*, not unfinished;
the sibling `else` two lines down is the trait path for every IR that
can answer. A rise that is genuinely correct belongs where a reader will
see it.

## 2. The baseline was stale for 14 files — a hole, not cosmetics

A stale allowance lets converted guards return while the check stays
green. Measured gaps:

```
self-healing.ts          allows 126, tree has 111
executor.ts              allows 112, tree has 104
moves.ts                 allows  44, tree has  39
default-workflow-hooks   allows  25, tree has   7
mission-feature-sync     allows   5, tree has   0
MissionControlPanel      allows   4, tree has   0        (+8 more)
```

**Only two of the fourteen are mine.** The other twelve are
already-merged conversions by other workers where nobody re-recorded.
Re-recorded all fourteen here rather than waiting for twelve PRs,
because until it happens the ratchet is not holding the 779 it exists to
hold. Flagging it plainly: those drops are other people's work being
locked in, not mine being claimed.

## 3. Routines created tasks into the column U11 deleted

The routine editor's "Target Column" defaulted to `triage`. That value
is submitted as the create step's `taskColumn`, and an **explicit**
column bypasses the workflow entry-column resolution added for
column-less creates (#2589) — so every routine saved with the untouched
default seeded its tasks into a column the board does not declare.

Defaulting to `todo` would be the same mistake one column over: a custom
workflow declaring no `todo` is seeded into an undeclared column just as
surely, because an explicit column overrides entry resolution whatever
its value. So the default sends **nothing** and each workflow's own
intake resolution decides.

The `triage` **option** is removed too, not merely un-defaulted — fixing
the initializer alone left the operator able to pick the deleted column
one click away, and it was the option labelled "Planning", the name the
merged `todo` column now displays. Removing it retires that label
inversion as well.

Found by scanning **membership** forms rather than comparisons: the
comparison census cannot see a `?? "triage"` default, so no count showed
this and nobody was looking. Revert-proof — restoring the default fails
with *"the default must not name a column at all"*.

## 4. "Worktrees off is INERT" had one unaudited reader

The constraint was that `maxWorktrees` become genuinely inert, "not set
very high and not skipped by convention". `resolveWorktreeCapacityLimit`
returns `null` for that, and its unit tests can only prove the
**resolver** is right — they cannot see a second reader, which is the
only way the constraint breaks.

Audited every `maxWorktrees` read that bounds anything. **Exactly two:**
`scheduler.ts` (the admission gate, via the resolver, single call site,
optional gate snapshot) and `self-healing.ts`'s `enforceWorktreeCap` —
`(settings.maxWorktrees ?? 4) * 2`, a **raw** read.

The second is **not a bug** and is left alone: it bounds worktree
*directories on disk* and only removes *idle* ones. Worktrees still
exist in OFF mode, so that bound must keep applying or idle directories
accumulate unbounded. Recorded consequence: in OFF mode the number still
governs disk retention while gating no admission — an edge you scoped
out. The note says explicitly **not** to unify the two readers: routing
hygiene through the resolver returns `null` in OFF mode and silently
removes the disk bound, which is a leak dressed as a simplification.

New ratchet requires every file bounding on `maxWorktrees` to be named
with a reason, and rejects a **stale** allowlist entry. Proven by
injecting `active >= (settings.maxWorktrees ?? 4)` into
`hybrid-executor.ts`.

---

## Deliberately NOT included

- **My own census script.** #2633 landed the canonical one, and it is
better than mine — an AST classifier *plus* an independent text
classifier with `--compare`, and a baseline that fails on unrecorded
**drops** as well as rises. Mine only caught rises. I deleted mine
rather than ship a second measuring instrument; three copies of "strip
comments" is the drift shape this program keeps paying for, so the
worktree ratchet now imports #2633's `stripComments`.
- **My TaskContextMenu fix.** Superseded, and by a better answer: main's
`isPureIntakeColumn` (intake *without* hold) keeps the merged Planning
column shown and suppresses only a bare Ideas capture, which resolves
the exact hold-lane objection coderabbit raised against my version. I
briefly clobbered that merged work by checking my old file out
wholesale, caught it in the diff, and reverted.

## Verification

`pnpm lint` clean · core + dashboard `tsc` clean · census suite 23/23 ·
worktree ratchet 8/8 · RoutineEditor 49/49 ·
`routes-task-retry-planning-column` 16/16 · `lifecycle-column-census
--strict` exits 0.

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

---

## Added after review (all four greptile threads were real, and two of
them mattered)

**The routine fix was half a fix.** `routine-runner.ts:515` *and*
`cron-runner.ts:982` both did `column: (step.taskColumn as Column) ||
"triage"` **after** the step is read, so every routine — including ones
saved through the fixed editor — still created tasks into the deleted
column. Both now omit it.

**The advanced steps editor MANUFACTURED the defect.**
`ScheduleStepsEditor.tsx` had three `triage` defaults: the new-step
template (`:64`), the per-step initializer (`:95`), and the select still
offering it (`:344`). So the path I had *not* fixed produced the bug by
default, on fresh data. Template names no column; initializer coerces a
persisted `triage`; `triage` removed from the options; empty submits
`undefined`.

**Four pre-existing tests pinned the defect** and are rewritten to the
corrected invariant rather than appeased:

| test | asserted |
|---|---|
| `cron-runner`: "defaults column to triage when taskColumn is not set"
| `column: "triage"` |
| `ScheduleStepsEditor`: "adds a create-task step..." | `taskColumn`
toBe `"triage"` |
| `ScheduleStepsEditor`: "allows saving create-task step..." | the
legacy column is **resubmitted** |
| plus the explicit-column case added beside each, so the fix cannot
swallow a deliberate choice |

**The allowlist hole was the worst finding.** `AUDITED_BOUNDS` was keyed
by FILE, so every bounding expression in an allowlisted file was exempt
— a second raw bound in `scheduler.ts` stayed green, the one case that
ratchet exists for. Per-expression now, and making it so **immediately
surfaced a real second bound the file-level version was hiding**
(`maxWorktreesGate.used >= maxWorktreesGate.limit`, safe by construction
since the snapshot is `undefined` in OFF mode). Proven by injection.

## Found while re-reading my own deletion, not reported

A **rendered tooltip** still named a deleted cap. The "Queued to plan"
badge read *"planning starts when a concurrency slot frees up
(maxConcurrent / globalMaxConcurrent)"*. The cross-project cap is gone —
capacity is two numbers per project — so it told operators their
planning waited on a limiter they can no longer find a setting for.
Names the surviving dimension only now.

## Coding (Ideas): enforcing #2651 rather than repeating it

I took the unowned coding-ideas IR merge, concluded it must not be done,
then found **#2651 had already implemented, reverted and documented
exactly that** — with better grounding than my own argument. It added no
test, so nothing stops the next person reaching the same dead end.

So this ships their reasoning as a ratchet, not a second opinion: triage
discovery keys on the column's `autoTriage`, so a merged column is
either never scanned (cards sit on a bootstrap stub until the **capacity
hold** releases them, sending **unplanned** work into in-progress —
worse than stalling) or scanning wins and the manual gate is gone. Their
scope caveat is kept: `autoTriage` is a general trait field, so only
*this preset's* collapse is dead, not manual intake as a concept. The
registry does not reject the merged shape, which is why prose was not
enough.

## Verification (re-run)

`pnpm lint` clean · core + engine + dashboard-app `tsc` clean ·
`lifecycle-column-census --strict` exits 0 ("every file matches its
baseline exactly") · routine-runner 24/24 · cron-runner 156/156 ·
ScheduleStepsEditor 41/41 · RoutineEditor 49/49 · worktree +
coding-ideas 12/12. TaskCard has 2 failures **pre-existing on main** —
confirmed identical with my changes stashed.

---

## Bears directly on the closing bar: this PR already removes the
67-guard ratchet slack

Measured on current `origin/main` with the census itself:

```
tree total: 787   baseline total: 854   SLACK: 67

FILES ABOVE BASELINE (1):
   +1  packages/dashboard/src/routes/register-task-workflow-routes.ts  (22 -> 23)

FILES BELOW BASELINE: 13, totalling 68 unrecorded conversions
   -18  core/default-workflow-hooks.ts (25->7)   -15  engine/self-healing.ts (126->111)
    -8  engine/executor.ts (112->104)             -5  core/task-store/moves.ts (44->39)
    -5  engine/mission-feature-sync.ts (5->0)     -4  core/live-agent-count.ts (10->6)
```

**The slack is not regression — it is 13 files of merged conversions
nobody re-recorded**, against exactly **one** rise. This PR re-records
the baseline **854 → 782 across 140 files**, which closes it.

**And the "+3 that slipped in" is +1, and it is mine.**
`register-task-workflow-routes.ts 22 → 23` is the v1-IR pre-WIP fallback
my #2621 added; it is justified (that branch runs only when the IR
declares no columns or nodes, so there is no role to resolve) but it
shipped with no marker and no baseline update — which is why `--strict`
has been **red on main since it merged**. Fixed here at the site with a
`DELIBERATE-LITERAL` marker rather than by bumping the baseline, because
a rise that is genuinely correct belongs where a reader will see it.

Sequencing note for the auto-lowering change: if this lands first, that
work is purely the mechanism (auto-lower, or fail with tighten
instructions) rather than a cleanup, and the two re-records will not
collide in the same file.

Also worth carrying into that mechanism, from building the same guard
here: **`--update` must refuse to RAISE.** An earlier version of mine
wrote current counts verbatim, so a developer who added a literal and
ran the documented update command locked the regression in as the new
ceiling — the mirror of the high-water problem. Lowering can be
unattended; raising should be a hand edit with the reason recorded.

## Third piece of residue from my own deletion

`updateGlobalConcurrency` in the dashboard API client PUT to
`/api/global-concurrency`, a route removed when the machine-wide cap
went. Zero callers; the only reference was the `legacy.ts` barrel
re-export. Deleted both. `fetchGlobalConcurrency` **survives on
purpose** — the GET route remains and serves live utilization telemetry
to the footer and Command Center; nothing gates on it.

That is the third: after the second raw `maxWorktrees` reader and the
"Queued to plan" tooltip. A deletion is not finished when the
enforcement goes — the client, the label and the tooltip outlive it.

---

## Re-greened the dashboard API tests: 117 failures on main, ONE root
cause

These would have polluted the closing verification pass, and nobody
owned them.

`api()` builds headers via `new Headers(...)` and returns
`Object.fromEntries(headers.entries())` — and `Headers.entries()`
**lowercases every key**, so the object reaching `fetch` is
`content-type`, not `Content-Type`. `ab87d0d80` then added
`x-fusion-client: dashboard-ui` for run-audit attribution. Both changes
are correct; neither is visible at a call site, so **114 assertions
across 7 files** kept asserting the old shape and went red together.

Fixed by naming the shape **once** in `app/test/apiRequestHeaders.ts`
rather than patching 114 literals — restating a shared fact 114 times is
what made a two-line client change look like 117 failures. Deliberately
not a loose `objectContaining`: these tests are the only thing pinning
that the attribution header is sent *at all*.

**117 → 4.** The remaining 4 are unrelated pre-existing CSS failures
(`task-detail-modal-tablet-width` ×3, `space-token-defined` ×1) —
confirmed identical on clean main with my changes stashed.

### A gap this surfaced, recorded not papered over

Three routes failed in the *opposite* direction — they send the old
shape because they call `fetch()` **directly**, bypassing `api()`, so
they never get the attribution header. `client.ts` claims the opposite:

> "Applied once here rather than per-call so no future mutation route
has to remember it."

That does not hold for a route that bypasses the helper it is applied
in. **Measured in `app/api/`: 8 files make direct `fetch()` calls and 7
include mutations (POST/DELETE)** — among them `ai-sessions.ts`'s
DELETE, which is the same class as the four-delete incident the header
was added for. So the attribution fix has a hole in exactly its
motivating case.

Not fixed here: routing those onto `api()` is a behaviour change across
the API layer and belongs to its owner, not to a test re-green. Those
assertions use a separate `API_JSON_HEADERS_NO_ATTRIBUTION` constant so
the gap stays **visible** — if a route is later moved onto `api()`, its
test fails and points at the note explaining why.

---

## This branch takes the triage bar 10 → 5, and makes `--strict` green

`node scripts/lifecycle-column-census.mjs` on this branch reports
**triage 5**, against **10** on `origin/main`. The five removed are the
ScheduleStepsEditor template/initializer/option and the RoutineEditor
default/option — the automation paths that were creating tasks into the
deleted column.

**`--strict` was also RED on clean main, twice over, and both causes
were the same mistake:** a thorough written rationale the tool cannot
read, because the marker was not where the census looks. The census
reads a comparison node's **leading comments**; a `DELIBERATE-LITERAL`
in the JSDoc above the enclosing function or declaration does not reach
the comparison inside it.

| site | why it is legitimate | why the tool could not see it |
|---|---|---|
| `columnRoles.ts:80` `isHoldColumnRole` | degrades to `columnId ===
"todo"` only when a column has **no resolved traits** — identical in
kind to `LEGACY_PRE_IMPLEMENTATION_COLUMN_IDS` directly above, which
escapes counting only because a Set is a membership form | rationale
written, **no marker token** |
| `MissionControlPanel.tsx` ×3 | the SDLC funnel **alias table** — maps
`to-do`/`ready`/`review`/`shipped` onto one display stage with an
explicit `other` bucket, and nothing branches on it | marker in the
JSDoc; the comparisons are arrow bodies **inside the array literal**,
which it does not reach |

The second only surfaced because converting the `triage` stage to a Set
removed its count and exposed the siblings — red gate, justification
sitting three lines above, unreachable.

Both are markers, no behaviour change. Neither is a conversion
candidate: resolving the funnel table to traits would **drop the
non-column aliases it exists to accept**.

**For the auto-lowering work:** the marker-placement rule is now the
recurring trap — three instances, three different authors, including me.
A marker that does not register is indistinguishable from no marker, and
the failure mode is a red gate with a written explanation nobody can act
on. If the census accepted a marker anywhere in the enclosing
declaration's comments, none of the three would have happened.

Baseline re-recorded per the tool's own instruction ("Re-record the
baseline in the SAME PR that lowered the count").


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

- **Bug Fixes**
- Task “Actions” menus no longer appear on bare cards in the Planning
column.
- Routines, scheduled tasks, and create-task steps now respect each
board’s configured workflow intake column instead of using a retired
default.
- Legacy tasks saved with the retired intake column are migrated to
automatic workflow resolution.
- Target-column selection now offers only “Automatic (workflow intake)”
and “Planning,” removing the obsolete option.
- Capacity/planning messaging and related UI tooltip text were
clarified; concurrency cap updates are managed per project.

- **Tests**
- Added/updated coverage for workflow intake resolution, create-task
target column behavior (including legacy coercion), capacity safeguards,
and API request consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 03:10:35 -07:00
gsxdsm
aa02db5782 fleet: scheduler.ts 28 → 27 + make the column-role predicates reachable from the other 80% of the backlog (#2690)
## Census

**722 → 721**; `packages/engine/src/scheduler.ts` **28 → 27**. Exactly
the one site converted. Baseline re-recorded in the same commit —
`--strict` flagged the stale allowance itself, and `in-progress` went
138 → 137.

## The unblocker (commit 1)

The role helpers live in `packages/dashboard/app/utils/columnRoles.ts`,
a dashboard-**app** module. Measured against the census:

| Location | Guards | Share | Helpers 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 |

**Only 20% of the backlog can call them at all.** #2685 widens the
helper *set* correctly; that is coverage, not location.
`packages/core/src/column-roles.ts` is the same flags-first /
legacy-id-fallback predicate placed where the other 80% can reach it —
core already exports `resolveColumnFlags`, so no new resolution
machinery comes with it.

**Semantics are mirrored from #2685, not invented**, so the two sets
cannot answer the same question differently: `complete` EXCLUDES
`archived`; `wip` keys on `countsTowardWip` (the same flag capacity
arithmetic uses); `review` accepts `mergeBlocker` OR `humanReview`. One
addition — `isTerminalColumnRole` for the `!== "done" && !== "archived"`
union, the most repeated shape in the backlog.

10 tests cover both modes of all 8 predicates, including the **degraded
no-flags fallback** — the half with no coverage when these lived only in
the dashboard app — plus the two cases that prove the predicate does
something rather than nothing: a renamed column carrying the right trait
answers yes, and a legacy id carrying the WRONG trait answers no.

## A trap every fleet worker converting engine code will hit

A new core export must be added to **both** `index.ts` and
`index.gate.ts`.

The `engine-core` gate project resolves `@fusion/core` to a bundle built
from `index.gate.ts` (`scripts/build-engine-core-gate-bundle.mjs`). An
export present only in `index.ts` is `undefined` at runtime under the
gate: 13 `scheduler-workflow-cutover` tests failed with `isWipColumnRole
is not a function`, in a file that does not mock `@fusion/core` at all.
The symptom points at the consumer, the cause is the barrel.

I nearly mis-attributed this. Baseline first:
`scheduler-workflow-cutover` is **42 passed on clean main**, so the 13
were mine — not pre-existing. That measurement is the only reason I
looked at the barrel instead of "fixing" the tests.

## The conversion (commit 2)

`scheduler.ts:1690`'s `isWipColumnTask` was a hand-rolled copy of
`isWipColumnRole` — it stored only `countsTowardWip` as a boolean and
re-implemented flags-first-then-legacy-id inline. It now stores the
resolved flags object and lets the shared predicate decide.

Behaviour is identical in all four states: column present with the flag
true or false (flags win), column absent from a resolved IR, and IR
resolution failed (both defer to the legacy id).

| Check | Result |
|---|---|
| `scheduler-workflow-cutover` | **42 passed** before and after |
| 21 scheduler/capacity/hold-release files | **372 passed** |
| `pnpm test:gate` | **726 passed** |
| `pnpm lint` | clean |

## Flagged and skipped, not guessed

**`scheduler.ts:1736` — a latent legacy-vocabulary defect, not a
conversion.** `if (task.column !== "in-progress") continue;` gates the
file-scope-lease loop on the literal, ~40 lines below capacity
arithmetic that is trait-aware. On a renamed WIP column the loop
silently does nothing while capacity counts the same cards correctly.
Converting it *changes behaviour* on renamed boards (from wrong to
right), which the fleet rules put out of scope — so it is flagged here
for whoever owns that fix. It is the same class as U10's six
legacy-vocabulary defects.

**`hold-release.ts:343`** — already marked `DELIBERATE-LITERAL`. It is
the legacy half of FN-5719's dual-accept pair; converting it would make
both halves compute the same answer, deleting the compatibility signal
*and* its divergence detector while looking like a cleanup. Untouched.

**`task-merge.ts:254`** — the documented fallback for callers that have
not proven lane identity; trait-aware callers pass
`skipColumnIdentityCheck`. Untouched.

**The other 14 `scheduler.ts` sites** have no flags in scope (e.g.
`isLegacyDependencySatisfied(dep: Task | undefined)`,
`shouldHoldActiveFileScopeLease(...)` — task-only pure functions).
Threading an IR in changes signatures and call graphs: behaviour change,
out of scope. This is why the cluster is 28 → 27 and not 28 → 0, and the
reachability measurement behind it is #2687.

No changeset: `@fusion/*` are private and no `@runfusion/fusion`
behaviour changes.
2026-07-30 03:01:36 -07:00
gsxdsm
dc50425e98 docs: correct 104 future-dated FNXC timestamps across 61 files (#2680)
## What

The FNXC convention exists so a reader can place a note against the
change that motivated it. A stamp dated *after* the edit landed defeats
exactly that.

This is program-wide drift, not one author's slip — I contributed to it
in my own commits this week, which is how I noticed it.

## Measured, on this tree

**104 stamps across 61 files** dated later than the day they were
written, from one day ahead to **2026-10-19 (81 days)**:

| count | date | count | date | count | date |
|---|---|---|---|---|---|
| 50 | 2026-07-31 | 6 | 2026-08-05 | 3 | 2026-08-13 |
| 17 | 2026-08-01 | 1 | 2026-08-07 | 1 | 2026-08-19 |
| 7 | 2026-08-02 | 1 | 2026-08-12 | 2 | 2026-08-26 |
| 11 | 2026-08-03 | | | 3 | 2026-10-19 |

An earlier number I circulated was ~70. That came from a narrower
pathspec and was wrong; **104** is the measurement.

## How

Each stamp is rewritten to the date of the commit that introduced **that
line**, via per-line `git blame` — deliberately *not* stamped uniformly
with today's date. A uniform stamp swaps a wrong date for a different
wrong date and flattens the ordering that makes these comments
navigable; blame preserves it. Times of day are untouched, and a blame
date in the future is clamped rather than trusted.

## Why the verification is listed

A docs sweep across 61 files is precisely where a stray edit hides, so
the safety claims are mechanical rather than asserted:

- every changed line begins with a comment marker — **no code touched**;
- **no test asserts an FNXC date later than today**, so no `toContain`
assertion on embedded source text can be silently invalidated (several
such assertions do exist);
- CSS files, which carry several of those assertions, are outside the
pathspec.

## Verified

lint clean · merge gate green (487 + 158 + 10 + 71) · `census --strict`
exit 0 · tsc clean for core, engine, and dashboard
(`tsconfig.app.json`).

**No behavior change.** Comment text only.

## Not done here

A guard preventing recurrence. A check that rejects an FNXC stamp dated
after the commit would stop this returning, but it needs a decision
about where it runs (lint rule vs. gate) and it is a behavior change to
CI — it does not belong riding inside the sweep it would police.
2026-07-30 02:38:58 -07:00
gsxdsm
543f4a556c Tell an already-converted fallback literal from an unconverted guard — 19 of 19 dashboard scan hits were the former (#2677)
The batch phase is about to hand per-file guard lists to cheap workers,
and the census currently cannot distinguish **"not yet converted"** from
**"converted, with a documented degradation."**

## The measurement that makes this a class, not a preference

A proximity scan for *"legacy literal near a role-resolved call"* — the
heuristic that produced #2670 and #2672 from the engine — returned **19
hits across the dashboard and zero defects.** Every one was:

```ts
if (flags) return flags.hold === true || flags.countsTowardWip === true;
return column === "todo" || column === "in-progress";      // reachable only without traits
```

That literal is **correct**: it answers for callers with no resolved
column metadata, which is the case `resolveLifecycleColumns` returns
`undefined`-for-the-whole-struct to preserve. A worker told to "convert"
it would delete the only answer available when traits are absent.

## And the difference is structural, so the parser can see it

In **both** engine defects the literal sat in a **separate statement
beside resolved data**, not in a fallback branch. Proximity cannot tell
those apart; an AST can.

`traitFallback` flags the ternary form and the **early-return** form
(which is how most are actually written), and deliberately does **not**
flag a fallback whose test is itself a column-*name* check — otherwise
any `if/else` over column names would launder itself.

## Reported beside the backlog, not subtracted from it

```
COLUMN guards (the backlog):   746
  of the column guards, 9 are trait-fallback branches (already converted)
```

A fallback literal is still a literal and should go when the trait path
becomes unconditional. This only says which **kind** of work it is.

**Advisory, and structurally so:** `traitFallback` never changes `kind`,
and the count lives *outside* `totals`. My first attempt put it in
`totals` and broke two existing suites that correctly deep-equal that
shape — an advisory number does not belong in the structure that defines
the bar.

## Revert proof

Forcing `traitFallback: false` fails **3 of 33** (both fallback forms,
plus the kind-unchanged case). The two *negative* cases pass under the
revert — which is the point: they assert what must **not** be flagged,
and a classifier that flags nothing satisfies them trivially. Worth
stating, because a revert proof that only counts failures would look
stronger than it is.

## Baseline

Re-recorded: `executor.ts` 87 → 85 was **main's own drift** from #2568
landing, so `--strict` was red on main again. #2668 made the re-record
possible; the auto-tighten (coordinator item 2) is still open, and this
is the third time in this program that a legitimate merge has left the
gate red for everyone else.

## Verification

62/62 across both census suites, `pnpm test:gate` **71/71**, `--strict`
exits 0, `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

* **New Features**
* Added reporting for legacy column comparisons found in trait-fallback
branches.
* Census results now include a separate count for these fallback-related
column guards.
* Human-readable reports display the new metric alongside the existing
backlog totals.

* **Tests**
* Added coverage for fallback detection across ternary, early-return,
and conditional patterns.
* Added safeguards to prevent false positives in resolved-data and
column-name checks.

* **Maintenance**
  * Updated baseline census metrics to reflect revised classifications.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:33:19 -07:00
gsxdsm
bc782d8d92 U12: resolve the move-path compatibility flag — trait hooks unconditional, legacy branch deleted (#2655)
U12's headline goal. The raw `experimentalFeatures.workflowColumns` flag
gated **every task move**; its six seams are now unconditional and the
flag, its last two readers, and the 124-line inline legacy branch are
deleted.

## Deleted, not converted

The flag-OFF branch goes with the gate. Converting a branch we intended
to delete would have left a second definition of every column side
effect alive to drift — the defect this program has spent its length
removing.

Both readers flip in **one commit** because they are not separable: the
preflight in `workflow-task-create-ops.ts` computes the
`movePolicyPreflight` that `moves.ts` consumes and validates. Un-gating
either alone either evaluates workflow move policies — with their
plugin-gate side effects — whose result is ignored, or validates against
a preflight that was never computed.

## Evidence, not assertion

**Equivalence (precondition 1).** `moves-flag-equivalence.test.ts`
(commit 1) ran the same journey under both flag states against live
PostgreSQL and diffed the persisted row: **identical across 128 fields**
plus an equal timing shape, over `todo → in-progress → in-review → todo
→ in-progress`. Mutation-verified both ways — stamping the flag-ON
branch, and diverging the reopen hook, each fail it.

**And there was stronger evidence already on main that isn't mine.**
U2b's `move-path-equivalence.pg.test.ts` ran *every* scenario once per
path and has been green across ~10 of them: `preserveStatus`,
`preservePause`, timing accounting, `preserveProgress`,
`preserveWorktree`, engine-source rehome, `in-progress → todo`. Two
independently built harnesses agreeing is the best evidence this
question has had.

**The flag was read by nothing in production.** `experimentalFeatures`
is global-only and no module writes it, so this path had never run for
any project without a stale persisted value. That is also why a green
suite was never evidence on its own — both paths were individually valid
and only one was live.

## Two claims of mine this PR corrects

**1. Seam 2 does not introduce new rejections.** I said in #2639 and in
the census that with the flag off there is *no* target validation, so
flipping would add refusals. Reproduced the opposite: a move to an
undeclared column already rejects on the legacy path with `Invalid
transition: … Valid targets: …`. I found it because the discriminator I
wrote to prove "the flag is the cause" failed.

**2. My first equivalence test proved nothing.** It used
`updateSettings`; `experimentalFeatures` is **global-only**, so
`getSettingsFast()` filtered the write out and `useWorkflow` was false
in *both* runs. Caught by stamping the flag-ON branch and watching the
test stay green. It now writes via `updateGlobalSettings` and **asserts
the flag took effect** before the journey. U2b's harness carries the
same warning independently — `MUST be updateGlobalSettings, NOT
updateSettings`.

## The user-visible change

Move rejections now report **workflow-resolved** targets instead of the
hardcoded legacy adjacency table. Concretely: `Valid targets:
in-progress, triage, archived` becomes `Valid targets: archived,
in-progress`. That is the fix, not a regression — the legacy table still
advertised `triage`, a column the default lineage stopped declaring at
#2515, so an operator following the old message was told to move
somewhere impossible.

Likewise a move *into* `triage` is now refused rather than stranding the
card in a column with no trait flags, invisible to every trait-driven
sweep until reconciliation re-homes it.
`live-move-path-undeclared-target.test.ts` characterised exactly that
defect and carried `it.todo("should REFUSE a move into a column the
task's workflow does not declare (U2b)")` — **this fulfils it.**

## Test migration

| file | change |
|---|---|
| `move-path-equivalence.pg.test.ts` | deleted — every scenario ran once
per path; purpose fully discharged |
| `workflow-capacity-invariant.pg.test.ts` |
`setPath("inline"\|"hooks")` → `assertMovePathLive()`; the probe is
**kept** so capacity cannot pass because moves were broken for an
unrelated reason |
| `store-movement.pg.test.ts` | asserts the refusal **and** that the
legitimate backward move still works, so it reads as a narrowing |
| `raw-workflow-columns-flag-census.test.ts` | deleted per its own
instructions — it was built to fail in both directions and fired exactly
as designed: `expected [] to deeply equal [3 readers]` |
| `moves-workflow-flag-seams.test.ts` | deleted — it pinned the six
seams this removes |

## Verification

Full core suite: **33 failed / 10 files — byte-identical to main's
baseline**, with **zero** files failing exclusively on this branch. I
measured the baseline by checking out `origin/main` and running the same
command, because the first comparison I made was by count alone and
would have blamed the flip for 8 files that were already red.

`pnpm lint` clean. `pnpm test:gate` green (10 / 132 / 482 / 71). `tsc -p
packages/core/tsconfig.json` clean. Core builds.

## Left in place deliberately

The `workflowColumns` settings key stays schema-tolerated and is already
in `HIDDEN_EXPERIMENTAL_FEATURE_KEYS`, so an upgraded project carrying a
stale value renders nothing and loads cleanly. Removing it from the
schema would risk rejecting those projects for no benefit now that
nothing reads it.

---

## Rebased onto current main — and `triage` reaches ZERO

| metric | before | after |
|---|---:|---:|
| `moves.ts` column guards | 39 | **15** |
| repo column total | 745 | **741** |
| **`triage` column guards** | 1 | **ABSENT (0)** |

`triage` is now absent from `byColumnId` entirely: no unconverted
`triage` guard remains anywhere in production source. Combined with
#2664 (the last one, in `TaskContextMenu`) this closes bar item 1.

The census behaved exactly as designed on the rebase: the flip *deletes*
guards, so `--strict` reported `moves.ts: allows 39, tree has 15` rather
than leaving a stale allowance, and the re-record lands in this PR's
diff.

## Verification on the rebased tree

- Full core suite: **33 failed / 10 files — identical to main's
baseline**, zero files failing exclusively on this branch (measured by
checking out `origin/main` and diffing the failing-file sets, not by
comparing counts).
- `pnpm test:gate` green (10 / 158 / 487 / 71).
- `pnpm check:lifecycle-columns` exits 0.
- `pnpm lint` clean, `@fusion/core` builds.

## Two review fixes carried in this PR

**P1 — optionless engine moves lost their bypass.**
`resolveWorkflowBypassGuardsImpl` did `void moveSource;` — it discarded
the resolved parameter and re-read `options?.moveSource`, so
`moveTask(id, target)` resolved to `"engine"` at the call site and
computed `bypassGuards === false`. Latent while the flag gated
validation; with the gate gone, an internal executor/merger/recovery
move made without an options object would be judged as a user move.

**P2 — the absence signal.** Emitting `workflowId` unconditionally would
have stamped `builtin:coding` onto every task with no explicit
selection, reporting a fallback as authoritative. Now emits the
selection directly, so absent still means "not resolved here".


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

## Summary by CodeRabbit

- **Bug Fixes**
  - Task moves are now validated against the task’s declared workflow.
- Invalid destinations are rejected with a clear error, and tasks remain
in their original column.
  - Valid backward moves continue to work as expected.
- Move behavior and lifecycle updates are now handled consistently
across workflows.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:19:32 -07:00
gsxdsm
a099813e94 fix(test): decouple the audit-emitter assertion from log formatting (last of the 4 red PG suites) (#2675)
Last of the four long-red live-PG suites. **This one is a stale test —
the only one of the four that is.**

## The cause

`withSeverityMarker` (`logger.ts:31`) deliberately wraps every message
in a machine-readable severity marker so the TUI log pane can colour by
level. The emitted string carries a `fnlvl=warn` marker and a
`[core-async-secrets-store]` subsystem tag ahead of the real text.

The assertion pinned the raw message with `toHaveBeenCalledWith`, so it
broke when that convention landed. It was coupled to log **formatting**,
not to the behaviour it exists to check.

## The fix

Rewritten to assert what it actually cares about: exactly one warning,
whose message **contains** the subsystem-tagged text, carrying the
underlying cause.

Both halves of the behaviour stay pinned — the `resolves.toMatchObject`
above proves the secret is still created when the audit emitter fails,
and this proves the failure is surfaced rather than swallowed.

**Mutation-verified rather than assumed green:** deleting the
`severityAuditLog.warn` call in `async-secrets-store.ts` fails with
`expected "warn" to be called 1 times, but got 0 times`. A
`stringContaining` assertion that passes because it matches nothing
would be worse than the brittle one it replaces.

## The four, complete

| suite | verdict |
|---|---|
| `store-wedge-resolution` | **product bug** — `42P18`, total runtime
failure of wedge resolution in PG (#2669) |
| `workflow-settings-project-identity` | **stale docs** — resolver
contradicted its own documented order (#2671) |
| `agent-logs-and-monitor` | **real defect** — funnel mis-bucketing from
the U11 merge; half fixed in #2674, half needs a product call |
| `central-archive-secrets` | **stale test** — this PR |

**Three of four were real problems**, sitting behind "pre-existing,
fails on main too". That phrase answers *whose* problem it is, not
*what* is wrong.

## Census, again

`check:lifecycle-columns` is **still** exiting 1 on `origin/main` —
`executor.ts: allows 87, tree has 85` — the same staleness flagged on
#2674. Re-recorded here too, because the blocking check stays red for
every open PR until some PR carries it, and I do not know which of #2674
/ this one lands first.

## Verification

`pnpm test:gate` green (10 / 158 / 487 / 71). Suite **14/15 → 15/15**.
`pnpm check:lifecycle-columns` exits 0 after the re-record. `pnpm lint`
clean.

No changeset: test-only plus an internal baseline.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:59:53 -07:00
gsxdsm
a6138abeff U12: DELIBERATE-LITERAL counts key on file AND column — closing the P1 left on merged #2661 (#2666)
Closes the P1 that was still open when #2661 merged.

## The hole

A per-file integer is offset **within a single file**: remove one
reviewed `todo` exemption, add an `in-review` one beside it, and the
number never moves. The fresh guard is invisible to the column counts
too, because deliberate findings are excluded from them — so `--strict`
goes green with a new lifecycle-column guard hiding inside an existing
marker.

Now keyed on **file AND column id**.

**Proven with the exact scenario:** swapping a marked `triage` for
`done` inside `TaskCard.tsx` leaves the per-file total unchanged and now
fails with

```
packages/dashboard/app/components/TaskCard.tsx (DELIBERATE-LITERAL: done): 0 -> 1
```

## The pattern worth naming

This is the **third** time this instrument has been defeated by an
aggregate:

| version | defeated by |
|---|---|
| repo-wide `totals.deliberate` | an addition in file A offset by a
removal in file B |
| per-file integer | an addition offset by a removal **in the same
file** |
| per-file per-column | — |

Each step narrows what can offset silently, and I walked into the next
one twice by fixing the *reported case* rather than the *shape*. Writing
it down because the same reflex will produce a fourth if someone adds
another aggregate here.

**The residual is deliberate, not an oversight:** a same-file
**same-column** swap still offsets. Two `todo` exemptions in one file
are interchangeable by definition, so there is nothing a reviewer could
act on. That is recorded at the site so the next person doesn't
rediscover it as a bug.

## Migration, again

The key **shape** changed (`file` → `file\0columnId`), which is the same
hazard as a missing field: comparing new keys against old reports every
existing marker as a fresh rise and pushes people to convert
already-reviewed literals. I hit it on the first run here — `TaskCard
(DELIBERATE-LITERAL: triage): 0 -> 2` — exactly as I did one shape
earlier in #2661.

Detected by the delimiter rather than a version field, since old keys
have none, and re-seeded on the next `--update-baseline`. 15 file+column
entries recorded.

## Verification

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

Independent of #2655; either order merges.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:02:25 -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
cef1b08af3 U12: the census baseline follows the count down — and goes in the merge gate (#2661)
Coordinator item 2. The census had the right mechanism and no teeth.

## The gap

`--strict` already fails on a rise **and** on an unrecorded drop — that
logic was correct. But nothing blocking ran it, so the baseline drifted
to **854 while the tree held 787**. That is **67 guards of regression
that would have merged silently**: a high-water mark wearing a ratchet's
name.

This is the same shape as the ceilings I tightened in #2647, one level
up. Worth saying plainly: I fixed the vitest ratchet's slack by hand and
did not check whether the *authoritative* instrument had the same
problem. It did, and by a much larger margin.

## Three changes

1. **`--strict` runs in `test:gate`.** The baseline cannot go stale
again without a red gate.
2. **Baseline re-recorded: 854 → 785** across 14 files (`triage` 38 →
9).
3. The single RISE is resolved honestly rather than absorbed.

## The +3 investigation

One file rose: `register-task-workflow-routes.ts` **22 → 23**. #2621
replaced one `task.column === "todo"` with `task.column === "triage" ||
task.column === "todo"` — a net **+1** that also reintroduced a `triage`
literal, while the PR title reported *"count 0 → 0"*.

Not an accusation. There was no gate for the author to check against,
and a hand-counted claim in a PR title is exactly the thing that goes
wrong without one. Change 1 is the fix.

**The literal is justified and stays**, marked `DELIBERATE-LITERAL`
rather than converted. It is the **v1-IR arm**: a v1 workflow yields no
role assignments, so `resolveLifecycleColumns` returns nothing and the
legacy pre-implementation ids are the only pre-WIP signal available. The
`else` branch directly below already resolves intake/hold for every v2
workflow. Converting this arm would not finish anything — it would
delete the only answer v1 boards have and admit
`in-progress`/`in-review` cards into a rebound that clears worktree,
branch and retry counters, which is the regression #2621 was fixing.

## Both directions proven

| direction | probe | result |
|---|---|---|
| rise | add `t.column === 'in-review'` | `live-agent-count.ts: 6 -> 7`,
exit 1 |
| drop | convert one guard | `self-healing.ts: allows 111, tree has
110`, exit 1 |

**The drop probe took three attempts to test honestly, and the first two
"passed" while proving nothing:**

1. I renamed a receiver (`task.column` → `Probe`) — the classifier is
**fail-closed**, so an unknown receiver is still counted and the number
never moved.
2. I targeted a site in `hold-release.ts` that carries a
`DELIBERATE-LITERAL` marker — not counted as a column guard at all, so
removing it changed nothing.

Only removing a counted comparison outright moved the number. Both false
negatives came from me assuming the probe worked because the command
exited the way I expected.

## On auto-rewrite vs fail-and-instruct

You offered either. The script already does **fail-and-instruct**, with
`--update-baseline` as the explicit re-record, and I kept it that way
rather than making the test rewrite the baseline during a run.

Reason: a silent downward rewrite means a conversion PR's own diff never
shows the number moving, so "census before/after in the PR body" becomes
unverifiable — the reviewer would have to re-derive it. Failing with the
new number in the message puts it in the diff where a human sees it, and
it costs one command.

## Verification

`pnpm lint` clean. `pnpm test:gate` green with the census in it — `every
file matches its baseline exactly` (10 / 132 / 487 / 71).

Note for the fleet launch: with `--strict` gating, **every** conversion
PR must now re-record the baseline in the same PR. That is the intended
cost, and it makes the fleet's "baseline must shrink by exactly the
converted count" rule mechanically enforced instead of a review
instruction.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:47:31 -07:00
gsxdsm
efbbc45eb0 U12: the LAST triage guard — Plan was offered on executing cards named triage (#2664)
The final `column === "triage"` in production source, and it was a live
defect rather than dead vocabulary.

## The defect

`isPreExecutionHoldColumn` ORed the legacy id with the traits
**unconditionally**:

```ts
return column === "triage" || flags?.intake === true || flags?.hold === true;
```

That is not a fallback. A resolved column merely *named* `triage`
answered true even when its own traits said work was underway — so the
context menu offered **Plan**, which re-plans, on a card that is already
executing.

Now flags-first, with the id as the documented no-metadata answer.

## Why the file's earlier conversion missed it

Every existing case in `TaskContextMenu.test.tsx` passes a column with
**no flags**, or with `hold`/`intake` set. All of them agree under both
forms, so the suite could not distinguish them. Nothing exercised a
column whose **name and traits disagree**, which is the only shape that
separates an OR from a fallback.

Three new cases cover it. Revert check: restoring the OR form fails the
first one — Plan reappears on a mid-flight card.

## The asymmetry is preserved, and now tested

The degraded set stays `{triage}` **alone**, deliberately not the
`{todo, triage}` used by `isPreImplementationColumnRole`. That helper
drives the preserve-progress prompt, where a flagless `todo` *should*
prompt because losing steps is unrecoverable. This drives Plan, where a
flagless `todo` must **not** offer to re-plan a card that may already be
planned. The file documented that difference; nothing asserted it. Now a
test does.

## On reaching zero honestly

The surviving literal is marked `DELIBERATE-LITERAL`. It is the degraded
answer, not an unconverted guard — there is no trait to read when
`flags` is `undefined`, which happens during first paint and for a card
in a column its workflow no longer declares. Deleting it would silently
withdraw Plan from exactly the stranded cards that most need
re-planning.

So **`triage → 0` means "no unconverted guards remain", not "the string
is gone"**, and I would rather say that than move a number by deleting a
fallback.

| branch | triage |
|---|---:|
| `origin/main` | 5 |
| this PR | **4** |
| #2655 (flag resolution, removes 4 in `moves.ts`) | 1 → **0** combined
|

I found it with the census's own AST classifier rather than grep — my
grep of the same tree returned only comment prose and would have had me
report the bar as met while a real defect sat in
`TaskContextMenu.tsx:179`.

## Verification

`pnpm lint` clean. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm
check:lifecycle-columns` exits 0 with the baseline re-recorded in this
PR (column 769 → 768, deliberate 12 → 13). `tsc -p tsconfig.app.json`
clean. `TaskContextMenu.test.tsx` 18/18.

Depends on nothing; stacks cleanly with #2655 and #2661.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:37:08 -07:00
gsxdsm
20878e9d5f census: count column: "<legacy>" query filters as a separate, separately-pinned instrument (backlog unchanged at 784) (#2650)
Pre-launch input for the 779-guard fleet. **The backlog number does not
move: 784 before, 784 after.** This adds a second number beside it.

## The problem it measures

A guard is not the only way a legacy column id decides behaviour:

```ts
const todo = await this.store.listTasks({ column: "todo", slim: true });
```

That is a **source query** — it selects the rows a sweep considers *at
all*. On a renamed or merged board it returns nothing, so a sweep whose
per-task predicate was correctly converted still does nothing, while
looking converted. `self-healing.ts:2849` names the pairing in prose,
and #2560 had to repair exactly that combination after a converted
predicate was left with a literal query.

The census walks comparison `BinaryExpression`s. A `PropertyAssignment`
is not one, so this class was invisible to the instrument **and to its
ratchet** — it could grow silently.

Measured: **83 query filters, 43 IR node definitions.**

I proved one live consequence earlier on #2648:
`recoverStuckMergeDeadlocks` cannot see a renamed board at all — the
renamed rows exist and none appear in its three-literal union
(`renamedInsideUnion=0`, on a live PG store).

## Why this matters *before* the fleet is briefed

The fleet rule is *"the baseline ratchet must shrink by exactly the
converted count."* In `self-healing.ts` — the largest batch at 111 —
both classes sit in the same functions, so today a worker either:

- converts only the comparisons → arithmetic is clean, and sweeps whose
source query still filters a dead literal stay blind; or
- converts the query too → the count does **not** move by the converted
amount, and a more-correct PR looks like a miscount.

The second punishes the better worker. With a second pinned number,
converting a query becomes visible work instead of an apparent error.

## Counted separately, deliberately

`totals.column` is a published shape — the baseline, the reporter, and
other workers' in-flight PRs read it, and the completion bar is defined
against it. Growing it would move a number the program is actively
driving to zero.

So the new counts live in `summary.properties` / `queryByFile`, under
their own baseline keys, with their own both-directions ratchet (same
rule as #2633's, including the stale-allowance half). `totals` keeps its
**exact** shape — two existing tests assert it with `toEqual`, and
breaking a contract others depend on mid-flight to add a number is not
worth it.

## Definitions are not queries

Workflow IR graph nodes carry `column:` to declare where a node lives —
`{ id: "review", kind: "...", column: "in-review" }`. That is the
lineage describing itself: not a lookup, not convertible, and ~43 of the
raw matches. They are told apart **structurally** (an `id`/`kind`
sibling in the same object literal), not by filename, so a definition
written anywhere classifies the same way.

## Baseline seeding, stated plainly

`--update-baseline` could not pin a **new** category: the regression
check runs before the write, and with no prior key every file reads as a
rise. I seeded the three new keys once, directly, leaving every guard
field byte-identical. The diff is purely additive — no removals.

## Finding, not caused by this change

**`--strict` is already red on clean main**:
`register-task-workflow-routes.ts` is **23** against a baseline of
**22**. Verified by stashing this branch and re-running on an unmodified
tree. Until that is reconciled the guard ratchet is passing nothing —
worth fixing before the fleet starts relying on it as the work order.

## Verification

- census suites **44 green**, 6 new cases: counted; kept out of the
backlog; definition-not-query; both instruments independent (a bug
routing comparisons into the query bucket would otherwise look clean on
both); `DELIBERATE-LITERAL` honoured; non-legacy id ignored
- `node scripts/lifecycle-column-census.mjs` → backlog still 784
- `pnpm lint` exit 0, `pnpm test:gate` exit 0 (695)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:27:18 -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
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
7871b28766 fix(core): bind the in-transaction capacity gate — one shared pool-id convention (NOT user-visible yet — see R2) (#2488)
## The bug

`moves.ts` asked `countActiveInCapacitySlotAsync` for occupants of pool
`"builtin:coding"`, while the counter buckets selection-less rows under
`DEFAULT_WORKFLOW_POOL_ID` (`"__default-workflow__"`). Nothing ever
landed in the pool being asked about, so the count came back **0** and a
finite limit could never bind.

## Root fix, not a literal swap

A shared *constant* would not have prevented this:
**`DEFAULT_WORKFLOW_ID` was already imported in `moves.ts` and the code
still wrote a literal.** So both sides now call a shared **function**,
`resolveCapacityPoolId` — "which pool does a selection-less task belong
to" has exactly one answer and no call site is in a position to disagree
with it.

The one variable serving two masters is split: a capacity **pool key**
(a bucketing sentinel that must not collide with a workflow id) and a
**workflow id** (telemetry, must stay a real id). The emitted
`TaskTransitioned` payload is byte-identical.

## Checked, not assumed: no second copy

`scheduler.ts:2514` and `:2536` do carry `?? "builtin:coding"` — but as
an **IR resolution key** (`resolveWorkflowIrById`), where a real
workflow id is required and the pool sentinel would not resolve at all.
Same literal, different concept, correctly used. A blanket replace would
have broken it.

## Something did depend on the gate being dead — exactly one thing

`move-path-equivalence.pg.test.ts` → *"UNPROVEN: in-transaction column
capacity did NOT reject on EITHER path in this fixture"*. It left the
cause open —

> something further in (`resolveColumnCapacity`'s limit resolution, or
what `countActiveInCapacitySlotAsync` counts as an occupant — a task
with no session/agent may not count) keeps the check from firing … This
suite does not establish which.

— and predicted its own obsolescence (*"if a future change makes this
reject, that is the capacity gate coming alive"*). **Neither guess was
right; it was the pool id.** Updated to assert the divergence with the
answer recorded — **not weakened**. Its fixture also had to start each
phase from an empty wip column: once the gate binds, the inline phase's
leftovers trip the cap on the *holder* move before the contended move
under test runs.

`schema-applier.test.ts` failed only in the full-suite run and passes in
isolation both with and without the fix — cross-file contamination, not
mine.

## Before / after — measured, both directions

`maxConcurrent: 1`, real PG store, real `moveTask`:

| | flagOFF / no selection | flagOFF / selection | flagON / no selection
| flagON / selection |
|---|---|---|---|---|
| **before** | ADMITTED | ADMITTED | **ADMITTED** ← the bug | REJECTED |
| **after** | ADMITTED | ADMITTED | **REJECTED** | REJECTED |

The E2E acceptance row asserts **held at cap 1 and admitted at cap 2 on
the same fixture**, so it cannot pass by simply never admitting
anything. **With the fix reverted that row fails**; the `admitted` case
still passes, as it should. The Phase A3 ratchet's two flipped
assertions also fail with the fix reverted.

Ratchet flipped exactly as its author specified: `DEFECT (R1)` becomes a
rejection, and `it.fails` on the invariant becomes a plain `it`.

## ⚠️ This is NOT user-visible yet — please read before merging

The premise this was approved on ("once it binds, cards that currently
slip through will start being held") **does not hold for this change
alone.** The whole capacity block sits inside `if (useWorkflow &&
workflowIr && fromColumn !== toColumn)`, and `useWorkflow` is
`experimentalFeatures.workflowColumns === true` — absent from
`DEFAULT_GLOBAL_SETTINGS`, with **no writer anywhere outside tests**.
That is Phase A3's R2, still live and now retitled `DEFECT (R2, STILL
LIVE)` with the measured matrix recorded in it.

So on merge: nothing changes for any real project. Making it actually
bind means **also** removing the `useWorkflow` condition — a materially
larger, genuinely user-visible change that I have not made unilaterally.
Escalated for a decision; if that lands, the changeset here should be
re-categorised.


## Review follow-up (48e79ffd9): the convention was still duplicated —
swept and ratcheted

The first pass added the resolver and routed the transactional gate +
counters, but **hold-release still derived the pool independently**.
Swept the repo: six sites name the sentinel, **five derive the
convention** and now call `resolveCapacityPoolId`
(`hold-release.ts:116/118/442/576`, `task-store-helpers.ts:290`). The
sixth, `scheduler.ts:1558`, names the default pool as a literal in a
capacity *diagnostic* — no selection input, nothing to disagree with —
so it keeps the constant.

**Does this change hold-release behavior? No, and it was never releasing
against the wrong pool.** hold-release computed `x ??
DEFAULT_WORKFLOW_POOL_ID`, which is exactly what the counter buckets
under; `moves.ts` (`?? "builtin:coding"`) was the sole disagreeing site,
and the first commit moved *it* into agreement with hold-release, not
the reverse. `resolveCapacityPoolId(x)` **is** `x ??
DEFAULT_WORKFLOW_POOL_ID`, so every routed site computes an identical
value for every input. **No second user-visible change rides along with
this PR** — the only behavior delta remains the gate binding on the
flag-ON path, which per R2 is still not the path production takes.
Evidence: hold-release + capacity suites **43/43 identical before and
after**.

**The resolver is now the only way to compute a pool id, not merely the
newest way.** `scripts/check-capacity-pool-id.mjs` fails on any inline
`?? DEFAULT_WORKFLOW_POOL_ID` outside `workflow-capacity.ts`, wired into
**both `pretest` and the blocking `test:gate`**. A review note would not
have sufficed: the original defect landed in a file that *already
imported* the canonical constant. Verified both ways — clean run scans
1124 files and passes; reintroducing the old hold-release expression
exits 1 and names the line.


## Review follow-up (a5b675503): the ratchet was rebuilt because it
would not have caught the bug

The first ratchet matched one spelling (`?? DEFAULT_WORKFLOW_POOL_ID`)
and the real defect used another (`?? "builtin:coding"`). **Verified:
reintroducing the original defect and running the old checker exits 0.**
A guard that reports success without checking is worse than no guard —
it stops anyone looking.

Rebuilt on the TypeScript AST with two rules. **Rule 1 (sink):** a value
reaching a capacity counter's `workflowId` must come from
`resolveCapacityPoolId`, or a local initialized from it — so it fires on
the original defect regardless of which literal was used, on one line or
twenty. **Rule 2 (sentinel):** no `??` onto the sentinel at any
qualification depth or as its raw value; multiline is one AST node and
caught by construction. `?? "builtin:coding"` is deliberately *not*
banned outright — it is the legitimate default for a *workflow* id in ~8
places, and is only a bug when it reaches a capacity pool.

**Fails closed three ways** that previously reported success without
inspecting: unreadable file, unparseable file, and an empty file listing
(the old script would have printed a green tick off a broken glob).

**Acceptance was not "passes on main".** Each form was reintroduced into
the real source and confirmed to fail: the original defect in
`moves.ts`, a multiline fallback, and a deeply qualified sentinel. All
are pinned in `capacity-pool-id-check.test.ts` (12 cases: 7 must-catch
starting with the reduced actual pre-fix `moves.ts`, 4 must-not-flag, 1
fail-closed) so the guard cannot silently narrow again.

Also added to `pretest:full`, which had omitted it.


### Follow-up (0be8df6ea): a dead rule found by fixing a test title

Splitting the mislabelled fail-closed test surfaced more than a
mislabel: **`ts.createSourceFile` is error-tolerant and does not throw
on malformed syntax**, so the `try/catch` behind the `unparseable` rule
was unreachable and that rule could never fire. The earlier "fails
closed three ways" claim was overstated — the guard advertised a
capability it did not have. Detection now reads `sf.parseDiagnostics`; a
partial AST can silently lack the `??` nodes and sink calls the rules
look for, so "did not parse" must not read as "inspected and clean".
Mutation-verified: reverting the detection fails that case and only that
case.

Test-file exclusion also moved to the repo's `{test,spec}.{ts,tsx}`
guideline shape — a `.spec.ts` under `packages/<pkg>/src/` was being
scanned as production source. Verified both ways: the `.spec.ts` is
skipped, and the identical content in a non-test file is still caught,
so the exclusion is scoped rather than a hole.

## Verification

- engine + core `tsc --noEmit` clean
- `pnpm test:gate` green (299 + 10 + 71)
- E2E 20/20; capacity + move-path suites 14/14
- full core PG: **1037 passed / 3 failed** — all three reproduce with
the fix stashed (pre-existing)
- engine-default: **279 failed** vs **280 at baseline** with the fix
stashed — pre-existing red lane, no regression
- hold-release + capacity suites: **43/43 identical before and after**
the resolver routing
- `check-capacity-pool-id` ratchet: 14/14 regression cases; clean over
1124 files; exits 1 on the original defect, a multiline fallback, and a
deeply qualified sentinel reintroduced into real source

🤖 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**
* Fixed capacity-limit accounting when workflow selection is missing by
consistently deriving the correct capacity pool id.
* Made capacity enforcement align across move and hold/release paths,
rejecting over-limit moves with `capacity-exhausted`.
* **Tests**
* Updated PostgreSQL and added an E2E scenario to verify the corrected
in-transaction gating behavior at `maxConcurrent` limits of 1 and 2.
* **Chores**
* Added an automated guard to detect inconsistent capacity pool id
fallback patterns in code.
* **Public API**
* Exposed `resolveCapacityPoolId` for consistent capacity pool id
derivation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:09:51 -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
93a403af67 fix(dashboard): import delete-attribution constants via browser-safe subpath
The client bundle aliases `@fusion/core` to the leaf `core/src/types.ts` to
keep Node-only dependencies out of the browser, so a package-root import of
`FUSION_CLIENT_HEADER`/`FUSION_DASHBOARD_UI_CLIENT` typechecked but failed
`vite build`:

  "FUSION_CLIENT_HEADER" is not exported by "../core/src/types.ts"

Follow the documented pattern instead of widening the root alias: declare a
`./task-delete-attribution` subpath export, add the matching Vite alias ahead
of the broader `@fusion/core` key (Vite matches in order), register the module
in the browser-safe-core allowlist, and import the subpath from the client.
`task-delete-attribution.ts` has no imports at all, so it is a safe leaf.

`app/utils/detectContentLanguage.ts` already warned about exactly this trap;
the miss was mine for verifying with typecheck, lint and test:gate but not
`pnpm build`, which is one of the four checks CI blocks on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:22:18 -07:00
gsxdsm
c7fa02f370 FN-8597: restore executor task-done invariant coverage
Restore the quarantined executor graph-completion invariant suite with real foreach projections.

- Exercise complete and partial expanded workflow-step projections at the merge boundary.
- Remove the rescued invariant suite from Vitest quarantine and clear its ledger entry.
- Extend the shared executor logger mock with the debug method required by the integration tip.

Files changed:
 .../__tests__/executor-task-done-invariant.test.ts | 267 +++++++++++++++++++--
 .../engine/src/__tests__/executor-test-helpers.ts  |   7 +
 packages/engine/vitest.config.ts                   |   7 -
 scripts/lib/test-quarantine.json                   |   8 +-
 4 files changed, 254 insertions(+), 35 deletions(-)

Fusion-Task-Id: FN-8597

Fusion-Task-Lineage: 05a08e31-7da0-4c93-86a0-9baf8db7ce52

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 10:04:36 -07:00
gsxdsm
084dd76d64 feat(release): write release copy with opus and draft tweets for betas too
- distillation runs on opus (env-overridable) with a 4-minute budget
- highlights must name the surface and outcome; vague filler is banned
- tweets target 200-280 chars with concrete changes and varied structure
- betas get their own tester-facing draft carrying `fn update --channel beta`
- prerelease openers read as "Fusion 0.74 beta:" instead of "Fusion 0.74-beta.0"
2026-07-24 23:15:36 -07:00
gsxdsm
330e4970f0 refactor(release): move the version-anchor package.json rewrite into the shared lib
Makes the re-anchor file mutation unit-testable alongside the anchor decision.
2026-07-24 23:01:11 -07:00
gsxdsm
dba9746287 fix(release): base the next beta on the shipped stable version
After a stable release, main stayed inside the old pre-mode cycle, so the next
beta numbered below the published stable (v0.73.0-beta.7 after v0.73.0) and the
dev checkout kept reporting the last beta.

- beta releases re-anchor a stale pre-mode cycle on the newest stable tag
- both channels refuse a version at or below the newest published stable
- stable promotion now back-merges release into main automatically (fail-soft
  on conflict) so the local dev version is the stable version
2026-07-24 22:59:19 -07:00
gsxdsm
c5e9a7956a fix(ci): stop watchdog false-kills of the grown core slice; actually upload timing artifacts
- Shard watchdog floor 25min (was 15): the July PG-cutover test growth pushed
  @fusion/core past 900s on contended CI runners; run 30075604930 killed a
  healthy core run at exactly the floor because the 27-day-old (still "fresh")
  undercounting timings snapshot tightened the budget to it — the same
  false-kill class as the 5->15min raise. Floor pin + in-band example updated.
- full-suite.yml timing upload: include-hidden-files — the .timings/ dot-dirs
  were silently excluded by upload-artifact@v4, so the step has uploaded
  nothing since it was added and the snapshot could never be refreshed from CI.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 12:18:24 -07:00
gsxdsm
42fe154abe FN-8533: add mobile planning comment actions
Make contextual plan comments reachable from the mobile action rail and restore focus after editing.

- Add responsive desktop and mobile comment triggers with contextual styling and documentation.
- Preserve the selected quote and restore the remounted trigger after canceling or adding a comment.
- Cover action placement, focus restoration, and browser interaction behavior.

Files changed:
 .changeset/fn-8533-mobile-planning-comments.md     |   7 ++
 docs/dashboard-guide.md                            |   2 +-
 .../dashboard/app/components/PlanningModeModal.css |  20 ++++
 .../dashboard/app/components/PlanningModeModal.tsx |  44 ++++++-
 .../__tests__/PlanningModeModal.css.test.ts        |  10 ++
 .../PlanningModeModal.planning-flow.test.tsx       |  40 +++++--
 .../PlanningModeModal.ui-interactions.test.tsx     |   5 +
 .../dashboard/app/planning-browser-e2e-fixture.tsx |   3 +-
 .../src/__tests__/planning-browser-e2e.test.ts     | 133 +++++++++++++++++++--
 packages/dashboard/vitest.config.ts                |  11 +-
 scripts/lib/test-quarantine.json                   |   5 -
 11 files changed, 242 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8533
Fusion-Task-Lineage: e0d561be-ecc8-456e-807e-a1dc677b5d9c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-23 09:33:25 -07:00
gsxdsm
1e05793876 fix(ci): green full-suite bookkeeping after origin/main cutover (#2392)
## Summary

Restores green merge-gate and package-default suites after repeated
`origin/main` merges brought workflow-graph ownership cutover drift into
CI.

- Align engine/dashboard/core tests with post-cutover contracts
(`moveTaskIf`/`deleteTaskIf`, graph handoff, worktree-pool reclaim via
`removeWorktree` + `RemovalReason`, multi-step RESUMING parse,
soft-pause merge requester, graph-terminal failure surfaces).
- Small product fixes needed for real regressions uncovered by the
suite: soft-delete refuse before graph routing, skip DUPLICATE
step-heading withhold when an explicit marker is present, PG schema
applier guards, and related bookkeeping (research promote tool inventory
/ migration seed, stop shell `psql` in PG admin DDL).
- Quarantine/ledger hygiene only where required by standing rules; no
timeout/worker appeasement.

## Verification

- `pnpm test:gate` ×2 green
- `@fusion/engine` full package suite green (~9083 tests)
- Targeted core/dashboard clusters green (schema applier, agent-runs UI,
settings descriptions, mobile close)

## Test plan

- [x] `pnpm test:gate` (twice)
- [x] `pnpm --filter @fusion/engine test`
- [ ] CI full suite / PR checks on this branch
- [ ] Confirm no unrelated product behavior changes beyond the listed
regression fixes

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

* **New Features**
* Added support for `roadmap-item` native structure kinds, including
native structure embeds and metadata validation.
  * Added Stable and Beta release channel options in General settings.
* Added per-action reporting target configuration with clearer “unset”
guidance.

* **Bug Fixes**
  * Improved heartbeat/prompt behavior when patrol is disabled.
  * Prevented deleted tasks from continuing through execution.
  * Made recovery for explicit duplicate redirects more permissive.
* Hardened database migration and test database cleanup to reduce flaky
failures.

* **Documentation**
* Updated settings text for release channels, reporting targets, and
inheritance/unset behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-21 23:09:30 -07:00
gsxdsm
7911fdb9b1 fix(release): preserve distilled changelog summaries across releases
syncRootChangelog rewrote every prior release from raw package notes, so only
the latest distilled Highlights view survived. Re-emit already-distilled bodies
on sync, keep the archive pointer outside version sections, and restore wiped
summaries from release history.
2026-07-21 20:29:46 -07:00
gsxdsm
eef5eb751e FN-8453: unify concurrency accounting and indicators
Unify live-agent capacity accounting across engine and dashboard.

- Derive Running and Waiting from workflow traits and durable agent liveness.
- Apply unified limits to planner, executor, and merge admission while updating dashboard indicators.
- Remove duplicate concurrency controls and document the unified operator model.

Files changed:
 .changeset/fn-8453-unified-concurrency.md          |   7 +
 docs/agent-tool-surface-full-loop.md               |   4 +-
 docs/architecture.md                               |   2 +-
 docs/dashboard-guide.md                            |   4 +-
 docs/settings-reference.md                         |   4 +-
 .../skill/fusion/references/fusion-capabilities.md |   4 +-
 .../core/src/__tests__/live-agent-count.test.ts    |  91 ++++----
 packages/core/src/index.gate.ts                    |   6 +
 packages/core/src/index.ts                         |   6 +
 packages/core/src/live-agent-count.ts              | 107 ++++++---
 packages/dashboard/app/App.tsx                     |  28 ++-
 packages/dashboard/app/api/board-workflows.ts      |   2 +
 packages/dashboard/app/components/Column.tsx       |   6 +-
 .../dashboard/app/components/EngineControlMenu.tsx |  26 ---
 .../dashboard/app/components/ExecutorStatusBar.tsx |  38 ++-
 .../dashboard/app/components/SettingsModal.tsx     |   1 -
 .../app/components/__tests__/Column.test.tsx       |   6 +-
 .../__tests__/EngineControlMenu.test.tsx           |  10 +-
 .../__tests__/ExecutorStatusBar.test.tsx           |  32 ++-
 .../command-center/CommandCenterControls.tsx       |  26 ---
 .../settings/sections/SchedulingSection.search.ts  |   9 -
 .../settings/sections/SchedulingSection.tsx        |  13 --
 .../app/hooks/__tests__/useExecutorStats.test.ts   |  12 +-
 packages/dashboard/app/hooks/useExecutorStats.ts   |  50 ++--
 .../src/__tests__/project-store-resolver.test.ts   |  11 +-
 packages/dashboard/src/project-store-resolver.ts   |  14 +-
 .../register-config-mcp-pi-settings-routes.ts      |   3 +-
 packages/engine/src/__tests__/concurrency.test.ts  | 123 +++++++++-
 .../engine/src/__tests__/project-engine.test.ts    |  34 +++
 packages/engine/src/__tests__/triage.test.ts       |   7 +-
 packages/engine/src/concurrency.ts                 | 207 ++++++++++++++++-
 packages/engine/src/project-engine.ts              | 151 ++++++++++--
 packages/engine/src/scheduler.ts                   |  82 ++++++-
 packages/engine/src/triage.ts                      | 254 +++++++++++++--------
 .../lib/dashboard-browser-safe-core-modules.json   |   5 +
 35 files changed, 991 insertions(+), 394 deletions(-)

Fusion-Task-Id: FN-8453

Fusion-Task-Lineage: 12cfa5df-675d-4fce-b17e-932376544239

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-21 15:30:31 -07:00
gsxdsm
634295c72f fix(planning): keep questions out of mailbox
Keep planning questions in their dedicated surface while preserving ntfy alerts, and tighten the desktop planning panes without changing compact or shared layouts.
2026-07-21 10:04:19 -07:00
gsxdsm
860533eff2 FN-8402: extract config, MCP, and Pi settings routes
Extract config, MCP, and Pi-settings handlers into a dedicated dashboard route registrar.

- Move seven configuration and MCP endpoint handlers out of the API-route orchestrator.
- Preserve registrar mount precedence and document the expanded route map.
- Add registrar coverage and update the inline-route modularity baseline.

Files changed:
 .../src/__tests__/mcp-documentation.test.ts        |   2 +-
 packages/dashboard/src/routes.ts                   | 303 +--------------------
 packages/dashboard/src/routes/README.md            |  84 +++---
 .../register-config-mcp-pi-settings-routes.test.ts |  61 +++++
 .../src/routes/create-api-routes-mount-sequence.ts |   2 +-
 .../register-config-mcp-pi-settings-routes.ts      | 275 +++++++++++++++++++
 scripts/lib/routes-modular-baseline.json           |   2 +-
 7 files changed, 385 insertions(+), 344 deletions(-)

Fusion-Task-Id: FN-8402

Fusion-Task-Lineage: f5f71f64-03cf-41cf-9fb2-33046b0c04bf

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-20 09:35:55 -07:00
gsxdsm
e1dddcbfae FN-8404: extract dashboard domain route registrars
Move maintenance, AI text assistant, and setup/activity endpoints into focused dashboard route registrars.

- Register the extracted domains in the API mount sequence.
- Preserve route precedence and document registrar responsibilities.
- Add registrar coverage and refresh the modular-route baseline.

Files changed:
 packages/dashboard/src/routes.ts                   | 948 +--------------------
 packages/dashboard/src/routes/README.md            |  82 +-
 .../register-ai-text-assistant-routes.test.ts      |  49 ++
 .../register-setup-activity-routes.test.ts         |  54 ++
 .../register-system-maintenance-routes.test.ts     |  48 ++
 .../src/routes/create-api-routes-mount-sequence.ts |   8 +-
 .../routes/register-ai-text-assistant-routes.ts    | 327 +++++++
 .../src/routes/register-setup-activity-routes.ts   | 320 +++++++
 .../routes/register-system-maintenance-routes.ts   | 320 +++++++
 scripts/lib/routes-modular-baseline.json           |   2 +-
 10 files changed, 1176 insertions(+), 982 deletions(-)

Fusion-Task-Id: FN-8404

Fusion-Task-Lineage: d6b5be1f-e011-47fc-a7f8-5df89893766f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-20 00:46:14 -07:00
Phil Larson
fe9269b57b fix(i18n): restore Chinese roadmap duplicate labels (#2358)
## Summary
- restores the missing Simplified Chinese duplicate-roadmap report label
- restores the missing Traditional Chinese duplicate-roadmap report
label
- adds a patch changeset for the catalog correction

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


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

* **Bug Fixes**
* Restored Simplified and Traditional Chinese translations for duplicate
roadmap report titles.
* Updated the roadmap reporting UI text to clarify when a report is
already in the roadmap and ask whether to add the user’s data point.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 00:18:53 -07:00
gsxdsm
5c67b19cb2 FN-8394: rescue deterministic quarantined tests
Restore reliable test coverage and delete quarantined tests that could not be rescued.

- Replace process- and database-dependent tests with bounded dependency seams
- Restore stabilized CLI, dashboard, and plugin test coverage
- Remove unrescuable bundle and merge-worktree test suites and clear the quarantine ledger

Files changed:
 packages/cli/src/__tests__/bundle-output.test.ts   | 519 ------------
 .../src/commands/__tests__/task-lock-retry.test.ts |  10 +
 packages/cli/vitest.config.ts                      |   8 -
 .../TaskDetailModal.tab-persistence.test.tsx       |   2 +-
 .../__tests__/TaskDetailModal.test-helpers.ts      |   7 +
 .../src/__tests__/dev-server-process.test.ts       | 391 ++++-----
 packages/dashboard/src/dev-server-process.ts       |  22 +-
 packages/dashboard/vitest.config.ts                |  21 +-
 .../merge-reuse-task-worktree.slow.test.ts         | 876 ---------------------
 packages/engine/vitest.config.ts                   |   7 -
 .../src/__tests__/process-lifecycle.test.ts        |  21 +-
 .../fusion-plugin-grok-runtime/vitest.config.ts    |   2 -
 .../src/__tests__/async-quality-store.pg.test.ts   | 148 +++-
 plugins/fusion-plugin-quality/vitest.config.ts     |   3 +-
 scripts/lib/test-quarantine.json                   |  43 +-
 15 files changed, 323 insertions(+), 1757 deletions(-)

Fusion-Task-Id: FN-8394

Fusion-Task-Lineage: e949b33e-b8d5-4f73-a002-e550b97ee125

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 18:59:36 -07:00
gsxdsm
b07f207b00 FN-8403: extract automation and plugin route registrars
Move automation and plugin HTTP handlers out of the routes aggregator into focused domain modules.

- Extract live automation streaming and step-execution helpers
- Register automation, routine, webhook, and plugin handlers through the domain registrar
- Document plugin route ordering and update modular-route baselines

Files changed:
 packages/dashboard/src/routes.ts                   | 2779 ++------------------
 packages/dashboard/src/routes/README.md            |    3 +-
 .../dashboard/src/routes/automation-live-run.ts    |  322 +++
 .../src/routes/automation-step-execution.ts        |  445 ++++
 .../src/routes/plugin-bundled-runtimes.ts          |   88 +
 .../src/routes/register-plugins-automation.ts      | 1540 ++++++++++-
 scripts/lib/routes-modular-baseline.json           |    2 +-
 scripts/line-count-baseline.json                   |    2 +-
 8 files changed, 2604 insertions(+), 2577 deletions(-)

Fusion-Task-Id: FN-8403

Fusion-Task-Lineage: 2072d39b-6335-4163-a56a-7d418edc9095

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 17:25:02 -07:00
gsxdsm
e0e395a715 FN-8365: enforce dashboard route registrar mount order
Keep dashboard API registration modular while preserving Express route precedence.

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

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

Fusion-Task-Id: FN-8365

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 15:53:07 -07:00
gsxdsm
b2c784b3f8 FN-8381: remove flaky dist-barrel test
Remove the repeatedly quarantined extension dist-barrel test while retaining source-level listing coverage.

- Delete the CPU-bound dist-barrel regression test after its fourth quarantine cycle.
- Remove its quarantine exclusion and ledger entry.
- Document retained source-level formatting and truncation coverage.

Files changed:
 .../src/__tests__/extension-dist-barrel.test.ts    | 204 ---------------------
 packages/cli/src/__tests__/extension.test.ts       |   4 +-
 packages/cli/vitest.config.ts                      |   6 +-
 scripts/lib/test-quarantine.json                   |   5 -
 4 files changed, 6 insertions(+), 213 deletions(-)

Fusion-Task-Id: FN-8381

Fusion-Task-Lineage: ba6e61e1-fba1-4308-9d9d-1d3f387aa5e9

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

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

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

Fusion-Task-Id: FN-8368

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 14:08:51 -07:00
gsxdsm
845d82ec67 fix(ci): restore full-suite after verification request + lucide mock gaps (#2332)
## Summary

Main Full Suite shards have been red after recent landings. Root causes:

1. **Executor tests** — `execute()` now polls
`getTaskVerificationRequestAsync` (chat-enqueued verification). Shared
`createMockStore()` (and soft-delete inline store) lacked the method, so
nearly every execute-path suite failed with `is not a function`.
2. **TaskDetailModal suites** — `NativeStructurePreview` imports `Map` /
`Lightbulb` / `BarChart3` / `Target` / `CircleAlert` from lucide; the
shared TaskDetail lucide mock omitted them, so suites failed at import.
3. **Grok process-lifecycle** — 15s bound stress timed out under
full-suite load without product-bug evidence → quarantined on sight per
AGENTS.md.

## Test plan

- [x] `executor-task-done-blocked`, `executor-fast-mode-workflows`,
concurrent-execute race
- [x] `executor-step-session`, plan-only scope leak, review-step
indexing
- [x] `TaskDetailModal.create-pr` + `TaskDetail.mobile-transition`
- [ ] Full Suite CI on this PR

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

* **Improvements**
* Added `html2canvas` support in the dashboard to enable HTML-to-canvas
rendering needed for visual structure previews.
* **Tests**
* Updated task execution test mocks to handle task verification-request
flows reliably.
* Improved task deletion safeguard coverage and related execution
behavior checks.
* Enhanced test stubs to support structure preview rendering elements
during modal-related tests.
* **Chores**
* Quarantined a timing-sensitive process lifecycle test and refreshed
quarantine tracking to improve full-suite stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-19 09:50:22 -07:00
gsxdsm
de2669fd17 fix(ci): mock createAgentTask for route tests; quarantine merge-reuse slow flake (#2327)
## Summary
- Default `createAgentTask` in dashboard `@fusion/engine` mock so
planning/subtask create routes return 201 (FN-8277).
- Mock `findRecentTasksBySourceParentTaskId` on github/planning route
stores.
- Quarantine `merge-reuse-task-worktree.slow.test.ts` (engine-slow load
flake, run 29663725381).

## Evidence
- Prior full green: Full Suite run **29663526777** on #2325.
- Tip red class: routes-github/planning 500 + engine-slow lease
residual.

## Test plan
- [x] routes subtask create-tasks / shared branch groups tests green
locally
- [ ] Full Suite all 4 shards + engine-slow green on main tip after
merge

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved task and subtask creation test coverage to correctly handle
parent-scoped duplicate checks.
  * Updated test behavior to return reliable task creation results.

* **Tests**
* Quarantined a flaky integration test from the slow test suite to
improve test run reliability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-18 16:08:14 -07:00
gsxdsm
569abcc258 fix(ci): quarantine CLI bundle-output and dist-barrel load flakes (#2325)
## Summary
Quarantine `bundle-output.test.ts` and `extension-dist-barrel.test.ts`
after tip Full Suite shard 4 (run 29662476909) hit package-lane-only
desktop build ENOENT + 10s beforeAll timeout. Prior green Full Suite:
run 29662309385 on #2323.

## Test plan
- [ ] Full Suite all 4 shards green on main after merge
2026-07-18 15:29:02 -07:00
gsxdsm
6268433d37 fix(ci): parent-task uniqueness mocks, roadmap search index, flake quarantine (#2323)
## Summary
- FN-8277 parent-scoped uniqueness: mock
`findRecentTasksBySourceParentTaskId` in heartbeat/triage/split suites.
- FN-8326: index `reportRoadmapDedup` in Settings search.
- Quarantine re-flaked `dev-server-process` under full-suite API load
(run 29661202279).

## Test plan
- [x] Targeted createTask / search-index tests green locally
- [ ] Full Suite all 4 shards green on main after merge
2026-07-18 14:49:24 -07:00
gsxdsm
89b42ed1d6 fix(ci): ideation heartbeat tools, review-artifact i18n, tab-persist quarantine (#2321)
## Summary
- Heartbeat customTools inventory includes FN-8295 ideation tools (63
total).
- Non-en i18n parity for FN-8286 `reviewArtifacts` Command Center +
settings keys.
- Quarantine `TaskDetailModal.tab-persistence.test.tsx` (CI load flake;
green focused thrice).

## Test plan
- [x] heartbeat expected-tools case green
- [x] i18n-gate-coverage + parity green
- [ ] Full Suite all 4 shards green on main after merge
2026-07-18 14:13:28 -07:00
gsxdsm
17ee1a8040 fix(ci): restore Full Suite bookkeeping after concurrent main landings (#2316)
## Summary
- Align heartbeat `customTools` expectations with FN-8294 mission
hierarchy tools (43→58).
- Refresh `COORDINATION_EXEMPT_TOOLS` snapshot for `fn_mission_list` /
`fn_mission_show`.
- Backfill `commandCenter.portability.*` for non-en locales and map
`reportMode` / `reportModeByAction` / `embeddedPostgresMaxConnections`
into settings default-description inventory with i18n help text.
- Realign FN-8064 skip-narration unit test with store-owned proactive
chat (no tool-side `appendAgentLog`).
- Quarantine load-sensitive `async-quality-store.pg.test.ts` (5s timeout
+ leftover psql under full-suite shard load; run 29657633544).

## Test plan
- [x] `pnpm --filter @fusion/engine exec vitest run`
gating-classifications + executor-prompt + heartbeat expected-tools case
- [x] `pnpm --filter @fusion/i18n exec vitest run` i18n-gate-coverage +
parity
- [x] `pnpm --filter @fusion/dashboard exec vitest run`
settings-default-descriptions
- [ ] Full Suite all 4 shards green on main after merge

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

## Summary by CodeRabbit

* **Settings**
* Added clearer, localized help text for report modes and per-action
overrides, including inheritance behavior.
* Added advanced embedded database connection-limit settings and
validation guidance.

* **Localization**
* Expanded translations for report settings, database tuning, and
organization configuration import/export workflows across supported
languages.

* **Tests & Maintenance**
* Updated test coverage and expectations for expanded tools and
reporting behavior.
  * Quarantined a flaky database-related test.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-18 12:48:13 -07:00
gsxdsm
6bf20ea249 fix(ci): quarantine CLI lock-retry flakes + settle planning tab-lock (#2305)
## Summary
- Full Suite after FN-8271 restore turned red again
([29648952207](https://github.com/Runfusion/Fusion/actions/runs/29648952207)):
- **shard 4**: `mcp-lock-retry` / `task-lock-retry` 5s timeouts under
package-lane load
- **shard 3**: planning “never acquires a tab lock…” —
`respondToPlanning` never called after Small/Continue
- Re-quarantine the two CLI lock-retry files in ledger +
`packages/cli/vitest.config.ts` (no timeout appeasement).
- Planning tab-lock test: select Small via radio role, wait for checked,
longer `waitFor` on respond.

## Test plan
- [x] lockstep-cli-quarantine
- [x] planning tab-lock interaction test
- [ ] Full Suite all 4 shards green on main

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved the reliability of the dashboard planning flow by using more
precise controls and bounded waits during automated interactions.

* **Tests**
* Quarantined two intermittently timing-out CLI integration tests to
reduce full-suite instability.
* Documented the quarantine reasons and tracking details for the
affected tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-18 08:30:28 -07:00
gsxdsm
afb2ed0650 FN-8271: restore quarantined CLI tests under shard load
Restore affected CLI and engine tests by removing load-amplifying fixture work and synchronizing fake-timer recovery.

- Replace the dist-barrel PostgreSQL fixture with an injected in-memory task store.
- Move mission and goal tool coverage to the shared PostgreSQL harness and complete plugin-store mocks.
- Return rescued CLI and heartbeat tests to default lanes and clear their quarantine records.

Files changed:
 .../src/__tests__/extension-dist-barrel.test.ts    | 90 ++++++++--------------
 .../__tests__/extension-mission-goal-tools.test.ts | 27 ++++---
 packages/cli/src/commands/__tests__/plugin.test.ts | 11 +++
 packages/cli/vitest.config.ts                      | 21 +----
 .../src/__tests__/heartbeat-error-recovery.test.ts | 33 ++++----
 packages/engine/vitest.config.ts                   |  7 +-
 scripts/lib/test-quarantine.json                   | 78 +------------------
 7 files changed, 84 insertions(+), 183 deletions(-)

Fusion-Task-Id: FN-8271

Fusion-Task-Lineage: 212a3ec7-db6b-4e80-97c3-1c704822cf60

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 07:48:45 -07:00
gsxdsm
e436635abd FN-8270: restore PostgreSQL migration quarantine tests
Restore seven PostgreSQL-compatible engine suites to the active test runs.

- Model asynchronous insight and goal-store collaborators in reporter and diagnostics tests.
- Await PostgreSQL audit reads in merger reliability tests.
- Remove the restored suites from Vitest exclusions and the quarantine ledger.

Files changed:
 .../__tests__/backlog-pressure-reporter.test.ts    | 19 +++++++----
 .../dependency-blocked-todo-reporter.test.ts       | 15 ++++++---
 .../goal-injection-diagnostics-wiring.test.ts      | 15 ++++++---
 .../__tests__/merger-cwd-fallback-removed.test.ts  | 13 +++++---
 .../integration-worktree-state.test.ts             | 13 +++++---
 .../merge-runner-spawn-enoent-prevention.test.ts   | 15 +++++---
 .../meta-chain-auto-close.test.ts                  |  9 ++++--
 packages/engine/vitest.config.ts                   | 20 ++----------
 scripts/lib/test-quarantine.json                   | 37 +---------------------
 9 files changed, 73 insertions(+), 83 deletions(-)

Fusion-Task-Id: FN-8270
Fusion-Task-Lineage: 65be82ef-f4d4-4a8a-b3cc-63486ee0823a
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-18 04:40:18 -07:00