Commit Graph

6991 Commits

Author SHA1 Message Date
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
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
642a4fa264 consolidate/u12 — U12 consolidation: 4 live defects, the AST ratchet fail-closed, and the moves.ts flag scoped (#2647)
One branch, one PR, per the consolidation directive. Contents
file-by-file below.

**Supersedes #2625** (its overlapping conversions landed via U11's
#2624/#2626/#2636; only the parts nobody else did are folded here).
**#2630 and #2639 stay open** — both green with zero threads, per rule
3.

## Four live defects, each measured

**1. Every planning card renders an actions menu.**
`TaskContextMenu.tsx` still had `shouldShowActionsMenu: task.column !==
"triage"` on main *after* the rest of that file was converted. Since
#2515 removed the id, the condition is TRUE for every card, so the
suppression stopped applying anywhere — including on cards whose menu is
empty, the orphaned click target the Surface Enumeration rule exists to
catch.

Found **twice independently**: by reading the guard, and again by the
invariance test below, which failed on main with `shouldShowActionsMenu`
true on one lineage and false on another. That is the argument for an
invariance property over per-site conversion — the file had already been
converted "2 → 1" and the survivor was the live one.

**2. Worktree upcoming-work list empty on renamed boards.**
`groupByWorktree` filtered `t.column === "todo"`. On the default board
the id and the role coincide so every existing test passed; renamed, it
matched nothing and a whole panel read as idle.

**3. Hold-lane FIFO ordering lost on renamed boards.**
`sortTasksForDisplayColumn` gated priority-then-FIFO on `column ===
"todo"`, degrading to the generic id-ordered sort elsewhere. Cards
simply appear in the wrong order, silently.

**4. The AST ratchet still failed open** — fourth time in that file,
third found by review. `receiverName` understood only one-level property
access and bare identifiers, so `task["column"]`, `metadataColumn(entry,
"to")`, ternaries, `(task!.column)` and backtick literals were dropped.
**Measured on main: `in-progress` 196 → 197, `in-review` 211 → 213** —
three real guards nobody counted, including `metadataColumn(entry, "to")
=== "in-review"` in `reliability-metrics.ts`. Now walks wrappers,
resolves calls to the callee name, and emits a `<SyntaxKind>`
**sentinel** for anything unnameable: counted *and* trips the
classification guard, so a human judges it instead of it vanishing.

## Per-file guard counts

| file | before | after |
|---|---:|---:|
| `app/components/TaskContextMenu.tsx` | 1 | **0** |
| `app/utils/worktreeGrouping.ts` | 1 | **0** |
| `app/components/taskSorting.ts` | 1 | **0** |

The other dashboard files I had converted reached 0 via U11's PRs; where
our work overlapped I took theirs during the rebase, including two
places where theirs was **stronger** than mine — they deleted Column's
unreachable quick-create arm outright (with fixtures migrated) where I
had converted it, and they verified the same `isPreExecutionHoldColumn`
degraded-set asymmetry I did, independently.

## Flip precondition: the moves.ts flag is scoped, not flipped

`move-target-declared-census.test.ts` answers precondition 2 with
measurement. 41 engine `moveTask` calls have literal targets — `todo`
27, `in-progress` 7, `done` 6, `archived` 1 — and **all four are
declared by the default lineage**, so the default board is not the
exposure. `triage` appears only in a comment noting `replan-target.ts`
used to hardcode it. My own grep had said `todo=29`; the AST says 27,
because grep counts comments.

The exposure is **custom** lineages: 20 of the 41 carry no
`recoveryRehome` and would reject with unknown-column post-flip; 21 are
exempt via the #1411 carve-out, which makes that carve-out load-bearing.

I did not flip the flag. It is six seams, not the `789`/`837` pair every
summary including mine described, and seam 2 turns on *new refusals*
rather than swapping equivalent implementations — a green suite says
nothing about that. #2639 pins the blast radius.

## Tests

- `column-role-id-invariance.test.tsx` — hold traits fixed, vary only
the column id across MERGED / LEGACY / RENAMED; every decision must
agree. Drives the real consumers, so a component keeping an inline
comparison fails it. Includes a unanimous-and-**false** case so it can't
be satisfied by a predicate hardwired to true. **This is the test that
caught defect 1 on main.**
- `worktreeGrouping.test.ts` — includes two cards both in a column named
`staging`, one hold and one not, asserting opposite answers. That
assertion is impossible under a board-wide column-id set, which is why
hold resolution is keyed per task via `getEffectiveTaskWorkflowId`
(#2625 review).
- `taskSorting.test.ts` — discriminates on the **tiebreak**, not
priority: both branches sort by priority, so my first version passed for
the wrong reason. Equal-priority cards whose `createdAt` order disagrees
with their id order.
- `no-hardcoded-lifecycle-columns.test.ts` — 16 detector cases: 11
shapes counted, 4 legitimate ignored, one asserting the sentinel path.

Revert checks, all run: menu suppression → diff names the field;
worktree → `expected [] to include 'FN-50'`; sort → `FN-2, FN-9` instead
of `FN-9, FN-2`; ratchet → the 3 recovered guards disappear.

## One site that should never be converted

`MissionControlPanel.tsx:46` — `{ id: "triage", match: (c) => c ===
"triage" || c === "signal" || c === "backlog" }` is a deliberate
name-similarity heuristic for the SDLC funnel; it matches synonyms and
folds unknown columns into an "other" bucket so custom columns still
contribute. Converting it changes what the funnel displays. Like the
`live-agent-count` fallbacks, it belongs in a documented floor — **the
ratchet's target is that floor, not zero.**

`DocumentsView.tsx:73` is convertible but the file has no column flags
at all, so a real fix means plumbing board-workflow metadata into a view
that doesn't fetch it — its own unit of work.

## Verification

`pnpm lint` clean. `pnpm test:gate` green (10 / 482 / 71). `tsc -p
packages/dashboard/tsconfig.app.json` and `packages/core/tsconfig.json`
clean. Core ratchet + seam suites 24/24. Dashboard target suites 37/38 —
the one failure is the pre-existing `"Back to In Progress"` label
casing, confirmed identical on the base.

---

## Added after the initial push

**5. `TaskCard` lost inline editing on renamed boards; `TaskDetailModal`
kept it.** Still live on main: the modal resolved field editability from
traits in U10/R8, the card used a hardcoded `{triage, todo}` set with
**no trait path at all** — even though `taskColumnFlags` was already in
scope. On a renamed board the title was editable in the modal and the
pencil was missing from the card. Body moved unchanged into
`isFieldEditableColumnRole` so the two surfaces cannot drift again.

The veto traits are the substance: a column can legally carry `hold`
**and** a WIP or review trait, and a plain `intake || hold` check would
let an operator rewrite a description while a session executes against
it.

Coverage gap **measured, not assumed**: mutating `canEdit` back to the
hardcoded set left `TaskCard*` at the same failure count as the
unmutated run — nothing caught it. The four render cases assert the real
`aria-label`; that mutation now fails with `Unable to find an accessible
element ... name 'Edit task'`.

**6. The ratchet's target is a documented FLOOR, not zero** — and this
changes the completion bar.

Zero is not reachable, and chasing it means breaking working code. Two
categories are permanent, now protected as positive assertions so a
future sweep cannot "finish the job" by deleting them:

- `MissionControlPanel.tsx`'s `FUNNEL_STAGES` is a deliberate
**name-similarity** heuristic — it matches `signal`, `backlog`, `to-do`,
`ready`, `shipped` and folds unrecognised columns into an "other" bucket
so a custom board still contributes counts. It is not asking whether a
column has the intake trait; it buckets arbitrary column *names* for
display. Asserted on the **synonym list**, because the synonyms are what
prove it is name matching — if they disappear the site has changed
character and the exemption stops applying.
- `live-agent-count.ts`'s no-flags arm is reachable (a remote store is
deliberately given an empty flag map; a card in an undeclared column has
no flags at all) and deleting the literal makes such a card match **no**
arm, so the queued total silently under-reports a stranded card.

A count with an undocumented floor invites someone to drive it to zero.

**Not done, and why:** `DocumentsView.tsx:73` is convertible but that
file has no column flags anywhere, so a real fix means plumbing
board-workflow metadata into a view that does not fetch it — its own
unit of work, not something to smuggle into a conversion.

**Re-verified after these commits:** `pnpm lint` clean, `pnpm test:gate`
green (10 / 482 / 71), `tsc` clean on core and `tsconfig.app.json`, core
ratchet suite 26/26, `columnRoles` 10/10, `TaskCard.test.tsx` 384/386
(the 2 are pre-existing CSS assertions). `TaskDetail*` is 130 failed /
551 passed **both with and without** this change — verified by stashing,
so pre-existing and unrelated.

---

## Flag resolution: preconditions 1 and 2 are now DISCHARGED.
Precondition 3 is blocked, and by evidence.

**Precondition 1 — the side-effect equivalence proof — done.**
`moves-flag-equivalence.test.ts` runs the same journey under both flag
states against live PG and diffs the persisted row. **Result:
identical** — whole-row equality across 128 fields plus an equal timing
shape, over `todo → in-progress → in-review → todo → in-progress`.

That test was **wrong twice** before it meant anything, and both times
it was passing:

1. **It proved nothing.** `experimentalFeatures` is **global-only**, and
`moves.ts` reads `getSettingsFast()`, which filters global-only keys out
of the project layer. My `updateSettings` write was silently discarded,
`useWorkflow` was false in *both* runs, and the "proof" compared the
legacy path against itself. Found by stamping the flag-ON branch and
observing the test still passed. Now written via `updateGlobalSettings`,
and the helper **asserts the flag took effect** before the journey runs.
2. **The journey was forward-only**, so it never reached the reopen
hook's field resets (`status`, `error`, `blockedBy`, pause clearing) — a
mutation there passed. Extended with a backward move and a re-entry.

Mutation-verified after both fixes: stamping seam 3, and diverging the
reopen hook, each fail the comparison.

**Precondition 2 — done, and its answer is a blocker.** The census says
the default board is safe: all 41 literal engine move targets are
declared by the default lineage. But **20 of those 41 carry no
`recoveryRehome`**, so on a custom lineage that does not declare `todo`
/ `in-progress` / `done`, seam 2 would start rejecting them with
unknown-column. That is a user-facing break on custom boards, not a
theoretical one, and it is not fixed by the equivalence proof — seam 2
adds *new refusals* rather than swapping implementations.

**So the flip is one step away, and the step is not mine to take
alone:** those 20 call sites need to resolve their target from the
task's workflow (or justify `recoveryRehome`), and they live across
engine lanes in `moves.ts` caller territory — U2b/MAIN. Flipping before
that trades a dormant flag for broken custom boards.

What remains for precondition 3 once those land: flip both readers
**atomically** (`moves.ts` + `workflow-task-create-ops.ts`, since the
latter computes the preflight the former consumes), delete the flag-OFF
branch with its guards, and drop the settings key.

---

## CORRECTION: seam 2 is not a blocker. My earlier claim was wrong.

I stated in #2639 and above that "with the flag off there is **no**
target-column validation on the move path", so flipping would introduce
new refusals. **That is not what happens.** Reproduced against live PG:
the identical custom-lineage move rejects with the flag **OFF** as well
—

```
Error: Invalid transition: 'backlog' -> 'todo'. Valid targets: building
```

Transition validation is already in force on the flag-OFF path. So for
the shape in question — an engine move to a column the task's own
workflow does not declare — **the move already fails today**, and seam 2
introduces no new break for it. The 20 census sites lacking
`recoveryRehome` are broken on a custom lineage *now*, not broken by the
flip.

I found this because the discriminator I added to prove "the flag is the
cause" failed. Had I written the test to my assumption it would have
passed and the false claim would have shipped — the same way the
equivalence test passed while proving nothing until I tried to make it
fail.

**Revised precondition status:**

| precondition | status |
|---|---|
| 1 — side-effect equivalence | **discharged** — identical rows,
mutation-verified both directions |
| 2 — seam-2 exposure census | **discharged, and it is not a blocker** —
the rejection predates the flag |
| 3 — flip both readers atomically, delete the flag-OFF branch, drop the
settings key | **the remaining work** |

So the flip is no longer gated on fixing 20 engine call sites. What it
is still gated on is precondition 3 being done atomically across
`moves.ts` and `workflow-task-create-ops.ts` (the latter computes the
preflight the former consumes), which is `moves.ts` caller territory.

Three cases now cover seam 2: the flag-ON rejection, the flag-OFF
rejection (asserting the error *message*, so a change in which guard
rejects stays visible rather than reading as agreement), and the #1411
`recoveryRehome` carve-out succeeding — pinning why that carve-out is
load-bearing and must not be tidied away.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:13:58 -07:00
gsxdsm
177c2309d9 consolidate/u11: a hold column is a planner lane only if it precedes wip (real defect in merged code) + funnel aliases (triage 11 -> 10) (#2645)
**Consolidation branch for u11/u7.** Supersedes #2624. Contents changed
substantially while it sat unmerged — this body reflects what is
actually in it now.

## Measured with the authoritative census, not grep

`node scripts/lifecycle-column-census.mjs` — **triage guards 11 → 10.**

## 1. A real defect in merged code: a hold column is only a planner lane
if it precedes implementation

Found by greptile on #2616, verified by me, fixed here at the source
because that PR cannot land.

`resolveLifecycleColumns` returns `hold` as the **first** hold-trait
column in declared order, with no positional constraint relative to wip
(`workflow-lifecycle-traits.ts`: `hold:
first(LIFECYCLE_ROLE_FLAGS.hold)`). A workflow using a hold trait for a
**mid-pipeline wait** — a pause after implementation starts — therefore
had that column returned as its planner lane, and
`reconcileMissionFeatureState` demoted the feature to `triaged`. The
mission board reported started work as not-yet-started: silent, and
wrong in the direction that makes a roadmap lie.

This is my defect, introduced in #2610.

**Why it survived:** every lineage anyone has tested puts the hold *in
front* of wip, so the default and Ideas boards are unaffected and no
existing test could see it.

**The fix is positional, with a deliberate asymmetry.** A hold column
counts only when it appears before wip in declared order. When wip
cannot be located the hold is left **out** rather than guessed —
including it wrongly demotes live work on the roadmap, while excluding
it wrongly costs only a `triaged` transition the next reconcile
re-applies.

Mutation-verified: dropping the positional test fails the mid-pipeline
case and nothing else.

## 2. MissionControlPanel funnel aliases

Assessed and **deliberately not trait-converted**. These are heuristic
*name aliases* for a canonical SDLC stage — the matcher already accepts
`signal`/`backlog`/`ready`/`shipped` because it buckets arbitrary
boards, with an `other` fallback. Post-#2515 a default board's planning
cards sit in `todo` and count at the Todo stage, leaving Planning at
zero: the funnel reporting where cards *are*, not a guard that stopped
firing. Hoisted to a named set so it stops reading as unconverted.

This is the **DISPLAY-ALIAS** class the census still lacks — receiver
*is* a column id, purpose is presentation rather than a lifecycle
decision. `DocumentsView`'s status dot is the other one. Without that
bucket a ratchet will keep demanding conversions that make the product
worse.

## What I dropped, because main's version was better

The original #2624 carried a `TaskContextMenu` conversion. #2626 landed
`isPureIntakeColumn` — intake **without** hold — while mine treated any
intake-flagged column as intake. That's wrong for a **merged Planning
column**: it carries both traits, cards there wait for capacity and have
real actions, so I would have suppressed the menu where it belongs — a
new regression in place of the one I was fixing. Theirs is correct. Mine
is gone, along with its now-invalid test and a helper nothing else used.

## Verification

- merge gate green (482 + 132 + 10), engine tsc clean, dashboard tsc
clean, lint clean
- 7 planner-lane tests green, mutation-verified

No changeset: `@fusion/engine` and `@fusion/dashboard` are private.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-30 00:00:46 -07:00
gsxdsm
3aa942ee5f capacity: spawned agents count against the project agent count (#2579)
Two configurable numbers per project. `maxSpawnedAgentsPerParent` (5)
and `maxSpawnedAgentsGlobal` (20) were a **third and fourth** limiter
with private budgets invisible to both.

## This closes a hole, not just knobs

A spawned child **is** an agent and gets **its own git worktree**
(branched from the parent’s — the tool’s own description says so), but
children were counted by **neither** capacity gate. A fan-out could put
up to 20 extra worktrees on disk while the scheduler believed the
project was at its configured limit. The operator’s two numbers were
simply wrong about what was running.

## The old caps also measured the wrong thing

`totalSpawnedCount` decrements on child cleanup, but the per-parent
**set** is cleared only when the **parent task** ends. So
`maxSpawnedAgentsPerParent` throttled *cumulative* spawns across a
task’s life rather than *concurrent* ones — a long-running task could
exhaust its budget with five children that had all long since finished,
and the operator had no way to see why.

## Fix

`fn_spawn_agent` gates on the same project agent count every other lane
uses (`computeTopLevelConcurrencyClaimedFromStore`) plus live children.
One number, one answer, no private budget that can disagree with the
board.

The refusal names **Max Concurrent Tasks** — a control the operator
actually has. The old messages pointed at settings that no longer exist,
which is worse than no message: it sends someone hunting for a knob that
is not there.

## Verification

**Revert-proof, measured:** restoring the private budgets turns **3 of
the 4** new cases red — a project at 1/1 could still spawn, which is
precisely the hole. `executor.ts` restored byte-identical.

`pnpm lint` clean · core + engine `tsc` clean · `pnpm test:gate` green
(414 + 10 + 71) · new suite 4/4 · `settings-default-descriptions` 4/4.

There was no spawn-capacity test before this; the file is new.

🤖 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**
* Spawned agents now count toward the project’s **Max Concurrent Tasks**
capacity.
* Agent spawning is blocked when capacity is reached, including
concurrent spawn attempts.
* **Bug Fixes**
  * Prevented over-allocation during simultaneous agent spawns.
  * Restored available capacity when agent creation fails.
* **Changes**
  * Removed separate per-parent and global spawned-agent limits.
  * Updated settings to reflect the revised capacity controls.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:34:41 -07:00
Phil Larson
15b21dead1 fix(dashboard): reconcile task state through live API (#2595)
## Summary

- add a project-scoped live API route for updating individual task
checklist steps
- add an atomic live API route for resolving stale durable wedge
episodes
- prevent operator repair tooling from opening a second embedded store
that can diverge from the running dashboard backend

## Why

Legacy graph-native workflow runs can retain successful
`workflowStepResults` while their narrative checklist remains at 0/N.
The existing `fn task update` fallback may open a separate embedded
store, producing split-brain writes that do not accumulate in the live
dashboard backend. There was also no API surface for the existing atomic
wedge-episode resolver.

## Verification

- `pnpm exec vitest run
src/routes/__tests__/register-task-workflow-routes.step-update.test.ts`
— 5/5 passing
- `pnpm build` in `packages/dashboard` — passing
- full managed runtime workspace build — passing
- deployed to the managed local runtime and used to reconcile six legacy
review-deadlock tasks
- live board audit: zero `in-review-stall-deadlock` paused reasons
- exact local and Tailscale dashboard roots: HTTP 200 with 16,926-byte
bodies


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

* **New Features**
* Added live API endpoints to update individual task checklist steps
with validation (step index and allowed status values).
* Added an endpoint to reconcile/resolve stale task “wedge” episodes,
resolving only the matching active episode and returning conflicts on
mismatches.
* **Tests**
* Expanded route tests for step updates and wedge resolution, including
consistent 404 behavior for soft-deleted and missing tasks, plus
conflict and invalid-input cases.
* Expanded PostgreSQL coverage for wedge resolution persistence and
concurrent episode replacement scenarios.
* **Bug Fixes**
* Improved task-lookup error handling so soft-deleted tasks are
consistently treated as “not found” (HTTP 404).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 23:27:32 -07:00
gsxdsm
347107d8e4 Phase B — TaskContextMenu.tsx: intake by ROLE (2 → 1), and two conversions I dropped rather than force (#2626)
**Claimed:** `packages/dashboard/app/components/TaskContextMenu.tsx`

| file | before | after |
|---|---:|---:|
| `TaskContextMenu.tsx` | **2** | **1** |

## The real bug

`shouldShowActionsMenu: task.column !== "triage"` meant *"a bare card in
a pure intake lane has no actions worth showing yet."*

Post-U11 the literal does not go dead — it **inverts**. A default
Planning card is `todo`, so the condition is true and the menu shows
unconditionally. That is right for the hold half (cards waiting for
capacity do have actions), but the guard has stopped distinguishing
anything — and it would show a full action menu on a bare Coding (Ideas)
capture, which is the case it existed to suppress.

Resolved to `intake AND NOT hold` — a *pure* intake lane — which
reproduces all four shapes rather than picking a winner:

| workflow | column traits | menu |
|---|---|---|
| legacy `triage` | intake only | suppressed *(as before)* |
| legacy `todo` | hold only | shown *(as before)* |
| merged Planning | intake + hold | shown *(matches the Todo half, where
cards wait)* |
| Ideas `ideas` | intake only | suppressed *(a bare captured idea)* |

Its degraded arm now defers to `isIntakeColumnRole`, so the legacy
intake id lives in `utils/columnRoles.ts` only.

## The remaining site is audited, not overlooked

I routed `isPreExecutionHoldColumn` through
`isPreImplementationColumnRole` — same question, one definition — **and
then reverted it.** Its degraded-mode answer is wider: its legacy set is
`{todo, triage}`, this predicate's was `{triage}` alone.

They differ **for a reason.** That helper drives the preserve-progress
prompt, where a flagless `todo` *should* prompt because losing steps is
unrecoverable. This one drives the Plan affordance, where a flagless
`todo` must **not** offer to re-plan a card that may already be planned.

Consolidating added `plan` to flagless `todo` cards — caught by
*"exposes Plan only for pre-execution hold columns"*. Identical trait
path, non-interchangeable fallbacks. Kept separate with the difference
recorded rather than made to look shared.

## Two conversions I dropped rather than force

**1. A `ListView.tsx` 5 → 0 conversion.** Main changed underneath it:
the U12 worker centralized the same fallbacks into
`utils/columnRoles.ts`. Their approach is on main and other files
already call it, so I took theirs and dropped mine rather than fight for
my version through a rebase conflict.

**2. A `strandedColumnFlags.ts` seam** that resolved an undeclared
column's role from the workflow's **rebound target**, so the degraded
arms could be *deleted* rather than documented.

I built it, tested it, wired it into ListView — and then their
`columnRoles.ts` identified a state my seam cannot serve: the **pre-load
window**, where the board renders before the workflows fetch resolves
and there are no columns at all, hence no rebound target to borrow from.
Their analysis is more complete than mine, the fallback is genuinely
undeletable, and shipping an unused module is worse than shipping
nothing.

Worth recording because I twice reported these arms as permanently
unconvertible, then thought I had a way to convert them, and was wrong
for a reason worth knowing: **there are two degraded states, not one.**
The stranded-card half is resolvable; the pre-load half is not.

## Verification

10 of 11 green in this suite. The one failure — `"Back to in-progress"`
vs `"Back to In Progress"` — is **pre-existing**, verified by stashing
this change and re-running against clean `main`.

Dashboard app typecheck and lint clean.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 23:03:26 -07:00
gsxdsm
50ebf3c543 TAKING cli/project.ts (fn project reported 0 running agents) + two test fixes — dashboard conversions WITHDRAWN in favour of #2626 and #2636 (#2631)
Three app-cluster conversions plus the evidence that they behave on a
renamed AND a merged board.

## Per-file guard counts

| file | before | after | note |
|---|---|---|---|
| `packages/cli/src/commands/project.ts` | 0 | 0 | not a comparison site
— see below |
| `packages/dashboard/app/components/TaskContextMenu.tsx` | 2 | 2 |
**count does not move — deliberate, see below** |
| `packages/dashboard/app/components/Column.tsx` | 2 | 2 | **count does
not move — deliberate, see below** |

**Read this before scoring the PR against the bar.** You said a claim
that does not move your number is not done, so I am telling you up front
that *this PR does not move it*, and why.

Both dashboard conversions are **fallback-preserving**:

```ts
const isIntakeColumn = columnFlags ? columnFlags.intake === true : column === "triage";
```

The literal survives as the no-flags branch, so the grep still counts
it. That is the shape the sibling code already uses
(`isPreExecutionHoldColumn`, same file, converted earlier in the
program), and dropping the fallback would make an unresolved-column
render *lose* the affordance a second way. What changes is the
**behaviour when flags exist** — which is what the mutation results
below measure.

If you want these to zero out the count, the fallback has to go, and
that is a separate decision about whether an unresolved column should
fail open or closed. Say the word and I will do it as a follow-up; I did
not make that call unilaterally because it is not reversible from a
rendering standpoint.

`cli/project.ts` was never a comparison site at all — it fed **raw
rows** to `isRunningAgentTaskShape`, so the helper's own internal legacy
fallback kicked in and `fn project` reported **0 running agents** on any
renamed board. Fixed by resolving the IR per task before counting.
Nothing to subtract.

## Two of the three had a test that looked like coverage and was not

- **`Column.tsx`** — the quick-create gate is `workflowMode ||
isIntakeColumn`. Every pre-existing intake case in `Column.test.tsx`
*also* passes `workflowMode`, so the `||` short-circuited and **none of
them ever reached the trait lookup**. Added cases that omit
`workflowMode`, the only path where the conversion changes the answer.
- **`TaskContextMenu.tsx`** — the intake suppression was asserted only
for the legacy `triage` id, the one board shape where a broken
conversion still returns the right answer.

Mutation-verified rather than asserted:

| mutation | result |
|---|---|
| `isIntakeColumn` → `column === "triage"` | **2 of 88 fail** (exactly
the renamed and merged cases) |
| menu suppression → `task.column !== "triage"` | **1 of 12 fail** |

## A pre-existing red I fixed on the way past

`uses VALID_TRANSITIONS and in-review back-to-progress labels` was
**already failing on origin/main**. #2521 correctly moved the "Back to
X" label onto the host's `columnLabel` function; this file's stub is
`(column) => column`, so the hardcoded `"Back to In Progress"`
expectation was left over from the pre-#2521 hardcode and nothing had
updated it.

Matching the raw id would have made it pass while proving nothing, so
instead that one case gets a display-like label function — the assertion
now fails both if the "Back to" prefix regresses **and** if the label
stops routing through `columnLabel`. Strengthened, not relaxed. Counts
against completion criterion #2.

## Verification

- `Column.test.tsx` + `TaskContextMenu.test.tsx`: **100 passed**
- `tsc -p tsconfig.app.json` (the root config does not cover `app/`) and
the CLI typecheck: clean
- `pnpm test:gate`: green

🤖 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:30 -07:00
gsxdsm
f14059e8d3 Retry still refuses cards parked mid-planning on 5 builtins (survives #2614; count 0 → 0, defect-only) (#2621)
**Rebased onto main after #2614 landed this file.** That PR's conversion
already took the tracked count for `register-task-workflow-routes.ts` to
**0**, so this PR does **not** move your number and I am not claiming it
does.

| file | before | after |
|---|---:|---:|
| `packages/dashboard/src/routes/register-task-workflow-routes.ts` | 0
(post-#2614) | 0 |

What it fixes is a **live 400** that #2614 left in place. Measured on
current main: **9 of this file's 14 retry tests fail** without the
change below.

## The defect

`POST /api/tasks/:id/retry` must answer *"does this card sit where its
workflow **plans**?"*, because the yes-branch is **destructive** — it
stamps `needs-replan` **and deletes PROMPT.md**. Two predicates stood in
for that question and neither answered it:

- **#2614** resolved the **intake** column. Correct for the merged
lineage; wrong wherever intake and the planning column differ.
- The older arm asked `!workflowHasColumn(ir, "triage")`.

**Measured across all 12 builtins:** *not one* plans in `triage`, while
**seven** still declare that column. So for the five that declare
`triage` **and** run every plan node in `todo` — `quick-fix`,
`review-heavy`, `compound-engineering`, `design`, `legacy-coding` — the
predicate is `false` and a `planning`/`needs-replan` card sitting in
**its own planning column** is refused outright:

```
400 — "Task is not in a retryable state (current status: needs-replan)"
```

The operator has no button at all on a card parked mid-planning. The
mirror-image fault is destructive rather than obstructive: a workflow
that plans anywhere other than `todo` had a `todo` card's PROMPT.md
deleted for a re-plan nobody asked for.

## Fix

`workflowPlansInColumn` asks the graph. Planning nodes are recognised by
the **semantic markers** the builtins carry — `config.seam ===
"planning"` and an **exact** `workflowAction` set (measured vocabulary:
`plan-replan`, `code-review`, `pre-merge-remediation`) — with node ids
as a backstop.

Deliberately **not** a `startsWith("plan")` prefix. That was my first
attempt and greptile was right to kill it: it matched in the
**destructive** direction, classifying a custom `plan-execute` column as
a planning column, which deletes a specification. An unlisted planning
action costs a replan (recoverable, card stays retryable); a
wrongly-listed one costs a spec (not). Hence opt-in.

### Second concern, split out

Narrowing the destructive branch must not narrow **retryability** —
those were one boolean and are two questions. A card parked outside its
planning column would otherwise fail the gate and answer 400: that
trades *a card which loses its spec* for *a card nothing can rescue*. It
stays retryable via the non-destructive branch, scoped to pre-WIP
columns so no `in-progress`/`in-review` status gains a path it lacked.

A **v1 IR** declares neither columns nor nodes, so placement is
**unanswerable** rather than answered "no".
`workflowDeclaresColumnModel` distinguishes the two — reading that
silence as "past planning" is exactly what 400'd a v1 planning card.

## Verification

All three greptile P1s on the earlier revision were real and are fixed
with revert-proof tests (bespoke planning-node ids; the v1 regression I
introduced; my own loose action prefix).

`pnpm lint` clean · dashboard `tsc` clean · `pnpm test:gate` green (132
+ 10 + 482 + 71) · core 11/11 · dashboard 119/119
(`retry-planning-column` + `stale-merge-status` + `routes-tasks`).

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:39:33 -07:00
gsxdsm
31e49b684a TAKING default-workflow-hooks.ts + executor.ts + live-agent-count.ts + 6 dashboard files: reopen semantics by role, and the census's blind spot in both directions (13 sites) (#2628)
Batched conversion of every lifecycle-column guard I hold, plus the
three the census could not see. **Six files to zero, repo-wide 60 → 49
by a comment-stripped unanchored sweep.** Each conversion has an
isolated revert proof and a paired negative case, and the one code move
is a separate commit from the behavior changes.

## Per-file before → after

Counts from a comment-stripped, unanchored `(===|!==) ["']triage["']`
sweep over `packages/*/src` + `plugins/*/src`, excluding tests.

| file | before | after | note |
|---|---:|---:|---|
| `core/default-workflow-hooks.ts` | 4 | **0** | |
| `core/task-store/moves.ts` | 5 | **4** | only the flag-ON mirror
converted; the flag-OFF inline block is the parity reference and stays |
| `engine/executor.ts` | 3 | **0** | **absent from the 45-guard list** —
see below |
| `core/live-agent-count.ts` | 2 | **0** | duplication removed; answer
deliberately unchanged |
| `engine/replan-target.ts` | 2 | **0** | both were comment prose, not
guards |
| `core/agent-prompts.ts` | 3 | **0** | ROLE comparisons, never column
guards |
| `engine/usage-limit-detector.ts` | 2 | **0** | ROLE comparisons |
| `dashboard/app/components/DocumentsView.tsx` | 1 | **0** | real column
guard |
| `dashboard/app/components/TaskChatTab.tsx` | 2 | **0** | ROLE |
| `dashboard/app/components/AgentLogViewer.tsx` | 1 | **0** | ROLE |
| `dashboard/app/components/effective-model-resolution.ts` | 1 | **0** |
ROLE |
| `dashboard/app/hooks/useTasks.ts` | 1 | **0** | ROLE |
| `dashboard/…/command-center/MissionControlPanel.tsx` | 1 | 1 | alias
table, marked `DELIBERATE-LITERAL` with its reason |

## The census errs in BOTH directions

This is the finding I would most like carried into the remaining work.

- It **flagged 10 sites that were never column guards.** `role ===
"triage"` / `agentType === "triage"` compare an **AGENT ROLE**. The
planner *lane* is named `triage` and keeps that name — U11 removed the
*column*. Worse than noise: the obvious "finish the migration" edit is
to rename the role, and that silently empties the planner's prompt
template and mis-binds its model markers. `PLANNER_AGENT_ROLE` now names
it, so the two vocabularies are distinguishable by grep and a rename
fails loudly (revert proof: 4 tests, two of them pre-existing).
- It **missed 3 real guards in `executor.ts`**, because the pattern
matches `column`/`toColumn`/`fromColumn` and those locals are named
`from` and `originColumn`. A census keyed on variable names will keep
missing guards wherever a local was named for its role in the function.

## Two real defects, not tidying

**1. A renamed board could merge with its re-review never run.**
`default-workflow-hooks.ts` is named for the default workflow, but the
store runs it on the flag-ON path for *every* workflow — the trait
registry resolves hooks by trait id, not by workflow. Its reopen
predicates listed the default lineage's column names, so on a renamed
board **no reopen effect fired at all**. One of them clears
`workflowStepResults`, which `getTaskMergeBlocker` reads: a card bounced
out of review carried its old `passed` result back in, and that
satisfies the merge gate. Same regression the graph-owned-crossing
carve-out exists to prevent, arriving through the other door. (Two
smaller ones rode along: failure state never cleared on a renamed
reopen, and an operator dragging a card back to the queue never parked
it, so the scheduler re-dispatched what they had just pulled back.)

**I forgot the carve-out on my first pass, and that was worse than not
converting.** A role-resolved clear plus a *name*-matched exemption
means a renamed board takes the clear and never the exemption,
destroying the remediation input the graph had just written. My own
paired negative test caught it.

**2. The last-resort recovery for completed-but-stranded work did not
exist off the default lineage.** In `recoverCompletedTask`,
`promotedFromPlannerColumn` was false on a renamed board, so finished
work resting in the planning lane was never promoted — the code fell
through to `handoffTaskToReview` straight from the planning column, and
role adjacency has no planning → review edge, so the handoff was
rejected and the card stayed stuck with its work complete. I converted
the promotion **target** too: resolving the lane and then moving to a
literal `in-progress` is the half-conversion I have already been burned
by twice this program, where the guard starts admitting cards and the
move then sends them to a column the board does not declare.

## E2E evidence

`renamed-board-reopen.pg.test.ts` drives a **real PostgreSQL store** and
a real `moveTask` on a workflow whose columns carry the standard traits
under non-default names. The unit tests cannot show this: if `moves.ts`
passed `undefined`, every unit case still passes via the no-basis
fallback while the real board keeps the old behavior. **Proof it is
load-bearing: forcing `moveLifecycleColumns` to `undefined` fails 2 of
3.** The executor suite covers both the split-role and the MERGED
post-U11 shape.

## Revert proofs, isolated per site

| change reverted | result |
|---|---|
| reopen predicate → literal names | 4 of 10 fail |
| reopen field clears → literal names | 2 of 10 fail |
| `userPaused` hold lane → literal `todo` | 1 of 10 fail |
| graph carve-out → literal names | 1 of 10 fail |
| store passes `undefined` lifecycle columns | 2 of 3 fail (real PG) |
| `promotedFromPlannerColumn` → literals | 3 of 7 fail |
| two-hop condition → `=== "triage"` | 1 of 7 fails |
| promotion target → `"in-progress"` | 3 of 7 fail |
| `isPlannerColumnFor` → literals | 1 of 7 fails |
| live-agent-count: one arm dropped | 2 of 11 fail |
| DocumentsView: trait branch removed | 3 of 7 fail |
| planner role renamed to `"planner"` | 4 fail (2 pre-existing) |

Every conversion is paired with a negative case (a forward move, a
not-a-planner-lane card, a default-lineage card, a renamed column with
no traits), so neither "always fire" nor "never fire" can pass for
"resolve the role".

## Deliberately NOT converted, with reasons

- **`moves.ts` flag-OFF inline block (4).** That branch *is* the legacy
path, kept verbatim so the two can be parity-checked. Converting it
erases the reference implementation.
- **`live-agent-count.ts`'s no-flags fallback.** Reachable, and there is
nothing to resolve from — `enrich…FromFlags` exists for callers with
board flags rather than an IR, so a column missing from that map is the
renamed case. "Not intake" is as much a guess as "todo is intake", and
Running/Waiting are complements, so a card matching neither arm is
reported as neither and the footer's queued total under-reports it. The
real fix is at the caller; four new cases pin that flags override the
legacy answer **in both directions**. What did change is the
duplication: two hand-written copies of one rule now call one named
function.
- **`MissionControlPanel`'s `FUNNEL_STAGES`.** An alias table of column
*names* where `triage` sits beside `signal` and `backlog`. Command
Center aggregates across projects, so there is no single workflow to
resolve traits from — the honest conversion is a data change, not a
predicate change.
- **`DocumentsView` with no traits.** Same no-basis rule; the documents
list is full of historical columns absent from the current board. A case
asserts a renamed column with no traits still reads as "working",
documenting the gap rather than hiding it.

## Fixture findings

Each cost a red run that looked like the code under test:

- a `merge-blocker` column needs a reachable merge-class node, or
`parseWorkflowIr` rejects the workflow;
- a back-edge must be `kind: "rework"`, and a rework edge is legal only
**into** a node with `config.reworkRegion: true`;
- a workflow gets role-level transitions only when it declares wip +
review + complete + **archived** plus a planning lane — without the
archived column, adjacency falls back to order-derived neighbours and
`checking -> queued` is not a legal move at all;
- `recoverCompletedTask` only *reaches* the promotion seam when nothing
is left to gate; without passed `plan-review`/`code-review` rows it
re-enters the workflow graph and returns first, so a naive fixture
silently tests the wrong branch and every assertion reads "no moves
happened" for an unrelated reason.

## Verification

- `pnpm test:gate` **71/71**
- new suites: 10/10 reopen-semantics, 3/3 renamed-board-reopen (real
PG), 7/7 executor-planner-lanes, 7/7 documents-status-dot, 4/4
planner-role-is-not-a-column
- neighbours: 132 + 10 + 482 (gate shards), 350/351 engine
planning/replan suites, 64/64 agent-prompts, 51/51 usage-limit-detector,
11/11 live-agent-count, 11/11 dashboard hook/log suites
- the single engine failure (`executor-fast-mode-workflows.test.ts` ›
"raw fast mode still invokes non-executable review seam nodes")
**reproduces with my changes stashed** — pre-existing on `origin/main`
- typechecks clean for core, engine, and dashboard-app
(`tsconfig.app.json`; `tsconfig.json` checks nothing under `app/`);
`pnpm lint` clean

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:39:14 -07:00
gsxdsm
890d588891 Phase B — Column.tsx 2→0 and taskActivity.ts 2→0 (U11's cluster to zero) (#2636)
**Claimed:** `Column.tsx`, `taskActivity.ts` — both to zero.

| file | before | after |
|---|---:|---:|
| `packages/dashboard/app/components/Column.tsx` | **2** | **0** |
| `packages/dashboard/app/utils/taskActivity.ts` | **2** | **0** |

## `Column.tsx` — two different fixes, because the two sites are
different problems

**The preserve-progress prompt** routed through
`isPreImplementationColumnRole`. This is the *same* question that helper
was written for — ListView asks it about a move target, Column asks it
about itself — and the degraded id sets are identical (`{todo,
triage}`), so the consolidation is exact.

I verified the sets matched **before** consolidating, because the
sibling case is not interchangeable: `isPreExecutionHoldColumn` in
`TaskContextMenu` drives the Plan affordance and its degraded set is
`{triage}` alone. Routing *that* through this helper added `plan` to
flagless `todo` cards, caught by an existing test. **Same shape,
identical trait path, non-interchangeable fallbacks.**

**The legacy-board arm** (`workflowMode || column === "triage"`) —
deleted, on the third attempt.

I deleted it twice before and reverted both times because four Column
tests render without `workflowMode`. That was the delete-only rule
working, but **my conclusion from it was wrong**: a behaviour change
means the branch was not dead *for those callers*, and the callers are
**fixtures, not production**. Board is Column's only consumer and passes
`workflowMode` at all three render sites. Defending an unreachable arm
so four tests keep passing preserves the tests, not the behaviour.

Two notes for anyone converting the remaining dashboard files:

- I did **not** default `workflowMode` to `true`, which was the tempting
one-liner. `isArchived`, `isHoldColumn` and `isWipProcessingColumn` all
switch on that same flag, so a global default would silently reinterpret
every other fixture in an 85-test file.
- **"Four tests break" was itself an underestimate.** Two more FN-770
fixtures surfaced only after the first two were fixed, because they
render their own explicit `column="triage"` block instead of using
`defaultProps`. The blast radius only became accurate by fixing it in
waves.

## `taskActivity.ts` — composed, not copied

The degraded arm now composes `utils/columnRoles`' predicates instead of
naming ids. **No local copy** — which is the failure mode #2625 hit from
the other direction.

Equivalent *by construction*:

| lane | composition | resolves to |
|---|---|---|
| intake | `isIntakeColumnRole(undefined, col)` | `triage` |
| hold | `isPreImplementationColumnRole(...)` **and not** intake |
`todo` |

reproducing `col === "triage" || (col === "todo" && isReplanning)`
exactly, since the shared pre-implementation set is `{todo, triage}` and
the shared intake id is `triage`.

Deliberately phrased as *"pre-implementation and not intake"* rather
than a second id list: if either shared set changes, this composition
follows it instead of silently disagreeing with the file next door. That
disagreement is precisely what bit the `TaskContextMenu` consolidation
above.

**I previously reported this site as blocked on `TaskCard.tsx` (U12's)**
— on the theory that the arm could only die once every caller supplied
resolved flags. Wrong framing: the arm doesn't need to become
*unreachable*, it needs to stop *naming ids*. Composing the shared
predicates does that without touching any caller.

## Verification

**1139 of 1141** green across `app/utils`, `Column` and `TaskCard`
suites. The two `TaskCard` failures are **pre-existing** — verified by
stashing this change and re-running, where they fail identically.
Dashboard app typecheck and lint clean.

Takes U11's cluster to zero except `TaskContextMenu.tsx`, whose
remaining site is covered in **#2626** and whose second site is a
documented non-consolidation.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:37:43 -07:00
gsxdsm
d438cd1d13 U12 drift: register-task-workflow-routes.ts — resolve the intake column (7 -> 1) (#2614)
**File claimed:
`packages/dashboard/src/routes/register-task-workflow-routes.ts`.**
Per-file lifecycle-column guard count: **7 → 1**, and the 1 is comment
prose (line 3758), so this file is done for completion-bar item 1.

## The bug this fixes

`retrySpecification` decided "this Retry is a re-plan, not a generic
retry" with `task.column === "triage"`. `status: "planning"` is
retryable **only** through that flag — it is not in the generic
`failed`/`stuck-killed` set. So on any lineage whose intake column is
not literally named `triage`, a card visibly sitting in planning got
`400 Task is not in a retryable state`. The operator's Retry button did
nothing, with no error to explain why.

Post-#2515 that includes the **default** workflow:
`columnsWithFlag(resolveDefaultWorkflowIr(), "intake")` is `["todo"]`
and the default's columns are `[todo, in-progress, in-review, done,
archived]` — `triage` is not declared at all. The pre-existing `todo`
fallback below it papered over the default case (it fires when the
workflow has no `triage`), which is why this did not show up as a total
outage; custom and renamed lineages had no such cover.

Now: `const retryIntakeColumn = await
resolveIntakeColumnForTask(scopedStore, task.id)`.

## Red-green, measured

`packages/dashboard/src/__tests__/plan-approval-intake-column.test.ts` —
new case, custom lineage with intake `backlog`, card in `backlog` with
`status: "planning"`:

- with the change: `200`
- with `task.column === "triage"` restored: **`AssertionError: expected
400 to be 200`**

The fixture uses `planning` deliberately. A `failed` fixture would pass
either way through the generic retryable set and prove nothing.

## What is NOT tested, and why not

This PR also removes four `&& task.column !== "triage"` disjuncts I
added earlier while widening the P0 approve/reject guard. **Those are
untestable by construction** and I am not claiming coverage for them:
removing an extra acceptance only shrinks what the guard accepts, and no
case can feed these routes a `triage` card now that no shipped lineage
declares one. I re-widened one guard and confirmed the suite stays green
— i.e. nothing depends on the disjunct in either direction. That is the
honest result, not a passing test.

## Three fixtures updated, not guards re-widened

`stranded-refinements-routes.test.ts` failed with three `expected 400 to
be 200` — the same failures that made me widen in the first place. This
time I probed instead: `BASE_TASK` had `column: "triage"`, a column the
default workflow no longer declares, so a 400 is **correct** and the
fixtures were pre-merge artifacts describing a board shape the product
stopped shipping. Changed to `column: "todo"` with the resolver output
recorded in the file.

## Observation, deliberately not fixed here

The `todo` fallback at ~2642 (`retrySpecification =
!workflowHasColumn(workflowIr, "triage")`) is now near-dead: for the
merged default the first branch already fires. It survives only for a
lineage that has a `todo` column, no `triage`, and some *other* intake
column — where treating a `todo` card as planning is arguably wrong.
Deleting it is a behaviour change with its own blast radius, so it does
not ride along in a conversion commit.

## Verification

`pnpm lint` clean. `pnpm test:gate` green (10 / 482 / 71). Target
suites: `plan-approval-intake-column.test.ts` 8/8,
`stranded-refinements-routes.test.ts` 12/12.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:34:31 -07:00
gsxdsm
1469d57477 U12 drift: ListView.tsx — one tested column-role helper (5 -> 0) (#2620)
**File claimed: `packages/dashboard/app/components/ListView.tsx`.**
Per-file lifecycle-column guard count: **5 → 0** (3 live, 2 in comment
prose that described the deleted code).

## What was actually wrong

All three live sites were *already* flags-first. The defect was that
each carried its own inline copy of the same fallback:

```ts
targetFlags ? Boolean(targetFlags.intake || targetFlags.hold) : column === "todo" || column === "triage"
```

Three copies, none reachable from a test, each reading like a lifecycle
rule rather than the degraded mode it is. A fourth copy was the natural
next step.

## Why the fallback survives instead of being deleted

`columnFlagsById` is legitimately empty in two states: the pre-load
window before the workflows fetch resolves, and a card stranded in a
column its workflow no longer declares. A bare `flags.intake === true`
returns false in both, and **both failures are silent** — the Planning
badge stops appearing, and a backwards move stops asking whether to
preserve step progress, so the operator loses completed steps with no
prompt and no error. Deleting the fallback is not the cleanup it looks
like.

So it is kept, named (`isPreImplementationColumnRole`,
`isIntakeColumnRole` in `app/utils/columnRoles.ts`), defined once, and
documented with that reason at the definition. The legacy ids now live
in a named `LEGACY_PRE_IMPLEMENTATION_COLUMN_IDS` set — a last-resort
guess, not a comparison masquerading as a rule.

## Tests, and the case that never had one

`app/__tests__/columnRoles.test.ts` (6). The degraded branch is now
covered for the first time — it was unreachable while inline inside two
`handleMove` closures and a `useCallback`.

It also pins the **inversion** a fourth copy would eventually get wrong:
a resolved column whose traits say it is *not* pre-implementation must
not be overridden by an id that happens to be `todo` or `triage`. That
is the direction that trains operators to dismiss the prompt.

Mutation-checked, measured:

| mutation | result |
|---|---|
| ignore the flags argument (`return LEGACY_….has(columnId)`) | **4
failed / 2 passed** |
| ignore the id fallback (`return Boolean(flags?.intake \|\|
flags?.hold)`) | **4 failed / 2 passed** |

## Behaviour preservation

`ListView.test.tsx` + `workflow-resolved-columns.test.tsx`: **260
passed**, unchanged. The extraction is a pure move — the two helper
bodies are the inline expressions verbatim, with the id set hoisted.
`pnpm lint` clean. `tsc -p tsconfig.app.json` clean (the app config, not
the root one that silently skips `app/`).

No changeset: behaviour-preserving refactor.

## Backlog measured on `origin/main` at time of writing

48 total. `self-healing.ts` (10) is the capacity worker's;
`register-task-workflow-routes.ts` (7) is my #2614. Remaining unowned in
this area after this PR: `TaskCard.tsx` 4, `TaskDetailModal.tsx` 3,
`TaskContextMenu.tsx` 2, `Column.tsx` 2, `taskActivity.ts` 2. Several of
those hold the *same* fallback pattern and can now call this helper
rather than grow another copy.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:23:46 -07:00
gsxdsm
a3a7f16977 Drift 4/4: reset reported a successful reset as a 409 "limbo" conflict — plus the audit verdict for every other site in the file (#2582)
## Drift 4/4 — a second live bug in the routes file, plus the audit for
the rest of it

**Stacks on #2571** (the P0). Merge that first.

### The bug

`POST /tasks/:id/reset` resolves its destination through
`resolveReboundColumnForTask` — the task's own workflow rebound column —
and then verified the outcome against the literal `todo`. **Twice.**

So on any workflow whose rebound column is not `todo` — Coding (Ideas),
any custom or renamed lineage — a reset that **succeeded** was reported
as a `409` "limbo state" conflict. The mover and its own verification
disagreed about where the card was supposed to land.

Both checks now compare against `resetColumn`, which is already in scope
two lines above the first one.

### Revert-proof, after I caught my own vacuous test

My first version of this test **passed with the fix reverted**. The
reset route demands `{ confirm: true }` and was 400ing before it ever
reached the column check, so `expect(status).not.toBe(409)` was
trivially true. That is the third time in this program a route/DOM
assertion has looked like coverage while checking nothing, and the
second time I have caught it in my own test.

With the confirmation sent, the reverted form fails: `expected 409 not
to be 409` — a correctly-reset card reported as limbo.

### Audit of the remaining sites in this file

| site | fires after #2515? | verdict |
|---|---|---|
| 2597/2607 manual retry | yes | **SAFE, by design.** Falls back to a
`todo` branch gated on the workflow declaring no `triage` column —
exactly the merged shape. Written for Coding (Ideas); the merge made the
default match it. |
| 4584 respecify | yes | **SAFE.** Already `column === "triage" \|\|
column === respecifyTarget`, and `respecifyTarget` resolves the intake
column. |
| 2887/2917 reset verification | **no** | **FIXED here** — false 409 on
a successful reset. |
| 1121 awaiting-planning enrichment | partially | **BROKEN, deliberately
not converted** — see below. |

### The one I chose not to convert, and why

`1121` filters on `column === "todo"`, so a workflow whose waiting lane
is named otherwise gets no enrichment and silently falls back to the
heuristic.

I converted it and **reverted**. Resolving each task's hold column needs
a per-task workflow read, and this is the board-load path whose own
comment exists because unbounded reads here *"turn a board load into
thousands of reads"*. My version did those reads for **every task before
the enrich limit applied** — trading a silent degradation for a
load-time regression on every board.

Converting it properly needs the hold column resolved per **workflow**
from data the board payload already carries, not per task from the
store. That is a real change with a measurable cost, not a rename. Left
with the cost written at the site rather than quietly skipped, and
flagged here so it is tracked rather than forgotten.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, dashboard typecheck
green. Route suites: 9 passed, including
`stranded-refinements-routes.test.ts` unchanged at 5.

### My drift set, final

| file | before | after | PR |
|---|---|---|---|
| `TaskCard.tsx` | 8 | 3 | #2558 |
| `ListView.tsx` | 5 | 3 | #2566 |
| `taskActivity.ts` (found underneath) | 1 | 1 | #2566 |
| `TaskDetailModal.tsx` | 4 | 3 | #2577 |
| `register-task-workflow-routes.ts` | 10 | 11 → 9 | #2571 + this |

Routes went 10 → 11 in #2571 (guards widened to accept resolved-intake
**or** `triage`, so a P0 fix could not reject anything previously
allowed) and back to 9 here. Every other survivor is the documented
no-metadata fallback: flags are absent during the pre-load window and
for a card stranded in a vanished lane, and a bare trait read would drop
the affordance in exactly those states. They retire with the load
window, not with a rename.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:31:02 -07:00
gsxdsm
a56253f426 P0: plan approve/reject rejects EVERY card on a merged planning column — operator-visible stall, cannot approve or reject (#2571)
## P0 — plan approve/reject is dead for cards on a merged planning
column

**This is the "card stuck with nothing to rescue it" case you asked to
hear about immediately.** Found auditing my files after #2515.

### What happens

#2515 removed `triage` from the merged default lineage — one
pre-implementation column, id `todo`, displayed "Planning". Four routes
guard with:

```ts
if (task.column !== "triage") throw badRequest("Task must be in 'triage' column ...")
```

On a workflow with no `triage` column that condition is **true for every
card**, so the routes reject all of them:

| route | effect on a merged-lineage card |
|---|---|
| `POST /tasks/:id/approve-plan` | 400 — **cannot approve** |
| `POST /tasks/:id/reject-plan` | 400 — **cannot reject** |
| `task_refine` route (×2) | 400 — refine blocked |

A card parked `awaiting-approval` can be **neither approved nor
rejected**. It is stuck, the operator is being asked for a decision they
have no way to give, and nothing throws to reveal it.

### Why it is the worst variant of this drift

Everything we have chased so far is a guard that silently **stops**
firing. This is a guard that silently starts firing on **everything** —
same root cause, opposite symptom, and worse, because the failure is
visible to the operator as a task that demands an answer and refuses
every one.

### The fix, and a deliberate choice

The guards resolve the workflow's own intake column through the existing
`resolveIntakeColumnForTask`, and they **widen rather than replace**: a
card is accepted if it is in the resolved intake column **or** in
`triage`.

That is on purpose for a P0. The fix cannot reject anything the route
previously allowed, so it carries no regression risk of its own.
Narrowing to the resolved column alone is a follow-up once the legacy id
is gone everywhere — not something to do under time pressure on a route
that gates operator decisions.

I found the value of that when a strict replacement broke 3 pre-existing
tests in `stranded-refinements-routes.test.ts`. The widened form passes
all of them **and** the new P0 cases.

### The convergence number goes UP, and I am not hiding it

Live-code `column === / !== "todo" | "triage"` in
`register-task-workflow-routes.ts`: **10 → 11**.

Each converted guard keeps the legacy id as an explicit second
condition, so a widened guard has two literals where it had one. The
metric counts id literals; it does not know the guard is now strictly
more correct. Reporting the direction that is true rather than the one
that looks better — and flagging that this file's number will only fall
once the widening can be removed.

### Revert-proof

Restore either bare literal and the matching case fails with **400 where
200 is expected**, on a `todo` card with `awaiting-approval`. A third
case pins that the guard still **narrows** — an `in-progress` card is
still rejected — so this cannot be mistaken for deleting the check.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, dashboard typecheck
green. New suite 3 passed; `stranded-refinements-routes.test.ts` back to
5 passed (it was 3 failed under the strict form).

### Still auditing

`TaskDetailModal.tsx` conversion is in flight on a separate branch.
`TaskCard.tsx` (#2558) and `ListView.tsx` + `taskActivity.ts` (#2566)
are already open — and note #2566 covers `isTaskAgentActive`, whose
planner-lane clause has the *silent* version of this same bug: planning
cards read as idle everywhere at once.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:11:50 -07:00
gsxdsm
3c46ecca14 Drift (unowned): the planner-activity signal was never written after #2515 — the three badge conversions were reading an empty field (#2594)
## Drift, unowned: the planner-activity signal was never being written

**Stacks on #2577.** Merge order: #2566 → #2577 → this.

### This is what made the other three PRs cosmetic

`addRecentPlannerActivityForFreshAgentLog` in `useTasks.ts` stamped
`recentAgentActivityAt` **only for cards literally in `triage`**. #2515
removed that column from the default lineage, so after that merge the
stamp never happened for a default-workflow card.

Every consumer downstream then had **no data to act on**, however
correctly it resolved its own column traits:

- the pulsing Planning badge (TaskCard, ListView)
- the agent-active row border
- the column header's executing count

So #2558 / #2566 / #2577 convert the *readers* of a field that nothing
was *writing*. They ask the right question of an empty value. This is
the fix that gives them something to read — and it was on nobody's drift
list. I found it chasing why a badge test would not go green.

### The decision, and why I did not thread metadata here

The hook processes SSE and has no resolved column metadata. The lane is
matched by id against **both** shapes — pre-merge `triage`, post-merge
`todo`.

Over-stamping a legacy hold-lane card is harmless: every consumer
additionally requires the column to be an **intake** lane before
rendering anything, so the extra timestamps are filtered downstream.
Threading board context into this hook to avoid a harmless over-stamp
would be a much larger change for no behavioural gain, so I widened
instead and wrote the reasoning at the site.

### Also converted

`Column.tsx`'s move-progress prompt. Unlike the same prompt in
TaskCard/ListView/TaskDetailModal, this component's `column` **is** the
drop target, so its own `columnFlags` are the target's traits — no
lookup needed. Worth noting because the same-looking regex meant three
different things across four files, which is exactly why these were
converted one at a time.

### Revert-proof

Restore `task.column !== "triage"` and the merged-column case fails:
`expected undefined to be '2026-07-28T12:00:01.000Z'` — nothing stamped,
badge has nothing to render. A companion case pins that the stamp still
**narrows**: an `in-progress` card is not planner activity.

### Verification

`pnpm test:gate` (482 + 10 + 71), `pnpm lint`, dashboard typecheck
green. `useTasks.test.ts` + `Board.test.tsx`: 210 passed.

### Audited and deliberately left

| site | verdict |
|---|---|
| `Column.tsx:550` `workflowMode \|\| column === "triage"` | Dead in
practice — `workflowMode` is true whenever lanes resolve, so the
disjunct only matters with no metadata at all. Not worth a change. |
| `taskSorting.ts:73` `column === "todo"` | Still correct for the
default (the merged column keeps that id); wrong only for a renamed
workflow's hold lane. Needs the sort to take flags — a wider signature
change than this PR's scope, and cosmetic (ordering) rather than a lost
affordance. |
| `worktreeGrouping.ts:77` | Same shape as above; grouping only, no lost
control. |

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 11:11:12 -07:00
gsxdsm
bad35775a1 Drift 3/4: Task Detail intake affordances from traits — the UI half of the #2571 approve/reject stall (4→3) (#2577)
## Drift conversion 3 of 4 — Task Detail, the UI half of the #2571 stall

**Stacks on #2566.** Merge order: #2558 → #2566 → this. (#2571 is the P0
and is independent — merge it first regardless.)

### Convergence number

Live-code `column === / !== "todo" | "triage"` in `TaskDetailModal.tsx`:
**4 → 3**

All three survivors are the documented no-metadata fallback, same shape
as TaskCard and ListView: `workflowMoveMetadata` is `null` until the
detail payload resolves, and a bare trait read would drop these controls
during that window.

### This is the UI half of the P0

`isAwaitingApproval` and the standalone Delete button were both gated on
`task.column === "triage"`. On the merged lineage (#2515) that is false
for every card, so a task parked `awaiting-approval` **loses its
Approve/Reject controls in the one surface that shows them**.

#2571 fixes the routes that *reject* those actions. This fixes the UI
that stops *offering* them. Either half alone leaves the operator stuck
— one with buttons that 400, the other with no buttons at all.

### Three conversions

| site | was | now |
|---|---|---|
| `isAwaitingApproval` + standalone Delete | `column === "triage"` |
resolved column's `intake` |
| `requiresExecutionModeReplan` | `todo \|\| in-progress` | `hold \|\|
countsTowardWip` |
| move-progress prompt | source column ids | **target** column's flags |

The replan rule is "this card may already hold a plan or a live
execution context" — which the traits state directly;
`todo`/`in-progress` was the Default workflow's spelling of it.

The move prompt is the mistake I made first in TaskCard, where its
regression test caught that the site tests the move **destination**, not
the card. Carried the lesson here rather than repeating it.

### Tested through a pure seam, and why

`requiresExecutionModeReplanForTest` is exported so the rule can be
asserted as a function of (column id, flags).

Asserting it through the modal means booting async detail loading to
observe one boolean — and an earlier DOM-level attempt at exactly this
class of assertion (in #2566, ListView) **passed with the conversion
reverted**, because the text it matched also appears in a column header.
I am not repeating that. A seam discriminates; that DOM test did not.

Revert-proof: restore `column === "todo" || column === "in-progress"`
and the merged-column case fails, because that column is `intake + hold`
and carries no `countsTowardWip`. The suite also pins that the rule
still **narrows** (a complete lane needs no replan) and that the legacy
fallback is unchanged when flags are absent.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, dashboard typecheck
green.

**No new failures**: `TaskDetailModal.rendering.test.tsx` reports the
same 28 pre-existing failures with and without this change, diffed by
test *name* against a stashed clean tree.

### Drift set status

| file | before | after | PR |
|---|---|---|---|
| `TaskCard.tsx` | 8 | 3 | #2558 |
| `ListView.tsx` | 5 | 3 | #2566 |
| `taskActivity.ts` (found underneath) | 1 | 1 | #2566 |
| `TaskDetailModal.tsx` | 4 | 3 | this |
| `register-task-workflow-routes.ts` | 10 | 11 | #2571 (P0, widened on
purpose) |

Survivors are no-metadata fallbacks except the routes, where the guards
deliberately accept resolved-intake **or** `triage` so a P0 fix cannot
reject anything previously allowed. Those retire together once the
legacy id is gone board-wide.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:26:21 -07:00
gsxdsm
3cef9c226e Drift 1/4: TaskCard planning affordances from traits, not "triage" (8→3 measured; one site needed a new wire fact, one conversion was wrong and the tests caught it) (#2558)
## Drift conversion 1 of 4 — TaskCard.tsx

Taking the dashboard surfaces from the drift review. This is the
board-card one; ListView, TaskDetailModal and
register-task-workflow-routes follow separately so each stays
revertable.

### Convergence number

`task.column === / !== "todo" | "triage"` in `TaskCard.tsx`: **8 → 3**

The three survivors are **one documented fallback**, not scattered
checks. `getTaskColumnFlags` (Column.tsx) returns `undefined` when a
card's column is absent from the resolved metadata and is not the
rendering column — the pre-load window, and a card stranded in a lane
its workflow dropped. Converting to bare trait reads would have removed
every planning affordance in exactly those states, so the legacy ids
survive **once**, at the role helpers, plus the move prompt resolving
its own target. They retire with the load window, not with this change.

I'd rather report 8 → 3 with the reason than 8 → 0 with a regression
behind it.

### Why this file was urgent

Every planning affordance was gated on `task.column === "triage"`. Land
U11 — merged column keeps id `todo`, `triage` deleted — and each
comparison silently becomes false: **delete button, awaiting-approval
controls, planner badge, step list all vanish from planning cards.**

### One site needed a new fact on the wire, not a renamed comparison

`showStartAction` was `intake === true && column !== "triage"`. That
hardcoded id was standing in for *"an intake column that does not
auto-triage"* — a distinction that lives in trait **config** (`intake`
with `autoTriage: false`) and was invisible to every client.

It also **inverts** under U11: with `triage` deleted, `column !==
"triage"` is vacuously true, so a Start button would appear on **every
planning card**. `describeColumns` now derives `manualIntake`
server-side and the gate reads it. Renaming the comparison would have
shipped the inversion.

### One conversion was wrong, and the tests caught it

I first converted the move-progress prompt to the card's *own* column
role. The original tests the move **destination** — moving a card *back*
into a pre-implementation lane is what risks discarding step progress.
`confirms preserving progress before moving` failed immediately on an
`in-progress → todo` move. It now resolves the target column's flags.

That is the argument for red-green per site rather than pattern-matching
the comparison: the regex looks identical at both sites and means
different things.

### Revert-proof

New `TaskCard.u11-merged-column.test.tsx` renders cards in the
**post-U11 shape** — id `todo`, traits `intake + hold`, no `triage`
anywhere — and asserts Delete, the planner badge and the step list still
appear; that Start does **not** (auto-triaging); and that a
manual-intake lane does get it. Revert any converted site and the
matching case fails, because these cards are not in `triage` and never
will be again.

### Fixture updates, and why they are not weakening

- Start-affordance cases now pass `manualIntake`, which the server
supplies for a manual intake lane.
- "omits the Start button for the triage column even when intake is
flagged" → "for an **AUTO-triaging** intake column". The rule was never
about the id; the title said it was.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, both dashboard typechecks
green. **No new failures**: `TaskCard.test.tsx` reports the same 2
pre-existing failures with and without the change, verified by diffing
failing test *names* against a stashed clean tree rather than comparing
counts.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 09:54:16 -07:00
Phil Larson
01a75f9edc fix(desktop): typecheck streamed model downloads (#2493)
## Summary
- make the fetch response-body cast explicit across DOM and desktop
TypeScript library definitions
- preserve the existing async byte-stream runtime behavior

## Test plan
- `pnpm --filter @fusion/dashboard exec vitest run
src/stt/__tests__/model-manager.test.ts --reporter=dot`
- `pnpm --filter @fusion/desktop typecheck`
- `pnpm --filter @fusion/dashboard typecheck`
- `node scripts/check-changeset-format.mjs`

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

* **Bug Fixes**
* Improved desktop build typechecking for streamed speech-model
downloads by refining how streamed response bodies are interpreted for
TypeScript.
* Preserved runtime behavior, including streaming, integrity/hash
checking, file writing, and cancellation handling.
* **Maintenance**
* Updated the release metadata so this fix is published with the correct
patch classification.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 09:15:28 -07:00
gsxdsm
6721bdc652 U12 part 7: the List view never self-healed a card's workflow — extract Board's FN-7591 refetch and wire it up (#2530)
## U12 part 7 — the List view never self-healed a card's workflow

**Stacks on #2528.** Merge that first.

Paying off something I owed on #2525: greptile pointed out that a task
whose `taskWorkflowIds` entry is absent — or present but resolving to a
workflow that does not declare the task's stored column — gets no
per-workflow move metadata, so its menu falls back to the neighbour
approximation and **stays there until some unrelated refresh happens**.

Board has forced one board-workflows refetch for exactly this since
FN-7591. List had none. So the degraded state persisted longest
precisely where it is most likely: a **just-created card**, which is
when a workflow was actually chosen.

I said there that porting the self-heal deserved its own change rather
than riding along in a move-menu fix. This is it.

### Two commits, deliberately separable

**1. Extraction — move only.** Board's ~55 lines (refs, suspect-mapping
predicate, signature guard, deferred macrotask) become
`useUnmappedWorkflowRefetch`. Copying them into ListView would have
created a second copy of subtle race-avoidance logic to keep in sync.

Evidence it is a move: with comments and the new wrapper signature
stripped, the hook's **41 body lines** and the **42 removed from Board**
differ by exactly one line — the `}` that closed Board's enclosing
scope. Nothing added, removed or reordered. The original FNXC notes
travel with the code, since they are the reason each line exists.
Board's suite is green with no expectation edits.

**2. Wiring — behaviour change.** ListView calls the hook.

### Revert-proof

Remove the hook call from ListView and the new case fails:
`fetchBoardWorkflows` is never called a second time, so the mapping
never resolves. A companion case pins the other half — a fully-mapped
board must **not** refetch, so the signature guard cannot turn a healthy
list into a loop. It measures calls made *after* the initial load
settles, because mount fetch and switcher-open legitimately call the
fetcher and counting from zero would measure those instead.

### Two existing tests needed fixture corrections — neither a regression

Both because the self-heal now fires **correctly** where the fixture did
not expect a fetch:

- `refreshes workflow columns when workflow metadata SSE arrives`
chained two `mockResolvedValueOnce` payloads. The file-level cache seed
maps no tasks, so first paint saw FN-001 as unmapped and the repair
fetch ate the payload the test asserts on. Seeded that test's own
first-paint cache, and added a trailing default — the SSE swap
(`backlog` → `ready`) leaves FN-001 in a column its workflow no longer
declares, so a repair fetch there is right, and without a fallback it
resolved `undefined` and wiped the payload.

Worth stating plainly: both fixtures had quietly depended on List
*never* self-healing. That dependency is what the change removes.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (18
steps), dashboard typecheck green. ListView + Board suites: **320
passed, 0 failed, 0 skipped**.


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

* **Bug Fixes**
* List and Board views now self-recover when task-to-workflow mappings
are missing or incorrect, avoiding degraded workflow UI until a later
refresh.
* Workflow recovery retries are more robust and coordinated to handle
delayed/failed refreshes.
* Recovery behavior correctly stops/reset when switching projects or
unmounting.

* **Tests**
* Added comprehensive ListView coverage for unmapped-workflow self-heal,
including retry timing, StrictMode effect replay, SSE refresh
interactions, and mapped-vs-unmapped scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

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

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

## Why

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

## Verification

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


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

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

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

- **Tests**
- Added/updated unit tests to validate the new bounded remediation
behavior and messaging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 00:05:29 -07:00
Phil Larson
9a8fc409ff fix: persist manual task pauses (#2536)
## Summary

- persist an explicit `userPaused` latch when operators pause tasks
through CLI, MCP, dashboard task routes, or mission stop
- keep automatic/internal pauses distinct (`userPaused` remains false
unless explicitly requested)
- clear the latch on unpause
- route the flag through in-memory and PostgreSQL task stores
- add contract coverage across core, CLI, MCP, dashboard task routes,
and mission stop

## Why

A manually paused task could lose the reason for its pause across
dashboard/runtime restart. Startup recovery then treated it like an
internally interrupted task and reclaimed it, restarting automation
against the operator’s intent. Manual pauses must survive restart and
remain non-runnable until explicitly unpaused.

## Verification

- core pause durability tests: 2 passed
- CLI task/extension tests: 150 passed; PostgreSQL integration lane
remains active in CI
- dashboard route tests: 261 passed
- `@fusion/core`, `@runfusion/fusion`, and `@fusion/dashboard`
typechecks passed
- full workspace build passed with pnpm 10.33.0
- changeset validation and `git diff --check` passed
- live aggregate runtime verification also confirmed
`paused=true,userPaused=true` survived a normal dashboard restart with
zero active tasks


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

- **Bug Fixes**
- Manual task pauses now persist across application restarts and
recovery.
- Pauses initiated via the CLI, dashboard, MCP tools, and mission stop
controls are recorded as explicit user actions.
  - Automatically paused tasks remain eligible for recovery.
  - Unpausing clears the durable manual-pause state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 00:04:28 -07:00
gsxdsm
3ff98aae56 U12 part 8: delete the lossy normalizeColumn + behaviour ratchet — and the definitive answer on the raw flag (2 reads left, both U2b's) (#2535)
## U12 part 8 — deletes the lossy `normalizeColumn`, and ratchets it
shut

Independent of the #2525 → #2528 → #2530 stack; touches only
`@fusion/core` exports.

This closes **one of the two `@deprecated (workflowColumns, U12)`
markers** the unit was named for.

### The hazard

`normalizeColumn` coerced an arbitrary value to a **legacy** column,
rewriting every workflow-defined custom id to `triage`. Silent data loss
for any project whose workflow declares a column outside the six
built-ins — and it sat one line away from `normalizeColumnId`, which
sanitises structurally and passes real ids through.

The dashboard picked the wrong one for its entire task-ingest path until
that was diagnosed; `useTasks.ts` and `routes-trait-rekey.test.ts` still
carry the notes from that fix. So this is not a hypothetical footgun —
it already fired once, on the surface where it mattered most.

Deleted rather than left deprecated because it has **zero callers
anywhere in the workspace**. It was pure exported hazard: a lossy
coercion next to its safe twin, waiting to be picked again.

### The ratchet is the point

`no-lossy-column-coercion-export.test.ts` bans the **behaviour, not the
identifier**: it walks every exported single-argument function whose
name mentions "column" and fails if one maps a valid custom id onto a
different legacy id. Re-adding `normalizeColumn` under any name trips
it.

Verified by actually reintroducing the function — **two of the three
cases fail, including the name-agnostic one**. That last detail is what
stops it being a guard that checks nothing.

Coverage stated plainly: deleting an unused export has no behaviour to
revert-check. The compile is the proof it had no callers; the ratchet is
the proof it cannot return.

---

## Answering the standing question: does anything still read the raw
`workflowColumns` flag?

**Yes. Exactly two sites, and both are U2b's.** I am not able to close
this out, and here is the complete list rather than a summary:

```
packages/core/src/store.ts:38,43                                  ← the definition
packages/core/src/task-store/moves.ts:9,363                       ← `useWorkflow`
packages/core/src/task-store/workflow-task-create-ops.ts:11,351   ← move-policy preflight
```

That is the whole list in production code. Everything else that greps is
a comment, a test that writes the flag deliberately to exercise the dead
path, or the unrelated `workflowColumns.*` i18n namespace for the
Columns editor panel.

**Why I have not deleted the settings key.** It cannot go while those
two read it — the key is what they read. And the two are not separable
from each other: `workflow-task-create-ops.ts:351` computes the
`movePolicyPreflight` that `moves.ts` consumes and validates, and
un-gating the preflight alone would start evaluating workflow move
policies (with their plugin-gate side effects) while the branch that
consumes the result stays off. That is a behaviour change with no
consumer, which is worse than either state.

**Status of the blocker.** U2b has not landed. `main` at `919f68f9b`
still has both reads; the program's merged history goes `#2466 → #2467 →
#2468 (characterisation only) → #2469 → #2479 → #2500 → #2512 → #2513`,
with no convergence PR. PR #2468 was Phase A2 **steps 1–2 only** — the
differential characterisation — and the convergence that deletes one of
the two move paths was never merged.

So the honest state of the unit: everything U12 owns is done except the
two reads that U2b owns, and the settings key that cannot be deleted
until they are gone. If you want me to take U2b itself, say so — I have
the inventory and the divergence list, and I would want the current U2b
worker stood down from `moves.ts` first.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:25 -07:00
gsxdsm
18d654a5ff capacity, part 3: delete the globalMaxConcurrent setting, API and UI (#2529)
Part 3 of the capacity simplification, and the half that removes the
**knob**. Enforcement (shared semaphore, runtime wiring) went in #2509;
this removes everything an operator or API client can still see, so
nothing is left readable-but-ignored.

## Deleted

Settings key + schema default · CentralCore’s
`getGlobalConcurrencyState` / `updateGlobalConcurrency` /
`acquireGlobalSlot` / `releaseGlobalSlot` and the `concurrency:changed`
event · the whole Global Concurrency block in `async-central-core` ·
`PUT /api/global-concurrency` · the Scheduling · Global settings section
· the footer and Command Center global sliders · the dead
`getGlobalConcurrencyLimit` reader whose only caller went in #2509.

## Kept, deliberately

**`GET /api/global-concurrency` survives as telemetry only** — live
`currentlyActive` / `projectsActive` from CentralCore’s side-effect-safe
source. “How busy is this machine?” is still a real question once the
cap that used to answer it is gone. It no longer reports
`globalMaxConcurrent`/`queuedCount`: those came from the deleted cap and
from slot bookkeeping production code never incremented, so publishing
them was publishing zeros dressed as state.

**`useGlobalConcurrency` becomes read-only.** Everything that existed to
*persist* went with the cap — the 500 ms debounce, the save-state
machine, the commit-on-close/unmount flush, the slider clamp, the
`interactive` gate. The module-level shared store is **kept**: its
original justification (two mounted consumers drift apart with private
copies) holds for a polled read exactly as it did for a cap, and one
fetch now serves both.

The live “N running (all projects)” readout survives in both surfaces,
moved onto the per-project row.

## Two sections become one

Scheduling · Global existed to host exactly one control. With it deleted
the section renders an empty pane, so the Global/Project pair merges
back into **“Scheduling”**. An empty nav entry is a promise of settings
that are not there.

## One real fix found on the way

`SchedulingSection`’s `concurrencyLoading` gated the **project**
concurrency inputs on the **global**-concurrency fetch — never the right
source, since `maxConcurrent` and `maxWorktrees` come from the settings
form. It is repointed at the form’s own load, preserving the invariant
it existed for: a concurrency input stays disabled until its live value
arrives, so an operator cannot overwrite a resolved limit with a blank
fallback.

## Migration

A stored `globalMaxConcurrent` is **ignored** — it is a project-blob key
nothing reads, so dropping it needs no schema change. The
`central.global_concurrency` **table** is dropped in a follow-up; this
slice stops seeding and reading it first, so that drop has no live
writer to race.

## Verification, and how the wider suite was controlled

`pnpm lint` clean · core/engine/dashboard `tsc` clean · `pnpm test:gate`
green (309 + 10 + 71) · dashboard settings/footer/command-center/hooks
**2237/2237** · core `central-core-backend` 9/9.

The broader dashboard suite shows failures, and I checked rather than
assumed: running the suspect files on **clean main** reproduces
`api-git` (49), `TaskDetailModal.rendering` (28) and `settings-mobile`
(17) identically. Two were genuinely mine —
`SettingsModal.scheduling-merge` (0 on main, 17 on this branch: my nav
rename) and one `settings-mobile` picker case asserting `scheduling` is
a scoped pair — and both are fixed.

Tests for deleted behaviour are removed with it (footer
confirm/cancel/flush/dedupe, global marker geometry, the hook’s PUT
case, the CentralCore slot cases), each carrying a note on what it
guarded and where the surviving **project-side** equivalent lives.
Fixture-only references were updated, not deleted.

Nothing booted.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:13 -07:00
gsxdsm
7003dc9803 U12 part 6: Board re-rendered every column on every state change — one inline arrow, measured with a memo-comparator probe (#2528)
## U12 part 6 — Board re-rendered every column on every state change

**Stacks on #2525.** Merge that first.

`canDropTask` was allocated as a fresh inline arrow, per column, per
render:

```tsx
canDropTask={(taskId) => canDropTask(taskId, columnDef.id, selectedWorkflow.id)}
```

`Column` is `React.memo`, and a new function identity on any prop
defeats that entirely. So **any** Board state change — collapsing
Archived, changing Done sort, opening the workflow switcher —
re-rendered every column and every card beneath it, not just the
affected one.

Bound through a `useMemo` cache keyed by lane + column. After the fix,
collapsing Archived re-renders exactly one column: `archived`.

### Measured, not guessed

I instrumented `React.memo`'s comparator to print which props actually
change identity on a collapse toggle. For every unaffected column the
answer was exactly one:

```
PROBE todo         changed: canDropTask
PROBE in-progress  changed: canDropTask
PROBE in-review    changed: canDropTask
PROBE done         changed: canDropTask
PROBE archived     changed: canDropTask,collapsed     <- the one that should re-render
```

After:

```
PROBE archived     changed: collapsed
```

### Why this hid, and why my first attempt failed

Two things worth recording, because both were mistakes I made in this
program:

**The test was pointed at dead code.** "keeps unaffected columns stable"
measured the **legacy single-lane board**, whose props were all stable —
so it passed for a long time while covering nothing operators use.
Deleting that board in part 1 repointed it at the real board, where it
failed 3-vs-2. I skipped it then rather than weaken it to the observed
number, and said it needed its own investigation. This is that
investigation.

**My first fix was wrong and I was right to revert it.** In part 1 I
tried a `useRef` cache invalidated by `useEffect`, it did not fix the
test, and I reverted it as unproven rather than ship it. The reason is
now clear: the effect runs *after* the render that populated the cache,
so it wipes the very bindings that render created and the next render
allocates fresh ones — the invalidation defeated the cache. `useMemo`
keyed on the resolver has no such window; the map lives exactly as long
as the closure owning it.

### Revert-proof

The test is un-skipped **with the fix, not with a new expected number**.
Restore the inline arrow at either call site and it fails 3-vs-2 again.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, dashboard typecheck
green. Board, Board.canDropTask, workflow-resolved-columns and
board-no-legacy-flash: 132 passed, 0 failed, **0 skipped** — the skip
introduced in part 1 is gone.


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

- **Bug Fixes**
- Move menus now show exactly the destinations permitted by each custom
workflow, including non-adjacent moves.
- Invalid or hidden destination columns are excluded from move options.
  - Older workflow data continues to use a compatible fallback behavior.

- **Performance**
- Improved board responsiveness by preventing unaffected columns and
cards from re-rendering when archived sections collapse or Done sorting
changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:07 -07:00
gsxdsm
da0351857e U12 part 5: put real workflow adjacency on the wire — custom-workflow move menus were guessing (measured), and the VALID_TRANSITIONS shortcut is gone (#2525)
## U12 part 5 — the move menu was guessing; now it asks the graph

**Stacks on #2521** (same file). Merge that first.

The context menu had **no adjacency data at all**, so it did two wrong
things at once: it approximated move targets from a column's
**neighbours in declared order**, and — because that approximation is
strictly weaker than the real graph — it kept a `VALID_TRANSITIONS`
shortcut for any workflow whose column-id set matched the six built-ins.

Measured, the approximation loses real operator moves:

| current | workflow graph | neighbour approximation |
|---|---|---|
| `in-progress` | in-review, todo, triage, done | todo, in-review |
| `todo` | in-progress, triage, archived | triage, in-progress |
| `done` | todo, triage, archived | in-review, archived |

So **every custom workflow has been offering a guess**: menu entries the
store would reject, and legal moves it never offered. The built-ins were
fine only because the shortcut bypassed the guess entirely.

### The fix

`BoardWorkflowColumn` gains `moveTargets`, resolved by
`resolveAllowedColumns` — *the same resolver `moveTaskInternal`
validates against*. The menu now offers exactly what the store will
accept, for any workflow. Threaded through all four metadata builders
(Board, Lane, ListView, TaskDetailModal).

Optional on the wire, deliberately: a client older than this field keeps
the neighbour fallback rather than losing its move menu mid-upgrade.

### Why deleting the legacy shortcut is safe

Not an assertion — a measurement, then a pin.
`resolveAllowedColumns(BUILTIN_CODING_WORKFLOW_IR, c)` is **identical to
`VALID_TRANSITIONS[c]` for all six columns, order included**:

```
triage       ["todo","archived"]                     == VALID  SAME
todo         ["in-progress","triage","archived"]     == VALID  SAME
in-progress  ["in-review","todo","triage","done"]    == VALID  SAME
in-review    ["done","in-progress","todo","triage"]  == VALID  SAME
done         ["todo","triage","archived"]            == VALID  SAME
archived     ["done"]                                == VALID  SAME
```

`builtin-adjacency-matches-legacy-transitions.test.ts` pins it so the
equivalence cannot drift silently — if the built-in workflow's edges
change without `VALID_TRANSITIONS` following, default menus change shape
and that test fails first. It compares **order** too, since the menu
renders targets in the order it receives them, so a reorder is
operator-visible.

Default-workflow menus are therefore byte-identical. Custom ones stop
guessing.

### What's left of the legacy vocabulary here

`COLUMNS` is gone from `TaskContextMenu` — deleting the shortcut removed
its last use. `VALID_TRANSITIONS` survives for exactly one thing: the
**no-metadata load window**, documented at the site. I measured removing
that in #2521 and it left Task Detail with no move options during load,
which is a regression rather than a cleanup. It retires when the load
window does.

### Revert-proof, two ways

- Drop the `declaredTargets` branch → the custom-workflow case fails:
the neighbour fallback returns `["backlog","building"]`, missing the
legal `shipped` jump **and** offering `backlog`, which that graph
forbids. That is exactly the defect class shipped to every custom
workflow today.
- A second case pins that an adjacency edge into a column the board
cannot show is **dropped**, not rendered as a dead menu entry.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast`, core
+ dashboard typechecks green.

**No new test failures**: five suites report 31 failures with and
without the change — an identical, pre-existing set, verified by diffing
failing test *names* against a stashed clean tree, not by comparing
counts.


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

* **Bug Fixes**
* Move menus for custom workflows now show only the destinations
permitted by that workflow.
* Task-specific workflow rules are applied consistently across boards,
lists, lanes, and task details.
  * Invalid or unavailable destinations are excluded from move options.
* Existing clients remain supported when workflow destination data is
unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:00 -07:00
gsxdsm
ebc89310bc U12 part 4: derive the move menu's "Back to" label from workflow traits (plus two legacy reads I did NOT delete, with measurements) (#2521)
## U12 part 4 — the move menu's "Back to" label followed hardcoded
column ids

`getTaskMoveTransitions` is shared by Board cards, List rows and Task
Detail. It labelled a backwards move with:

```ts
column === "in-progress" && task.column === "in-review"
  ? t("taskDetail.move.backToInProgress", "Back to In Progress")
```

Two hardcoded lifecycle ids **and** a hardcoded English column name. On
a workflow that renames those lanes the condition never matched, so the
affordance silently vanished — and had it matched, it would have
announced "In Progress", a column absent from that board. Same
legacy-vocabulary class U10 removed from Board and U12 removed from
ListView, surviving in the context menu all three surfaces render.

Now keyed on the traits it was approximating: the **current** column
carries `mergeBlocker`, the **target** carries `countsTowardWip`, and
the label interpolates the column's own name through a new
`taskDetail.move.backTo` key (added to all six locales).

### Scope I deliberately held back

**The set of moves labelled "Back to" is unchanged.** For
`builtin:coding` the traits resolve to exactly `in-review` and
`in-progress`.

I first generalised this to "any target earlier in the workflow's
declared order" — arguably nicer, and I had it working. Then I measured
it: it relabels moves this change never set out to touch. **18 assertion
sites across three suites** flip from "Move to" to "Back to" (e.g. a
card in In progress gets "Back to Todo", "Back to Planning").
Same-set-different-derivation is the honest scope here; widening which
moves read as backwards is a separate, visible product decision, not a
side effect of a vocabulary fix.

### Two things I chose not to delete, and why

Both are still-live `VALID_TRANSITIONS` reads in this file. Neither is
removable today, and the reason is the same missing wire field —
documented at both sites rather than left as a puzzle.

**1. The default-column-set shortcut.** `TaskContextMenuColumnMetadata`
carries id/label/flags but **no adjacency**, so the workflow branch can
only guess targets from a column's neighbours in declared order.
Measured against the real graph that is a strict loss:

| current | `VALID_TRANSITIONS` | neighbour-derived |
|---|---|---|
| `in-progress` | in-review, todo, triage, done (4) | todo, in-review
(2) |
| `todo` | in-progress, triage, archived (3) | triage, in-progress (2) |
| `done` | todo, triage, archived (3) | in-review, archived (2) |

Deleting that read is not a cleanup — it drops real operator moves
(archive from Todo, straight-to-Done from In progress). Note the guard
keys on the column **id set**, so a workflow that merely renames the six
built-ins still takes this path and still gets correct targets; only
reordering or replacing them falls through to the weaker logic.

**2. The no-metadata fallback.** I removed it first, on principle, and
measured the result: `workflowMoveColumns` is optional at both call
sites (`workflowMoveMetadata?.moveColumns`, `taskMoveColumns`) and
genuinely undefined until board-workflows resolves, so dropping it left
Task Detail with **no move options during load**. That is a live surface
degraded to satisfy a purity rule, so it is not shipped. Unlike Board
and ListView — where the legacy path was provably unreachable — this one
is reachable and useful.

Both retire the same way: put each column's allowed targets on the
board-workflows payload so the load window has real data instead of a
guess. That is a server + wire + client change and belongs in its own
slice.

### Revert-proof

The renamed-workflow fixture declares `signoff` (mergeBlocker) and
`building` (countsTowardWip). Restore the id literals and the new case
fails — `Move to Building` instead of `Back to Building` — which no
relabelling of the old hardcoded string could satisfy, since that string
names a column absent from the board. The same case asserts the forward
move keeps "Move to Shipped", so the rule stays a distinction rather
than a blanket relabel.

### Verification

`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast`,
dashboard typecheck green.

**No new test failures**, established properly: the three suites this
touches report 30 failures both with and without the change, and I
diffed the failing test *names* against a stashed clean tree rather than
comparing counts — the sets are identical. (An earlier count-only
comparison had me chasing two failures that turned out to be my own new
assertions.)

Also regenerates `packages/i18n/src/resources.d.ts` via `pnpm
i18n:types`. That picks up **~45 lines of pre-existing drift** from
earlier merges that did not regenerate it; the file is generated, and
leaving it stale would omit the new key from the types. Flagged so the
extra lines are not mistaken for scope creep. Note
`packages/dashboard/app/locales/` is gitignored (copied from
`packages/i18n/locales/`), so only the canonical locales are committed.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:19:42 -07:00
gsxdsm
3badc244a7 U12 part 2: bind the three U5 reconciliation guards — USER-VISIBLE (and one path that couldn't run under PostgreSQL at all) (#2512)
## U12 part 2 — the three U5 reconciliation guards now actually fire

USER-VISIBLE. Taken on standing authority; here is exactly what changed
for operators.

All three read the RAW `experimentalFeatures.workflowColumns` key via
`store.workflowColumnsFlagOn()`. Nothing in production writes it, so all
three have been inert since the workflow-columns cutover.

| Guard | Before (every real project) | After |
|---|---|---|
| Workflow edit removing an **occupied** column | Save succeeded; cards
left in a column the workflow no longer declares | Save fails with
`OccupiedColumnsError` unless `rehomeTo` is supplied |
| Workflow **delete** | Occupant capture returned `[]`; cards sat in the
deleted workflow's columns until the next engine start | Cards move to
the default workflow's entry column as part of the delete |
| Workflow **switch** | Never reconciled; the `reconciliation` field in
the declared return type was never populated | Card in an undeclared
column moves to the resolved target; a declared column is preserved |

Both consumers already handle the new outcomes and needed no change:
`register-workflow-routes.ts` maps `OccupiedColumnsError` to a
structured 409 carrying per-column occupant counts, and
`fn_workflow_update` returns a retryable structured result. The
dashboard editor's `rehomeTo` retry flow becomes reachable for the first
time. I only updated two stale "flag-ON" comments there — that code was
correct all along and simply never fired.

### What an operator actually sees (USER-VISIBLE — read this bit)

Four changes to what the board and the API do. Nothing here is silent.

1. **Editing a workflow to remove a column that has cards in it now
FAILS.** Previously the save succeeded and the cards were left in a
column their workflow no longer declared. The dashboard shows the
existing 409 with per-column occupant counts and prompts for a re-home
target; retrying with `rehomeTo` moves the cards and saves. Removing an
EMPTY column is unaffected.
2. **Deleting a workflow moves its cards immediately** to the default
workflow's entry column, instead of leaving them until the next engine
start.
3. **Switching a task's workflow moves the card** when the new workflow
does not declare its current column. A card whose column IS declared
stays exactly where it is. The API response now carries the
`reconciliation` summary it always promised.
4. **A switch whose re-home would be REJECTED is now refused before
anything is written.** If the destination column is at its WIP limit,
the switch fails with a structured 409 (`workflow-switch-rehome-failed`)
naming the task, both columns and the reason — and **nothing changes**:
the task keeps its current workflow AND its current column. Retry after
making room. Previously this combination committed the selection and
then silently reported a move that never happened, leaving selection and
column disagreeing.

**Can a torn card still happen? Yes, in one narrow case, and here is how
you recover.** If the destination fills in the window between the
pre-flight and the move, the selection is already committed and the card
ends up in a column its new workflow does not declare. That case is not
silent: it writes a `task:workflow-switch-torn` run-audit row, and the
error carries `selectionCommitted: true` with both columns. Recovery:
make room in the destination and move the card there, or switch the task
back — and if neither happens, the R7 startup sweep
`reconcileUndeclaredTaskColumns` re-homes it on the next engine start.
The card is never lost; it is visible in a lane the board may not draw
until one of those runs.

The one thing to watch after merge: (1) converts a previously-silent
success into a visible failure, so an operator mid-edit on a busy
workflow will start seeing a 409 they never saw before. That is the
point — the alternative was stranding their cards — but it is the change
most likely to generate a "this used to work" report.

### The thing that made this more than a gate removal

Un-gating the switch guard surfaced that
`selectTaskWorkflowAndReconcileImpl` read the task through
`store.readTaskFromDb` — the **synchronous SQLite** reader, which throws
under PostgreSQL:

```
TaskStore.db: SQLite Database is not available in backend mode
```

The flag returned before that line, so the gate was hiding a path that
**could not execute at all in the production backend**, not merely a
disabled feature. Ported to the async `readTaskRow`. Found by the new
tests, not by reading the code.

### Review round 2 (both findings real, both fixed)

**Torn write with no alarm — fixed by ORDERING, not by a louder
message.** My first attempt only made the error loud, which left the
torn state intact. The real fix is that the deterministic rejection
cause (destination at its WIP limit) is now checked BEFORE
`selectTaskWorkflow` commits, by resolving the target IR straight from
`workflowId` instead of through the task's selection. Nothing commits on
that path.

For the residual race the failure is loud AND recorded: `rehomeOccupant`
now returns `{ moved, error? }` (additive; sweep callers ignore it), the
switch writes a `task:workflow-switch-torn` run-audit row, and throws
`WorkflowSwitchRehomeFailedError` with `committed: true`. Consumers
translate it: the dashboard route returns a structured 409 with
`selectionCommitted`, and `fn_task_set_workflow` returns the same fields
— no more generic "something went wrong".

**Fabricated column for a deleted task.** My first fix fell back to
`fromColumn` when the final read found no row, so a task soft-deleted
mid-switch was reported as having its old column *preserved*. Absent now
reads as absent (the optional `reconciliation` is omitted). Extracted as
the pure `buildSwitchReconciliation` seam because the window is not
reachable through the public call — `selectTaskWorkflow` rejects an
already-deleted task up front — so it is a genuine race, and I test the
decision directly rather than asserting it from reading the code.

### Revert-proof, measured

New `workflow-reconciliation-production-shape.pg.test.ts` — 6 cases,
with the flag **never written**, which is the configuration every real
project has. Each flip reverted individually:

- re-gate the edit guard → **2 failures** (OccupiedColumnsError case;
rehomeTo re-home case)
- re-gate the delete capture → **1 failure** (card stays in
`custom-hold`)
- restore the switch early return → **2 failures** (`reconciliation`
undefined; card does not move)
- all three in place → **6/6 green**

Round-2 fixes, also measured:
- restore the `fromColumn` fallback → the "row is gone" case fails
(reports `preserved: true` for a deleted task)
- drop the `!outcome.moved` throw → the capacity-blocked case fails
(resolves instead of raising)
- **move the capacity pre-flight back AFTER the commit → the case fails
on the SELECTION assertion** (expected `WF-002`, received `WF-001`),
i.e. it proves the ordering, not the wording

The pre-existing coverage in `workflow-authoritative-reads.pg.test.ts`
reached the occupied-column guard by **writing the flag ON itself** —
same pattern as the ListView/Board suites in part 1. Its flag write is
removed; it now runs in the production shape.

### Where I nearly got this wrong

My first revert harness was buggy and I briefly concluded the delete
re-home was **redundant** — I had probed the stored column and seen
`triage` with what I thought was the flip reverted. It wasn't.
`workflow-ops.ts` contains two identical `const occupantTaskIds = await
store.listWorkflowOccupantTaskIds(id, false)` lines (field-reconcile
block, delete path), so my first-match edit reverted the wrong one.
Re-run anchored on surrounding context, the delete case fails as
predicted. Recorded in the test header as a caution. I also chased and
**refuted** a scarier hypothesis along the way — that an unrelated
`updateTask` coerces a custom column back to `triage`. It does not; the
column survives.

### Deliberately NOT in this PR

The v1-IR rollback-compat persistence (`downgradeIrToV1IfPure`) on the
workflow UPDATE path. It shared the same `flagOn` variable, which is how
it surfaced: **one flag read was feeding two unrelated decisions, so the
flag has more decision sites than call sites** — my earlier 9-site
inventory undercounted. It chooses the stored *shape* of the graph
rather than gating a guard, so it is a persistence-format change with a
different blast radius. It now reads the flag explicitly, behaviour
unchanged, for a follow-up.

The `moves.ts` group remains U2b's.

### Verification

`pnpm test:gate` (307 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17
steps), both typechecks green. Full `packages/core` PostgreSQL suite:
**1042 passed, 3 failed** — `central-archive-secrets.test.ts`
(log-prefix assertion) and
`workflow-settings-project-identity.pg.test.ts` (×2, project-id
resolution). I confirmed the identical 3 failures on a stashed clean
tree: pre-existing, unrelated. No Fusion instance booted.


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

## Summary by CodeRabbit

* **Bug Fixes**
* Workflow edits now prevent removal of occupied columns unless cards
are moved to a specified destination.
* Cards are automatically re-homed when workflows are deleted or
switched.
* Workflow switches now check destination capacity before committing and
provide clear conflict details when re-homing fails.
* Reconciliation results now indicate whether cards were moved or
preserved.


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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:52:46 -07:00
gsxdsm
743df98aa4 capacity, part 1: merge pinned at 1, worktrees-off mode, and one dead knob deleted (#2502)
First slice of the capacity simplification. Operator: *"just have two
capacity — overall per project agent count and max worktrees. Remove all
other capacities and counts."* Plus two later additions: **merge is
always 1, fixed**, and **worktrees off ⇒ limit by total agents only**.

Three independently revertable commits. No limiter is added anywhere;
one is deleted, one is made structurally absent, and one is pinned.

---

## 1. Merge concurrency ratcheted at 1 (test-only)

I was asked to add a limiter if merge concurrency could be raised. **It
cannot** — there is no setting, workflow property, pool or trait config
anywhere that raises it, so this adds no code and pins what already
holds.

Serialization lives in the **pump**: `drainMergeQueue`’s `mergeRunning`
re-entrancy latch, `activeMergeTaskId` as a single-slot identity, the
`mergeBodyInFlight` next-generation latch, and one `ProjectEngine` per
projectId.

**Not** in the merge-queue lease, which is a per-task ROW (`primaryKey
[projectId, taskId]`) — two tasks can hold leases simultaneously by
construction, and it has exactly one caller (the worktree-reuse
handoff). Ordinary merges never take it. A lease-level test would have
been describing an invariant that layer has never held.

The second half guards the other direction: a merge-concurrency
*setting* would not fail the pump ratchet — it would sit unread until
someone wired it up.

**Revert-proof:** deleting the latch → `expected 1 times, but got 2
times`; deleting the `finally` → latch-stuck; injecting
`maxConcurrentMerges: 2` → fails naming the key; injecting a
`maxParallelLanes` merge-trait field → fails naming the field. Sources
restored byte-identical after each injection.

## 2. `worktreesEnabled` — off means the worktree limit cannot bind

No worktrees-off mode existed (no
`worktreesEnabled`/`useWorktrees`/`worktreeMode` anywhere — only
worktree *configuration*).

**Why not `maxWorktrees: 0`, which needs no new key:** it deadlocks. `??
4` keeps `0` (not nullish), the gate is `used >= limit`, so `0 >= 0`
holds **on an empty board** and nothing ever dispatches — while the
operator-visible reason reads `gate=maxWorktrees; used=0/0`, a limiter
that looks like it is working while the board is dead. It also needs the
Command Center `{min:1}` clamp relaxed. So `0` costs the gate rewrite
*and* the clamp change *and* encodes a mode as a magic value.

**Off is absence, not a big number.** `resolveWorktreeCapacityLimit`
returns `number | null`; `ConcurrencyGateDiagnostic.maxWorktreesGate` is
now optional, so consulting a worktree limit in OFF mode does not
type-check. A gate holding `Infinity` can start binding again the moment
someone "fixes" a comparison; an absent gate cannot.

That paid for itself immediately: making it nullable surfaced a
**second, independent** worktree gate (`activeWorktrees >= maxWorktrees`
early-return) that a skip-by-convention approach would have missed
silently.

**Scope, deliberately:** this is a statement about *counting*, not
isolation. It does not make concurrent agents safe to share one checkout
and builds nothing toward that — the non-worktree paths that exist today
are fallbacks to the operator’s own tree, one of which caused FN-8600.

**Revert-proof:** a resolver ignoring the flag turns both OFF scheduler
tests red while every ON test stays green — they reuse the *same*
fixture (5 in-progress, limit 4) that pre-existing tests prove blocks,
so the pair moves in opposite directions. Removing `disabled:` reddens
the UI test.

## 3. `maxTriageConcurrent` deleted — it controlled nothing

**Measured: zero enforcement reads.** The only `.maxTriageConcurrent`
reference in the repo was a route echoing it back in `/config`. FN-8453
removed the pool it gated and left the knob shipping in
`DEFAULT_SETTINGS`, the settings type, the section registry, the API
response and six i18n catalogs, doing nothing, for releases.

Historical FNXC comments are **updated, not deleted** — they explain a
real past incident; they now say "planning admission slot" so they stop
implying a live setting. Tombstoned so it cannot return.

`/config` loses a field; safe in-repo since `fetchConfig`’s own return
type never declared it.

---

## Two corrections worth recording

- I earlier reported `maxWorktrees` had **no** Settings UI. Wrong —
`WorktreesSection.tsx:47`; my grep was truncated by `head`. It changed
the placement (toggle beside it, rather than a duplicate key in
Scheduling).
- I planned to assert the queued-reason string is rewritten in OFF mode.
Measured that it is **unreachable**: when `maxConcurrent` binds, the
sweep bails before the per-task reason and logs nothing. The test
asserts absence instead.

Two near-misses caught before commit: a pre-existing FN-7505 guard
caught my *new* key missing a description mapping; and editing i18n via
`json.load/dump` silently dropped unrelated duplicate keys
(`autoUpdateAndRestart` in `fr`) — Python keeps only the last of a
duplicated key. Redone textually, every catalog re-validated.

## Verification

`pnpm lint` clean · core/engine/dashboard/i18n typecheck clean · `pnpm
test:gate` green (309 + 10 + 71) · capacity/worktree suites 11/11 ·
engine merge-invariant + scheduler 45/45 · dashboard settings 114/114.
Rebased onto current main and re-verified.

Nothing was booted at any point.

🤖 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 a project setting to enable or disable running tasks in
worktrees.
* Disabling worktrees removes worktree capacity limits from task
scheduling.
* The “Max Worktrees” setting is disabled when worktree execution is
turned off.

* **Changes**
* Removed the unused triage concurrency setting from configuration and
dashboard responses.
* Updated scheduling diagnostics and queue messages to reflect disabled
worktree capacity limits.
  * Added localized labels and help text for the new setting.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:52:31 -07:00
gsxdsm
fd6d005333 U12 part 1: delete the legacy board path (262 ListView + 39 Board tests were measuring it; 9-site flag inventory, moves.ts group blocked on U2b) (#2500)
## U12, part 1 of 2 — and one blocker you need to route

The unit's headline deletion
(`isWorkflowColumnsCompatibilityFlagEnabled`) is **blocked by U2b** and
is not in this PR. What is here is everything that could be deleted
without making a convergence decision that belongs to another unit.

### The blocker

PR #2468 landed as `b941d3cba` — but that was **Phase A2 steps 1–2 only:
the differential characterization**. The convergence (pick a path,
delete the other, delete the flag) has not landed;
`feature/workflow-move-path-convergence` is still live.

Deleting the raw flag **is** that convergence.
`move-path-equivalence.pg.test.ts` says so in its own header, and its
second `describe` is literally *"the flag gates MORE than side
effects"*. The plan makes this a blocking unit with an equivalence
*proof obligation* and an explicit "stop and escalate rather than
reconcile silently" note. So I stopped.

### Inventory: every read of the raw flag, with a verdict

Nine sites. All false in production because nothing writes
`experimentalFeatures.workflowColumns`.

**Blocked on U2b — one branch, not separable:**

| Site | Silently disabled today | Visible if flipped |
|---|---|---|
| `moves.ts:312` `useWorkflow` | typed `TransitionRejectionError`,
workflow adjacency, the shared transition invariants (merge-blocker
*trait* generalization), plugin column gates, the `transitionPending`
marker, `workflowId` in `task:move` run-audit, and the trait-hook
side-effect path | Yes — rejections change **type and message** |
| `moves.ts:931` | the in-transaction capacity gate.
`resolveColumnCapacity` never runs | Yes — WIP limits begin binding |
| `workflow-task-create-ops.ts:351` |
`prepareWorkflowMovePolicyPreflight` returns `undefined` unconditionally
→ **workflow/plugin move policies have never been evaluated** | Yes —
new rejections |

On #2488: the pool-id sentinel fix is correct *and* still inert. Two
dead layers stacked — the gate it fixed is inside `if (useWorkflow &&
…)`.

**Not blocked, but each moves operators' cards — deferred to PR 2 per
your call:**

| Site | Silently disabled today |
|---|---|
| `workflow-ops.ts:183` | `OccupiedColumnsError` + `rehomeTo` when a
workflow edit removes an **occupied** column. Today the save succeeds
and strands the cards |
| `workflow-ops.ts:344` | occupant re-home on workflow **delete** |
| `workflow-definitions.ts:700` | workflow-**switch** reconciliation,
and the `reconciliation` field in the API response |

I verified these three are **not** coupled to `moves.ts`:
`rehomeOccupant` reaches a custom target via the
`isWorkflowDeclaredRecoveryRehome` carve-out (`moves.ts:641`), which
exists because the repair "silently no-oped on every store open" before
it.

**Not blocked, no behaviour change for current binaries** (also PR 2):
`project-store-ops.ts:687` + `lifecycle-ops.ts:1119` —
`downgradeIrToV1IfPure` on persist, for *binary-downgrade* rollback.
Needs a round-trip test, not an assumption.

### What this PR deletes

**Dashboard.** `workflowColumnsEnabled` was a literal `true` at all
three `MainContent` call sites; the server hardcodes `flagEnabled:
true`. Gone: Board's legacy single-lane board (55 lines mapping the
hardcoded `COLUMNS` enum — the last board surface deriving columns from
the legacy vocabulary, an R8 violation that survived U10);
`tasksByColumn` and its cache ref, orphaned with it; ListView's
`LEGACY_LIST_COLUMNS` (the ListView copy of the synthesized-trait-flags
defect U10 fixed in Board); both props; the `shouldHydrateCache` gate;
TaskDetailModal's `flagEnabled` early return. **Neither Board nor
ListView imports the legacy column enum any more.**

**Core.** `evacuateCustomColumnsToLegacy` (#1409) — both triggers
require the previous settings to have the flag ON, which no writer
produces. `runWorkflowColumnsIntegrityPass` — no caller anywhere,
superseded by `reconcileUndeclaredTaskColumns` (registered in startup
recovery), and it read through the sync SQLite handle, so invoking it
under PostgreSQL would have thrown rather than reconciled.

**Migration answer:** a project with `workflowColumns: false` persisted
needs no migration and no read-time drop. Nothing in this PR reads the
key, and it stays in `HIDDEN_EXPERIMENTAL_FEATURE_KEYS` so Settings
still suppresses it rather than resurrecting it as an unknown setting.
Proven by tests, no instance booted.

**`flagEnabled` stays on the wire** as a constant. Removing it changes
the response shape, and a browser tab outliving a server upgrade would
read the missing field as "off" and degrade. One boolean, no client
branches on it, droppable a release later.

### Measured

- Production sources: **-332 / +131** (net **-201**). Additions are
almost entirely FNXC comments recording why each branch was unreachable.
- Dashboard production only: -168 / +93.
- Core: -164 / +38.

### The finding I'd actually flag

`Board.test.tsx` and `ListView.test.tsx` both left
`workflowColumnsEnabled` unset and stubbed `fetchBoardWorkflows` with a
**never-resolving promise**. Under the old gate that rendered the
**legacy** board — so **262 ListView tests and 39 Board tests were
asserting against a configuration production never reached**, and a real
regression in the workflow board or list would not have failed either
file. Same shape as the other four: looked enforced, wasn't.

Both now seed the first-paint lane cache with the default workflow's
**real** columns (ids and names copied from
`BUILTIN_CODING_WORKFLOW_IR`) — the same seam production uses.
Repointing them surfaced assertions that encoded legacy-only values:
`"In Progress"`/`"In Review"` (real IR names are `"In progress"`/`"In
review"`), and Planning Mode asserted to receive `null` as the workflow
id, which is only what `getTaskPlanningWorkflowId` returns when
`workflowMode` is false.

`"Back to In Progress"` is **not** one of those — it is a hardcoded i18n
string in `TaskContextMenu:210`, not derived from the column name. Left
alone, and flagged: it will not follow a renamed column. That's U11
vocabulary territory.

**One test is SKIPPED, not weakened** — "keeps unaffected columns stable
when archived collapse toggles". Pointed at the real board the invariant
is **false**: toggling the archived column re-renders unaffected columns
(measured: todo renders 3×, not 2×). Pre-existing production behaviour
this deletion exposed, never covered because the test measured the dead
path. I ruled out the obvious causes (every callback prop is
`useCallback`; the per-column task memo's deps exclude
`archivedCollapsed`; memoizing the inline `canDropTask` binding did
**not** close it — I wrote that fix, could not prove it with a failing
test, and **reverted it**). The reason is recorded at the test: un-skip
with a fix, never with a new expected number.

### Verification

`pnpm test:gate` (299 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17
steps), and both package typechecks green. `settings-defaults.test.ts >
warns once per process for legacy cwd-main mode` fails —
**pre-existing**, confirmed by stashing my changes and re-running. No
Fusion instance was booted.

### Routing request

Per your call: the `moves.ts` group and the final removal of
`isWorkflowColumnsCompatibilityFlagEnabled` go to **U2b**, inside the
convergence PR where the equivalence proof already lives. The
divergences their characterization suite does **not** yet cover: plugin
column gates, the `transitionPending` marker, `workflowId` in
`task:move` run-audit, and move-policy preflight.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:45:40 -07:00
gsxdsm
a271f1868f U10: dashboard renders workflow-resolved columns (6 legacy-vocabulary defects, incl. a silently-disabled open-PR guard) (#2492)
Phase D / **U10** of the workflow-owned-lifecycle program (**R8**). This
unit **blocks U11** (merge Todo into Planning) — the board must render
IR-resolved columns before the column shape can change.

## What was wrong

Six dashboard surfaces answered a column question from the legacy
`COLUMNS` / `VALID_TRANSITIONS` vocabulary rather than the card's own
workflow IR. Each is a defect today, and each is a way U11 would ship
visibly broken.

| # | Surface | Defect |
|---|---|---|
| 1 | Board — All workflows | Appended **every** legacy column id to the
lane union with synthesised flags → a phantom lane for a column no
workflow declares, labelled with the raw id, ordered by the enum index
with an alphabetical tie-break that scrambled a custom workflow's
declared order |
| 2 | ListView | `if (groups[column])` **silently dropped** a row whose
stored column the workflow no longer declares — no lane, no row, no
error |
| 3 | Move menu | A card stranded in an undeclared column got an **empty
move list** — the one surface that could rescue it offered nothing |
| 4 | Task Detail | Header badge rendered the raw stored id;
title/description editing gated on the literal `{triage, todo}` — a
renamed planning lane lost Edit with nothing on screen to explain it |
| 5 | `board-workflows` | The built-in lifecycle label map was an
**override**, not a fallback, so it replaced a name a built-in
deliberately chose |
| 6 | `POST /tasks/:id/move` | The open-PR backward guard used
`COLUMNS.indexOf(...)` → **-1 on any renamed board**, and the guard
treats a negative index as "allow" |

**#6 is the one worth reading twice.** The guard did not start rejecting
the wrong things — it stopped existing. On a renamed board an operator
could drag a card backward out of review with an open GitHub PR,
orphaning it, and nothing failed. This is precisely the "a converted
guard silently stops firing" row in the plan's risk table, reached
through a rename rather than a conversion.

The re-engage copy of that same guard is **deliberately left on the
legacy enum**, with a comment saying why: it is gated on literal
`in-review` / `in-progress` end to end, so converting only its indices
would make it *weaker* (a workflow declaring `in-review` but not
`in-progress` would score -1 and disable it). U5 owns that lane.

## Evidence

**Every fix has a test that fails when the fix is reverted.** With the
six production files stashed and the tests kept, **10 of the 28 tests
fail**:

- Board aggregate: phantom lane present (2)
- ListView: stranded card dropped, desktop **and** mobile (2)
- Move menu: empty move list for a stranded card (1)
- Task Detail: badge shows `staging`, Edit missing in a renamed intake
**and** hold lane (3)
- `board-workflows`: `builtin:lead-generation`'s `triage` renders as
"Planning" (1)
- Move route: backward move between renamed columns **allowed** with an
open PR (1)

The other 18 are regression pins on behaviour that must not change
(default-workflow lane order and labels, legacy `in-review →
in-progress` block, legacy editable columns, forward moves, terminal
PRs).

**Measured, not estimated.** The label-map clobber was quantified
against the built-in IRs actually in tree: **4 column names replaced — 3
case-only variants ("In progress" → "In Progress"), 1 genuine semantic
rename.** Only the rename is a user-visible defect; the fix preserves
the case normalisation rather than churning the default board.

## Surface enumeration (AGENTS.md)

Desktop **and** mobile — the breakpoint is `(max-width: 768px),
(max-height: 480px)`, so landscape phones exceed 768 wide and match on
height. Column states: empty, populated, duplicate id across two
workflows, and a column no workflow declares. Views: single-workflow
lane, All-workflows aggregate, list, move menu, task detail, move route.

## Regression check

Full dashboard suite, both sides of the change:

| | Test Files | Tests |
|---|---|---|
| Before | 41 failed / 1068 | **296 failed** / 21195 |
| After | 42 failed / 1071 | **297 failed** / 21223 |

`+28` total is exactly the tests this change adds. The single failure
delta is `register-model-routes-kimi-k3-supplemental`, which **fails
identically on this branch's base when run in isolation** — shard-order
dependent, unrelated to columns. **Zero regressions attributable to
U10.** The ~296 pre-existing dashboard failures are inherited from main
and are flagged to the coordinator, not touched here.

`pnpm test:gate`, `pnpm lint`, both dashboard typechecks
(`tsconfig.json` and `tsconfig.app.json`), `pnpm smoke:boot`, and `pnpm
check:changesets` are green.

## Not in this unit

- `Board`'s legacy single-lane `COLUMNS.map` fallback still exists.
`MainContent` passes `workflowColumnsEnabled` unconditionally, so it is
unreachable in-app, but proving that is a deletion argument and this is
not a deletion unit — flagging rather than removing.
- `ListView`'s `LEGACY_LIST_COLUMNS` fallback, same reasoning.
- `flagEnabled` on the wire (U2 noted U10 retires it once no client
reads it) — four clients still branch on it; retiring it is a
client-shape change that belongs with U11's shape work.
- `GET /api/tasks?column=` still validates against `COLUMNS`, rejecting
a workflow-declared custom column as a list filter. Server-side filter
surface, not a rendering decision.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:48:08 -07:00
dependabot[bot]
4819c26349 chore(deps-dev): bump jsdom from 27.4.0 to 29.1.1 (#2450)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 27.4.0 to 29.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jsdom/jsdom/releases">jsdom's
releases</a>.</em></p>
<blockquote>
<h2>v29.1.1</h2>
<ul>
<li>Fixed <code>'border-radius'</code> computed style serialization. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed computed style computation when using
<code>'background-origin'</code> and <code>'background-clip'</code> CSS
properties. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Significantly optimized initial calls to
<code>getComputedStyle()</code>, before the cache warms up. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
</ul>
<h2>v29.1.0</h2>
<ul>
<li>Added basic support for the ratio CSS type. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed <code>getComputedStyle()</code> sometimes returning outdated
results after CSS was modified. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
</ul>
<h2>v29.0.2</h2>
<ul>
<li>Significantly improved and sped up <code>getComputedStyle()</code>.
Computed value rules are now applied across a broader set of properties,
and include fixes related to inheritance, defaulting keywords, custom
properties, and color-related values such as <code>currentcolor</code>
and system colors. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed CSS <code>'background</code>' and <code>'border'</code>
shorthand parsing. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
</ul>
<h2>v29.0.1</h2>
<ul>
<li>Fixed CSS parsing of <code>'border'</code>,
<code>'background'</code>, and their sub-shorthands containing keywords
or <code>var()</code>. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed <code>getComputedStyle()</code> to return a more functional
<code>CSSStyleDeclaration</code> object, including indexed access
support, which regressed in v29.0.0.</li>
</ul>
<h2>v29.0.0</h2>
<p>Breaking changes:</p>
<ul>
<li>Node.js v22.13.0+ is now the minimum supported v22 version (was
v22.12.0+).</li>
</ul>
<p>Other changes:</p>
<ul>
<li>Overhauled the CSSOM implementation, replacing the <a
href="https://www.npmjs.com/package/@acemir/cssom"><code>@acemir/cssom</code></a>
and <a
href="https://github.com/jsdom/cssstyle"><code>cssstyle</code></a>
dependencies with fresh internal implementations built on webidl2js
wrappers and the <a
href="https://www.npmjs.com/package/css-tree"><code>css-tree</code></a>
parser. Serialization, parsing, and API behavior is improved in various
ways, especially around edge cases.</li>
<li>Added <code>CSSCounterStyleRule</code> and
<code>CSSNamespaceRule</code> to jsdom <code>Window</code>s.</li>
<li>Added <code>cssMediaRule.matches</code> and
<code>cssSupportsRule.matches</code> getters.</li>
<li>Added proper media query parsing in <code>MediaList</code>, using
<code>css-tree</code> instead of naive comma-splitting. Invalid queries
become <code>&quot;not all&quot;</code> per spec.</li>
<li>Added <code>cssKeyframeRule.keyText</code> getter/setter
validation.</li>
<li>Added <code>cssStyleRule.selectorText</code> setter validation:
invalid selectors are now rejected.</li>
<li>Added <code>styleSheet.ownerNode</code>,
<code>styleSheet.href</code>, and <code>styleSheet.title</code>.</li>
<li>Added bad port blocking per the <a
href="https://fetch.spec.whatwg.org/#bad-port">fetch specification</a>,
preventing fetches to commonly-abused ports.</li>
<li>Improved <code>Document</code> initialization performance by lazily
initializing the CSS selector engine, avoiding ~0.5 ms of overhead per
<code>Document</code>. (<a
href="https://github.com/thypon"><code>@​thypon</code></a>)</li>
<li>Fixed a memory leak when stylesheets were removed from the
document.</li>
<li>Fixed <code>CSSStyleDeclaration</code> modifications to properly
trigger custom element reactions.</li>
<li>Fixed nested <code>@media</code> rule parsing.</li>
<li>Fixed <code>CSSStyleSheet</code>'s &quot;disallow modification&quot;
flag not being checked in all mutation methods.</li>
<li>Fixed <code>XMLHttpRequest</code>'s <code>response</code> getter
returning parsed JSON during the <code>LOADING</code> state instead of
<code>null</code>.</li>
<li>Fixed <code>getComputedStyle()</code> crashing in XHTML documents
when stylesheets contained at-rules such as <code>@page</code> or
<code>@font-face</code>.</li>
<li>Fixed a potential hang in synchronous <code>XMLHttpRequest</code>
caused by a race condition with the worker thread's idle timeout.</li>
</ul>
<h2>v28.1.0</h2>
<ul>
<li>Added <code>blob.text()</code>, <code>blob.arrayBuffer()</code>, and
<code>blob.bytes()</code> methods.</li>
<li>Improved <code>getComputedStyle()</code> to account for CSS
specificity when multiple rules apply. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Improved synchronous <code>XMLHttpRequest</code> performance by
using a persistent worker thread, avoiding ~400ms of setup overhead on
every synchronous request after the first one.</li>
<li>Improved performance of <code>node.getRootNode()</code>,
<code>node.isConnected</code>, and <code>event.dispatchEvent()</code> by
caching the root node of document-connected trees.</li>
<li>Fixed <code>getComputedStyle()</code> to correctly handle
<code>!important</code> priority. (<a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a>)</li>
<li>Fixed <code>document.getElementById()</code> to return the first
element in tree order when multiple elements share the same ID.</li>
<li>Fixed <code>&lt;svg&gt;</code> elements to no longer incorrectly
proxy event handlers to the <code>Window</code>.</li>
<li>Fixed <code>FileReader</code> event timing and
<code>fileReader.result</code> state to more closely follow the
spec.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9b9ea7e10b"><code>9b9ea7e</code></a>
29.1.1</li>
<li><a
href="07efb7821c"><code>07efb78</code></a>
Optimize computed style comparison</li>
<li><a
href="5f66329902"><code>5f66329</code></a>
Fix background-origin/background-clip in background shorthand</li>
<li><a
href="ad8af77ecc"><code>ad8af77</code></a>
Fix border shorthand handling</li>
<li><a
href="5a3e88ea9b"><code>5a3e88e</code></a>
29.1.0</li>
<li><a
href="73db204172"><code>73db204</code></a>
Update dependencies and dev dependencies</li>
<li><a
href="a7168a579d"><code>a7168a5</code></a>
Support ratio CSS unit type</li>
<li><a
href="15346e055b"><code>15346e0</code></a>
Fix style cache invalidation</li>
<li><a
href="2a1e2cdb44"><code>2a1e2cd</code></a>
29.0.2</li>
<li><a
href="4097d66ba1"><code>4097d66</code></a>
Resolve computed CSS values lazily in CSSStyleDeclaration</li>
<li>Additional commits viewable in <a
href="https://github.com/jsdom/jsdom/compare/v27.4.0...v29.1.1">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for jsdom since your current version.</p>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:50:22 -07:00
dependabot[bot]
af3d78d2cd chore(deps-dev): bump vitest from 4.1.8 to 4.1.10 (#2446)
Bumps
[vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)
from 4.1.8 to 4.1.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitest-dev/vitest/releases">vitest's
releases</a>.</em></p>
<blockquote>
<h2>v4.1.10</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li><strong>browser</strong>: Check fs access in builtin commands
[backport to v4]  -  by <a
href="https://github.com/hi-ogawa"><code>@​hi-ogawa</code></a>,
<strong>Hiroshi Ogawa</strong> and <strong>OpenCode
(claude-opus-4-8)</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10680">vitest-dev/vitest#10680</a>
<a href="https://github.com/vitest-dev/vitest/commit/5c18dd267"><!-- raw
HTML omitted -->(5c18d)<!-- raw HTML omitted --></a></li>
<li><strong>vm</strong>: Fix external module resolve error with deps
optimizer query for encoded URI [backport to v4]  -  by <a
href="https://github.com/SveLil"><code>@​SveLil</code></a> and <a
href="https://github.com/hi-ogawa"><code>@​hi-ogawa</code></a> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10661">vitest-dev/vitest#10661</a>
<a href="https://github.com/vitest-dev/vitest/commit/bae52b511"><!-- raw
HTML omitted -->(bae52)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/vitest-dev/vitest/compare/v4.1.9...v4.1.10">View
changes on GitHub</a></h5>
<h2>v4.1.9</h2>
<h3>🐞 Bug Fixes</h3>
<ul>
<li>Fix <code>importOriginal</code> with optimizer and query import
[backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>David
Harris</strong>, <strong>Codex</strong>and <strong>Vladimir</strong> in
<a
href="https://redirect.github.com/vitest-dev/vitest/issues/10546">vitest-dev/vitest#10546</a>
<a href="https://github.com/vitest-dev/vitest/commit/a5180190c"><!-- raw
HTML omitted -->(a5180)<!-- raw HTML omitted --></a></li>
<li><strong>browser</strong>:
<ul>
<li>Wait for orchestrator readiness before resolving browser sessions
[backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus
O'Connor</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10555">vitest-dev/vitest#10555</a>
<a href="https://github.com/vitest-dev/vitest/commit/7fb29651a"><!-- raw
HTML omitted -->(7fb29)<!-- raw HTML omitted --></a></li>
<li>Wait for iframe tester readiness before preparing [backport to v4] -
by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10497">vitest-dev/vitest#10497</a>
and <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10556">vitest-dev/vitest#10556</a>
<a href="https://github.com/vitest-dev/vitest/commit/fbc626c40"><!-- raw
HTML omitted -->(fbc62)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>mocker</strong>:
<ul>
<li>Hoist vi.mock() for vite-plus/test imports [backport to v4] - by
<strong>Hiroshi Ogawa</strong>, <strong>LongYinan</strong>,
<strong>Claude Opus 4.8</strong> and <strong>Vladimir</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10548">vitest-dev/vitest#10548</a>
<a href="https://github.com/vitest-dev/vitest/commit/2c9559c02"><!-- raw
HTML omitted -->(2c955)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>pool</strong>:
<ul>
<li>Prevent test run hang on worker crash [backport to v4] - by
<strong>Ari Perkkiö</strong> and <strong>Jattioui Ismail</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10543">vitest-dev/vitest#10543</a>
and <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10564">vitest-dev/vitest#10564</a>
<a href="https://github.com/vitest-dev/vitest/commit/934b0f587"><!-- raw
HTML omitted -->(934b0)<!-- raw HTML omitted --></a></li>
</ul>
</li>
</ul>
<h5><a
href="https://github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9">View
changes on GitHub</a></h5>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="db616d227b"><code>db616d2</code></a>
chore: release v4.1.10 (<a
href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/10718">#10718</a>)</li>
<li><a
href="bae52b5112"><code>bae52b5</code></a>
fix(vm): fix external module resolve error with deps optimizer query for
enco...</li>
<li><a
href="a7a61e78c7"><code>a7a61e7</code></a>
chore: release v4.1.9 (<a
href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/10598">#10598</a>)</li>
<li><a
href="934b0f587c"><code>934b0f5</code></a>
fix(pool): prevent test run hang on worker crash (<a
href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/10543">#10543</a>)
[backport to v4] (#...</li>
<li><a
href="7fb29651af"><code>7fb2965</code></a>
fix(browser): wait for orchestrator readiness before resolving browser
sessio...</li>
<li><a
href="a5180190c1"><code>a518019</code></a>
fix: fix <code>importOriginal</code> with optimizer and query import
[backport to v4] (#...</li>
<li>See full diff in <a
href="https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:40:08 -07:00
dependabot[bot]
ff58df22e4 chore(deps-dev): bump @vitest/coverage-v8 from 4.1.8 to 4.1.10 (#2445)
Bumps
[@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8)
from 4.1.8 to 4.1.10.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitest-dev/vitest/releases">@​vitest/coverage-v8's
releases</a>.</em></p>
<blockquote>
<h2>v4.1.10</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li><strong>browser</strong>: Check fs access in builtin commands
[backport to v4]  -  by <a
href="https://github.com/hi-ogawa"><code>@​hi-ogawa</code></a>,
<strong>Hiroshi Ogawa</strong> and <strong>OpenCode
(claude-opus-4-8)</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10680">vitest-dev/vitest#10680</a>
<a href="https://github.com/vitest-dev/vitest/commit/5c18dd267"><!-- raw
HTML omitted -->(5c18d)<!-- raw HTML omitted --></a></li>
<li><strong>vm</strong>: Fix external module resolve error with deps
optimizer query for encoded URI [backport to v4]  -  by <a
href="https://github.com/SveLil"><code>@​SveLil</code></a> and <a
href="https://github.com/hi-ogawa"><code>@​hi-ogawa</code></a> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10661">vitest-dev/vitest#10661</a>
<a href="https://github.com/vitest-dev/vitest/commit/bae52b511"><!-- raw
HTML omitted -->(bae52)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a
href="https://github.com/vitest-dev/vitest/compare/v4.1.9...v4.1.10">View
changes on GitHub</a></h5>
<h2>v4.1.9</h2>
<h3>🐞 Bug Fixes</h3>
<ul>
<li>Fix <code>importOriginal</code> with optimizer and query import
[backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>David
Harris</strong>, <strong>Codex</strong>and <strong>Vladimir</strong> in
<a
href="https://redirect.github.com/vitest-dev/vitest/issues/10546">vitest-dev/vitest#10546</a>
<a href="https://github.com/vitest-dev/vitest/commit/a5180190c"><!-- raw
HTML omitted -->(a5180)<!-- raw HTML omitted --></a></li>
<li><strong>browser</strong>:
<ul>
<li>Wait for orchestrator readiness before resolving browser sessions
[backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus
O'Connor</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10555">vitest-dev/vitest#10555</a>
<a href="https://github.com/vitest-dev/vitest/commit/7fb29651a"><!-- raw
HTML omitted -->(7fb29)<!-- raw HTML omitted --></a></li>
<li>Wait for iframe tester readiness before preparing [backport to v4] -
by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10497">vitest-dev/vitest#10497</a>
and <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10556">vitest-dev/vitest#10556</a>
<a href="https://github.com/vitest-dev/vitest/commit/fbc626c40"><!-- raw
HTML omitted -->(fbc62)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>mocker</strong>:
<ul>
<li>Hoist vi.mock() for vite-plus/test imports [backport to v4] - by
<strong>Hiroshi Ogawa</strong>, <strong>LongYinan</strong>,
<strong>Claude Opus 4.8</strong> and <strong>Vladimir</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10548">vitest-dev/vitest#10548</a>
<a href="https://github.com/vitest-dev/vitest/commit/2c9559c02"><!-- raw
HTML omitted -->(2c955)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>pool</strong>:
<ul>
<li>Prevent test run hang on worker crash [backport to v4] - by
<strong>Ari Perkkiö</strong> and <strong>Jattioui Ismail</strong> in <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10543">vitest-dev/vitest#10543</a>
and <a
href="https://redirect.github.com/vitest-dev/vitest/issues/10564">vitest-dev/vitest#10564</a>
<a href="https://github.com/vitest-dev/vitest/commit/934b0f587"><!-- raw
HTML omitted -->(934b0)<!-- raw HTML omitted --></a></li>
</ul>
</li>
</ul>
<h5><a
href="https://github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9">View
changes on GitHub</a></h5>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="db616d227b"><code>db616d2</code></a>
chore: release v4.1.10 (<a
href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8/issues/10718">#10718</a>)</li>
<li><a
href="a7a61e78c7"><code>a7a61e7</code></a>
chore: release v4.1.9 (<a
href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8/issues/10598">#10598</a>)</li>
<li>See full diff in <a
href="https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/coverage-v8">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:21:32 -07:00
dependabot[bot]
a7856e8a90 chore(deps): bump @xyflow/react from 12.11.0 to 12.11.2 (#2447)
Bumps
[@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react)
from 12.11.0 to 12.11.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/releases">@​xyflow/react's
releases</a>.</em></p>
<blockquote>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.11.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5825">#5825</a> <a
href="5072914791"><code>5072914</code></a>
- Only create an <code>XYDrag</code> instance for draggable nodes.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5847">#5847</a> <a
href="742860c293"><code>742860c</code></a>
- Stop the <code>MiniMap</code> from re-rendering on every store
update.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5846">#5846</a> <a
href="17c64a1585"><code>17c64a1</code></a>
- Apply the viewport pan/zoom transform imperatively so the
<code>Viewport</code> component only renders once.</p>
</li>
<li>
<p>Updated dependencies [<a
href="c707267bc8"><code>c707267</code></a>,
<a
href="0c0cebc08c"><code>0c0cebc</code></a>,
<a
href="56cf3b0036"><code>56cf3b0</code></a>,
<a
href="a01bb6bf51"><code>a01bb6b</code></a>,
<a
href="9b3f390e21"><code>9b3f390</code></a>,
<a
href="cdfcbeb961"><code>cdfcbeb</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.79</li>
</ul>
</li>
</ul>
<h2><code>@​xyflow/react</code><a
href="https://github.com/12"><code>@​12</code></a>.11.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5815">#5815</a> <a
href="87da45cdf5"><code>87da45c</code></a>
- Fix <code>FinalConnectionState</code> type so it preserves the
discriminated union.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5823">#5823</a> <a
href="a6afc91160"><code>a6afc91</code></a>
- Update attribution link</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5824">#5824</a> <a
href="6b1dac50ff"><code>6b1dac5</code></a>
- Do not fire on pane click when connection ends on pane</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5818">#5818</a> <a
href="4ddc2d8420"><code>4ddc2d8</code></a>
- Provide the shared handle config (<code>connectOnClick</code>,
<code>noPanClassName</code>, <code>rfId</code>) through context instead
of subscribing to the store in every <code>Handle</code>, so a store
update no longer runs a selector once per handle</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5817">#5817</a> <a
href="8df250b095"><code>8df250b</code></a>
- Reduce per-handle work on store updates by returning a shared
connection state while no connection is in progress.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5822">#5822</a> <a
href="a6249de56c"><code>a6249de</code></a>
- Return stable reference for edge position when connected node gets
deleted</p>
</li>
<li>
<p>Updated dependencies [<a
href="ceb8604047"><code>ceb8604</code></a>,
<a
href="87da45cdf5"><code>87da45c</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.78</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/xyflow/xyflow/blob/main/packages/react/CHANGELOG.md">@​xyflow/react's
changelog</a>.</em></p>
<blockquote>
<h2>12.11.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5825">#5825</a> <a
href="5072914791"><code>5072914</code></a>
- Only create an <code>XYDrag</code> instance for draggable nodes.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5847">#5847</a> <a
href="742860c293"><code>742860c</code></a>
- Stop the <code>MiniMap</code> from re-rendering on every store
update.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5846">#5846</a> <a
href="17c64a1585"><code>17c64a1</code></a>
- Apply the viewport pan/zoom transform imperatively so the
<code>Viewport</code> component only renders once.</p>
</li>
<li>
<p>Updated dependencies [<a
href="c707267bc8"><code>c707267</code></a>,
<a
href="0c0cebc08c"><code>0c0cebc</code></a>,
<a
href="56cf3b0036"><code>56cf3b0</code></a>,
<a
href="a01bb6bf51"><code>a01bb6b</code></a>,
<a
href="9b3f390e21"><code>9b3f390</code></a>,
<a
href="cdfcbeb961"><code>cdfcbeb</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.79</li>
</ul>
</li>
</ul>
<h2>12.11.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5815">#5815</a> <a
href="87da45cdf5"><code>87da45c</code></a>
- Fix <code>FinalConnectionState</code> type so it preserves the
discriminated union.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5823">#5823</a> <a
href="a6afc91160"><code>a6afc91</code></a>
- Update attribution link</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5824">#5824</a> <a
href="6b1dac50ff"><code>6b1dac5</code></a>
- Do not fire on pane click when connection ends on pane</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5818">#5818</a> <a
href="4ddc2d8420"><code>4ddc2d8</code></a>
- Provide the shared handle config (<code>connectOnClick</code>,
<code>noPanClassName</code>, <code>rfId</code>) through context instead
of subscribing to the store in every <code>Handle</code>, so a store
update no longer runs a selector once per handle</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5817">#5817</a> <a
href="8df250b095"><code>8df250b</code></a>
- Reduce per-handle work on store updates by returning a shared
connection state while no connection is in progress.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/xyflow/xyflow/pull/5822">#5822</a> <a
href="a6249de56c"><code>a6249de</code></a>
- Return stable reference for edge position when connected node gets
deleted</p>
</li>
<li>
<p>Updated dependencies [<a
href="ceb8604047"><code>ceb8604</code></a>,
<a
href="87da45cdf5"><code>87da45c</code></a>]:</p>
<ul>
<li><code>@​xyflow/system</code><a
href="https://github.com/0"><code>@​0</code></a>.0.78</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3ab66eff0d"><code>3ab66ef</code></a>
chore(packages): bump</li>
<li><a
href="01d58b69ff"><code>01d58b6</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5846">#5846</a>
from AlaricBaraou/perf/viewport-imperative-transform</li>
<li><a
href="742860c293"><code>742860c</code></a>
perf(react): skip minimap re-renders when its geometry is unchanged</li>
<li><a
href="17c64a1585"><code>17c64a1</code></a>
perf(react): apply the viewport transform imperatively instead of
re-renderin...</li>
<li><a
href="775bdd6b67"><code>775bdd6</code></a>
Merge pull request <a
href="https://github.com/xyflow/xyflow/tree/HEAD/packages/react/issues/5825">#5825</a>
from AlaricBaraou/perf/xydrag-draggable-only</li>
<li><a
href="576a43d5be"><code>576a43d</code></a>
refactor(react): pass nodesDraggable to prevent wrong initial state</li>
<li><a
href="eedbc81fe3"><code>eedbc81</code></a>
chore(packages): bump</li>
<li><a
href="c7bdce429e"><code>c7bdce4</code></a>
chore(pane): cleanup</li>
<li><a
href="cd5ec450b5"><code>cd5ec45</code></a>
chore(react/pane): cleanup</li>
<li><a
href="f919daa393"><code>f919daa</code></a>
fix(pane): do not fire pane click when connection ends on pane</li>
<li>Additional commits viewable in <a
href="https://github.com/xyflow/xyflow/commits/@xyflow/react@12.11.2/packages/react">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:12:43 -07:00
gsxdsm
710d56b2db U4 trim: delete the dependency-blocked-todo feature (unreachable in production) and revert 5a2de7d (#2477)
Stacked on #2474. Base is `feature/workflow-vocabulary-u4-dead-code` —
do not merge before it.

Deletes an **entire feature that has never executed in production**, and
reverts `5a2de7d`, which only threaded resolved lifecycle columns
through it.

## Reachability evidence — the whole basis for this

```
surfaceDependencyBlockedTodos          ← in NEITHER sweep registry; no caller in
  └─ getDependencyBlockedTodoReporter()      engine/dashboard/cli — only tests
      └─ engine/dependency-blocked-todo-reporter.ts   ← sole caller of ↓
          └─ core/computeDependencyBlockedTodoReport
```

self-healing owns two name-based sweep registries (`runStartupRecovery`,
58 entries; `runMaintenance`, 76). `surfaceDependencyBlockedTodos` is in
**neither**, so nothing ever invoked the chain below it. Its four tests
passed while proving nothing about production.

## Why delete rather than wire it up

Wiring was the tempting option and is the riskier one. Switching on a
450-line path that has never run — whose tests therefore establish
nothing about its behavior against real data — is a **behavior change
with unquantified blast radius**. This program already refused exactly
that move for the **pool-id sentinel**, a one-line change that would
switch on dormant enforcement across every project. This is the same
class of move at ~450× the size.

Deleting is also the recoverable direction: git keeps the feature, and
it can be resurrected deliberately — with tests that prove it *runs* —
if dependency-blocked reporting is actually wanted.

## The settings keys go with it

`dependencyBlockedTodoReportEnabled` defaulted `true` while driving
nothing. A schema/API-visible switch that lies about what the system
does is worse than no switch. (It had no dashboard UI field — the
dashboard test allowlist already recorded it as *"no UI field"*.) Four
sibling tuning keys are removed with it.

## Against my own earlier work

`5a2de7d` threaded resolved lifecycle roles into
`computeDependencyBlockedTodoReport` and its reporter, answering a
review finding I confirmed as real. **The code was correct; the impact
claim was not**, because the path never executes. Neither the reviewer
nor I checked *reachability* before agreeing the defect mattered — only
correctness. A correction is posted on that thread in #2470.

**Scope limit on that admission:** the same finding also described
*incorrect scheduler ordering*. That half runs through
`buildUnblockWeightMap` in `task-priority.ts`, which is **live** and was
already threading `terminalColumns` (B1, `434b385`). Scheduler ordering
was never affected, before or after.

## What survives

`blocker-fanout.ts` **stays** — it is live via `task-priority.ts`. Only
the plural `holdColumns` option added by `5a2de7d` is reverted, since
the deleted report was its sole consumer. `holdColumn` (singular, from
B1) remains.

## Net

**1,244 deletions / 5 insertions across 15 files** — ~450 production
lines, ~684 test lines, 5 settings keys.

## Verification

- `tsc --noEmit` clean in **core, engine, and dashboard-app**; `pnpm
lint` clean
- merge gate green (299 + 10 + 71)
- self-healing suite: 411 passed, 1 **pre-existing** failure
(`archiveStaleDoneTasks`)
- dashboard settings-descriptions suite green
- `settings-parity.test.ts` has one **pre-existing** failure
(`agentToolOutputMaxChars` overlap) that fails identically with these
changes stashed — unrelated to this deletion

No changeset: `@fusion/core` and `@fusion/engine` are private.

🤖 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 quiet-window backlog health diagnostics for stalled items in
review, with repeat-alert suppression.
  * Added default thresholds for backlog-pressure alerts.

* **Changes**
  * Removed dependency-blocked todo reporting and related alerts.
* Removed the dependency-blocked todo enable/disable setting; remaining
tuning options are no longer active.
* Updated the workflow hold classification to use a single todo column.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 14:54:32 -07:00
gsxdsm
4158cf1ab7 Phase A: workflow-owned lifecycle foundation (U1, U2, U3) (#2467)
Phase A (Foundation) of
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`.
Three units, one commit each. No operator-visible behavior change.

## U1 — Lifecycle-column resolution seam

`resolveLifecycleColumns(ir)` returns `{ intake, hold, wip, review,
complete, archived }` — the first column carrying each trait,
`undefined` for a role no column carries.
`resolveTaskLifecycleColumns(store, taskId, cache?)` is the store-aware
form; the cache is caller-owned so a sweep reads one IR per workflow
rather than one per card.

A v1/column-less IR resolves to `undefined` for the **whole struct**
rather than a struct of undefined roles. A caller must be able to
distinguish "this workflow declares no hold column" (a real shape to
honor) from "no column vocabulary at all" (skip and log) — only the
second licenses conservative fallback.

Nothing consumes the seam yet; Phases B–D convert the ~207 hardcoded
column literals onto it.

## U2 — Delete the pre-cutover parity machinery (delete-only)

**`workflow-columns-settings.ts`** — `isWorkflowColumnsEnabled` had the
body `return true`. Six live call sites branched on it, so every
flag-OFF arm was dead code that read as a supported configuration.
Deleted; surviving side inlined at self-healing's transitionPending
sweep, the scheduler's per-column capacity diagnostic, merge-trait's
policy resolver, the board-workflows payload, two task-workflow routes,
and the CLI TUI's column enrichment.

**`workflow-parity.ts`** — asserted the default workflow's adjacency
*equals* the legacy `VALID_TRANSITIONS`. U11 deliberately breaks that
equality by merging Todo into Planning, so this is not a stale assertion
to update; it is a contract against the target state. Its emitter
(`workflow-parity-observer.ts`) is already a tombstone, so
`getWorkflowParitySummary` and `computeWorkflowColumnsGraduationReport`
aggregated run-audit rows nothing writes and had no caller outside
`TaskStore`. Both store methods go with it.

`flagEnabled` stays on the board-workflows **wire** as a constant `true`
— shipped dashboard clients still branch on it, and changing the
response shape is not a deletion. U10 retires the field once no client
reads it.

The `legacy-tombstones` ratchet is extended to both files plus seven
symbols, each with the reason it is gone.

### ⚠️ Finding: the third listed deletion was NOT dead

The plan also lists "the flag-off inline move path" in
`task-store/moves.ts`. It is **not** deleted, per U2's execution note
("any behavior change found while removing a branch means the branch was
not dead").

That path is gated on `isWorkflowColumnsCompatibilityFlagEnabled`
(`store.ts:38`) — a **different** function from the always-true public
helper. It reads the raw `experimentalFeatures.workflowColumns` setting,
which nothing in production sets (`settings-schema.ts:396` — "no default
flags are emitted"; zero non-test writers; the operator's own
`~/.fusion/settings.json` has no such key). So `useWorkflow` is false
for effectively every real project: the flag-OFF inline side effects are
the **live** default move path and the flag-ON `default-workflow-hooks`
path is the dead one. The code says so itself at `moves.ts:638`.

Deleting that branch would swap every project onto an untravelled code
path — a behavior change, not a deletion.

**Carry this into Phases B and C, stated plainly so the plan's error is
not repeated:**

> **The inline move path in `moves.ts` is LIVE.
`default-workflow-hooks.ts` (the trait-hook path) is DEAD.** KTD-6
asserted the inverse. Until the convergence unit lands, **nothing may
assume trait hooks run** — a guard, sweep, or subscriber written against
`applyDefaultWorkflowMoveEffects` would never fire in production and
would still pass its tests.

Convergence is **not** attempted here. It is its own unit (Phase A2)
with a proper equivalence proof, per operator decision.

### U3's emit point is on the LIVE path — the seam is not born dead

Worth stating explicitly because it is the failure mode that would make
every later subscriber silently never fire: the `TaskTransitioned` emit
is **not** inside the `if (useWorkflow)` branch. That block closes at
`moves.ts:1212`; the emit sits at `:1214`, beside the existing
`store.emit("task:moved", …)`, on the unconditional post-commit path. It
therefore fires on **both** the live inline path and the dead hooks
path, and the convergence unit inherits the obligation to keep it firing
on whichever path survives — same events, same order, same payloads.

The graph-side emitters (`NodeEntered`, `RunSuspended`) carry the same
risk from a different direction: the bus refuses an invalid payload
*silently* by design, so an emitter regression would stop the event with
no test failure. They are asserted end-to-end through the real bus —
"did a subscriber actually receive it", not "was emit called" — because
a spy passes on a refused payload. The `moveTaskInternalImpl` emit does
**not** yet have that end-to-end assertion against a real store move;
that proof belongs to the convergence unit, which has to build the
both-paths fixture anyway.

## U3 — Post-commit event seam with a transactional outbox

**The bus is not a queue, not a transaction participant, and not a
delivery guarantee.** Durable follow-on work uses the transactional
outbox — a `workflow_work_items` row written *inside* the transition
transaction (the shape `createCompletionHandoffWorkflowWork` already
uses). "Emit after commit, let a subscriber enqueue the work" has a
crash window where a process dies between commit and subscriber, leaving
no event *and* no work-item row, so required work is skipped permanently
with nothing to recover from. Post-commit subscribers therefore carry
only losable reactions.

Emission is consequently lossy and isolated by design: a throwing or
rejecting subscriber is caught and logged, cannot roll back the
transition, and cannot stop the others. Deliveries append to one serial
chain, so two transitions on a task deliver in commit order.

The ids/outcomes-only rule is **mechanised, not documented** —
run-audit's equivalent lives only in prose and has been violated
repeatedly. A payload carrying an object body or a prose string is
refused at the emit boundary and never reaches a subscriber or log sink.
It degrades rather than throws: the emitter is post-commit, so a shape
bug must not become a lifecycle failure.

Emit points: `TaskTransitioned` from the single post-commit point in
`moveTaskInternalImpl`; `NodeEntered` and `RunSuspended` from the graph
column boundary, the latter *after* the durable continuation is
persisted so an observed suspension implies a resumable run.

`registerWorkflowEventSubscribers` (engine) is empty on purpose —
U7/U8/U10 move real reactions onto it, each with the characterization
test proving the reaction was non-authoritative first.

## Verification

- `pnpm test:gate` — green (2/10, 16/299, 1/71).
- `pnpm lint`, `pnpm build`, `tsc --noEmit` on core and engine — green.
- U1: 20 tests in `workflow-lifecycle-traits.test.ts`, including the
fully-renamed-workflow case (fails if the resolver falls back to a
literal) and a shared-cache read-count assertion.
- U2: `legacy-tombstones.test.ts` green with the extended ratchet;
`board-workflows`, `merge-trait`, `workflow-graph-executor-parity`, and
move-hook suites green with no expectation edits.
- U3: 20 bus-invariant unit tests (isolation, ordering, the allowed-key
and required-key halves of the ids-only rule, lossiness) plus 3
end-to-end emitter-delivery tests; 5 outbox tests against a **real
PostgreSQL** work-item table (crash survival, rollback, at-least-once
redelivery on lease expiry, idempotent handler → one effect,
dropped-subscriber vs. durable work). A hand-written fake of the lease
predicate would only prove the fake redelivers.

**Not verified:** the `moveTaskInternalImpl` emit is confirmed on the
unconditional post-commit path by structure and by the surrounding
tests, but is *not* yet asserted end-to-end against a real store move on
both flag settings — that is Phase A2's fixture. The engine subscriber
registry ships empty by design, so no production subscriber exercises
the bus end-to-end yet. `settings-defaults.test.ts` has one pre-existing
failure on `main` (a logger-prefix mismatch in the
`mergeIntegrationWorktree=cwd-main` warning) — confirmed present on a
clean tree, unrelated to this branch.

🤖 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**
* Workflow lifecycle columns are now derived from workflow definitions,
supporting renamed and custom workflows.
* Added post-commit lifecycle events for task transitions, node entry,
and run suspend/resume with validated payloads.
* Follow-on processing for lifecycle emissions is now more robust
(rollback-safe, at-least-once delivery, idempotent handling).
* **Bug Fixes**
* Workflow board responses, task enrichment, and promotion no longer
depend on workflow-columns feature-flag gating.
  * Subscriber failures no longer impact committed workflow transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 13:30:13 -07:00
gsxdsm
8b039a543e fix(desktop): advance Pi runtime pin to 0.82.1 for packaging PR lane (#2465)
## Summary
- Advance the matched Pi runtime pin (`pi-ai`, `pi-coding-agent`,
`pi-agent-core`, `pi-tui`) from **0.82.0 → 0.82.1** so
electron-builder's production-dependency walk accepts `pi-agent-core`'s
`pi-ai@^0.82.1` requirement.
- Fixes the Desktop packaging PR-lane failure:
`Production dependency @earendil-works/pi-ai not found for package
@earendil-works/pi-agent-core` (required `^0.82.1`).
- Keep the workspace override guard; update pin-policy fixtures and CLI
package-config expectations.
- Tighten the advisory packaging step-order test so it asserts against
the real `electron-builder --dir` step (not a missing release-only step
name that previously passed via `indexOf === -1`).
- Run `pnpm dedupe` so the packaging lane's lockfile dedupe
early-warning is clean.

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

## Test plan
- [x] `node scripts/check-pi-versions-pinned.mjs`
- [x] `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs`
- [x] `pnpm --filter @runfusion/fusion exec vitest run
src/__tests__/package-config.test.ts`
- [x] `pnpm --filter @fusion/desktop exec vitest run
src/__tests__/release-workflow.test.ts`
- [x] `pnpm dedupe --check`
- [ ] GitHub: Desktop packaging (should run full packaging walk —
lockfile/package.json touched)
- [ ] GitHub: PR Checks (Lint, Typecheck, Build, Gate)
2026-07-26 23:47:49 -07:00
gsxdsm
99c9f14ee0 feat: run Plan Review in the planning lane with a Plan Review badge (#2462)
## What

Plan Review, planning, and the replan loop move from the implementation
column into the **planning lane** (`todo`), so a task under
specification never holds a WIP slot. The card crosses into
`in-progress` exactly once, at `parse`, released by the scheduler.

Operators also finally see a **Plan Review** badge while the gate runs —
it was previously invisible on the default workflow.

## The part that made it possible

Moving the node is ten lines. It was attempted three times and reverted
each time, because a graph run with no durable continuation replayed
from `start` and dragged an in-progress card *backward* out of the WIP
column, firing `abort-on-exit` and stranding it in a pre-WIP column with
no releaser.

So this PR adds the graph **entry contract** —
`resolveColumnResumeNode`:

| Card is in | Resumes at |
|---|---|
| `triage` | `start` |
| `todo` | `plan` |
| `in-progress` | `parse` — never re-plans, never moves backward |
| `in-review` | first review node — gates are not skipped |

`ir.columns` is ordered and that order is the lifecycle order; rework
and failure edges are excluded so the entry point is always the main
path. The proof it's the right fix: **`executor-task-done-invariant`
passes unmodified** after failing every previous attempt.

## Also in here

- **Release gate narrowed twice.** `isUnplannedForExecution` applies its
pre-release plan-review gate only when the node's column equals the
card's column *and* the group is enabled for the task. The enablement
check fixes a real deadlock — a task with Plan Review toggled off was
held forever waiting for evidence nothing would ever write.
- **Badge cleanup.** Gate badge reads "Plan Review" instead of the
ambiguous "Reviewing" and no longer hides behind a lane restriction; the
status badge stops duplicating it; `planning` renders as "Planning"
instead of the raw engine token.
- **Coding (Ideas)** renames its planner column to "Planning" (id `todo`
unchanged) and loses its private planning-node re-home — the graph it
clones is already plan-in-place.
- **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose
column its workflow no longer declares. Written for a follow-up, kept
because it makes any column edit survivable.

## Test changes

Scheduler and release fixtures now model a card whose Plan Review passed
— the state every real card is in when the capacity sweep sees it. A
held unreviewed card is the gate working, and that path stays owned by
`pre-release-plan-review.test.ts`.

New `workflow-graph-entry-contract.test.ts` covers the invariant at
every lifecycle position, plus the gap-column and remediation-node
cases.

## Verification

Gate 299 + 70 + 10, dashboard badge suites 672, engine
workflow/entry/executor suites 147, core 122. Lint and typecheck clean.
Full engine suite sits at the pre-existing baseline (notifier /
plugin-runner / notification-service, untouched by this).

## Follow-up

Removing the Todo column entirely is a separate ~207-site
lifecycle-vocabulary refactor — planned in
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
(companion docs PR).

🤖 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**
* Plan Review now runs in the Planning lane before implementation
begins.
* Cards resume from their current workflow column without replaying
earlier steps.
* Added automatic recovery for cards stranded in outdated workflow
columns.
* **Improvements**
  * Renamed the Coding (Ideas) planner column to “Planning.”
* Refined Plan Review gating to respect enabled settings and the card’s
current column.
* Updated planning and Plan Review badges for clearer, consistent labels
across cards and lists.
* **Bug Fixes**
* Improved workflow transitions and release behavior around planning,
review, and execution.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:42:46 -07:00
gsxdsm
0e3d2a2265 refactor: delete meta-task auto-archive and automated recovery follow-ups (#2461)
Deletes two pieces of automated "meta" machinery that filed and
garbage-collected cards restating state already on the task that failed.
Net **-1015 lines**.

## Why

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

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

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

## What changed

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

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

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

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

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

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

## Test changes — read this one

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

## Verification

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

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

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

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

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


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

## Summary by CodeRabbit

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

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

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

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:38:58 -07:00
Phil Larson
d19c82b33b test(dashboard): align modal and locale fixtures (#2460)
## Summary
- query Task Detail modal assertions from the document after its
FloatingWindow portal migration
- sync secondary locale catalogs with the current English key structure

## Test plan
- `pnpm --filter @fusion/dashboard exec vitest run
app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx
--silent=passed-only --reporter=dot`
- `pnpm i18n:status`
- `pnpm --filter @fusion/dashboard build`

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

* **Documentation**
* Added missing localization entries for workflow selection/help text,
action-based report targeting, agent tool output limit hints,
auto-update/restart messaging, and task refinement status labels across
Spanish, French, Korean, Simplified Chinese, and Traditional Chinese.

* **Tests**
* Updated task detail modal tests to query the correct rendered document
root so portal/modal content is asserted reliably.
* Minor test formatting adjustments to keep assertions consistent
without changing coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-26 22:36:09 -07:00
Phil Larson
11887625f1 test(dashboard): align touch fixture with FloatingWindow (#2459)
## Summary
- update the touch-resize browser fixture assertions for New Task's
FloatingWindow migration
- verify the shared nine touch targets, resize handle ID, and unified
geometry persistence key

## Test plan
- `corepack pnpm --filter @fusion/dashboard build`
- `corepack pnpm --filter @fusion/dashboard exec vitest run --project
dashboard-browser-touch --silent=passed-only --reporter=dot
src/__tests__/task-modal-touch-resize-browser.test.ts`
- `corepack pnpm check:changesets`


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

* **Tests**
* Updated tablet touch-resize coverage for task-detail to use the shared
southeast resize handle and corrected expected hit-target counts.
* Refreshed hit-target detection and drag target assertions after
switching the test surface to the task-detail/floating window layout.
* Updated persistence/geometry validations to compare the stored unified
width/height against the resized panel dimensions.
* Reworked desktop vs tablet assertions, including tighter overlay
padding checks and standardized shared resize-handle sizing
expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-26 22:35:53 -07:00
Victor Canô
d6c917d726 feat(dashboard): add view and settings-section enumeration API (#2453)
## Summary

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

Two additions, both inert unless called:

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

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

## What actually changed — one source of truth

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

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

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

## Drift protection

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

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

## Design notes / decisions for review

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

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

## Verification

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


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

## Summary by CodeRabbit

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

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

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

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-26 22:34:18 -07:00
gsxdsm
3f33cb000f feat: per-origin workflow selection + feedback-derived refinement titles
Two task origins had no workflow picker in front of the operator and always
inherited the project default: `fn task create` (CLI + the `fn_task_create`
agent tool) and refinement tasks. Add a Project General setting for each, where
blank/unset means "Selected workflow" (the operator's current Board lane,
falling back to the project default) and a concrete id pins that origin.

Because the Board lane lives in browser localStorage, non-browser callers could
not resolve "Selected workflow" at all. `boardSelectedWorkflowId` mirrors the
lane into project settings so they can. Note this makes the mirrored lane
project-scoped: two operators on one project share it, last switch wins. The
Board never reads it back, so the only effect is which workflow a newly created
task inherits.

Resolution is `TaskStore.resolveOriginWorkflowOverrideId(origin)`: pinned
setting -> mirrored lane -> `undefined` to inherit each caller's existing
default-workflow path unchanged. A deleted or fragment id degrades to inherit
rather than throwing, so a stale settings value can never break task creation.
An explicit `workflow_id` argument to `fn_task_create` still wins.

Separately, a refinement is now titled by the operator's own feedback via the
shared `deriveFallbackTaskTitle`, not `Refinement: <parent title>`. Ten
refinements of one task previously rendered ten identical titles, so the board
could not tell them apart while the text saying what each one asked for sat in
the description. Provenance moves to a `Refines <id>` card chip alongside the
existing detail-view parent link and dependency edge.

Verified: merge gate (299 tests), lint, full build, and typecheck for core, CLI,
and dashboard all pass. New coverage: origin resolution across both origins and
the full precedence ladder, the two settings pickers, the board-lane mirror,
refinement titling (including sibling distinctness), and the card chip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:33:25 -07:00
gsxdsm
e4cb957b57 FN-8619: migrate modal geometry to FloatingWindow
Unify migrated modal geometry and responsive behavior under FloatingWindow.

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

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

Fusion-Task-Id: FN-8619

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

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

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

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

Fusion-Task-Id: FN-8621

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 19:52:32 -07:00
gsxdsm
7715a83e6f FN-8624: fix task detail activity right padding
Keep Task Detail activity insets symmetric while reserving overlay clearance only for first-row content.

- Move Live/Feed overlay clearance from the activity container to first-row elements.
- Preserve responsive overlay spacing and the full-width Interventions timeline.
- Cover modal, pop-out, embedded, and responsive activity layouts with updated tests.
- Add a patch changeset for the Task Detail padding fix.

Files changed:
 .changeset/fn-8624-task-detail-inset.md            |  7 ++++
 .../dashboard/app/components/TaskDetailModal.css   | 41 ++++++++++------------
 .../TaskDetailModal.attachments-and-tabs.test.tsx  |  6 ++--
 .../__tests__/TaskDetailModal.css.test.ts          | 21 ++++++-----
 ...etailModal.responsive-and-dependencies.test.tsx | 25 +++++++------
 5 files changed, 54 insertions(+), 46 deletions(-)

Fusion-Task-Id: FN-8624

Fusion-Task-Lineage: 578cfe86-3021-421d-841b-2670efc10a5d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 19:45:47 -07:00
gsxdsm
d3c1955866 FN-8625: remove heartbeat controls from agent org charts
Keep org charts read-only for heartbeat configuration while preserving controls on board and list surfaces.

- Remove heartbeat toggle props, markup, and styling from org chart nodes
- Cover both org chart layouts and board heartbeat availability with regression tests
- Add a patch changeset for the dashboard behavior fix

Files changed:
 .../fn-8625-remove-org-chart-heartbeat-toggle.md     |  7 +++++++
 packages/dashboard/app/components/AgentsView.css     |  7 -------
 packages/dashboard/app/components/AgentsView.tsx     | 14 +++++---------
 .../__tests__/AgentsView.orgchart.test.tsx           | 20 ++++++++++++++++++++
 .../app/components/__tests__/AgentsView.test.tsx     | 20 +++++++++++---------
 5 files changed, 43 insertions(+), 25 deletions(-)

Fusion-Task-Id: FN-8625

Fusion-Task-Lineage: 454c05e6-fe31-486b-975a-80ba5e8c518c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 19:18:53 -07:00