Commit Graph

4962 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
gsxdsm
89b4621a93 FN-8607: migrate shared modals to FloatingWindow
Standardize agent, onboarding, and utility modal behavior on the shared FloatingWindow contract.

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

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

Fusion-Task-Id: FN-8607

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 18:57:02 -07:00
gsxdsm
bdfa5186c4 FN-8622: capture Chromium CDP modal screenshots
Add assertion-guarded CDP evidence for migrated Agent List and Setup Wizard modal touch geometry.

- Mount production modal surfaces in the resize browser fixture
- Verify tablet resize and drag behavior plus true-phone sheet behavior
- Store the captured phone and tablet screenshots as regression evidence
- Extend mobile sheet styling to the migrated modal hosts

Files changed:
 .../dashboard/app/components/FloatingWindow.css    |   8 ++
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  17 +++-
 .../fn-8607/phone-fullscreen-sheet.png             | Bin 0 -> 17823 bytes
 .../fn-8607/tablet-agent-list-after.png            | Bin 0 -> 21489 bytes
 .../fn-8607/tablet-setup-wizard-after.png          | Bin 0 -> 81964 bytes
 .../task-modal-touch-resize-browser.test.ts        |  99 +++++++++++++++++++++
 6 files changed, 120 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8622

Fusion-Task-Lineage: 3fabd950-bac0-47f4-9649-08709db31e02

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

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

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

Fusion-Task-Id: FN-8620
Fusion-Task-Lineage: 2a912260-3292-4b13-bcf0-9de5a3df8ccd
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 18:35:10 -07:00
gsxdsm
73b2a32e2b test: backfill dashboard test mocks for new api and viewport exports
Concurrent dashboard work added runtime exports that hardcoded module
mocks did not expose, so any suite rendering the affected component threw
"No <export> is defined on the mock". Adds the missing `../api` exports
(system-info probe, update install/restart, cloudflared, provider key and
login helpers, git remotes/branches) and the `isTabletTouchViewport`
viewport helper across the 26 suites that mock those modules.

