Commit Graph

12391 Commits

Author SHA1 Message Date
Phil Larson
b85a5d4531 fix(core): bound compound engineering review remediation (#2532)
## Summary

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

## Why

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

## Verification

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


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

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

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

- **Tests**
- Added/updated unit tests to validate the new bounded remediation
behavior and messaging.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 00:05:29 -07:00
Phil Larson
72391c90b2 fix(engine): route workflow reviews through validator models (#2533)
## Summary

- classify review-type workflow steps with the existing review-step
classifier
- resolve their primary, fallback, and thinking-level settings from the
validator model lane
- retain per-step model overrides and executor-purpose workflow-step
tooling
- keep ordinary workflow steps on the execution lane
- make missing-fallback diagnostics identify the correct lane

## Why

Code Review, Plan Review, verification, and inline-review gates were
executed through the implementation model lane merely because they run
inside `executeWorkflowStep()`. That defeats configured reviewer-model
separation and can make the same model implement and validate its own
work.

This changes model selection—not the workflow-step session/tooling
contract—so review steps remain executor-purpose sessions while using
validator lane models.

## Verification

- `FUSION_PG_TEST_SKIP=1 corepack pnpm@10.33.0 --filter @fusion/engine
exec vitest run src/__tests__/executor-workflow-step-model.test.ts` — 14
passed
- `corepack pnpm@10.33.0 --filter @fusion/engine 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

* **Bug Fixes**
* Review-type workflow steps now route through the configured validator
model lane (instead of the execution lane).
* Validator primary/fallback and thinking-level settings are applied
correctly for review steps.
  * Step/task overrides still take priority over lane-based resolution.
* Fallback retry sessions now use the appropriate validator/executor
configuration, with lane-specific fallback guidance when fallback
settings are missing.
* **Tests**
* Expanded executor workflow-step model resolution and routing/fallback
precedence assertions for validator-lane behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-29 00:05:04 -07:00
Phil Larson
9a8fc409ff fix: persist manual task pauses (#2536)
## Summary

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

## Why

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

## Verification

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


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

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

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

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

### The hazard

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

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

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

### The ratchet is the point

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

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

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

---

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

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

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

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

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

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

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

---------

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

## Deleted

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

## Kept, deliberately

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

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

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

## Two sections become one

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

## One real fix found on the way

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

## Migration

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

## Verification, and how the wider suite was controlled

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

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

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

Nothing booted.

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

---------

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

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

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

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

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

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

### Measured, not guessed

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

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

After:

```
PROBE archived     changed: collapsed
```

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

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

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

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

### Revert-proof

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

### Verification

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


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

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

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

---------

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

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

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

Measured, the approximation loses real operator moves:

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

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

### The fix

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

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

### Why deleting the legacy shortcut is safe

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

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

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

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

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

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

### Revert-proof, two ways

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

### Verification

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

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


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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 23:23:00 -07:00
gsxdsm
919f68f9bc test(U9): cover the two unguarded merge safeguards and admit them to the gate (#2526)
**U9, PR5.** Closes the gap #2520 measured. Tests + gate config only; no
production behavior change.

## The gap

#2520 found that safeguards **1 (user pause)** and **4 (capacity
single-flight)** had **zero test coverage**. Deleting either guard
produced no new failure anywhere in the merge, project-engine,
self-healing, or concurrency suites. Both guards work correctly today —
nothing would have noticed if they stopped. U9 moves merge behind graph
nodes, so this is exactly the state not to convert on top of.

## Two tests

- **`merge admission excludes a user-paused card`** — safeguard 1, the
pause invariant re-ratified in #2486. Without the `paused || userPaused`
filter, the admission provider offers a user-paused card to the merge
pump.
- **`drainMergeQueue is single-flight`** — safeguard 4. Asserted via
`reconcileStaleMergeActive`, the first statement *inside* the guard, so
the probe isolates the guard rather than dispatching a real merge.
(Driving a real drain crashed the vitest worker; probing the guard
directly is both safer and more precise.)

**Both are two-sided** — they assert the guard blocks *and* permits. A
one-sided test would still pass against a guard that rejects everything,
which is a real failure mode for a filter.

## Proven by mutation delta

Baseline fail-set vs mutated fail-set on the identical selection, NEW
failures only:

| Mutation | NEW failures |
|---|---|
| remove the pause filter | **1** — the pause test, and only it |
| remove the single-flight guard | **1** — the single-flight test, and
only it |
| filter rejects *everything* | **1** — proves not one-sided |
| drain *always* refuses | **1** — proves not one-sided |

## Gate admission

`project-engine.test.ts` joins the `engine-core` allow-list. **One file
proves five safeguards** — user pause, `autoMerge:false`, capacity
single-flight, the pre-enqueue merge-proof consult, and at-most-once
enqueue.

Before this, **none of the six safeguards was defended by blocking CI**.
A regression surfaced only in non-blocking full-suite, after the merge.

Measured, not assumed:

| | Files | Tests | Wall (3 runs) |
|---|---|---|---|
| before | 17 | 309 | 5.19 / 5.51 / 5.19s |
| after | 18 | 412 | 6.19 / 6.24 / 6.21s |

**+~1.0s against a ~60s ceiling.**

**Verified the gate fires**, rather than assuming the allow-list edit
took — the failure mode greptile caught in #2494:

- remove safeguard 1 → `pnpm test:gate` **exits 1** (1 failed / 411
passed)
- remove safeguard 4 → **exits 1** likewise
- restored → **exits 0**

Deterministic: store, runtime, merger and notifier all mocked; no real
git, no network, no real timers in these two cases.

## Reversible calls I made rather than asking

- **Added to `project-engine.test.ts` rather than a new file.** A
dedicated file would need ~200 lines of duplicated `vi.mock`
scaffolding; reusing the existing harness also means one gate admission
covers five safeguards instead of two.
- **Did not wait for U8.** These guard code that exists today and the
conversion needs them in place first.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:20:04 -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
99be8e6153 docs(U9): correct the safeguard baseline — safeguards 1 and 4 are NOT covered (#2520)
**U9, PR4.** Docs-only correction to a document already on `main`
(#2511). No changeset.

## I got #2511 wrong, and it matters

#2511's table claimed all six merge safeguards were verified. **Two of
them were not**, and the error is the same family this program exists to
stamp out: I reported the **absolute** failure count under mutation,
with **no baseline**.

`merge-error-recovery.test.ts` (10 failures) and `self-healing.test.ts`
(1) are **already red on clean `main`**. The "11 failed" I credited to
the row 1 mutation *was that pre-existing red*. The mutation added
nothing. I even flagged the identical `11 failed` on rows 1 and 3 as "a
red flag" in my own notes and then did not chase it.

## Re-measured as deltas

Baseline fail-SET vs mutated fail-SET on the identical selection,
reporting only NEW failures, each named:

| # | Safeguard | Baseline | Mutated | **NEW** | Verdict |
|---|---|---|---|---|---|
| 1 | user pause | 11 | 11 | **0** | **NOT COVERED** |
| 2 | `autoMerge:false` | 0 | 9 | **9** | covered |
| 3 | dependency gating | 0 | 5 | **5** | covered |
| 4 | capacity single-flight | 10 | 10 | **0** | **NOT COVERED** |
| 5a | merge-proof (pre-enqueue) | 0 | 1 | **1** | covered, thin |
| 5b | file-scope | 0 | 6 | **6** | covered |
| 6 | at-most-once | 0 | 3 | **3** | covered |

**Four hold. Two do not.**

- **Safeguard 1** is the pause invariant re-ratified in #2486. Removing
`task.paused || task.userPaused` from the merge admission provider
admits a **user-paused card into the merge pump** — and nothing fails.
- **Safeguard 4** is the single-flight guard that serializes merge.
Removing it permits concurrent `drainMergeQueue` entry — and nothing
fails.

Both guards **work correctly today**. What is missing is any test that
would notice if they stopped. That is exactly the state U9 must not
convert on top of — and #2511 said the opposite.

## Also corrected: nothing here is defended by blocking CI

The one gate-admitted file (`merger-merge-lifecycle.test.ts`) is not the
file that proves any surviving row. Rows 2/5a/6 rest on
`project-engine.test.ts`, row 3 on core's `task-merge.test.ts` — neither
is in the gate (core's gate is two PG tests via `test:pg-gate`).

## Three distinct ways the first pass was wrong

All recorded in the doc, because each produced a confident wrong answer:

1. **Absolute counts with no baseline** — rows 1 and 4. A mutation run
must diff fail-sets and report only new failures.
2. **Too-narrow selection** — an earlier pass measured rows 1 and 3 at
zero and I nearly filed two false gaps. Widening fixed row 3 but is also
how the pre-existing red crept in. Both directions need the baseline
diff.
3. **A harness that silently matched nothing** — the delta harness's
regex required a `|project|` segment in vitest's `FAIL` line.
`@fusion/core` does not emit one, so it parsed **zero** failures at both
baseline and mutation and printed "NOT COVERED" for row 3, which is
covered by 5 tests. A verification tool that reports success without
checking anything is worse than no tool; it must be tested against a
known-failing case first.

The harness now aborts on a no-op patch, asserts a clean restore, and I
validated its parser against a known-failing run before trusting it.

## Next

**PR5 writes the missing tests for safeguards 1 and 4**, then gate
admission. Neither should wait for U8 — they guard code that exists
today, and the conversion needs them in place first. That is the
reversible call I'm making rather than asking.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:19:35 -07:00
gsxdsm
5de083ef08 U8 PR4: declare the pending-review park as a graph node (inert) — and why the behavior move is blocked on the step-session chain (#2519)
Fourth PR of **U8 — the graph owns execution**. This is the IR half of
the pending-review routing move. **Inert: no behavior change.** The
behavior half is deliberately NOT in this PR, for a measured reason
below.

## What lands

A `review-handoff` seam node (`review-pending-handoff`, column
`in-review`) in `BUILTIN_CODING_WORKFLOW_IR`, with:

```
execute --outcome:review-pending--> review-pending-handoff --success--> end
```

An implementation session can end because a step is blocked on a pending
review: the agent cannot continue, and the card belongs in review rather
than in an error bucket (`status: failed` on an `in-review` row
deadlocks the merge queue). Today the **executor** performs that
transition inline, mid-session, and the graph finds out afterwards —
which is why `handleGraphFailure` carries `alreadyFinalizedToReview`, a
classifier whose only job is recognising a move the graph did not make.

Two design points worth recording, both verified against the interpreter
rather than assumed:

- **The edge goes to `end`, not to `review`.** Routing to the ordinary
`review` node would have continued the run into `merge-gate` and
`merge-attempt` on work whose steps are incomplete. "Hand off and stop"
is what the inline handoff does; the edge to `end` is what preserves it.
- **`outcome:` edges match on the node's VALUE and take priority over
generic `success`/`failure` edges** (`shouldTraverseEdge` /
`traverseChildren`). So this claims only the pending-review ending, and
a workflow that does not declare the edge falls through to its generic
`failure` edge — exactly today's behavior. That is what makes the
eventual move safe for user-authored graphs.

## Why the behavior half is not here — a measured finding

I implemented it, and backed it out. The record matters more than the
diff:

1. **`BUILTIN_CODING_WORKFLOW_IR` is not the default workflow.** It
backs `builtin:legacy-coding`; `builtin:coding` uses the
*stepwise-final-review* IR, which has no `execute` node — its
implementation runs as a `foreach` of `step-execute`.
2. **The foreach mechanism would work.** `runForeach` propagates a
failing instance's `value` up as the foreach node's own value, so a
`steps` node could carry an `outcome:review-pending` edge.
3. **But `stepExecute` flattens it first.** The seam returns `value:
result.outcome === "success" ? "step-done" : "step-failed"`, discarding
the exit before it can reach any edge.

So on the default workflow the exit cannot reach an edge, and a compat
classifier in `handleGraphFailure` keyed on the failure value cannot see
it either. **Removing the inline handoff therefore regressed the default
path**: the card stopped reaching `in-review` at all.
`executor-step-session.test.ts`'s FN-5436 case caught it —

```
FAIL  FN-5436: pending-review skip on no-fn_task_done exit
      > parks in-review when review request has no subsequent verdict
      expected "moveTask" to be called with [ 'FN-5436-B', 'in-review' ]
      Number of calls: 0
```

I could have made that green by relaxing the assertion. That would have
been appeasement of a test that was telling the truth, so the behavior
commit came out instead.

**Also caught, and worth noting as the ratchets earning their keep:**
the PR1 ownership ledger flagged the change as `runImplementation` 3 → 2
review handoffs and `handleGraphFailure` 0 → 1 — i.e. a *relocation*,
not an elimination, for every non-plain-`execute` shape. That number is
what turned "this move is good" into "this move is only good for one
workflow shape". And PR3's routing-unchanged pin plus its out-of-band
adjacency ratchet both fired, forcing the routing change to be declared
rather than slipping in.

## PR5

Thread the implementation exit through the step-session chain
(`runImplementationPhase` → `graphStepRunOnce` → `runGraphTaskStep` →
`runProjectedGraphTaskStep` → `stepExecute`) so the seam can return
`review-pending` instead of flattening to `step-failed`; add the node +
edge to the stepwise IRs; then flip the execute seam and delete the
inline handoff **in one correct step** for every built-in shape at once.
The compat path for user-authored graphs is then a single named
classifier rather than a call buried two thousand lines into a session
loop.

## Verification

- `builtin-coding-workflow-ir` + `builtin-workflows` — 76 tests green
(the layout-completeness contract required a layout entry for the new
node; it is placed off the main line because the park is an exit, not a
stage)
- `executor-step-session` + ownership ledger + exit events — 50 tests
green, unchanged
- `pnpm test:gate` green (309/10/71); `pnpm lint` clean
- Changeset included (`patch`, `internal`)

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:19:28 -07:00
gsxdsm
e9bfd0d313 test(engine): prove the admission-control agent count on a renamed board — and two of my own cases were vacuous (#2516)
Test-only. Fifth E2E family. Closes three of the four `live-agent-count`
classifications.

## Why this one is a scheduler bug, not a display bug

`live-agent-count.ts` classifies a card's column, and
`persistedTopLevelAgentSlotsFromStore` turns that into **the number
admission control compares against the cap**. So a mis-classified column
fails in whichever direction hurts:

| mis-classification | consequence |
|---|---|
| wip column not recognised | under-count → **over-admits past the
operator's cap** |
| complete column not recognised | a finished card counts forever →
**board silently stalls** |

Both are silent, and both land only on a renamed board.

Everything in the path is real: PostgreSQL store, real persisted
workflows, cards walked through the real transition policy, and the real
counting function resolving each card's own IR. Nothing about counting
is reimplemented here.

## Two of my own cases were vacuous — mutation-testing caught it

This is the more useful half of the PR.

**1. "does not count a card in the COMPLETE column" passed with the
terminal classification hardcoded to `done`.** `isRunningAgentTask`
rejects that card at the *wip* check anyway, so the test was really
asserting "shipped isn't a wip column". `terminalKind` short-circuits
**first**, so it only changes the answer for a card whose status would
otherwise make it count. Now covered by a complete card carrying a
live-looking `planning` status — the state a crashed run leaves behind,
which on a renamed board consumes a slot forever.

**2. The review/merge lane had no case at all.** A review status is
deliberately *not* globally live (a stale `fixing` in wip must not
consume capacity), so it is gated on `columnIsReviewOrMerge`. If the
renamed review lane isn't recognised, a genuinely-active reviewer stops
counting and admission control lets another agent in over the cap. Now
covered by a review card with an active merge-pipeline status.

Both new cases assert their fixture took effect first, so they can't
degrade back into the weaker version silently.

## Mutation-verified independently

| classification | mutation | result |
|---|---|---|
| `countsTowardWip` | → `"in-progress"` | 3 renamed cases fail |
| `complete` | → `id === "done"` | exactly the new terminal case fails |
| `mergeBlocker` | → `id === "in-review"` | exactly the new review case
fails |

## What this does NOT cover, stated plainly

The fourth classification, `columnIsIntakeOrHold`, is read only by the
**waiting** predicate, which the admission count never calls. It stays
in the ledger as unproven rather than being claimed by proximity — the
mistake I made last slice with `resolveMergeOrchestrationColumn`.

## Verification

- five live-E2E suites green together: **52/52**
- engine `tsc --noEmit` clean
- `pnpm test:gate` green (309 + 10 + 71)

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

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

## Summary by CodeRabbit

* **Tests**
* Added end-to-end coverage for live agent-count admission across
workflow lanes and lifecycle states.
* Verified slot handling for active, completed, held, and mid-review
tasks, including stale statuses.
* Confirmed mixed-lane counts and renamed board vocabularies produce
consistent results.
* **Documentation**
* Expanded coverage notes for live agent-count classifications and
waiting-state behavior.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:19:22 -07:00
gsxdsm
063978c289 U12 part 3: make the v1-IR persistence unconditional — after this, every raw-flag read is on the move path (U2b) (#2513)
## U12 part 3 — every remaining raw-flag read is now on the move path

**Stacks on #2512** (shares a line in `workflow-ops.ts`). Merge that
first.

**Behaviour-preserving. Not a single persisted byte changes.**

### What changed

The three v1-IR rollback-compat persist sites (#1405) all read `flagOn ?
ir : downgradeIrToV1IfPure(ir)`, where `flagOn` came from the retired
raw `experimentalFeatures.workflowColumns` key. No production writer
sets it, so **every real project has always taken the downgrade arm**.
Removing the branch is a runtime no-op; it deletes three flag reads.

Sites: `createWorkflowDefinitionImpl`, `updateWorkflowDefinitionImpl`,
and `insertWorkflowDefinitionSyncImpl` — whose `flagOn` *parameter* is
gone too, along with the plumbing that resolved it in
`migrateLegacyWorkflowStepsImpl`.

With those gone, **`TaskStore.workflowColumnsFlagOn()` has no callers
and is deleted.** Its six readers were the three U5 guards (part 2) and
these three persist sites.

### The decision I made, and why I went the other way

I had this slice scoped as "retire the v1 downgrade." **I rejected
that.** It is a compatibility affordance, not cutover machinery: it
fires only for a graph exactly equivalent to pure v1 (default columns,
default placements, no v2-only features), and `upgradeV1ToV2` re-reads
it into an identical v2 graph, so the runtime never sees a difference.
Retiring it would break a binary downgrade for zero benefit — and stale
binaries opening these databases is an **observed event** in this
project, not a hypothetical.

So the slice became the strictly better version of itself: same three
flag reads removed, no compat surface touched.

### Why this matters for sequencing

`isWorkflowColumnsCompatibilityFlagEnabled` survives. It is still read
by `moves.ts:363` and by `workflow-task-create-ops.ts:351`'s move-policy
preflight that feeds it. Removing those reads **is** the U2b move-path
convergence with its equivalence-proof obligation.

The point of deleting the wrapper is that it makes the remainder
enumerable:

```
$ grep -rn isWorkflowColumnsCompatibilityFlagEnabled --include=*.ts packages/ | grep -v __tests__
packages/core/src/store.ts:38                      <- the definition
packages/core/src/task-store/moves.ts:9,363        <- U2b
packages/core/src/task-store/workflow-task-create-ops.ts:11,351  <- U2b (feeds moves.ts)
```

**Every surviving read is on the move path.** U2b deletes the definition
and the unit closes.

### On coverage — stated honestly

This change is behaviour-preserving, so it has **no revert-proof test**,
and I am not going to claim one. `flagOn ? ir : downgrade(ir)` with an
always-false flag *is* `downgrade(ir)`.

What needed a guard is the next edit someone is tempted to make —
deleting `downgradeIrToV1IfPure` as dead cutover machinery. New
`workflow-ir-v1-rollback-persistence.test.ts` fails if it is removed,
and pins the exact boundary: the built-in coding workflow (named columns
+ traits) stays v2; a pure-v1-equivalent graph stores as v1 without the
synthesized `columns`; a downgraded graph re-parses to an **identical**
runtime graph (the property that makes unconditional application safe);
a graph with a custom column stays v2.

### Verification

`pnpm test:gate` (307 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17
steps), typecheck green. Core workflow-named suites: 383 passed, 1
failed — `workflow-ir-settings.test.ts > moved-key catalog ...`
(`expected 10 to strictly equal 3`), which I confirmed fails identically
on a stashed clean tree. Pre-existing, unrelated. No Fusion instance
booted.


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

* **Bug Fixes**
* Improved workflow persistence compatibility by consistently storing
pure v1-equivalent workflows in the compatible format.
* Preserved v2 workflows and custom column information when they are not
v1-equivalent.
* Retired obsolete feature-flag checks without changing stored workflow
or board behavior.

* **Tests**
* Added coverage for workflow version preservation, rollback-compatible
serialization, and custom columns.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:09:06 -07:00
gsxdsm
2e39763930 test(engine): prove agent-link hygiene on a renamed board — a leaked agent slot, not a stale link (#2514)
Stacked on #2510. Test-only. Closes the `task-agent-sync` ledger entry.

## The defect this reproduces, in the code's own words

`task-agent-sync.ts`'s conversion note:

> a move into a renamed terminal column matched nothing and this handler
returned early — so the agent kept a `taskId` pointing at a finished
card and stayed `running`, **with no error and no failing test**.

"No error and no failing test" is the whole problem — and the cost is
not a stale link. **The scheduler counts `running` agents against its
cap**, so on a renamed board every completed task permanently consumes
an agent slot until a human notices. A board would just get slower and
slower.

## Everything in the path is real

Real PostgreSQL `TaskStore`, real `AgentStore`, the real
`attachAgentLinkSync` subscribed to the store's real `task:moved` event
(the same call `in-process-runtime` makes), and a real `moveTask` to
trigger it. Assertions read the **agent row** back out of the store —
never "the handler was called".

## Mutation-verified

Forcing the legacy literal sets (the pre-conversion behavior) fails
**exactly the two renamed cases**, leaving the default-vocabulary floor
and both negatives green. So this reproduces the original defect rather
than merely covering the file.

## Negative half

An ordinary mid-lifecycle move (`wip → review`) must **not** release the
agent — given the same time to run as the positive case. "Clear the link
whenever the card moves" would drop the binding the moment work started,
a louder failure than the leak it fixes.

## Two anti-flake, anti-vacuity details

- **Async delivery.** `task:moved` is a plain EventEmitter and the
handler is async, so the assertions **poll the persisted row** to a
bounded deadline and fail with the row's actual contents. A fixed sleep
would flake in both directions.
- **The fixture asserts itself.** The link and `running` state are
verified *before* the move, so an agent that was never linked cannot
make this pass for the wrong reason — the failure mode I hit twice
already in this program.

## Verification

- four live-E2E suites green together: **39/39**
- engine `tsc --noEmit` clean
- `pnpm test:gate` green (307 + 10 + 71)

🤖 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**
  * Improved agent-link cleanup when tasks reach workflow completion.
* Ensured completed task links are released even when workflow columns
have been renamed.
* Preserved active agent links when tasks move through non-terminal
workflow stages.
* Improved reporting of link cleanup outcomes and handling of
synchronization errors.

* **Tests**
* Added live PostgreSQL end-to-end coverage for completion, in-progress
moves, and renamed-column scenarios.

<!-- 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:59 -07:00
gsxdsm
3badc244a7 U12 part 2: bind the three U5 reconciliation guards — USER-VISIBLE (and one path that couldn't run under PostgreSQL at all) (#2512)
## U12 part 2 — the three U5 reconciliation guards now actually fire

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Revert-proof, measured

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

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

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

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

### Where I nearly got this wrong

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

### Deliberately NOT in this PR

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

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

### Verification

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


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

## Summary by CodeRabbit

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


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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 18:52:46 -07:00
gsxdsm
3bbb6ffc6b capacity, part 2: delete the cross-project concurrency cap (enforcement half) (#2509)
Stacked on #2502 — review that first; this branch contains its three
commits.

Operator: two capacities **per project**. `globalMaxConcurrent` is a
machine-wide *third* limiter kept in a separate authority (a central-DB
singleton row) that every runtime had to subscribe to and periodically
re-reconcile. It goes.

This slice removes **enforcement and wiring only**. The setting key,
central DB state, API route and Settings UI come out in part 3, so each
half lands green and independently revertable.

**Deleted:** the shared `AgentSemaphore` instance in `ProjectManager`
and `ProjectEngineManager`; the per-project `ScopedAgentSemaphore` in
`InProcessRuntime`; the `globalSemaphore` runtime-config field; both
`concurrency:changed` subscriptions; ProjectManager’s 30s limit-refresh
poll; the residual-slot return on project stop.

The scheduler/triage semaphore gate is now simply **absent** — same
shape as the worktrees-off gate in #2502. `semaphoreGate?` was already
optional, so no gate object is constructed rather than one holding an
infinite limit. Absence cannot start binding again by accident.

---

## Two findings that changed the shape of this slice

**1. `AgentSemaphore` the class stays — my earlier estimate was wrong
and I withdraw it.**

I previously told the coordinator that ~75% of `concurrency.ts` (≈662 of
886 lines) was semaphore machinery that could go with this cap. That was
line-range arithmetic, and it was wrong. `AgentSemaphore` is a general
primitive with four consumers unrelated to the global cap:

| Consumer | Governs |
|---|---|
| `verification-concurrency.ts` | `maxConcurrentVerifications` |
| `research-orchestrator.ts` | research `maxConcurrentRuns` |
| `experiment-executor.ts` | `maxConcurrentExperiments` — **a knob
absent from my original inventory** |
| `step-session-executor.ts` | parallel workflow steps |

What goes is the global **instance** and its wiring, not the class. I
will report the measured `concurrency.ts` delta after part 3 rather than
repeat an estimate.

**2. `acquireGlobalSlot` / `releaseGlobalSlot` had no production callers
— only tests.**

So the cross-project cap had *two* mechanisms: the in-memory semaphore
(live) and a durable central-DB `currentlyActive` counter (dead — never
incremented by real work). Both deleted, along with the tests that
pinned the dead passthrough.

## The regression this almost introduced

`runWithMergeAdmission` in `project-engine.ts` opened with:

```ts
if (!semaphore) return await start();
```

Unreachable while a global semaphore always existed. With the semaphore
gone it would have fired on **every** merge and skipped
`projectAdmissionCoordinator.admitOldest` entirely — silently stopping
merges from counting against the **per-project** agent count.

That is the opposite of the intent: a merge *is* an agent and still
consumes one of the project’s slots; it just no longer consumes a
machine-wide one. So the early return is **deleted rather than left to
fire**. `admitOldest` already declares `semaphore` as optional and
enforces `maxConcurrent` independently of it (`claimed() + reservations
>= maxConcurrent`), so dropping the argument preserves per-project
admission and oldest-first fairness exactly.

Worth flagging as a pattern: this is the third time in this unit that a
branch which was *unreachable* became *always-taken* once a limiter was
removed. The type system caught the worktree one; this one was only
visible by reading the branch, because the semaphore was reached through
an `any` cast.

## Verification

`pnpm lint` clean · engine `tsc` clean · `pnpm test:gate` green (309 +
10 + 71) · project-manager + hybrid-executor + merge-single-flight +
scheduler 93/93.

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 18:52:39 -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
35b0df1838 U11 PR2: entry contract under the merged column + a real intake-column bug the audit surfaced (#2503)
Second small PR for **U11**. Two commits: a tests-only entry-contract
pin, then a **real present-day bug fix** the audit surfaced.

## The audit you asked for, finished — no design fork

You named four surfaces as the remaining risk. All four can take a
combined `intake` + `hold` column. One needed a code change; here it is.

| Surface | Verdict | Evidence |
|---|---|---|
| `isUnplannedForExecution` | Safe | PR1 (#2495) — passed unmodified; a
mutation now fails exactly the merged-column test |
| Capacity hold / release | Safe | PR1 — `hold-release.ts:260` already
accepts intake **or** hold |
| `start`'s column / entry contract | Safe | commit 1 — all 6 assertions
passed unmodified |
| `createTask` intake wiring | **Broken today** | commit 2 — fixed,
revert-proven |
| *(also found)* triage auto-discovery | Needs conversion |
`triage.ts:1382` — deferred to PR3, see below |

## Commit 1 — entry contract under the merged column (tests only)

All 6 new assertions passed on the first run. **Regression floor, not
evidence of a fix** — I could not make them fail and am not claiming
otherwise.

They pin one real behavioral **difference** rather than asserting
sameness everywhere: the merged shape answers `start` where the split
shape answers `plan`, because `start` becomes the first node in that
column once the columns collapse. That is equivalent *only* because
`start` reaches the specification node by a single unconditional success
edge — asserted, so if a node is ever inserted between them this fails
instead of silently admitting an unspecified card into implementation.

Also pinned: past planning both shapes agree exactly; a card past the
merged column still never resumes at a planning node (the backward drag
that fires `abort-on-exit`); and a row persisted in the **deleted**
`triage` column resolves to `undefined`, safe only while the executor's
start-node fallback exists.

## Commit 2 — a real bug, found by the audit

The intake column was resolved **only** as a by-product of materializing
workflow steps. A create supplying `enabledWorkflowSteps` without an
explicit `workflowId` takes **neither** materialization branch, so
`resolvedEntryColumn` stays `undefined` and `column:` falls through to
the hard-coded `|| "triage"`.

Today, on Coding (Ideas), that lands the card in `triage` — **a column
that workflow does not declare.** Created straight into a phantom lane.
Measured: the new test fails `expected 'triage' to be 'ideas'` against
unmodified sources.

**Why it blocks U11.** Once `triage` leaves the coding IRs this stops
being an Ideas edge case and becomes the default workflow's behavior for
every create down this path: the card lands in an undeclared column
**and** — because `isIntakeColumn` keys on the same `"triage"` literal —
gets `generateSpecifiedPrompt` instead of the bootstrap seed. Triage
admits a card for planning only when its `PROMPT.md` reads as a seed, so
a placeholder spec is classified "already planned" and never planned.
The card sits in Planning forever with no log line in any lane —
**FN-8587's exact failure mode, promoted from one edge case to every new
card.**

The fix resolves the intake column **side-effect-free** (read the IR,
ask which column carries `intake`). It deliberately does *not* call
`materializeDefaultWorkflowSteps`, which would persist step rows the
caller explicitly opted out of by supplying its own toggles.
Unresolvable workflow returns `undefined` and each call site keeps its
legacy fallback, so no path loses behavior when the IR cannot be read.

Applied to both create paths. Branch ordering preserved in both — the
explicit empty-toggle case (`length === 0` hydrating back as `[]`) still
runs, now nested rather than sequential.

**Revert check:** with `task-creation.ts` reverted, *"lands a Coding
(Ideas) task in ideas even when enabledWorkflowSteps is supplied"* fails
`expected 'triage' to be 'ideas'`. The companion bootstrap-`PROMPT.md`
assertion passes either way today — it is correct **by accident of the
`"triage"` literal** — and is kept precisely because that accident
disappears with U11.

## Verification

37 tests green across the three intake/create suites; 119 across the
entry-contract, merged-column and lifecycle suites; `pnpm test:gate`
green (307 + 10 + 71); lint and core typecheck clean. Changeset added.

## Deferred to PR3, with the line numbers

`discoverReadyPlanningTasks` has two hardcoded branches:

```ts
(t) => t.column === "triage" && isTaskStillInPlanningStage(t)   // triage.ts:1382
(t) => t.column === "todo"   && !this.processing.has(t.id) …    // triage.ts:1389
```

Delete `triage` and branch 1 matches nothing for coding cards; branch 2
then does all the work and is **narrower** (it admits only
`needs-replan` or bootstrap-stub cards). Commit 2 is what makes branch 2
sufficient — every new card now gets a real bootstrap seed. They cannot
double-fire: a card is in `todo` xor `triage`.

Two adjacent sites are already merged-shape-ready: `triage.ts:3899`
skips the redundant same-column move for a plan-in-place card, and
`triage.ts:753`'s stale-status sweep already scans both columns.

Then the ~10-line IR change, then the migration proof.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:35:14 -07:00
gsxdsm
9d3e53d0c5 U8 PR3: the implementation phase announces HOW it ended — including when the executor moved the card itself (#2507)
Third PR of **U8 — the graph owns execution**. Independent of everything
merged so far; small, green, revertable on its own.

## The problem this makes visible

`result.taskDone` is the entire language the execute seam has for
talking to the graph:

```ts
if (result.taskDone) return { outcome: "success", value: "implemented" };
return { outcome: "failure", value: paused ? "implementation-paused" : "implementation-incomplete" };
```

The endings that one bit cannot express are exactly the ones the
implementation phase **transitions itself**:

- a session that paused *after* the work was already complete →
finalizes to review inline;
- a session that stopped because a step is blocked on a pending review →
hands off to review inline (a pending-review block is a wait, not a
failure; marking it failed deadlocks a row that is both `in-review` and
`failed`).

The graph then sees `taskDone === false`, reports
`implementation-incomplete`, and `handleGraphFailure` compensates with
`alreadyFinalizedToReview` / `completionFinalized` — classifiers whose
entire job is recognising a move the graph did not make.

**That was invisible.** An out-of-band transition and a genuine
implementation failure were indistinguishable in logs, in events, and in
tests. You cannot remove a transition you cannot see, and you cannot
prove you removed it either.

## What lands

A closed `ImplementationExit` enum
(`engine/executor/implementation-exit.ts`) reported from six
completion-adjacent exits in `runImplementation`, announced by the
execute seam as `NodeCompleted.exit` on the U3 lifecycle bus. Two ids
are flagged as out-of-band — the ones where the executor, not the graph,
performs the transition.

**Routing is unchanged, and that is the point.** The seam returns
byte-identically what it returned before for every exit, so this PR
cannot move a card. The routing move needs new IR edges and lands
separately; splitting them is what keeps both independently revertable.
Per R5 an exit id is a **reaction** — nothing branches on one, and
dropping every subscriber must change no outcome (a named U8 test
scenario, asserted here).

`NodeCompleted.exit` is added to the event key allow-list deliberately —
which is exactly what that allow-list is for — and carries closed enum
ids only, never prose.

## Revert-proofs, each observed failing

| Injected change | Result |
|---|---|
| Remove the emit entirely | **6 failures** |
| Let an exit change the returned outcome | **2 failures** (the
routing-unchanged pins) |
| Delete one `reportImplementationExit(...)` call site | **1 failure**
(the wiring ratchet) |

**The third proof exists because of a hole I found in my own tests.**
These tests stub `runImplementationPhase` — the only way to reach all
six exits deterministically — which means deleting a real call site left
the entire file **green**. A stubbed seam can only prove the seam. I'd
also written "every exit is reported — the signal is real, not a
placeholder" in the header, which the tests did not support. Both are
fixed: there is now a ratchet asserting every enum id is wired at a real
call site and that each out-of-band id sits adjacent to the handoff it
describes, and the header says what the tests actually prove.

## Scope

**6 of `runImplementation`'s ~28 dispositions** (per the ownership
ledger merged in #2490), chosen as the ones the routing move needs. The
remaining ~22 report nothing yet — the ledger, not this enum, stays the
record of that gap, and the module says so.

## Verification

- 15 new tests + ledger + graph-boundary + task-done-blocked +
graph-requeue-gate + step-session + review-verdicts + tool-failure-retry
— **9 files, 115 tests green**
- `@fusion/core` `workflow-events` — 20 tests green (allow-list change
covered)
- `pnpm test:gate` green (17/307, 2/10, 1/71); `pnpm lint` clean; `tsc
--noEmit` clean on both packages
- Changeset included (`patch`, `internal`), passes `check:changesets`

## Next

PR4 is the routing move itself: `review-handoff-pending-review` becomes
a graph outcome with its own IR edge, and `alreadyFinalizedToReview`
becomes provably unreachable for that path. The IR edge change will be
its own commit, separate from the seam change.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:35:07 -07:00
gsxdsm
d5030c55ea test(engine): prove BOTH rebound paths on a renamed board — 2 more ledger entries closed (#2510)
Stacked on #2508. Test-only.

## Why this site matters more than most

`resolveReboundTarget` answers one question: **where does a recovered
card go back to?**

Keyed on the literal `todo`, a recovered card on a renamed board is
requeued to a column that board **does not declare**. That is not
cosmetic — an undeclared column carries no trait flags, so `findColumn`
returns undefined and the card becomes invisible to every trait-driven
sweep: nothing schedules it, nothing releases it, the board does not
draw the column. **The "recovery" strands the card harder than the
failure it was recovering from.**

One of the two covered paths, `reconcileUndeclaredTaskColumns`, exists
*specifically* to repair that state — which makes it the worst possible
place for this bug to live.

## Covered, each mutation-verified independently

| site | mutation | result |
|---|---|---|
| `reconcileUndeclaredTaskColumns` | target → `"todo"` | exactly the 2
renamed cases fail |
| `autoRecoverWorktreeSessionStartFailure` | rebound → `"todo"` |
exactly the renamed requeue fails |

Neither needs git — the corrected ledger lens from #2508 (*what the
function touches*, not *what family it sits in*) made that obvious
rather than assumed.

## Both negatives included

"Re-home anything whose column looks wrong" would be a louder failure
than the strand it repairs, so: a card whose column **is** declared is
left alone, and an operator `userPaused` park is never undone.

## Fixture finding, kept in-file

`updateTask({ userPaused: true })` leaves the field `undefined` on both
`getTask` and `listTasks({slim:true})`. Seeding it that way produced a
card the sweep **correctly** saw as unpaused — a broken fixture that
would have read as a broken guard, and would have looked like a real
safety hole in the paused-park protection.

Found by probing the persisted row rather than trusting the write. Now
seeded through the integer column directly, and the test asserts the
seed took effect *before* exercising the sweep, so this cannot silently
regress into a vacuous pass.

## Verification

- three live-E2E suites green together (lifecycle, merge-family,
rebound-family)
- engine `tsc --noEmit` clean
- `pnpm test:gate` green (307 + 10 + 71)

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:35:01 -07:00
gsxdsm
aeef592187 docs(U9): safeguard baseline — six merge safeguards verified by mutation (#2511)
**U9, PR3.** Docs-only, one new file, zero production changes. No
changeset (internal docs). Independent of #2504.

This is the six-row safeguard table required as a U9 artifact —
delivered **verified**, replacing the partial, unearned version I put in
#2494.

## Every row proven by mutation

Break the guard in production, run the cited tests, confirm red,
restore. A cited test that does not fail is not evidence.

| # | Safeguard | Consulted at | Result under mutation |
|---|---|---|---|
| 1 | user pause | `project-engine.ts:645` (merge admission filter) |
**11 failed** / 1865 passed |
| 2 | `autoMerge:false` | `project-engine.ts:2797`
`allowsAutoMergeProcessing` | **9 failed** / 250 passed |
| 3 | dependency gating | `task-merge.ts:402` unresolved-dependency
reason | **5 failed** / 94 passed |
| 4 | capacity | `project-engine.ts:3178` single-flight `mergeRunning` |
**11 failed** / 1445 passed |
| 5a | merge-proof (pre-enqueue) | `project-engine.ts:2609`
`getTaskMergeBlocker` consult | **1 failed** / 272 passed |
| 5b | merge-proof (file scope) | `merger-file-scope.ts:200`
`FileScopeViolationError` | **6 failed** / 172 passed |
| 6 | at-most-once | `project-engine.ts:2730` `mergeActive` dedupe | **3
failed** / 263 passed |

**All six hold. Nothing is currently broken.** Per-row test attribution
is in the doc.

## Finding 1 — one of nine safeguard test files runs in blocking CI

Only `merger-merge-lifecycle.test.ts` is in the `engine-core`
allow-list. The core gate is two PG tests (`test:pg-gate`) and does not
include `task-merge.test.ts`.

AGENTS.md: CI blocks on Lint/Typecheck/Build/Gate, and "a red
non-blocking run is information, not a merge stopper."

So a change breaking **user pause on merge admission, dependency gating,
capacity single-flight, or the file-scope invariant** does not block a
PR today — it goes red in full-suite, after the merge.

Acceptable for a lane nobody is rewriting. Wrong for the lane U9
rewrites next. **Recommendation: admit the highest-value safeguard tests
to the gate before conversion begins, with the budget cost measured** —
engine-core is 5.36s against a ~60s ceiling, so there is room, but I
won't assume it. Proposed as PR4.

## Finding 2 — safeguard 5a rests on a single non-gate test

Removing the pre-enqueue merge-blocker consult fails exactly one test.
Thinnest of the six, on a destructive-risk gate. Its sibling 5b is well
covered (6 tests), so the invariant isn't unguarded — but the consult
that keeps a blocked task out of the queue very nearly is.

## Methodology note, because it cost an hour

**Rows 1 and 3 initially measured ZERO failures and looked like coverage
gaps. Both were wrong** — the test selection was too narrow. Widening
row 1 from three files to
`project-engine|merge|concurrency|self-healing` turned 0 failures into
11. Row 3's real coverage lives in `@fusion/core`'s suite, which `pnpm
--filter @fusion/engine` never runs, even though the engine config
aliases `@fusion/core` to source so the mutation *was* live.

I nearly reported two false gaps. Recorded as two rules: a narrow
mutation run cannot prove absence of coverage, and cross-package guards
need cross-package runs.

## Scope

New file only — deliberately **no** edit to
`docs/workflow-policy-ownership-map.md`, because #2504 already edits
that file at the same anchor and I want both PRs independently
revertable. Cross-link follows once both are on main.

Verified: no production diff, all mutations restored, `pnpm lint` clean.

## Not covered, stated rather than implied

Reviewer-lane safeguards; FN-7720 operator bypass; FN-8492
orphaned-pending-step rewrite; branch-group promotion sequencing. Each
needs its own verified row before the matching conversion.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:50 -07:00
gsxdsm
4eaa509024 test(engine): prove merge finalization on a renamed board — 3 ledger entries closed, and the ledger itself corrected (#2508)
Test-only. Closes three `auto-merge-finalization` entries from the
unproven-sites ledger.

## Why this one first

It is the **last move a card makes**. Keyed on the literal `done`, a
renamed board's proven-merged card is moved to a column its own workflow
does not declare — or refused and left stranded in review **with the
work already landed**. That is the most expensive failure shape in the
lifecycle, and nothing had run it against a renamed workflow.

## The ledger was wrong, and is corrected in this PR

My own ledger said this family *"needs a REAL git worktree, branch, and
squash … an engine-slow real-git lane, not another table row"*.

`finalizeProvenAutoMergeTask` **needs no git at all** — the merge proof
is a field on the row. It was reachable the whole time. The inference
came from *the family the code sits in* rather than from what the
function actually touches, and it parked reachable coverage for a slice.
The correction is written into the ledger so the remaining entries get
re-checked the same way rather than inheriting the assumption.

## A second correction, from mutation-testing rather than reading

I first claimed `resolveMergeOrchestrationColumn` as covered because it
sits in the same resolver as the other two. **All cases passed with it
hardcoded.** It changes only whether finalization records a
column-mismatch *repair* — never where the card lands, which is why the
other cases are blind to it.

It got its own case. Keyed on `in-review`, a renamed board's card
resting in `checking` compares unequal, so **every ordinary finalization
would be audited as repairing a mismatch that never existed** — a
healthy board reads as one constantly self-healing, and the audit trail
operators use to spot real strandings fills with false positives.

Sitting next to covered code is not coverage.

## Mutation-verified independently

| mutation | result |
|---|---|
| `completeColumn` → `"done"` | 3 fail — both renamed cases + the
differential |
| `isCompleteColumn` → `id === "done"` | exactly the already-done case
fails |
| `mergeColumn` → `"in-review"` | exactly the new audit case fails |

## Shared fixture extracted (pure move)

The vocabulary + IR builder moved to `_workflow-vocabulary-fixture.ts`
so the two suites cannot drift into testing different workflows — two
copies of a differential fixture is precisely how a renamed-workflow
test starts passing for reasons unrelated to the code under test. The
lifecycle suite is unchanged: **20/20 before and after**. The
`mergeOrchestration` trait is an opt-in option so the existing suite's
IR stays byte-identical.

## Fixture note worth keeping

Seeding needed **completed steps**: task creation parses three pending
steps out of the bootstrap PROMPT even with `applyDefaultWorkflowSteps:
false`, and `getTaskHardMergeBlocker` refuses on them (`"task has
incomplete steps"`). Found by the suite blocking on **both**
vocabularies — the signature of a broken fixture rather than a broken
guard.

## Verification

- 51/51 across the lifecycle, merge-family, ratchet and hold-release
suites
- engine `tsc --noEmit` clean
- `pnpm test:gate` green (307 + 10 + 71)

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:35 -07:00
gsxdsm
8288e4a8ab U7 PR3: the specification reaction acts on what finalize DID, not on the fact that planning stopped (#2506)
Completes the pair started in #2498. That landed the outcome; this makes
the engine's reaction consume it.

## The bug

`onSpecifyComplete` fired on **every** finished specification, because
the seam announcing it fired unconditionally. So a card parked at the
manual plan-approval gate — finalize writes `status:
"awaiting-approval"` and **returns early**, before the release move —
was logged as `Specified X → todo` and had a Plan Review run armed for a
plan the operator had not approved.

#2491 stopped the **seeder** from acting on that, defensively, at the
seeder. This removes the reason it was ever asked. Both layers are
deliberate and neither is redundant:

- the seeder guard covers **every caller**, including self-healing's
re-seed;
- this one stops the engine doing work nobody asked for, and stops it
telling the operator something false about their own board.

`released` is the only outcome that licenses arming a run — the only one
meaning the card crossed into the hold column (or was already resting
there, plan-in-place) and is the graph's now. `parked` belongs to a
human; `withheld` belongs to the caller's retry budget.

## The event still fires on every outcome

Deliberately. Dropping the reaction for a non-release would also drop
the runtime's `recordActivity()` idle signal, and a reaction that
silently does not happen is harder to reason about than one that happens
with an accurate payload. R5's division of labour: **the seam announces,
the subscriber decides what a given outcome licenses.**

## Why there is a new extracted function

`reactToSpecificationComplete` is pulled out of the inline
`InProcessRuntime` callback for the same reason the continuation drain
was in #2491: the callback is built inside a class whose construction
attaches to the real central project registry, so no test could
distinguish *"the reaction respects the outcome"* from *"the reaction
ignores it"*.

**Revert proof:** with the outcome gate removed from the reaction, **5
of 8 fail**.

## Two call-site decisions worth naming

**`tryFinalizeExplicitDuplicateMarker` reports through a mutable ref,
not a widened return type.** Its boolean answers a *different* question
— "was this a duplicate marker at all?" — and 16 existing tests assert
it directly. I tried the widened return first and it turned all 16 red.
Expectation edits are exactly how a behavior change travels disguised as
churn, so I backed it out. **This diff touches zero existing test
expectations.**

**A duplicate-marker redirect reports `parked`**, which is accurate: it
deletes, flags, or clears the marker; it never releases the card into
the hold column.

## A fixture note — third of this shape on the program

My "task vanished between release and reaction" case passed `undefined`,
which triggered the harness **default parameter** and silently handed
the reaction a live task — making it a duplicate of the control rather
than the case it claimed to be. It now passes `null`, with a comment
saying why.

Running tally of near-false-greens on this unit, all the same family: a
fake that ignores its predicate (#2491), a stub that ignores its
callback (#2498), a default parameter that swallows the interesting
input (here). Each was caught by the test failing for the *wrong reason*
and being read rather than fixed.

## Verification

| Check | Result |
|---|---|
| new suite | 8/8 |
| 15 triage / planning / continuation suites | 361/361, **no expectation
edits** |
| `tsc --noEmit` (engine) | clean |
| `pnpm lint` | clean |
| `pnpm test:gate` | green (307 + 10 + 71) |
| `pnpm check:changesets` | clean |

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:29 -07:00
gsxdsm
a2b4ca76ac U11: delete the unreachable legacy todo dispatcher from scheduler.schedule() (-929 lines, pure deletion) (#2505)
Based on `main`. **Pure deletion — no behavior change**, because the
deleted code cannot execute.

## Found while trying to convert it

This started as a U11 slice to make the scheduler's dispatch path
resolve its column by trait. Per the lesson from the dependency-blocked
feature I checked reachability *before* converting:

```ts
function shouldRunWorkflowColumnScheduler(_settings: Settings): boolean {
  return true;                       // parameter UNUSED, body a literal
}
...
if (shouldRunWorkflowColumnScheduler(settings)) {
  await this.runHoldReleaseSweepPass(tasks, settings);
  ...
  return;                            // UNCONDITIONAL, at the block's own depth
}
<929 lines of legacy pull-from-todo dispatcher>   // unreachable
```

The guard takes an **unused** parameter and returns a **literal**, so
the branch is statically always taken, and it ends in an **unconditional
`return`**. Everything after it in `schedule()` is unreachable.

`tsc` doesn't flag it because the condition is a function call rather
than a literal — which is exactly why 929 lines survived the U6 cutover.
The replacement was added *in front of* the old dispatcher rather than
*instead of* it, and the in-file comment says so outright:

> the hold/release sweep owns todo→in-progress pickup, so do not fall
through into the legacy pull-from-todo dispatcher after the sweep runs

## Why this matters beyond line count

**4 of the 15 `"todo"` literals in `scheduler.ts` live in this dead
region.** Converting them would have been pure waste — and worse, it
would have reported progress against the U11 critical path while
changing nothing. 11 live sites remain and are the real work.

## Corroborating evidence

Six imports became unused and are removed with it:
`resolveDependencyOrder`, `sortTasksByPriorityFanoutThenAgeAndId`,
`buildUnblockWeightMap`, `TransitionRejectionError`,
`isUnplannedSeedPrompt`, `DEFAULT_WORKFLOW_POOL_ID`.

That the dead region was their **only** consumer in this file is itself
evidence: a live dispatcher would still need dependency ordering and
priority sorting.

## Why no new test

The proof here is **static, not behavioral** — an unconditional `return`
before the code. A test cannot demonstrate absence of execution more
strongly than the control flow already does, and one that passed both
before and after would be theatre.

The evidence that nothing depended on it: **all 100 scheduler tests and
the full merge gate pass unchanged.**

## Measured

`scheduler.ts` **3,726 → 2,797 = −929 lines.**

Unlike every consolidation in this program, this is a **genuine net
reduction** — nothing was moved elsewhere.

## Verification

100 scheduler tests green across all 7 scheduler suites; merge gate
green (307 + 10 + 71); tsc clean; lint clean.

No changeset: `@fusion/engine` is private.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-28 17:15:23 -07:00
gsxdsm
6ee20d9817 docs(U9): correct merge-stack slice statuses to measured wiring state (#2504)
**U9, PR2.** Docs-only, no changeset (AGENTS.md: internal docs).

## Why

All seven slices in `docs/plans/workflow-owned-merge-stack/` were marked
`draft-stack-handoff` — accurate when drafted 2026-06-09, wrong now. **A
worker picking up this stack cold would have re-implemented S04, which
has already landed.** I nearly did.

## Measured, against `main @ 46f35323c`

| Slice | Was | Now | Evidence |
|---|---|---|---|
| S02 projection | draft | `landed-unwired` |
`projectMergeRequestToWorkflowWorkItem` implemented, **0 production
callers** |
| S03 scheduler claim | draft | `landed-unwired` |
`claimDueWorkflowWorkItem` implemented; its only caller is S05's
processor, itself unwired |
| S04 IR regions | draft | **`landed`** | `merge-gate`, `merge-retry`,
`manual-merge-hold`, `merge-attempt`, `recovery-router` present in the
coding IR |
| S05 runtime driver | draft | `landed-unwired` | `runWorkItem` /
`processDueWorkflowWorkItem` implemented, exported from `index.ts`, **0
production callers** |
| S06/S07/S08 | draft | `not-started` | merge still runs through
`merger.ts` + the live `ProjectEngine.mergeQueue` pump |

## The finding that changes U9's sequencing

`WorkflowWorkItemKind` is `task | merge | retry | manual-hold |
recovery`. The only live pump —
`InProcessRuntime.drainWorkflowContinuations` — filters `kinds:
["task"]`. The generic processor that would claim the other four kinds
has **no production caller**.

So the entire merge-lane work-item vocabulary is dormant: **zero
writers, zero readers.**

**I checked whether this is a live bug and it is not.** Nothing in
production writes a non-`task` kind — the only two writers
(`plan-review-continuation.ts`, `workflow-column-boundary-hooks.ts`)
both go through `replaceActiveTaskWorkflowContinuation`. Nothing is
stranded today. I'd rather say that plainly than let a scary-sounding
finding stand unqualified.

But it produces a hard ordering constraint, now recorded in S07:

> **S07 must not land before S03/S05 are actually driven.** S07 is the
slice that starts writing `merge`-kind work items. If it lands first,
those items are created and never claimed — a card that reaches the
merge boundary and silently stops.

This also reframes U9's job on S02/S03/S05: **wire them, don't build
them.**

## Scope discipline

Docs-only — `git diff --stat` is 9 files, all under `docs/`. No
production code, no tests, no behavior. `pnpm lint` clean.

I also fixed the three parent-plan lines asserting the slices are "all
still `draft-stack-handoff`", and the four landed slices' Stack Role
paragraphs that would otherwise contradict their own new Measured State
block. Leaving those stale would recreate exactly the defect this PR
fixes.

Related: #2494 pins the S04 caveat — the IR regions are declared but
their config is read by nothing.

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

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

## Summary by CodeRabbit

- **Documentation**
- Updated workflow-owned merge planning documents with current
implementation and wiring statuses.
- Added measured wiring details showing which workflow capabilities are
active, implemented but unused, or not started.
- Clarified sequencing requirements to ensure merge processing is not
enabled before prerequisite workflow paths are operational.
- Corrected slice metadata and references to reflect the latest measured
state.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:15:16 -07:00
gsxdsm
46f35323cf fix(core): make the capacity gate actually bind for real projects (R2) — USER-VISIBLE (#2499)
Follow-up to #2488 (merged). **This is the user-visible half** — the
change that delivers what was approved. #2488 alone is latent.

## One line

`workflow-capacity.ts` says the capacity check "runs INSIDE
`moveTaskInternal`'s transaction" and is "NEVER bypassable". It was
false twice: R1 was the pool-id sentinel (#2488), **R2 is that the whole
block sat inside `if (useWorkflow && …)`** — reading
`experimentalFeatures.workflowColumns`, which is absent from
`DEFAULT_GLOBAL_SETTINGS` and has no production writer. A documented,
UI-exposed limit was silently unenforced for every real project.

**Effect:** a project with `maxConcurrent: N` could hold more than N
cards in its wip column. Now the move is refused with
`capacity-exhausted`.

## Scope is deliberately narrow

**Only the capacity check is un-gated.** `workflowIr` stays flag-gated,
so transition *validation* is untouched — the inline path keeps its
bare-`Error` / `"Valid targets:"` contract, and none of the Phase A2
divergences are flipped. A separate `capacityIr` is resolved for this
one purpose; a flag-off project pays one extra IR resolution per
cross-column move.

## The release path already expected this

`hold-release`'s own docstring:

> the in-txn capacity check is **NOT a guard — it still runs** (KTD-10),
so two holds racing into one slot serialize: exactly one commits, the
other rejects with `capacity-exhausted` and retries next sweep

and it reserves worktree + semaphore slots *before* issuing a move
specifically so it can release them on that rejection. **That handler
was dead code.** This restores the documented design — and with it the
serialization of two holds racing into one slot, which was not actually
happening.

## Measured blast radius — not estimated

| suite | with R2 | baseline | new failures |
|---|---|---|---|
| core PG (real store) | 1037 passed / 3 failed | 1037 passed / 3 failed
| **0** |
| engine-default | 279 failed / 9167 | 279 failed | **0**
(failing-file-set diff) |

The three core-PG failures are the same pre-existing ones that reproduce
with everything stashed. Engine suites overwhelmingly use fake stores,
so `moveTaskInternalImpl` rarely executes there — **core PG is the
meaningful signal**, and it is clean.

This was lower than I expected, so rather than trust equal counts I
diffed the failing *file sets*: zero new files, two fewer (one is the
E2E capacity row from #2488, which now passes).

## Acceptance

Flipped exactly as Phase A3 specified: `DEFECT (R2, STILL LIVE)` →
`FIXED (R2)`, and move-path-equivalence's capacity `DIVERGENCE` →
`CONVERGED`. **Both fail with this change reverted** (verified: 2 failed
/ 12 passed).

## Why I proceeded without a decision

I had escalated R2 and had no answer. Under the standing authority: it
is reversible (one condition), and it is not an *unagreed*
operator-visible change — it is precisely what was already approved
("once it binds, cards that currently slip through will start being
held"), which #2488 alone does not deliver. My recommendation was option
B and I acted on it. Revert is one PR.

Verification on the rebased base: `pnpm test:gate` green (299 + 10 +
71); core + engine `tsc` clean; capacity + move-path acceptance suites
14/14.

🤖 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**
- Column WIP limits are now enforced when moving tasks into full
columns.
- Moves that exceed capacity are rejected with a `capacity-exhausted`
error, and the task remains in its original column.
- Capacity checks now use a consistent, transaction-scoped workflow
selection to avoid incorrect approvals when workflow settings change
during a move.
- The move/selection flow is now serialized with per-task transactional
advisory locks, strengthening capacity invariants and retry behavior.
  - Existing transition validation behavior remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:28:00 -07:00
gsxdsm
e4004c8694 U9 baseline: pin merge-region IR config as a dead policy authority (test-only) (#2494)
**U9, PR1 of several.** Test-only, no production code touched. This is
the characterization baseline the plan's Execution note asks for before
the merge lane converts.

## The finding

`builtin-coding-workflow-ir.ts` declares merge-region policy that **no
engine code reads**:

| IR declaration | Consumed by |
|---|---|
| `merge-retry` → `{ policy: "merge", maxAttempts: 3 }` | nothing —
`retry-backoff` handler is `async () => ({ outcome: "success" })`
(`workflow-node-handlers.ts:728`) |
| `merge-manual-hold` → `{ release: "manual" }` | nothing — returns a
constant `manual-required` |
| `branch-group-*` → `{ maxReworkCycles: 3 }` | nothing — returns a
constant `success` |

Live merge policy authority is elsewhere, on two separate axes:
- **conflict** retries — `settings.maxAutoMergeRetries` (default 3),
already covered by `auto-merge-retry-cap-settings.test.ts`
- **transient** retries —
`ProjectEngine.MAX_AUTO_MERGE_TRANSIENT_RETRIES = 5`
(`project-engine.ts:545`)

So the IR is a **third, dead authority**. These are different axes, not
a same-axis contradiction — but a reader looking at the IR would
reasonably take the declared numbers as live, and nothing currently says
otherwise. U9's acceptance criterion is "merge policy changes via IR
config alone, with no code change"; that fails today and this pins why.

## Why characterization rather than a fix

Making these handlers config-driven is a **merge behavior change**, and
the `requestMerge` primitive it routes through lives at
`executor.ts:7383` — inside U8's blast radius. U9 is sequenced behind U8
precisely so the merge lane converts onto an executor that is already
substrate. Landing the behavior change now would change merge semantics
on an executor about to be reshaped. It lands inside U9 proper.

When U9 wires a node kind onto its IR config, the matching case here
goes **red** and the U9 commit must move that kind out of
`CONFIG_BLIND_MERGE_REGION_KINDS`. That is the ratchet working.

## Proof it fails when reverted

A test that passes with the change reverted is not a test. The "change"
here is the test itself, so the honest analogue is mutating the
characterized production behavior. Three independent mutations, each
reverted after measuring:

| Mutation | Result |
|---|---|
| `retry-backoff` honours `config.maxAttempts` (what U9 will do) | **2
failed** / 6 passed |
| `manual-merge-hold` honours `config.release === "external-event"` |
**2 failed** / 6 passed |
| IR declaration drift: `maxAttempts: 3` → `7` | **1 failed** / 7 passed
|

Measured: 8 tests, 4.16s. `pnpm lint` clean. Tree restored to clean
after each mutation.

The assertions are behavioral, not string matches: each handler is
invoked with two contradictory configs (opposite budgets, opposite
release modes, disjoint surfaces) and asserted to return deep-equal
results.

## Six safeguards

This PR changes no production behavior, so no safeguard is altered by
it. The full six-row table with test attribution is the required
artifact for the **conversion** PR, not this one. Baseline located so
far, to be completed and verified by mutation before any conversion
lands:

| # | Safeguard | Consulted at (today) | Test attribution |
|---|---|---|---|
| 1 | user pause | `project-engine.ts:645` (`task.paused \|\|
task.userPaused`) | not yet verified |
| 2 | `autoMerge:false` | `allowsAutoMergeProcessing` —
`project-engine.ts:2797`, `merger.ts:7178` | not yet verified |
| 3 | dependency gating | not yet located | not yet verified |
| 4 | capacity | not yet located |
`workflow-column-boundary-capacity.test.ts` (unverified) |
| 5 | merge-proof | `getTaskMergeBlocker` — `project-engine.ts:2609` |
`merger-file-scope-invariant.test.ts`,
`merger-diff-volume-gate.slow.test.ts` (unverified) |
| 6 | at-most-once merge | `activeMergeTaskId` single-flight —
`project-engine.ts:693`/`:2729` | not yet verified |

Rows 3, 4 and all attributions are honestly incomplete rather than
asserted — I will not present a table I have not earned.

## Also found, for the coordinator

- **Slice statuses are stale.** S02/S03/S04 in
`docs/plans/workflow-owned-merge-stack/` are all marked
`draft-stack-handoff` but S04 has **landed** (the merge-region IR nodes
above), S03's `claimDueWorkflowWorkItem` is implemented and wired via
`workflow-work-processor.ts`, and S02's
`projectMergeRequestToWorkflowWorkItem` is implemented with **zero
production callers**. S06/S07/S08 are genuinely not started. Doc
correction coming as its own small PR.
- **S1 prerequisite verified present, not assumed** — all four store
methods live in `store.ts`, migration `0031` in tree. No S1-completion
gap.
- **Second control plane into the merge lane:** `self-healing.ts:3198`
and `:7200` call `enqueueMerge` directly, bypassing the graph. That
needs to become a recovery-fact/wake (the stack's R6) during U9.

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

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

* **Tests**
* Added a new test suite to cover U9 merge-region behavior across
supported workflow node types.
* Verified merge-region results are consistent across built-in,
contradictory, and missing configuration inputs.
* Documented current behavior for retry backoff (always succeeds) and
manual merge hold (fails as manual-required).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:45:55 -07:00
gsxdsm
eaea082259 U8 PR2: the execution-policy ladder resolves its own workflow's columns (the wip literal made retry, escalation and loop protection unreachable) (#2497)
Second PR of **U8 — the graph owns execution**, independent of
[#2490](https://github.com/Runfusion/Fusion/pull/2490) and of every
other unit. Small, green, independently revertable.

## The defect

`handleGraphFailure`'s execution-policy ladder — FN-7863/FN-7926
dispatch-loop terminalization, FN-7996 tool-failure retry, FN-7998
escalation — decided a task's own lifecycle by naming `"todo"` and
`"in-progress"` **literally, at 9 sites**. U5b converted the executor's
*rebounds* to `resolveReboundColumnFor`; these were left behind, each
sitting somewhere an awaited resolver could not reach: inside
synchronous `updateTaskAtomic` mutators, inside fire-and-forget resume
closures, and in conditions evaluated before any resolution happened.

**The severe one is the wip gate, and it fails silently in the worst
direction:**

```ts
if (live.column !== "in-progress") {
  // "Workflow graph run ended after task already advanced — no further action needed"
  return;
}
```

Under a workflow that renames the implementation column, that is true of
a card sitting in **its own wip column**. So the graph failure was
swallowed whole — no terminal park, no status, no error, nothing on the
board — and the scheduler re-dispatched the same doomed run. Every later
branch sits behind that gate, which is why the retry budgets, the
escalation, and the bounded terminalization were **unreachable rather
than mistargeted**.

This is precisely the failure the program's problem frame predicts: *a
guard that stops matching disables a recovery path invisibly and the
suite stays green.* I found it because my first renamed-column test for
the escalation site could not reach the escalation code at all.

Two further sites misbehave once the gate is passable:

- **FN-7998 node escalation** wrote `column: "todo"` inside the atomic
claim — parking the card where no workflow declares it, which is on the
plan's **"Stop implementation if"** list and what R7 exists to clean up
after. The scheduler's effective-node resolution, the entire point of a
node escalation, never runs.
- **FN-7863/FN-7926's `live.column === "todo"` arm** is the classic
guard that stops matching. In-process the `executeNodeSelfRequeued`
marker covers the same case, so this degrades only on the **durable**
arm — after a restart, or for a second `TaskExecutor` instance in the
process, where the column read is the only evidence the inner executor
requeued. A progressing card then falls through to the terminal sink and
is parked `failed`.

## The fix

Resolve hold and wip **once per graph failure** through U1's
`resolveTaskLifecycleColumns` and thread the pair through the ladder.
Both fall back to the legacy literal when the workflow cannot be
resolved, so an unresolvable workflow keeps exactly its pre-conversion
behavior rather than guessing. One IR read on a terminal recovery path —
not an enumeration loop.

## Red-green, measured

**3 of the 8 new tests fail with this commit's executor change
reverted:**

```
FAIL  FN-7998 … > requeues a node escalation to the RENAMED hold column, not the literal todo
FAIL  FN-7998 … > still does not move the card for a MODEL-target escalation
FAIL  FN-7863/FN-7926 … > recognises an inner-executor requeue that landed in the RENAMED hold column
      Tests  3 failed | 5 passed (8)     ← reverted
      Tests  8 passed (8)                ← with the fix
```

The other **5 pass both ways by design**, and I am not claiming them as
red-green — they are the regression floor:

- default coding workflow still resolves hold → `todo`, wip →
`in-progress` (byte-identical);
- an unresolvable workflow still uses the legacy literals;
- the in-process self-requeue marker still works when no workflow
resolves;
- and a **negative case** proving the dispatch-loop gate stays narrow —
a card still in its wip column with no marker is a genuine execute
failure and must NOT be swallowed as a benign recovery. Widening that
gate to "any column" would have been the easy wrong fix.

## Scope

Deliberately the execution-policy ladder only. **20 further column
literals remain in the same method's pause-abort, merge, and in-review
regions** — they belong to U5's executor slice (B4, not started) and
U9's merge lane, and are untouched here. Flagging the overlap: this PR
edits `executor.ts`, so whoever takes U5-B4 should rebase onto it rather
than converting these 9 sites again.

## Verification

- 8 new tests + the preserved-behavior suites
(`executor-tool-failure-retry`, `executor-graph-requeue-gate`,
`executor-task-done-blocked`, `executor-graph-boundary`,
`executor-stuck-requeue-preserve-progress`,
`executor-paused-abort-todo-benign`, `executor-abort-provenance`) — **9
files, 112 tests, green**
- `pnpm test:gate` — green (2/10, 16/299, 1/71); `pnpm lint` clean; `tsc
--noEmit` on `@fusion/engine` clean
- Changeset included (`patch`, category `fix`), passes `pnpm
check:changesets`

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

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

* **Bug Fixes**
* Fixed execution recovery for workflows with renamed lifecycle columns
so retry, escalation, and loop-protection behaviors correctly follow the
workflow’s declared hold/WIP columns.
* Preserved legacy behavior for default workflows and continued safe
handling when lifecycle columns can’t be resolved.
* **Tests**
* Added a Vitest suite validating execution-policy “ladder” behavior for
renamed columns, including node escalation, dispatch-loop gating, and
fail-closed scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 15:45:47 -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
2934cccad8 U7 PR2: finalize reports what it did with the card — a refused planning handoff is retried, not counted as recovered (#2498)
## The bug

`finalizeApprovedTask` has ~25 exit points and returned `void`, so no
caller could tell *"the card was handed off"* from *"finalize gave up"*.
Both callers assumed success.

`recoverApprovedTask` returned `true` **unconditionally** after
finalize, and `handleStuckAbortRequeue` treats `true` as "recovery done,
stop here". So when the release move was **refused by the planning-stage
guard** (FN-8361), or the store could not perform the move at all,
recovery reported success and the card's stuck-retry budget was skipped
— nothing re-planned it, nothing escalated it, and it sat in the planner
column holding a finished spec.

The refusal was already logged loudly by FN-8596's visibility work. The
return value was the part still lying.

## Three states, not a boolean

This is the load-bearing decision in the PR:

| Outcome | Meaning | Retry? |
|---|---|---|
| `released` | crossed into the hold column, or already resting there
(plan-in-place) | n/a — handed off |
| `parked` | deliberate, terminal-for-now: awaiting manual plan
approval, duplicate decision, operator pause, deleted duplicate | **no**
— a human owns it |
| `withheld` | finalize could not complete the handoff, nothing waiting
on a human | **yes** — caller's budget owns it |

`recoverApprovedTask` returns `outcome !== "withheld"`, so **`parked`
still returns `true`**. Narrowing to `=== "released"` is the tempting
simplification and it is wrong: it would send the stuck handler down its
draft path and stamp `needs-replan` over a plan a human is mid-review on
— a worse bug than the one being fixed. That is asserted, and the
assertion fails under exactly that narrowing.

## Why a mutable report, not a return at each exit

Threading a return through 25 exits is 25 chances to mis-classify a
branch, and mis-classifying turns a truthfulness fix into a lifecycle
bug. The report defaults to `parked`, which is equivalent to today's
observable behavior at every exit — so the plumbing is **inert
everywhere except the three sites explicitly classified**. Adding a
state to an exit is then a deliberate, reviewable act rather than a
diff-wide judgement call.

Only **two** exits are marked `withheld`, both in the release block,
both already warning loudly. Deliberately *not* marked:

- the `updatePlanningStateIfStillCurrent` guard — FN-8024 says a normal
scheduler advance legitimately lands there; the card has moved on, so a
retry would be wrong.
- `recoverMissingPromptBeforeRelease` — it owns its own recovery budget;
retrying would double up.

## Revert proofs (measured)

| Reverted | Result |
|---|---|
| `recoverApprovedTask` back to unconditional `true` | `Tests 2 failed
\| 3 passed (5)` |
| narrowed to `outcome === "released"` | `Tests 1 failed \| 4 passed
(5)` — the approval-park control |

The second row is the point: the park case is load-bearing, not
decoration.

## A fixture note that nearly produced a false green

A `vi.fn()` stub for `updateTaskAtomic` that ignores its callback makes
**every** finalize report "no longer in the planning stage" and return
before the release — silently collapsing every case into the same
uninteresting early exit. My first run was 3 failures for that reason,
not the reason I expected. The fake now applies the patch, and the
control asserts `moveTaskIf` was actually reached. Same class as the
`moveTaskIf` fake caught on #2491; recording it so the next person
recognises the shape.

## Scope

The other caller — `specifyTask`'s unconditional `onSpecifyComplete` —
is **not** gated here. Reaching it needs a live planning session, so
gating it without first extracting the reaction would be a change I
cannot prove, which is exactly the finding review caught on #2491's
deferral. That lands next, on this plumbing.

## Verification

| Check | Result |
|---|---|
| new suite | 5/5 |
| 11 triage/planning suites (triage, finalize-duplicate-lineage,
stuck-requeue-preserve-draft, explicit-duplicate-marker, preflight,
plan-artifact-writeback, refinement-routing, planning-wake,
planning-evacuation, …) | 326/326 |
| `tsc --noEmit` (engine) | clean |
| `pnpm lint` | clean |
| `pnpm test:gate` | green (299 + 10 + 71) |
| `pnpm check:changesets` | clean |

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:36:20 -07:00
gsxdsm
8aba310d78 U11: resolve the worktree-acquisition requeue column by trait (2 sites, both branches) (#2496)
Based on `main`. First of my U11 conversion PRs — small, green,
independently revertable.

Both heartbeat worktree-acquisition requeue sites hardcoded `"todo"`.

## Why this is critical path, not a renamed-workflow nicety

**U11 deletes the `todo` column from the builtin workflows.** After
that, these two sites would requeue every acquisition-failed card into a
column that no longer exists.

## Both sites converted together

They are different branches of the same failure:
- the **bounded-retry** requeue, and
- the **retry-cap-exhausted** terminal park.

Converting one and not the other would leave the rarer path — which
fires only after three consecutive failures, so it's the one least
likely to be noticed — still writing the literal.

Target is the KTD-10 ordering via `resolveReboundTarget` (hold → intake
→ first column): the same helper `self-healing` and `mesh-lease-manager`
already use for "requeue a recovered card", so the recovery paths cannot
drift apart.

## What is deliberately untouched

`preserveStatus: true` on the exhausted path. It exists because
reopen-to-todo semantics would otherwise wipe the `status: "failed"`
written immediately before (FN-7721) — changing the column must not
disturb that flag. A test asserts the full options object, not just the
column.

Fail-soft to the legacy id: a requeue must not be abandoned because a
workflow lookup failed, or the card is left holding a worktree it could
not acquire. Covered by a regression-floor test.

## Verification

- **Mutation-verified:** restoring the literal fails 2 of the 3 new
tests
- 7 tests green (3 new + the 4 pre-existing worktree tests, unchanged)
- tsc clean, lint clean, merge gate green (299 + 10 + 71)

## Measured progress

**2 of the 74** code-level `"todo"` sites in my unit (engine
recovery/scheduling core) are now trait-resolved.

Remaining in-unit: `self-healing` 48, `scheduler` 15, `triage` 8,
`replan-target` 1.
`stuck-task-detector` needs **no work** — all 4 of its occurrences are
comments, not code.

No changeset: `@fusion/engine` is private.

🤖 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**
* Tasks now return to the workflow’s configured hold column when
heartbeat worktree acquisition fails, including workflows that use a
renamed hold column.
* Retry and retry-limit handling now preserves task progress and, when
applicable, status.
* Added a safe fallback to the default “todo” column when workflow
details cannot be resolved.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 11:36:13 -07:00
gsxdsm
8492278fdd U11 PR1: pin the merged intake+hold column contract before the IR moves (a mutation proved the first 7 tests insufficient) (#2495)
First of several small PRs for **U11** (merge Todo into Planning).
**Tests only — no production change.** It lands the precondition so the
IR edit arrives on proven substrate instead of an assumption.

## Decision taken (reversible, proceeding on it)

**The surviving Planning column keeps the id `todo`; `triage` is
deleted.** Same board the operator asked for — one column labelled
"Planning", no "Todo" — via the cheaper and safer half.

Measured, comments excluded, non-test, `packages/*/src` +
`dashboard/app`:

| | guards | writes | fallbacks | total |
|---|---:|---:|---:|---:|
| `"todo"` | 121 | 68 | 9 | 323 |
| `"triage"` | 90 | 30 | 21 | 304 |

Deleting `triage` instead of `todo` also means **no data migration**
(every live card in `todo` is already in the surviving column) and **no
guard changes meaning** (`column === "todo"` still denotes the hold
column). Under the plan's letter the opposite is true, and worse than
"dead": because Coding (Ideas) keeps `todo` per R10/R11, a surviving
`column === "todo"` guard would stay live for Ideas cards while silently
never matching for Coding cards — workflow-dependent, not dead.

This is also a proven in-tree pattern rather than a new idea:
**`builtin:coding-ideas` already ships this exact merge** — id `todo`,
display name "Planning", `hold(capacity)` + `reset-on-entry`,
plan-in-place.

Consequence worth flagging: **U11 no longer waits on Phase B.** The 121
`todo` guards keep their meaning, so converting them becomes U12 cleanup
rather than a U11 blocker.

## What this PR pins

Nothing in tree has ever carried `intake` and `hold` on one column.
Every built-in splits them. KTD-1 asserts the merged shape works; that
assertion was untested.

## The result, reported as found

**All 11 assertions passed on the first run against unmodified
sources.** The merged column is already supported by trait resolution,
the capacity sweep, and the release gate. **I could not make the first
seven fail**, so they are a regression floor — not evidence of a fix,
and I am not claiming them as one.

What makes them worth keeping is that they are *differential*: the same
scenario runs against the split-role vocabulary and the merged one and
asserts the role-level outcomes are **equal**, so a literal creeping
into any path fails the merged half while the split half stays green.

## The finding

**The first seven tests were not enough, and proving that is the point
of this PR.**

A mutation encoding the plausible-but-wrong belief *"an intake column
has no releaser"*:

```diff
- if (currentFlags.intake !== true && currentFlags.hold !== true) return false;
+ if (currentFlags.intake === true) return false;
+ if (currentFlags.hold !== true) return false;
```

left **all seven green**.

That belief is not hypothetical — it is stated verbatim in
`builtin-plan-review-group.ts`'s own FNXC comment as the reason Plan
Review lives in `todo` rather than `triage` today. Under U11 the
planning column **is** an intake column, so any code encoding it
silently stops holding unplanned cards and they release into
implementation with a bootstrap stub for a spec.

The gap: nothing reached `isUnplannedForExecution`. The mock store had
no `getTasksDir`, so both halves of the gate returned early — the tests
were exercising less than they appeared to. The fourth block drives it
with a real temp dir and a real bootstrap `PROMPT.md`.

**Re-running the same mutation now fails exactly one test — the
merged-column one — while its split-shape twin stays green.** That
discrimination is what the suite is for.

## Verification

26 tests green across this file plus `hold-release-renamed-columns`,
`hold-release-instrumentation`, and `pre-release-plan-review`. Lint
clean. No production file touched, so there is nothing to regress.

## Next PRs in this unit

1. Entry-contract test for `start` in a hold-carrying column (the
specific interaction the earlier, reverted attempt got wrong).
2. The ~10-line IR change itself — deliberately last, per KTD-7.
3. The intake-lane `triage` conversion: the 21 `?? "triage"` creation
defaults are the dangerous ones, since they would silently create cards
into a column that no longer exists.

`self-healing.ts` (11 triage guards + 4 writes) is the main worker's
file — I am not touching it and will hand over the line list rather than
race them.

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

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

## Summary by CodeRabbit

* **Tests**
* Added coverage for merged Planning column behavior across split and
merged workflow configurations.
* Verified intake and hold resolution, rebound targeting, capacity
hold/release outcomes, and execution gating.
* Confirmed planned cards are released appropriately while cards already
in progress are not unnecessarily held.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:36:05 -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
fbe7eb5c5a U7 PR1: the manual plan-approval gate was bypassable (3 planning-lane surfaces, 8/13 revert-proof) (#2491)
## What this is

The first slice of **U7 — the graph owns planning**. Characterizing the
planning lane's dual ownership turned up a live defect in the exact seam
the unit exists to remove, so this PR fixes that first and reports the
measured map of what U7 still has to move.

## The defect

The manual plan-approval gate parks a card by writing `status:
"awaiting-approval"` and **returning early** from `finalizeApprovedTask`
— before the release move. `specifyTask` then calls `onSpecifyComplete`
**unconditionally** afterwards. Three automated surfaces went on to
advance the parked card, each having re-derived its own weaker "may I
advance this?" check from `paused`/`userPaused` alone.

`isTaskBlockedOnApproval` (`packages/core/src/task-merge.ts`) already
declares itself *"the single shared predicate core and engine code must
consult before rebounding, requeuing, resuming, re-planning, or
otherwise advancing a task"*. **Measured: it had exactly one production
consumer** (`overseer-human-control-policy.ts`). Now four.

Reachable end to end for a **plan-in-place** card — one whose column
already equals the plan-review node's column (Coding (Ideas), or any
`needs-replan` revision resting in the default workflow's `todo`):

```
park at awaiting-approval
  → onSpecifyComplete fires anyway
  → a runnable plan-review continuation is seeded
  → the drain dispatches it
  → Plan Review runs on a plan the operator never approved
  → its evidence satisfies isUnplannedForExecution
  → the capacity sweep releases the card into In progress
```

Blast radius: projects that have manual plan approval switched on.
`planApprovalMode` defaults to auto-approve (FN-7557), so unset projects
have no gate to skip — but the operator who turns it on is precisely the
one who cares.

## Surface enumeration

Per AGENTS.md — fix the invariant, not the repro.

| # | Surface | Fix |
|---|---|---|
| 1 | `issueRelease` — the choke point for the sweep, `promoteHeldTask`,
`releaseHeldTaskByEvent`, and the scheduler's `reserveSlot` guard |
Guarded there rather than inside `isUnplannedForExecution`, because an
approval-held card is not "unplanned". Guarded **again** inside the
`moveTaskIf` predicate so a park landing mid-sweep cannot lose the race
(R6 — only the in-txn check is authoritative). Operator force-promote
(`allowUnplanned`) still waives it: that *is* a human decision about
this card. |
| 2 | **Both** continuation seeders —
`seedPreReleasePlanReviewContinuation` (normal completion) and
`evaluateStrandedHoldContinuation` (FN-8592 self-healing re-seed) |
Guard at the seam, not in the callers: the seeder itself checked
nothing, and its two callers each pre-checked a different subset. |
| 3 | `resolvePlanningContinuationCandidate` (drain classifier) |
**Skip, never orphan.** Cancelling terminalizes the item, so an approval
landing a minute later would have nothing left to resume and would need
a second repair to come back. |

## Measured, not assumed

The two hold shapes `isTaskBlockedOnApproval` accepts were **not equally
broken**. The `paused` + `pausedReason` shape was already refused by the
sweep and the drain — they happen to test `paused` — so it was refused
*for the wrong stated reason*, not advanced. Every genuine advance gap
is on the **status-only** shape, which is exactly what the gate writes.
Both are covered anyway, plus an `ORDINARY_PAUSE` counter-case so the
new check cannot quietly become a catch-all for every operator park.

## Revert proof

With the three production files reverted: **8 of 13 tests fail.** The 5
that still pass are the 3 controls and the 2 pause-shape rows the
pre-existing `paused` checks already covered.

```
·x··xxxxx·xx·      → Tests 8 failed | 5 passed (13)
```

## Verification

| Check | Result |
|---|---|
| new suite | 13/13 |
| hold-release (×2) + plan-review (×3) + pre-release-plan-review +
promote-force-unplanned | 43/43 |
| stranded-hold-continuation (×2) + continuation-selection +
planning-finished-wake + planning-service | 27/27 |
| scheduler-trait-dispatch | 9/9 |
| `pnpm --filter @fusion/engine exec tsc --noEmit` | clean |
| `pnpm lint` | clean |
| `pnpm test:gate` | green |
| `pnpm check:changesets` | clean |

## Two findings for the coordinator

**1. `triage.ts` is absent from the Phase B census.** The plan's
per-file table (535 sites) covers `self-healing.ts` (U4), the
executor/scheduler cluster (U5), and the core policy modules (U6).
`triage.ts` appears in none of them, so its lifecycle-column literals
are unowned scope — U7 absorbs them.

Measured with the plan's own methodology (block and line comments
stripped, code lines only): a naive quoted-literal grep of `triage.ts`
reports **50** sites, but **35 of those are the agent *role* string
`"triage"`**, not the column. The genuine lifecycle-column surface is
**15 sites**, of which 12 are planning-lane and 3 are `column !==
"done"` in duplicate search. The 50 figure would over-count by 3.3×.

**2. The graph's planning seam is a rubber stamp, in triplicate.**
`createAuthoritativeWorkflowSeams().planning` returns `{ outcome:
"success", value: "pre-specified" }`;
`WorkflowPlanningService.runPlanningSession` returns the same;
`createNoopLegacySeams().planning` is a bare success. The real
specification is ~1,000 lines of `triage.specifyTask`, entirely outside
the graph. That is the flip U7's remaining slices have to make, and it
is the reason the planning lane has two owners at all.

## Deliberately not in this PR

Triage's unconditional `onSpecifyComplete` call. That is the
**ownership** half — `finalizeApprovedTask` must report whether it
released, and the reaction must key on that outcome — and it belongs
with the seam flip, where finalize's outcome becomes the graph's edge
condition anyway, rather than as a half-measure now. With the three
guards above in place, the downstream damage is already contained; what
remains is a reaction firing for a non-event and an operator-visible log
line (`Specified X → todo`) that is untrue for a parked card.

🤖 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**
* Tasks awaiting manual plan approval are no longer automatically
planned, reviewed, started, or released into active work.
* Approval-held items are consistently skipped across planning
continuations and related workflows.
* Approval-held due work is deferred to prevent starvation while
waiting, and operator force-promotion still bypasses the gate.
* **Tests**
* Added regression coverage to ensure the manual approval hold behavior
remains invariant across multiple continuation scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:39:21 -07:00
gsxdsm
319e051c65 U8 PR1: pin the execution-lifecycle ownership ledger (measured: 28 executor-owned dispositions vs 3 graph handbacks) (#2490)
First PR of **U8 — the graph owns execution** (plan
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`,
line ~436). The plan states this unit "is expected to land as several
commits; it must not be attempted as one sweep", and
Execution-note-first: **characterization before ownership moves**. This
is that floor. **No behavior change.**

## Why a ledger and not a refactor

U8's goal is "the executor stops deciding *what happens next*" — and
that had no measurable form.

- **Executor line count does not measure it.** A 3,178-line
`runImplementation` can shrink substantially with every lifecycle
decision still exactly where it was.
- **A green suite measures it least of all.** Every disposition counted
below already has passing tests, because each one was *correct behavior*
when it was written. What is wrong is the **owner**, not the behavior.

So the unit needs a number, and the number has to exist *before* the
migration — a ratchet written afterwards cannot prove the migration
happened.

## The measured baseline

Counted from source, comments stripped, method bodies extracted by brace
matching:

| Method | `store.moveTask` | `handoffTaskToReview` | terminal
`status:"failed"` | `graphCompletion` handbacks |
|---|---:|---:|---:|---:|
| `runImplementation` (3,178 lines) | 16 | 3 | 9 | **3** |
| `handleGraphFailure` (~930 lines) | 0 | 0 | 7 | — |

**The implementation phase decides its own lifecycle 28 times and asks
the graph 3 times.**

These are measured, not estimated. My first `handleGraphFailure`
estimate was **wrong** (2 moves / 4 parks); the extractor corrected it
to 0 / 7 — the `moveTask` calls that read as belonging to that method
sit past its closing brace, in the recovery helpers below it. The
correction is in the ledger comment so the next reader does not repeat
the misread.

## The finding this makes concrete

`createAuthoritativeWorkflowSeams.execute` collapses that entire
implementation phase to one boolean:

```ts
if (result.taskDone) return { outcome: "success", value: "implemented" };
```

The graph has no vocabulary for *"the agent stopped because a step is
blocked on a pending review"* or *"the session paused after the work was
already complete"*. So the implementation phase performs those
transitions itself (`executor-exit-while-review-pending`,
`paused-after-completion`) and the graph finds out afterwards.

That is why `handleGraphFailure` carries `alreadyFinalizedToReview` /
`completionFinalized` — **classifiers whose entire job is to recognise a
move the graph did not make.** They are compensation for dual ownership,
and they are U8's acceptance test: they become unreachable, and then
deletable, exactly when the last out-of-band transition is gone. This PR
records that contract in source at the seam (FNXC comment), which is
where the next PR starts.

## Proof the guard fails on the defect

A ratchet that reports success without checking anything is worse than
no ratchet. Both failure modes were injected and observed:

1. **The defect it exists to catch** — injected one `await
this.store.moveTask(task.id, "in-review", {})` into
`runImplementation`'s completion path → ledger fails, `16 -> 17`.
2. **A broken guard** — injected a string literal containing `}` so
naive brace matching ends the body early → the size self-check fails at
**13 lines**, instead of silently reporting a comfortable zero for every
count.

Both injections were reverted; `git diff` against the pre-injection copy
is empty.

## Direction of travel

Executor-owned counts may only go **down**, and a decrement must land
with the disposition visible as a **graph outcome** — not merely
deleted. An increment is a new out-of-graph lifecycle decision and needs
a stated justification in its PR, not a quiet edit to the constant.

This is the precursor to U12's planned
`no-out-of-graph-lifecycle-writes.test.ts`; when the counts reach their
floor the assertion becomes "zero, outside the allowlist", and this file
is where that allowlist grows up.

## Preserved behaviors

Untouched, and re-run green as the regression floor for everything that
follows: FN-8141 honest-blocked exit
(`executor-task-done-blocked.test.ts`), FN-7996/FN-7998 tool-failure
retry + escalation (`executor-tool-failure-retry.test.ts`), FN-7863
dispatch-loop terminalization and FN-7926 completed-blocked parking
(`executor-graph-requeue-gate.test.ts`).

## Verification

- `pnpm --filter @fusion/engine exec vitest run` on the ledger + the
four preserved-behavior suites + `legacy-tombstones` — **6 files, 49
tests, green**
- `pnpm test:gate` — **green** (2/10, 16/299, 1/71)
- `pnpm lint` — clean; `tsc --noEmit` on `@fusion/engine` — clean

No changeset: test-only plus a source comment, no `@runfusion/fusion`
behavior change.

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

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

* **Tests**
* Added a lifecycle-ownership “source-scanning” test that analyzes the
executor’s task disposition patterns to ensure counts remain consistent
across execution and graph-failure flows.
  * Added safeguards to catch unintended changes to lifecycle handling.

* **Documentation**
* Documented the lifecycle-ownership boundary for task disposition
handling, including how completion and failure transitions are
consolidated and how related failure classifiers are affected.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

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

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

## Root fix, not a literal swap

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

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

## Checked, not assumed: no second copy

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

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

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

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

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

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

## Before / after — measured, both directions

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

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


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

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

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

## Verification

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

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

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

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:09:51 -07:00
gsxdsm
387e836432 U4 substrate PR1: extract the git-evidence readers (19/19 bodies byte-identical, self-healing.ts -449) (#2489)
Based on `main`. **PR1 of the substrate decomposition** — deliberately
small and boring.

## Mechanical proof (the point of this PR)

Every moved body diffed against its pre-move text:

```
BLOCK                            KIND     DIFF-vs-ORIGINAL
  LandedTaskCommit               iface    EMPTY
  commitOwnedByTask              helper   EMPTY
  escapeRegex                    helper   EMPTY
  shellQuote                     helper   EMPTY
  parseShortstat                 helper   EMPTY
  findLandedTaskCommit           method   EMPTY
  findAlreadyMergedTaskCommit    method   EMPTY
  refreshRemoteBaseRef           method   EMPTY
  readCommitTaskOwnership        method   EMPTY
  branchHasNoUniqueDiff          method   EMPTY
  baseHasExplicitTaskOwnership   method   EMPTY
  foreignTipRejection            method   EMPTY
  branchTipForeignOwnership      method   EMPTY
  isCommitReachableFromBranch    method   EMPTY
  findWorktreePathForBranch      method   EMPTY
  repoBranchExists               method   EMPTY
  readShortstatForSha            method   EMPTY
  readLandedFilesForSha          method   EMPTY
  isBranchTipMisboundToTask      method   EMPTY

RESULT: 19/19 BYTE-IDENTICAL modulo the enumerated deviations; 0 differ.
```

No condition reordered, no signature changed, no rename, no inlining, no
"while I am here" cleanup.

## The premise needed correcting — the cheap lesson this cut was meant
to buy

The brief described **13 pure functions**. They are 13 `private async`
**methods** closing over `this.options`, so nothing here is
byte-identical in the strict sense. Three deviations were structurally
unavoidable:

1. **`private` → `protected`** on the 14 methods and the `options` field
— a subclass cannot call a `private` base member. One token per
declaration.
2. **Two type annotations** rewritten:
`SelfHealingManager["readCommitTaskOwnership"]` → the base class, since
the original would be a circular import.
3. **Four module-level helpers moved along** and re-exported. Forced by
direction — the new module must not import `self-healing.ts`, so
everything the bodies call has to live beside them. `execAsync` and
`shellQuote` are imported back because call sites remain.

## The set is 14, not 13

`foreignTipRejection` had to come too, and it's what **closes** the
cluster — it depends only on `baseHasExplicitTaskOwnership` and
`branchHasNoUniqueDiff`, both already in the set. Without it the
extraction isn't self-contained and `this.options` isn't the only
external dependency.

## Base class, not free functions — deliberately

Converting to free functions would change all 14 signatures: a
behavior-adjacent edit riding inside a file move, which is exactly the
combination that hid the last four safeguard regressions. An abstract
base keeps `this` semantics, so every call site stays
`this.<method>(...)` and every body is unchanged text.

## Two pre-existing collisions, preserved exactly

- The detector module already exports a **free**
`findAlreadyMergedTaskCommit` sharing a name with the protected method;
inside the class body the bare identifier resolves to the import.
- `SelfHealingOptions` is declared in `self-healing.ts`, imported here
**type-only** so it's erased at runtime and creates no module cycle.

## Measured line delta — not an estimate

| | lines |
|---|---:|
| `self-healing.ts` | 13,394 → 12,945 = **−449** |
| new module | **+527** (454 moved verbatim, 73 scaffold) |
| **NET** | **+78** |

Same shape as every consolidation in this program: the target file
shrinks, the total grows slightly. At −449 for the first and safest cut,
the remaining substrate cuts plausibly take `self-healing.ts` under 11k
— but that is **file-size reduction, not code reduction**.

## Verification

464 passed across three engine suites with the single known
**pre-existing** `archiveStaleDoneTasks` failure; tsc clean; lint clean;
merge gate green (299 + 10 + 71).

No changeset: `@fusion/engine` is private.

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

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:50:22 -07:00
gsxdsm
0021bd363e U4: retire the surfacing family onto one policy-driven runner (measured: trim is ~34% of the estimate) (#2487)
Stacked on #2486. Base is `feature/u4-safeguard-scoped-to-mutation` — do
not merge before it.

Retires the surfacing family — `surfaceStalePausedTodos`,
`surfaceStalePausedReviews`, `surfaceInReviewStalled` — onto one
policy-driven runner. Migrated **together**, because they were three
copies of one skeleton and a fix applied to one had to be remembered for
the other two.

#2484's characterization suite is the regression floor and **passes
unchanged**.

## Two unconverted sites found in core

Without these the migration would have been **cosmetic for two of the
three**:

`getStalePausedReviewSignal` and `getInReviewStalledSignal` both
hardcoded `task.column !== "in-review"`. `getStalePausedTodoSignal`
gained the equivalent `holdColumn` parameter back in B1 — its two
siblings were missed, so they silently stopped matching for any workflow
that renames its review column. Both now take `reviewColumn`, defaulting
to the legacy id.

## Three bugs this work introduced, and the tests caught

Each was silent in the diff:

1. **I dropped the engine activation floor** from all three signal
calls. Wall-clock the engine wasn't running for is not quiet time, so
every sweep would have reported cards as stale purely because the engine
restarted. Caught by the **pre-existing** suite — not by my own
characterization floor, which is exactly why that floor wasn't
sufficient alone.
2. **I passed `task.column` as the role column**, making the signal's
own column check compare a value against itself — tautologically true,
so the check was silently deleted. The *resolved* role column is now
passed.
3. **The role gate was conditional on the role resolving**, so it was
silently absent for exactly the workflows whose role failed to resolve.
Now unconditional, with the legacy id as fallback.

## The shared test is a table

One row per sweep; every invariant asserted for all three — threshold
inheritance, declared-policy override, renamed role column, the negative
case outside the role column, at-most-once, activation floor, both pause
gates, non-positive-threshold disable, soft-delete. **Adding a fourth
surfacing sweep means adding a row.**

Its log mock **appends** to the card's log, because the at-most-once
dedup reads that log — a call-recording mock cannot observe suppression
at all.

## Measured line delta — this corrects the survey estimate downward

| | lines |
|---|---:|
| `self-healing.ts` | **−192 / +95 = −97** |
| new runner file | **+209** |
| core signal conversions | **+23** |
| **NET** | **+135** |

Three sweeps migrated **increased** total lines by 135 while shrinking
`self-healing.ts` by 97. Per sweep: **−32** in `self-healing.ts`.
**Break-even is 6.5 sweeps.**

Extrapolated to all 34 POLICY sweeps: **−1,099** in `self-healing.ts`,
**−890 net** once the runner is amortized.

My survey estimated **−2,612** for that bucket. The measured figure is
**34% of it**. The reconciler's size was never the issue — the estimate
assumed per-sweep bodies collapse to almost nothing, and they do not:
each keeps a real eligibility predicate, signal call, and operator
message.

## Verification

- 33 shared-family tests green
- 471 passed across five engine suites, with the single known
**pre-existing** `archiveStaleDoneTasks` failure
- 44 core signal tests green
- tsc clean in core and engine; lint clean; merge gate green (299 + 10 +
71)

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 support for overriding which workflow column is treated as the
relevant “in review” column for stale-paused and stalled-review
surfacing.

- **Bug Fixes**
- Tightened safety safeguards so only lifecycle-mutating recovery
actions are blocked when a card is user-paused; observational surfacing
remains allowed.
- Improved surfacing consistency across stale paused todos, stale paused
reviews, and in-review stalled cases, including stronger deduping and
cycle-aware behavior.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:40:08 -07:00
dependabot[bot]
081e55d70d chore(deps-dev): bump @capacitor/ios from 7.6.1 to 8.4.2 (#2449)
Bumps [@capacitor/ios](https://github.com/ionic-team/capacitor) from
7.6.1 to 8.4.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/capacitor/releases">@​capacitor/ios's
releases</a>.</em></p>
<blockquote>
<h2>8.4.2</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2">8.4.2</a>
(2026-07-14)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> explicitly grant URI permissions for image
capture intent (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)
(<a
href="6f2d328389">6f2d328</a>)</li>
</ul>
<h2>8.4.1</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1">8.4.1</a>
(2026-06-19)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> make SPM dependency patch work on prereleases
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)
(<a
href="6048e90171">6048e90</a>)</li>
<li><strong>cli:</strong> patch Capacitor SPM dependency version in
plugins (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8492">#8492</a>)
(<a
href="28bb2c6870">28bb2c6</a>)</li>
</ul>
<h2>8.4.0</h2>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0">8.4.0</a>
(2026-06-02)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> show only the requested system bar (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8480">#8480</a>)
(<a
href="4c6c3219af">4c6c321</a>)</li>
<li><strong>cli:</strong> revert live reload config on failure (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8485">#8485</a>)
(<a
href="1d031a4abe">1d031a4</a>)</li>
<li><strong>SystemBars:</strong> make <code>safe-area-inset-x</code>
available on API &lt;= 34 (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8424">#8424</a>)
(<a
href="e456de083e">e456de0</a>)</li>
<li><strong>SystemBars:</strong> respect <code>insetsHandling</code>
disable (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8481">#8481</a>)
(<a
href="d4ad7ffe39">d4ad7ff</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add method getDouble to plugin config (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/7638">#7638</a>)
(<a
href="93c72de40a">93c72de</a>)</li>
<li><strong>cli:</strong> add experimental packageOptions (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8471">#8471</a>)
(<a
href="258867b7bf">258867b</a>)</li>
<li><strong>cli:</strong> capture ios_package_manager in telemetry (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8482">#8482</a>)
(<a
href="b4b297a52f">b4b297a</a>)</li>
</ul>
<h2>8.3.4</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4">8.3.4</a>
(2026-05-12)</h2>
<p><strong>Note:</strong> Version bump only for package capacitor</p>
<h2>8.3.3</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3">8.3.3</a>
(2026-05-08)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/capacitor/blob/main/CHANGELOG.md">@​capacitor/ios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2">8.4.2</a>
(2026-07-14)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> explicitly grant URI permissions for image
capture intent (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)
(<a
href="6f2d328389">6f2d328</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1">8.4.1</a>
(2026-06-19)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> make SPM dependency patch work on prereleases
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)
(<a
href="6048e90171">6048e90</a>)</li>
<li><strong>cli:</strong> patch Capacitor SPM dependency version in
plugins (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8492">#8492</a>)
(<a
href="28bb2c6870">28bb2c6</a>)</li>
</ul>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0">8.4.0</a>
(2026-06-02)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> show only the requested system bar (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8480">#8480</a>)
(<a
href="4c6c3219af">4c6c321</a>)</li>
<li><strong>cli:</strong> revert live reload config on failure (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8485">#8485</a>)
(<a
href="1d031a4abe">1d031a4</a>)</li>
<li><strong>SystemBars:</strong> make <code>safe-area-inset-x</code>
available on API &lt;= 34 (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8424">#8424</a>)
(<a
href="e456de083e">e456de0</a>)</li>
<li><strong>SystemBars:</strong> respect <code>insetsHandling</code>
disable (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8481">#8481</a>)
(<a
href="d4ad7ffe39">d4ad7ff</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add method getDouble to plugin config (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/7638">#7638</a>)
(<a
href="93c72de40a">93c72de</a>)</li>
<li><strong>cli:</strong> add experimental packageOptions (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8471">#8471</a>)
(<a
href="258867b7bf">258867b</a>)</li>
<li><strong>cli:</strong> capture ios_package_manager in telemetry (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8482">#8482</a>)
(<a
href="b4b297a52f">b4b297a</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4">8.3.4</a>
(2026-05-12)</h2>
<p><strong>Note:</strong> Version bump only for package capacitor</p>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3">8.3.3</a>
(2026-05-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> copy plugin files in CocoaPods projects (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8467">#8467</a>)
(<a
href="b2d771926a">b2d7719</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2">8.3.2</a>
(2026-05-07)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> add cSettings support for compiler flags in
generated Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8448">#8448</a>)
(<a
href="0bd0676315">0bd0676</a>)</li>
<li><strong>cli:</strong> add system framework and weak framework
support in SPM Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8447">#8447</a>)
(<a
href="3232f0fe1d">3232f0f</a>)</li>
<li><strong>cli:</strong> correct Capacitor plugin SPM compat check (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8440">#8440</a>)
(<a
href="e5ccc451dd">e5ccc45</a>)</li>
<li><strong>cli:</strong> generate binaryTarget entries for custom
xcframeworks in Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8445">#8445</a>)
(<a
href="1f7e33fca4">1f7e33f</a>)</li>
<li><strong>cli:</strong> generate resource entries in Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8455">#8455</a>)
(<a
href="790bd27123">790bd27</a>)</li>
<li><strong>cli:</strong> handle Cordova plugins without iOS source
files (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8443">#8443</a>)
(<a
href="0da130eb7a">0da130e</a>)</li>
<li><strong>cli:</strong> link plugin dependencies in Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8457">#8457</a>)
(<a
href="b3c769e856">b3c769e</a>)</li>
<li><strong>ios:</strong> support Cordova plugins with Package.swift (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8438">#8438</a>)
(<a
href="139943b0c0">139943b</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1834b9740e"><code>1834b97</code></a>
Release 8.4.2</li>
<li><a
href="6f2d328389"><code>6f2d328</code></a>
fix(android): explicitly grant URI permissions for image capture intent
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)</li>
<li><a
href="b789b683f0"><code>b789b68</code></a>
chore: run <code>npm run fmt</code> to fix lint errors (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8516">#8516</a>)</li>
<li><a
href="7217b5215a"><code>7217b52</code></a>
Release 8.4.1</li>
<li><a
href="6048e90171"><code>6048e90</code></a>
fix(cli): make SPM dependency patch work on prereleases (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)</li>
<li><a
href="28bb2c6870"><code>28bb2c6</code></a>
fix(cli): patch Capacitor SPM dependency version in plugins (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8492">#8492</a>)</li>
<li><a
href="41fd9deb6d"><code>41fd9de</code></a>
Release 8.4.0</li>
<li><a
href="36b729d103"><code>36b729d</code></a>
chore: Decrease timeout for CI jobs from 60 to 30 minutes (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8476">#8476</a>)</li>
<li><a
href="4c6c3219af"><code>4c6c321</code></a>
fix(android): show only the requested system bar (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8480">#8480</a>)</li>
<li><a
href="d4ad7ffe39"><code>d4ad7ff</code></a>
fix(SystemBars): respect <code>insetsHandling</code> disable (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8481">#8481</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/capacitor/compare/7.6.1...8.4.2">compare
view</a></li>
</ul>
</details>
<br />

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:12:43 -07:00
dependabot[bot]
74d6513fae chore(deps): bump actions/upload-artifact from 4 to 7 (#2444)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers
can set the new <code>archive</code> parameter to <code>false</code> to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The <code>name</code> parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages,
we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a
href="https://github.com/Link"><code>@​Link</code></a>- in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@​Link</code></a>- made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
<h2>v6.0.0</h2>
<h2>v6 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Upload Artifact Node 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/719">actions/upload-artifact#719</a></li>
<li>fix: update <code>@​actions/artifact</code> for Node.js 24 punycode
deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/744">actions/upload-artifact#744</a></li>
<li>prepare release v6.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/745">actions/upload-artifact#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0">https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0</a></p>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="043fb46d1a"><code>043fb46</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/797">#797</a>
from actions/yacaovsnc/update-dependency</li>
<li><a
href="634250c138"><code>634250c</code></a>
Include changes in typespec/ts-http-runtime 0.3.5</li>
<li><a
href="e454baaac2"><code>e454baa</code></a>
Readme: bump all the example versions to v7 (<a
href="https://redirect.github.com/actions/upload-artifact/issues/796">#796</a>)</li>
<li><a
href="74fad66b98"><code>74fad66</code></a>
Update the readme with direct upload details (<a
href="https://redirect.github.com/actions/upload-artifact/issues/795">#795</a>)</li>
<li><a
href="bbbca2ddaa"><code>bbbca2d</code></a>
Support direct file uploads (<a
href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a
href="589182c5a4"><code>589182c</code></a>
Upgrade the module to ESM and bump dependencies (<a
href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a
href="47309c993a"><code>47309c9</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a>
from actions/Link-/add-proxy-integration-tests</li>
<li><a
href="02a8460834"><code>02a8460</code></a>
Add proxy integration test</li>
<li><a
href="b7c566a772"><code>b7c566a</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/745">#745</a>
from actions/upload-artifact-v6-release</li>
<li><a
href="e516bc8500"><code>e516bc8</code></a>
docs: correct description of Node.js 24 support in README</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 19:06:09 -07:00
dependabot[bot]
b848a13509 chore(deps): bump actions/checkout from 4 to 7 (#2443)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li><strong>[BREAKING]</strong> backport
<code>allow-unsafe-pr-checkout</code> to v6 by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2500">actions/checkout#2500</a></li>
<li>backport fixes to releases-v6 by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2527">actions/checkout#2527</a></li>
</ul>
<p><a
href="https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/">https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/</a>
for more details about this breaking change</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v6.1.0">https://github.com/actions/checkout/compare/v6.0.3...v6.1.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID
is set by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3d3c42e5aa"><code>3d3c42e</code></a>
prep v7.0.1 release (<a
href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a
href="28802689a1"><code>2880268</code></a>
escape values passed to --unset (<a
href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a
href="12cd2235ef"><code>12cd223</code></a>
trim only ascii whitespace for branch (<a
href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a
href="62661c4e71"><code>62661c4</code></a>
skip running unsafe pr check if input is default (<a
href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a
href="e8d4307400"><code>e8d4307</code></a>
Bump the minor-actions-dependencies group with 2 updates (<a
href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a
href="631c942040"><code>631c942</code></a>
eslint 9 (<a
href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a
href="4f1f4aec02"><code>4f1f4ae</code></a>
Bump actions/upload-artifact from 4 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a
href="ba097532fb"><code>ba09753</code></a>
Bump actions/checkout from 6 to 7 (<a
href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a
href="b9e0990d21"><code>b9e0990</code></a>
Bump docker/login-action from 3.3.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a
href="e8cb398be4"><code>e8cb398</code></a>
Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/checkout/compare/v4...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 18:56:31 -07:00
dependabot[bot]
17caa3d260 chore(deps): bump softprops/action-gh-release from 2 to 3 (#2442)
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2 to 3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/releases">softprops/action-gh-release's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.0</h2>
<p><code>3.0.0</code> is a major release that moves the action runtime
from Node 20 to Node 24.
Use <code>v3</code> on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
<code>v2.6.2</code>.</p>
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>Move the action runtime and bundle target to Node 24</li>
<li>Update <code>@types/node</code> to the Node 24 line and allow future
Dependabot updates</li>
<li>Keep the floating major tag on <code>v3</code>; <code>v2</code>
remains pinned to the latest <code>2.x</code> release</li>
</ul>
<h2>v2.6.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>Other Changes 🔄</h3>
<ul>
<li>chore(deps): bump picomatch from 4.0.3 to 4.0.4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/775">softprops/action-gh-release#775</a></li>
<li>chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/777">softprops/action-gh-release#777</a></li>
<li>chore(deps): bump vite from 8.0.0 to 8.0.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/781">softprops/action-gh-release#781</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/softprops/action-gh-release/compare/v2...v2.6.2">https://github.com/softprops/action-gh-release/compare/v2...v2.6.2</a></p>
<h2>v2.6.1</h2>
<p><code>2.6.1</code> is a patch release focused on restoring linked
discussion thread creation when
<code>discussion_category_name</code> is set. It fixes
<code>[#764](https://github.com/softprops/action-gh-release/issues/764)</code>,
where the draft-first publish flow
stopped carrying the discussion category through the final publish
step.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<h3>Bug fixes 🐛</h3>
<ul>
<li>fix: preserve discussion category on publish by <a
href="https://github.com/chenrui333"><code>@​chenrui333</code></a> in <a
href="https://redirect.github.com/softprops/action-gh-release/pull/765">softprops/action-gh-release#765</a></li>
</ul>
<h2>v2.6.0</h2>
<p><code>2.6.0</code> is a minor release centered on
<code>previous_tag</code> support for
<code>generate_release_notes</code>,
which lets workflows pin GitHub's comparison base explicitly instead of
relying on the default range.
It also includes the recent concurrent asset upload recovery fix, a
<code>working_directory</code> docs sync,
a checked-bundle freshness guard for maintainers, and clearer
immutable-prerelease guidance where
GitHub platform behavior imposes constraints on how prerelease asset
uploads can be published.</p>
<p>If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.</p>
<h2>What's Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md">softprops/action-gh-release's
changelog</a>.</em></p>
<blockquote>
<h2>0.1.13</h2>
<ul>
<li>fix issue with multiple runs concatenating release bodies <a
href="https://redirect.github.com/softprops/action-gh-release/pull/145">#145</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3d0d9888cb"><code>3d0d988</code></a>
release 3.0.2 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/818">#818</a>)</li>
<li><a
href="7e13ed4ac5"><code>7e13ed4</code></a>
fix: clarify release creation 404 errors (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/817">#817</a>)</li>
<li><a
href="e6c70a53cf"><code>e6c70a5</code></a>
fix: replace existing release assets on Gitea (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/816">#816</a>)</li>
<li><a
href="f345337888"><code>f345337</code></a>
fix: publish existing draft releases as prereleases (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/801">#801</a>)</li>
<li><a
href="d8a89a2066"><code>d8a89a2</code></a>
fix: upload small checksum assets reliably (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/815">#815</a>)</li>
<li><a
href="45ece40c31"><code>45ece40</code></a>
chore(deps): remove unused TypeScript tooling (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/814">#814</a>)</li>
<li><a
href="f6b913c3f9"><code>f6b913c</code></a>
feat: improve release error reporting and test coverage (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/813">#813</a>)</li>
<li><a
href="15f193d7d8"><code>15f193d</code></a>
chore(deps): upgrade TypeScript to 7 (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/812">#812</a>)</li>
<li><a
href="cc8268d46a"><code>cc8268d</code></a>
chore(deps): bump actions/checkout in the github-actions group (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/810">#810</a>)</li>
<li><a
href="fd0ed1e85b"><code>fd0ed1e</code></a>
chore(deps): bump the npm group with 3 updates (<a
href="https://redirect.github.com/softprops/action-gh-release/issues/811">#811</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/softprops/action-gh-release/compare/v2...v3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-07-27 18:49:25 -07:00
Phil Larson
3822e1081c test(engine): remove orphan dependency reporter coverage (#2483)
## Summary
- remove the remaining dependency-blocked reporter test after the
reporter was deleted in #2477
- prevent Vitest from failing during module collection on a deleted
import

## Test plan
- `pnpm --filter @fusion/engine typecheck`
- `pnpm --filter @fusion/engine build`
2026-07-27 18:36:51 -07:00
gsxdsm
17fdf2f0ae U4: scope the user-pause safeguard to lifecycle MUTATION, not observation (re-ratified) (#2486)
Stacked on #2482. Base is
`feature/workflow-vocabulary-u4-override-layer` — do not merge before
it.

Implements the coordinator's **re-ratification** of the user-pause
safeguard with a narrower definition.

## The invariant, written into the code

> The user-pause safeguard means **NEVER MUTATE LIFECYCLE STATE** of a
user-paused card. It does **NOT** mean never observe one.

Respecting a pause exists to stop the engine acting on a card *behind*
the operator who paused it — moving, rebounding, archiving, resuming. A
read-only diagnostic does the opposite: it tells that same operator what
their paused card is doing. Blinding them to their own paused work is
not safety; it is the engine deciding they should not be told.

The sentence is in the source, because the distinction is the whole
point and a future reader will otherwise re-broaden it.

## Why it needed narrowing

Ratified broadly first, that reading was caught suppressing the very
sweeps it was meant to protect. `surfaceStalePausedTodos` exists to
report cards that have sat paused too long — routing it through a
reconciler that suppresses paused cards turns a diagnostic into one that
**silently reports nothing**. Measured, not argued: #2484 proves that
sweep surfaces user-paused cards on current main.

## Scoped by action, never by sweep

`OBSERVATIONAL_ACTIONS` is an **allow-list**, so a newly added mutating
action is suppressed by default — the scoping fails closed. A sweep
cannot opt itself out.

`RecoveryActionKind` deliberately names actions the policy vocabulary
cannot yet author (`rebound`, `archive`, `requeue`, `resume`). The
scoping is only testable if those exist as values, and **a rule that
cannot be tested is a rule that erodes**. `parseWorkflowIr` keeps a
closed action list, so nothing becomes authorable by being named.

## Which field — chosen, not inherited

The gate reads `userPaused`, **not** `paused`. The two diverge
(`branch-group-ops.ts:128` says so outright), and `paused` also covers
engine-authored automation pauses like dispatch-storm, which carry no
operator intent to respect — gating on it would suppress recovery from
the engine's own throttles. The safeguard defers to a **human**
decision, so it keys on the field that records one.

## Both halves kept

The broad case is **narrowed, not deleted**: one test proves mutation is
still suppressed, one proves observation is now permitted. A future
reader must be able to tell the scoping was *deliberate* rather than
eroded by someone who found the broad rule inconvenient.

**Mutation-verified in both directions**, since either error is silent:

| mutation | result |
|---|---|
| re-broaden (suppress observation) | **2 tests fail** |
| over-narrow (`rebound` treated observational) | **3 tests fail** |

## Verification

46 tests green (28 safety + 18 inheritance); tsc clean; lint clean;
merge gate green (299+10+71).

No changeset: `@fusion/engine` is private.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-27 17:37:20 -07:00
gsxdsm
89284df85e E2E: table-driven converted-sweep coverage, two new sites, and an honest unproven-sites ledger (#2485)
Follow-up to #2475 (merged). Test-only, plus one test-utility seam.

## Why a table

#2475 proved one converted sweep. The count has since gone to **three**,
twice while this work was open — `surfaceStalePausedTodos` appeared
during #2475's review, and #2478 landed `recovery-reconciler.ts` while
this branch was open. A suite with a bespoke `describe` per sweep is a
coverage claim that quietly becomes false.

Replaced with a table of `(seed, run, acted, roles, observability)`. The
driver derives four assertions per entry:

| | positive | negative |
|---|---|---|
| **renamed vocabulary** | acts on the card | inert in a non-target
column |
| **default vocabulary** | acts (regression floor) | inert |

Adding a converted sweep is **one entry** — the #2478 site proved that
in practice, not in principle. `actsOnRole`/`inertRole` are keys of
`Vocabulary`, not column strings, so an entry cannot hardcode `todo` and
pass for the wrong reason.

## Two findings, both from mutation rather than reading

**1. The census was wrong about `recovery-reconciler.ts:198.`** It was
flagged as a `resolveLifecycleColumns` site, so the row was first
labelled as covering it. **Destroying that role resolution leaves all 18
tests green** — `decideRecovery` looks policy up by *column id* and
never consults a role. The row is relabelled to what it actually proves,
and mutation-verified against that instead: keying the reconciler's
policy lookup on the `todo` literal fails exactly its renamed test.

**2. `resolveRoleRecovery` is an unreachable export.** It is the only
use of `resolveLifecycleColumns` in that file and has **no production
caller anywhere** in engine, core, or dashboard. So that census line is
not a live converted site — it is a helper written ahead of its
consumer. **Not fixed here:** it is production code owned by the U4
slice, and whether the consumer is still to land or it should be deleted
is its author's call.

## Observability is now explicit in the type

`persisted-row` is the strong form. `returned-decision` is recorded as
**weaker evidence** and the reconciler row uses it, because
`reconcileRecovery` decides and does not apply — there is no row to
read. Naming it in the type is what stops a return-value assertion from
quietly passing as observed state, and it is what keeps the ledger
truthful per site.

## Harness seam

`PgTestHarness` now exposes its raw admin SQL client. The store
**stamps** `updatedAt`/`columnMovedAt` on every write, so `updateTask`
cannot express an aged row at all — the patch is accepted and the value
silently replaced with `now`. **Found by the new case failing on BOTH
vocabularies**, which is what distinguishes a broken fixture from a
broken guard. Seeding only; assertions still read back through the real
`getTask` path.

## Mutation verification

| Mutation | Result |
|---|---|
| revert **only** `recoverStrandedCompletedTodoTasks`'s resolution |
**exactly** that row's renamed test fails |
| revert **only** `surfaceStalePausedTodos`'s resolution | **exactly**
its own renamed test fails |
| reconciler policy lookup keyed on `todo` | exactly the reconciler
row's renamed test fails |
| `resolveRoleRecovery` role resolution destroyed | **nothing fails** →
finding #2 |
| `hold-release` `isHeldTask` keyed on `todo` | 5 of 18 fail; default
spine survives |
| `markMoveInFlight` dropped | both spine tests fail |

Per-site verification matters here: three rows could all be riding one
guard. They are not.

## The honest number

**Proven end to end: 5** (two self-healing sweeps, the reconciler's
policy lookup at the weaker observability, hold-release's capacity
release, and the graph boundary + `moveTask` + post-commit bus).

**Not proven: 11 call sites** — `merger.ts:324-326`,
`merger-ai.ts:1022,1039`, `auto-merge-finalization.ts:20-22`,
`executor.ts:1763,6339,6341`, `self-healing.ts:713,6732`,
`mesh-lease-manager.ts:61`, `task-agent-sync.ts:59`,
`core/task-store/reads.ts:130`, `core/live-agent-count.ts:63-75`, and
four dashboard route sites.

The ledger lives in the file, not just here, so it stays with the code.

## Where the table does not fit — reported, not papered over

The **merge/rebound family** cannot be a table row: those sweeps have no
observable persisted effect without a real git repository, so `acted`
cannot be written against the row at all. They need an engine-slow
real-git lane. The dashboard sites need an HTTP route test with a live
store. Both are different lanes, not missing entries.

## Verification

- 18/18 green; engine + core `tsc --noEmit` clean; `pnpm test:gate`
green (299 + 10 + 71)
- full core PG suite run (the harness is shared): 1036 passed, 3 failed
in `central-archive-secrets` and `workflow-settings-project-identity` —
**reproduce identically with this change stashed**, pre-existing

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:25:17 -07:00
gsxdsm
2dfac47917 U4: recovery policy as an OVERRIDE LAYER (unset defers to the operator setting) (#2482)
Stacked on #2478. Base is
`feature/workflow-vocabulary-u4-reconciler-slice` — do not merge before
it.

Implements the ratified precedence: **declared explicitly → policy wins;
left unset → defer to the project/global setting, exactly as today.**

## The design

`resolveEffectiveRecovery(declared, inherited)` composes the two **per
field**, so a workflow may declare a threshold while inheriting the
action. It mirrors the two-tier merge `effective-settings.ts` already
implements for workflow settings (a stored value overrides the base; a
declaration default only fills an absent key) rather than inventing a
fourth precedence system beside model selection, project settings, and
workflow settings.

**Absence stays absent.** `??` treats an explicitly-`undefined` field as
unset, so a policy is never normalized into a built-in default. The
distinction a naive implementation gets wrong:

> **equal-to-default is not the same as unset**

A declaration whose value happens to equal the legacy literal is a
*deliberate choice* and must still override a customized operator
setting. Only true absence defers.

An effective policy requires **both** halves — a threshold with no
action never fires, an action with no threshold has nothing to fire on —
so a half-resolved policy yields `undefined` rather than something
present but inert.

## The test that matters was written first, and failed

> a project with a CUSTOMIZED threshold and the policy key UNSET must
observe the customized value

This is where a green suite lies. "Read the policy, else use the
built-in default" passes every obvious test while silently resetting an
operator who tuned `stalePausedTodoThresholdMs` — no error, nothing in
any diff, the sweep just starts firing on a schedule nobody chose.

**Mutation-verified in both directions:**

| mutation | result |
|---|---|
| substitute a built-in default for the inherited setting | **5 tests
fail** |
| invert precedence (inherited beats declared) | **3 tests fail** |

## Upgrade guarantee

Asserted as a property over several operator values: an undeclared
workflow observes *exactly* the operator's value. That is what makes
landing the policy table a zero-behavior-change upgrade that touches no
project.

## What is NOT here

**`surfaceStalePausedTodos` is not retired.** Migrating it surfaced a
safeguard-semantics collision I escalated rather than resolved
unilaterally: the sweep exists to surface cards that have been
**paused** too long, but the reconciler's ratified user-pause safeguard
suppresses `surface` on user-paused cards — so migrating it as-is would
suppress a large part of what the sweep is for. `paused` and
`userPaused` are distinct fields that can diverge (see
`branch-group-ops.ts:128`). The sweep is untouched pending that
decision.

## Verification

- 34 tests green (10 new inheritance + 24 safety)
- `tsc --noEmit` clean, `pnpm lint` clean

No changeset: `@fusion/engine` is private.

🤖 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**
- Recovery decisions now correctly inherit operator settings when a
workflow does not specify a recovery policy.
- Workflow-specific recovery settings override inherited values,
including when matching built-in defaults.
- Recovery settings can now be applied independently by field, allowing
thresholds and stale-item actions to inherit separately.
- Recovery is suppressed safely when no complete policy is available,
preventing unintended recovery actions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 15:24:19 -07:00