Verified: all 26 files pass (1012 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:00:07 -07:00
gsxdsm
beebd270bd fix: make Queued to plan / Ready badges agree with the planning lane
TaskCard inferred "unplanned" from steps.length === 0 while triage's
todo-discovery and the scheduler's dispatch filter both decide from
PROMPT.md seed-ness, so the badges disagreed with the engine in both
directions: a real spec that parsed to zero steps read as "Queued to
plan" while the scheduler already treated it as a WIP-slot candidate, and
a re-seeded card still carrying old steps read as "Ready" while triage
was about to plan it. Either way the badge sent operators to the wrong
cap.

Adds the shared isTaskAwaitingPlanning predicate (replan park, missing
spec, seed-vs-real content) used by both triage's discovery and a new
best-effort `awaitingPlanning` enrichment on GET /api/tasks. TaskCard
derives both badges from that one value — strict complements — and keeps
the step count only as a fallback for SSE payloads and older servers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:48:46 -07:00
gsxdsm
01b27b13f1 test(dashboard): add per-character typing coverage to uncovered field surfaces
Sweeping the other views for the FN-8606 typing bug found no further breakage,
but it did expose why the bug shipped: almost all field coverage uses
fireEvent.change, which sets a value in one shot on a node it already holds and
never needs the input to stay mounted. A remount is invisible to it.

Adds expectStableTyping (types character by character via userEvent, then asserts
DOM node identity, accumulated value, and retained focus) and applies it to the
uncovered surfaces: the FN-8606-migrated AddNode, ConnectNode, NodeDetail,
Scripts, and WorkflowAddStep modals, plus the board's QuickEntryBox composer and
SubtaskBreakdownModal title editing. All pass — this is a detection floor, not a
fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:42:39 -07:00
gsxdsm
d193dcdbb0 chore(lint): ban React components declared inside another component
Adds fusion-react/no-nested-component-definitions, a custom rule in the house
style of the existing detached-spawn guard. A component declared in render is a
new element type every render, so React remounts its subtree on each parent
update and destroys focus, scroll, and local state.

This pattern shipped three times without review or tests catching it: FN-8606's
ModalShell left Planning Mode and Settings untypable, and MailboxModal's
ReplyContextExpandable collapsed expanded reply rows. Tests missed it because
fireEvent.change sets a value without needing the node to stay mounted.

The rule reports PascalCase functions (including memo()/forwardRef()-wrapped)
that return JSX and are declared inside another JSX-returning function.
Lowercase render helpers are deliberately allowed — they are the sanctioned fix.
Escape hatch: // nested-component-allowlist: <reason>.

Scoped to production .tsx, with a vitest guard for the rule itself. Hoists the
two pre-existing violations (ProviderStatusBadge, GitHubStatusBadge in
ModelOnboardingModal) to module scope so the rule lands clean at "error".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:34:45 -07:00
gsxdsm
c21fb0d1d0 fix(dashboard): stop Mailbox reply-context rows collapsing on unrelated re-renders
ReplyContextExpandable was declared inside MailboxModal's render, making it a new
element type on every render. Any parent update remounted the whole recursive
reply thread, so expanding one reply row collapsed the others and discarded their
DOM identity, focus, and scroll position.

Hoist it to module scope and pass the parent's reply state and handlers through an
explicit env prop so the element type stays stable. Adds a regression test that
expands one row, expands a second, and asserts the first keeps both its node
identity and aria-expanded state — same defect class as the FN-8606 ModalShell fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:23:48 -07:00
gsxdsm
7065d03895 fix(dashboard): keep Planning Mode and Settings typable after floating-window migration
FN-8606 declared the window shell as a component inside PlanningModeModal and
SettingsModal render. A component declared in render is a new element type on
every render, so React remounted the whole subtree on each keystroke, destroying
the focused input: Planning Mode dropped everything after the first character and
Settings text fields did the same.

Replace ModalShell with a plain renderModalShell(children) call so the returned
element types stay stable, and add a Planning regression test that types
per-character across both the modal and embedded surfaces (fireEvent.change
cannot observe this class of bug, which is why it shipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:18:43 -07:00
gsxdsm
2bb8537352 FN-8616: make agent tool-output limits configurable
Expose the shared agent tool-output budget as a scoped operator setting with an explicit no-limit option.

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

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

Fusion-Task-Id: FN-8616

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 16:16:27 -07:00
gsxdsm
743dc5f46e FN-8612: remove tablet task modal padding
Keep task modals dense on tablets without reducing their touch resize targets.

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

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

Fusion-Task-Id: FN-8612

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 15:42:53 -07:00
gsxdsm
65f6748e5b FN-8613: theme mailbox markdown links
Theme mailbox message anchors with dashboard tokens and verify link coverage.

- Style markdown anchor default, visited, hover, and focus-visible states with design tokens
- Cover inline, autolink, list, and table links while preserving file-path controls
- Add a patch changeset for themed mailbox links

Files changed:
 .changeset/theme-mailbox-markdown-links.md         |  7 ++++
 packages/dashboard/app/components/MailboxModal.css | 29 +++++++++++++
 .../__tests__/MailboxMessageContent.test.tsx       | 49 ++++++++++++++++++----
 3 files changed, 77 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-8613

Fusion-Task-Lineage: a8c7e8bb-8238-4018-bf95-38193f908eb4

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

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

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

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

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

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

Fusion-Task-Id: FN-8611

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

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

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

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

Fusion-Task-Id: FN-8605

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

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

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

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

Fusion-Task-Id: FN-8602

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

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 12:12:35 -07:00
gsxdsm
0d40bc41d1 fix(dashboard): stop badging the merge-blocker in-review stall
A pre-merge check reporting a blocker is the ordinary in-review resting state
rather than an exceptional one, so badging it marked routine cards abnormal.
Operator-requested removal.

Suppression is expressed as a code list next to the existing
no-worktree-no-merge-confirmed entry, so both surfaces that gate on
shouldShowInReviewStallBadge -- the card header badge and the Task Detail
diagnostic block -- drop it from one place. The previous carve-out only
suppressed merge-blocker while isActiveMergeStatus(task.status) held; that is
gone, and the test row that used to expect a badge for status undefined now
asserts the suppression is unconditional.

task.inReviewStall keeps being computed and stored -- only the affordance is
withheld -- so the Review tab, run-audit, and self-healing are unaffected.
No dead CSS: the shared .in-review-stall classes still serve the remaining
codes and no --merge-blocker rule existed. The card test asserts no empty
badge shell is left behind.

Two TaskDetailModal cases used merge-blocker only as a fixture for the
diagnostics row and jump-to-activity-entry behavior; repointed at
transient-merge-status-no-owner so they still guard what they were written for.

Note for follow-up: this badge was the board's only signal for a card blocked
on a failed pre-merge step. self-healing's needsOperatorBypass comment already
flags that such cards "sit silently" behind a generic badge; with the badge
gone they show nothing at all on the board.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, 693 tests passing
across the 6 affected suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 11:47:04 -07:00