d5030c55eafe7acbc2ce416bcb8dc7be2f5f0079
12372 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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)
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 --> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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) |
||
|
|
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>"not all"</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 "disallow modification" 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><svg></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=" |
||
|
|
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 --> |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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` |
||
|
|
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) |
||
|
|
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> |
||
|
|
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 --> |
||
|
|
3578903b16 |
U4: characterize surfaceStalePausedTodos before the policy migration (regression floor + safeguard evidence) (#2484)
Based on `main`, independent of #2482 — mergeable on its own. Pins what `surfaceStalePausedTodos` does **today**, so the first real policy migration is judged against *observed* behavior rather than against what the sweep looks like it should do. **No production code changed.** Passes against current main; must still pass after the migration. ## 1. Threshold source — the case you asked to see proven first The sweep reads `settings.stalePausedTodoThresholdMs` directly, so under the override layer an undeclared workflow must keep observing exactly that value. A migration that reaches for a declaration default instead finds the card fresh and **silently stops surfacing it** — resetting a deliberate operator choice with no error and nothing in any diff. Both directions are asserted, deliberately: - a customized **1h** threshold surfaces a 2h-old card; - the **same card is NOT surfaced** under the 24h built-in. Without that discriminator, "always surface" would pass the first test and prove nothing about which threshold was used. ## 2. User-paused cards — evidence for the open safeguard question This replaces my argument with a measurement. `getStalePausedTodoSignal` gates on `paused === true` and **never consults `userPaused`**, so a user-paused card that has sat too long **is surfaced today**. That test passes on current main. This is what blocks the migration: the reconciler's ratified user-pause safeguard suppresses the `surface` action for `userPaused` cards, so routing this sweep through it unchanged would **stop surfacing them** — a silent behavior change dropping a diagnostic operators rely on. `paused` and `userPaused` are distinct fields that can diverge (see the comment at `branch-group-ops.ts:128` about `userPaused` remaining true while legacy `paused` is false), so these are genuinely separable states rather than one condition spelled two ways. **The decision needed:** does the user-pause safeguard mean *"never act on a user-paused card"* or *"never mutate lifecycle state of one"*? It generalizes — `surfaceStalePausedReviews` and `surfaceInReviewStalls` are the same shape. ## 3. Things easiest to lose in a rewrite Also pinned: inert under `globalPause` and `enginePaused`; a non-positive threshold disables it entirely; an unpaused card is never surfaced; automation-paused and user-paused cards are not distinguished today. ## Verification 8 tests green against unmodified main; lint clean. No changeset: `@fusion/engine` is private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b133d521c4 |
U4 vertical slice: recovery-policy reconciler + ratified safety invariant (measured: engine ~780, real cost is a settings migration) (#2478)
Stacked on #2477. Base is `feature/workflow-vocabulary-u4-delete-dep-blocked` — do not merge before it. The smallest end-to-end slice of the U4 reshape, built to **measure** the real cost before committing to the full policy table. The survey's ~900-line reconciler figure was reasoned, not prototyped; this replaces it with numbers. **Everything here is additive and unwired. No behavior changes.** ## What lands | | lines | what | |---|---:|---| | `WorkflowColumnRecovery` (IR) | 42 | one key — `stalenessMs` + `onStale`. Optional and omitted when unset, so existing workflows serialize byte-identically. | | `recovery-reconciler.ts` | 176 | one engine: walks live cards, resolves each card's policy from **its own** workflow (per task, shared `irCache` — a 400-card board across three workflows reads three IRs), returns decisions. Decision and application are separate so the safety boundary is assertable without running an engine. | | `recovery-policy-safety.test.ts` | 156 | one-time. The **ratified invariant**. | ## Measured cost vs the ~900 estimate **Engine + IR types = 222 lines** for one action (`surface`) and one safeguard. Extrapolating the rest — `rebound` (target resolution, attempt budgets, backward-move proof, five more safeguards) ≈ +350, `archive` ≈ +50, the `budgets`/`dependencies` keys ≈ +150 — lands near **780**. So **~900 was a good estimate for the engine**, and the vertical slice does not move it much. That is the answer to the question asked. ## But the estimate's real miss is not lines **16 of the 34 POLICY sweeps read an operator setting today** — ~17 distinct policy-threshold keys, including `stalePausedTodoThresholdMs`, `inReviewStalledThresholdMs`, `taskStuckTimeoutMs`, `doneAutoArchiveDays`, `maxPostReviewFixes`. Moving those sweeps into workflow policy is **not a code refactor — it is a settings migration with operator-visible blast radius**, and it needs three decisions the line estimate never surfaced: 1. Does workflow policy **override** the global setting, or defer to it? 2. What happens to **existing projects** that already configured those settings? 3. Does an **unset** policy inherit the setting, or the built-in default? That is the gating question for the full table — not the reconciler's size. ## Why the sweep is not retired here Retiring `surfaceStalePausedTodos` requires builtin:coding to declare the policy **and** `stalePausedTodoThresholdMs` to migrate — or the behavior silently disappears for every existing project. That is the settings migration above, and it belongs behind its own decision rather than smuggled into a measurement slice. The reconciler is therefore **unwired — deliberately dead code**, for exactly as long as it takes to get that decision. ## The ratified safety invariant The six safeguards (user pause, `autoMerge:false`, dependency, capacity, merge-proof, at-most-once) live **outside** the policy table. A workflow must never be able to author a safety invariant away. Encoded two ways, because either alone is defeatable: - **structural** — the policy exposes only an allow-listed key set; adding a key requires editing the test and re-stating the safety argument (the friction is the point); - **behavioral** — a policy attempting every spelling of "ignore the user pause" has no effect. **Both halves mutation-verified**, because a safety test that cannot fail is worse than none: - making the reconciler honor a policy field that disables the user-pause safeguard → **fails** - adding an unreviewed key to the policy schema → **fails** A third test asserts the reconciler still **acts** on an unpaused card, so a reconciler that suppressed everything cannot pass by doing nothing. ## Scope limits stated rather than implied Only the `surface` action is implemented, so only its relevant safeguard is wired. `surface` mutates no lifecycle state; the other five gate lifecycle-**mutating** actions that do not exist yet, and wiring them now would be untestable dead code. A test records this so the absence reads as deliberate and must be updated when `rebound` lands. ## Verification - `tsc --noEmit` clean in core and engine; `pnpm lint` clean - merge gate green (299 + 10 + 71) - 23 safety tests green; `workflow-lifecycle-traits` green No changeset: `@fusion/core` and `@fusion/engine` are private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
2dce642ccc |
E2E validation: run a RENAMED-column workflow against a live engine (real graph + real PostgreSQL) (#2475)
Stacked on #2472 (`feature/workflow-vocabulary-b3-stranded-todo`). Test-only. No production file is touched. ## Why Every slice of this program has closed with the same caveat: *no renamed workflow was run against a live engine; all evidence is unit-level*. That caveat is load-bearing — eight times this session a test passed without exercising its subject. This PR removes it for the lifecycle spine. ## What actually runs `packages/engine/src/__tests__/workflow-lifecycle-live-e2e.pg.test.ts` drives the REAL pieces: - a **real PostgreSQL `TaskStore`** on a throwaway per-file database (shared PG harness; never the operator's DB, never port 4040), - the **real graph interpreter** (`WorkflowGraphTaskRunner`) with the **real column-boundary controller** wired to the **real `store.moveTask`** — all of its guards, traits, capacity reservation, and post-commit emission, - the **real scheduler release** (`runHoldReleaseSweep`), - the **real post-commit lifecycle bus** (`getWorkflowEventBus`), - the **real converted self-healing sweep** (`SelfHealingManager.recoverStrandedCompletedTodoTasks`, slice B3.1). Only the AI **seams** are scripted — the same boundary `testMode`/`mock` draws in production. **Assertion rule:** every lifecycle claim is asserted on **persisted state** (a fresh `getTask` with the store's task cache defeated, `run_audit_events` rows, `workflow_work_items` rows), never on "a function was called". The one spy — the event-bus subscriber — is asserted on the **received payload**, because the bus silently drops events that fail its shape check, so "emit was called" proves nothing. **Differential design:** the default-vocabulary (`todo`/`in-progress`/`in-review`/`done`) and renamed-vocabulary (`backlog`/`building`/`checking`/`shipped`) workflows come from ONE builder and differ ONLY in their four column ids. Any behavioral delta is attributable to the vocabulary alone. ## Coverage (9 tests, all green) | Scenario | What is proven | |---|---| | Default vocabulary, full spine | planning runs in the hold column, the card parks (graph does not self-promote), the **scheduler** performs hold→wip, the resumed run walks exec → review → merge-gate → end, persisted column is `done` | | **Renamed vocabulary, full spine** | identical, and no leg of the run touches any legacy column id | | Audit differential | the graph-owned boundary crossings are the same crossings node-for-node on both vocabularies; no legacy id appears in the renamed trail | | Event seam | a real subscriber **receives** a well-formed `TaskTransitioned` for the renamed `backlog`→`building` release and for the terminal move; `NodeEntered` arrives for every traversed node including `end` | | Crash / restart | exactly one durable continuation row at `exec`; a brand-new runner resumes from the row and the already-completed `planning` seam does **not** re-run; no duplicate continuation | | Converted sweep (B3.1) | a completed card in a **renamed** hold column is promoted (asserted on its persisted column), a card in the renamed **wip** column is not, and the default `todo` case still works | ## Mutation verification (both directions) Green suites are not evidence in this codebase, so both halves were falsified: 1. Keying `hold-release`'s `isHeldTask` on the `todo` literal → **5 of 6 spine tests fail, and the one that survives is the default-vocabulary one.** That is the exact signature the conversion program cares about. 2. Reverting slice B3.1's per-task hold-column resolution to the literal → **only the renamed stranded-todo test fails**; the default regression floor stays green. ## Findings surfaced by running it 1. **The IR validator refuses a `merge-blocker` column with no reachable merge-class node** ("the gate can never clear without one"). Kept rather than worked around — it means the review column here is genuinely gated. 2. **Entry into the merge region collapses to the legacy `merge` seam** (`MERGE_REGION_KINDS`), so a `merge-gate` node reaches the merge lane. Documented in the fixture. 3. **The transition policy refuses a direct hold → review move**, and it refuses it *workflow-resolved*: on the renamed board the only legal target is its own `building`, not `in-progress`. The recovery callback therefore promotes hold → wip → review rather than bypassing the policy. 4. **`moves.ts` still special-cases the `done` literal** (`if (toColumn === "done") clearNearDuplicateReferencesTo...`) after the post-commit emit. Not converted here and not in this PR's scope — flagged for the Phase B owner. ## Not driven end to end (stated plainly) - **Triage / specification.** The lifecycle starts from a task already bound to a workflow; `triage.ts` was not driven. The `planning` seam is scripted. - **Real merge.** No git worktree, no branch, no squash. `merge-gate` is pure policy; the `merge` seam is scripted. - **Lightweight / self-healing-off workflow.** The Tier 1 policy keys do not exist on this tip — there is no `policies` surface on the IR to set. Not drivable; not substituted with a unit test. - **Process-level crash.** The restart is an in-process one: a brand-new runner resuming from the persisted `workflow_work_items` row with no carried-over memory. No OS process was killed, so this proves durable-state resumption, not signal handling. ## Lane `.pg.test.ts` under the engine-default include glob, gated by `pgDescribe` so it skips cleanly with no PostgreSQL. The merge gate is untouched. Engine `tsc --noEmit` is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added comprehensive live PostgreSQL workflow lifecycle coverage, including graph execution, suspension and resume, scheduler capacity release, crash recovery, and durable continuation. * Added validation for renamed workflow column configurations and columnless task movements. * Added event delivery checks for task transitions and node entry events. * Added self-healing recovery for stranded completed tasks in valid hold columns. * **Refactor** * Centralized workflow boundary handling, including task moves, continuation state, audit events, and diagnostics. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
710d56b2db |
U4 trim: delete the dependency-blocked-todo feature (unreachable in production) and revert 5a2de7d (#2477)
Stacked on #2474. Base is `feature/workflow-vocabulary-u4-dead-code` — do not merge before it. Deletes an **entire feature that has never executed in production**, and reverts `5a2de7d`, which only threaded resolved lifecycle columns through it. ## Reachability evidence — the whole basis for this ``` surfaceDependencyBlockedTodos ← in NEITHER sweep registry; no caller in └─ getDependencyBlockedTodoReporter() engine/dashboard/cli — only tests └─ engine/dependency-blocked-todo-reporter.ts ← sole caller of ↓ └─ core/computeDependencyBlockedTodoReport ``` self-healing owns two name-based sweep registries (`runStartupRecovery`, 58 entries; `runMaintenance`, 76). `surfaceDependencyBlockedTodos` is in **neither**, so nothing ever invoked the chain below it. Its four tests passed while proving nothing about production. ## Why delete rather than wire it up Wiring was the tempting option and is the riskier one. Switching on a 450-line path that has never run — whose tests therefore establish nothing about its behavior against real data — is a **behavior change with unquantified blast radius**. This program already refused exactly that move for the **pool-id sentinel**, a one-line change that would switch on dormant enforcement across every project. This is the same class of move at ~450× the size. Deleting is also the recoverable direction: git keeps the feature, and it can be resurrected deliberately — with tests that prove it *runs* — if dependency-blocked reporting is actually wanted. ## The settings keys go with it `dependencyBlockedTodoReportEnabled` defaulted `true` while driving nothing. A schema/API-visible switch that lies about what the system does is worse than no switch. (It had no dashboard UI field — the dashboard test allowlist already recorded it as *"no UI field"*.) Four sibling tuning keys are removed with it. ## Against my own earlier work `5a2de7d` threaded resolved lifecycle roles into `computeDependencyBlockedTodoReport` and its reporter, answering a review finding I confirmed as real. **The code was correct; the impact claim was not**, because the path never executes. Neither the reviewer nor I checked *reachability* before agreeing the defect mattered — only correctness. A correction is posted on that thread in #2470. **Scope limit on that admission:** the same finding also described *incorrect scheduler ordering*. That half runs through `buildUnblockWeightMap` in `task-priority.ts`, which is **live** and was already threading `terminalColumns` (B1, `434b385`). Scheduler ordering was never affected, before or after. ## What survives `blocker-fanout.ts` **stays** — it is live via `task-priority.ts`. Only the plural `holdColumns` option added by `5a2de7d` is reverted, since the deleted report was its sole consumer. `holdColumn` (singular, from B1) remains. ## Net **1,244 deletions / 5 insertions across 15 files** — ~450 production lines, ~684 test lines, 5 settings keys. ## Verification - `tsc --noEmit` clean in **core, engine, and dashboard-app**; `pnpm lint` clean - merge gate green (299 + 10 + 71) - self-healing suite: 411 passed, 1 **pre-existing** failure (`archiveStaleDoneTasks`) - dashboard settings-descriptions suite green - `settings-parity.test.ts` has one **pre-existing** failure (`agentToolOutputMaxChars` overlap) that fails identically with these changes stashed — unrelated to this deletion No changeset: `@fusion/core` and `@fusion/engine` are private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added quiet-window backlog health diagnostics for stalled items in review, with repeat-alert suppression. * Added default thresholds for backlog-pressure alerts. * **Changes** * Removed dependency-blocked todo reporting and related alerts. * Removed the dependency-blocked todo enable/disable setting; remaining tuning options are no longer active. * Updated the workflow hold classification to use a single todo column. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
923a7c0fc9 |
U4 trim: delete the dead resetStepsIfWorkLost duplicate in self-healing (#2474)
Stacked on #2472 (Phase B slice B3.1). Base is `feature/workflow-vocabulary-b3-stranded-todo` — do not merge before it. First output of the **U4 reshape survey**: dead code removed with reachability evidence, not a redundancy argument. ## What is deleted **`SelfHealingManager.resetStepsIfWorkLost`** — 50 lines including its docblock and a section header left with no other member. Evidence: - `private`, with **zero in-file references** beyond its own declaration. - TypeScript already reported it as `declared but its value is never read` — the compiler has been flagging this. - The **live** implementation is `executor.ts:20080`, an independent copy that `executor.ts` actually calls (12667, 14664). The self-healing copy is an orphaned duplicate of it. - Not in either sweep registry, no test of its own, no caller anywhere in engine / dashboard / cli. Safe against every U4 constraint: it enforces none of user pause, `autoMerge:false`, dependency, capacity, merge-proof, or an at-most-once safeguard. ## Why the second approved deletion is NOT here `surfaceDependencyBlockedTodos` was approved alongside this one as a 28-line orphan. It is not. On inspection it is the **tip of an entire unreachable feature**: ``` surfaceDependencyBlockedTodos (in NEITHER registry, no production caller) └─ getDependencyBlockedTodoReporter() ← sole caller └─ engine/dependency-blocked-todo-reporter.ts 223 lines ← sole caller of ↓ └─ core/dependency-blocked-todo-report.ts 184 lines ``` ≈ **450 production lines across three files, plus 684 lines of tests in four files.** The operator-visible setting `dependencyBlockedTodoReportEnabled` defaults `true` in `settings-schema.ts` and drives nothing. Deleting only the approved 28-line tip would be **strictly worse than leaving it** — it orphans the getter and field and strands 407 lines of module with no remaining reference to explain why. Escalated for a decision (delete the subtree / wire the feature up / leave it) rather than resolved unilaterally. ## Verification - `tsc --noEmit` clean, `pnpm lint` clean - self-healing suite: **415 passed**, 1 failure **pre-existing** (`archiveStaleDoneTasks` — fails identically before this change) 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 * **Behavior Changes** * Removed automatic detection and reset of task steps when no unique work is found on a task branch. * Tasks with completed or in-progress steps will no longer be automatically returned to pending based on this condition. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
a4dee1162e |
Phase B slice B3.1 (U4): resolve the hold column in recoverStrandedCompletedTodoTasks — query and guard together (#2472)
Stacked on #2471 (Phase B slice B2). Base is `feature/workflow-vocabulary-b2` — do not merge before it. **First landable slice of U4 (self-healing.ts).** One sweep, one PR, per the phase's sub-split rule. ## The finding: the guard and the query must convert together `recoverStrandedCompletedTodoTasks` promotes a card whose steps are all done/skipped but which is still sitting in the hold column — finished work that never handed off to review. It decided *"is this card in the hold column?"* **twice**, and both were literal: | | was | |---|---| | the QUERY | `listTasks({ column: "todo", slim: true })` | | the GUARD | `task.column !== "todo"` | **Either half alone is a green diff with zero behavior change.** A correct guard behind a literal query never runs; a converted query behind a literal guard rejects every row it just fetched. This is the shape that made B1's stale-paused-todo fix cosmetic, and the phase brief predicted more of it here — correctly. I proved it rather than asserting it: - literal **QUERY** restored (converted guard kept) → **3 tests fail** - literal **GUARD** restored (converted query kept) → **2 tests fail** Neither half passes the suite alone. ## Falsification came first Per the brief I tried to prove the work unnecessary before doing it. It is necessary, and the evidence is empirical, not assumed: the 7 tests were written against unmodified code and 3 failed. Unlike B2's hold-release — which turned out already converted — **self-healing is uniformly unconverted at the query level**: 53 of its sweeps carry a hardcoded `column:` filter (survey in the worker report). ## Negative half, per the brief A completed card resting in a WIP or review column is **not** promoted. Dropping a column filter without a per-task hold check would promote finished cards out of every column — laundering work past review, a louder bug than the silent one being fixed. ## Test-harness hazard (will recur in every remaining U4 slice) The pre-existing self-healing store mock returns its fixture from `listTasks` **regardless of arguments**. A renamed-hold test on that harness passes while the query stays hardcoded, because the mock hands the sweep rows the real store never would. The new harness **honors** the column filter, and one test asserts the query is no longer scoped to the literal. This is documented in the new file's header for whoever writes the next slice. ## Cost The column filter is gone, so the cheap non-column rejections (paused / executing / incomplete steps / errored / no-commits / skip-bypass taint) run **first and synchronously**; only survivors pay an IR resolution, shared through an `irCache`. A board spanning three workflows resolves three IRs regardless of card count. `includeArchived: false` preserves what the column filter did implicitly. The hold column resolves **per task** — a board spans workflows, and a card in *another* workflow's hold column must not be promoted. ## One pre-existing assertion changed, deliberately `self-healing.test.ts` pinned `listTasks` being called with `{ column: "todo", slim: true }`. That query shape changed on purpose; the assertion now pins the new one. The behavioral assertions either side of it (one qualifying card, promoted exactly once) are untouched and still pass. ## Carried A3 questions — both answered **Q1 — does the sync/SQLite counter have the same pool-id mismatch?** **Not applicable: there is no sync counter.** `occupantsByColumnForWorkflowImpl` and `listWorkflowOccupantTaskIds` are async/PG-only and throw without an initialized `AsyncDataLayer`; the sync twin went with the PG cutover. There is no second counter that could mismatch. The surviving pool-id sentinel sites are `project-store-ops.ts:767/819` and `moves.ts` — both parked by operator decision, untouched here. **Q2 — are custom workflows with an explicit numeric limit affected?** **No, by design.** `resolveColumnCapacity` gives `config.limit` top precedence (`configLimit` → `limitSetting` → default-workflow read-through → `Infinity`), and `resolveWipBudgetColumns` documents that a column with an explicit numeric limit is **independent — its budget is itself alone**. Such a column never pools, so there is no pool id to mismatch. Read-only analysis; no code changed for either question. ## Remaining U4 scope (not in this PR) 214 literal occurrences across ~70 methods; **53 sweeps carry a query-level column filter**. Hold-gated sweeps still to convert: `clearStaleBlockedBy`, `reclaimSelfOwnedBranchConflicts`, `reconcileCompletedTask`, `recoverMergedReviewTasks`, `recoverStuckMergeDeadlocks`, plus non-query `todo` guards in `recoverPausedAbortFailures`, `reconcileDependencyBlockingLeases`, and others. `surfaceStalePausedTodos` was already converted (B1 follow-up) and is verified intact on this branch. ## Verification - 7 new tests green; **both mutations kill the suite** - self-healing suite: 415 passed, **1 failure pre-existing** (`archiveStaleDoneTasks` — confirmed identical by stashing my changes) - merge gate green (299 + 10 + 71) - `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** * Improved recovery of completed tasks stranded in workflow-specific hold columns, including renamed hold columns. * Preserved recovery for built-in workflows while correctly handling boards with mixed workflow configurations. * Prevented recovery for tasks in non-hold columns or with paused, incomplete, or errored states. * Added fallback handling when workflow details cannot be resolved. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
02b0f4f860 |
Phase B slice B2: U5 small movers — 12 literal sites converted, plus a negative result on hold-release (#2471)
Stacked on #2470 (Phase B slice B1). Base is `feature/workflow-vocabulary-conversion` — do not merge before it. ## What this is Phase B slice B2 — the U5 small movers. **12 literal sites converted, plus one negative result.** The plan estimated 36 sites. A survey found 12 genuinely-convertible ones, and separately found that the plan's headline hold-release scenario **was already fixed**. Both are reported below rather than padded into a bigger-looking diff. ## The negative result (commit 1) The plan named hold-release.ts as a target on the scenario *"release readiness must hold and release identically for a RENAMED hold column."* I wrote that test first, to prove it broken. **It is not broken.** All five assertions passed against unmodified `hold-release.ts`. U6/KTD-5 had already converted the module — `isHeldTask`, `resolveReleaseTarget`, and `dependencySatisfied` each resolve the task's IR. **`hold-release.ts` has no production change in this PR.** The tests are kept as a regression floor: the invariant rests on three independent trait resolutions any of which could be "simplified" back to a literal, and nothing else covered a renamed vocabulary end-to-end through the sweep. **I verified the tests can actually fail.** Mutating `isHeldTask` back to `task.column === "todo"` kills all five. Without that check, a green run against unmodified code is indistinguishable from a test asserting something trivially true. Two drafting notes kept in the file: the renamed ids deliberately avoid colliding with any legacy literal, and the first draft's two dependency tests used a `capacity` hold — which never consults dependencies at all, so one passed **vacuously**. Both now use a `dependency` hold. ## The 12 conversions Each was red-green: the renamed-workflow test written first and **observed failing**, then made to pass. | Site | Was | Now | |---|---|---| | `task-agent-sync` CLEAR_COLUMNS | `{done,archived,todo,triage}` | resolved complete+archived+hold+intake | | `task-agent-sync` isParkedTaskColumn | `{todo,triage}` | `parkedColumns` param (hold+intake) | | `task-agent-sync` handler branch | `to === "todo" \|\| "triage"` | resolved parked set | | `mesh-lease` parked guard | `task.column !== "todo"` | resolved rebound column | | `mesh-lease` rebound move | `moveTask(id,"todo")` | resolved rebound column | | `mesh-lease` audit decisionPath | `=== "todo" ? … : …` | same resolved column | | `mesh-lease` audit newColumn | `… : "todo"` | same resolved column | | `merger-ai` already-finalized | `=== "done" \|\| "archived"` | resolved complete+archived | | `merger-ai` ×4 rebounds | `moveTask(id,"todo")` | shared `resolveFinalizeReboundColumn` | Rebound targets all use KTD-10 `resolveReboundTarget` (hold → intake → first column), the helper `self-healing.ts:714` already uses — reused, not invented. ## Three findings worth reading **1. The mesh-lease bug was in the AUDIT, not the move.** The guard and the audit were *independent* `=== "todo"` comparisons, so `newColumn` asserted the card landed in `todo` regardless of what the move did. For a workflow with no `todo` column that produced a lease-recovery trail naming a nonexistent column — and run-audit is the only post-hoc record of a lease recovery. Now resolved once and threaded to both, so they are structurally incapable of disagreeing. **2. The merger-ai failure mode was not what I predicted.** I expected the already-finalized guard to fail open and re-merge a finished card. The red run showed it actually throws `Cannot merge FN-1: task is in 'shipped', must be in 'in-review'` — a hard error blaming the column, on a task whose real state is "already done". The thing preventing the re-merge is *itself* a literal in core's `getTaskMergeBlocker`, outside this slice. Two bugs coinciding, not a design. **3. A fourth site had to move that wasn't on the list.** `evaluateParkedAgentTaskLink` calls `isParkedTaskColumn` internally. Converting only the handler would have left the preservation branch on legacy ids after the caller resolved a renamed workflow — trading a stale-link bug for a **worse** dropped-link bug (a live agent's link cleared mid-run). ## Deliberately NOT converted Both keep their literals with the reason recorded at the site under a greppable `DELIBERATE-LITERAL` tag: - **`hold-release.ts:326` `legacyDependencySatisfied`** — the FN-5719 dual-accept half. Converting makes both halves compute the same answer, deleting the compatibility signal *and* its divergence detector while looking like a cleanup. - **`replan-target.ts` final fallback** — its value is precisely that it is *not* trait-resolved; resolving it against the workflow is the stranded-card bug it was written to fix. ⚠️ **The U12 literal ratchet does not exist in the tree yet.** The brief assumed an allowlist to add entries to; there is none. `grep -rn DELIBERATE-LITERAL packages/*/src` enumerates the sites it must admit. ## What I could NOT verify - **One of the four merger-ai rebound sites is untested.** The `landWorkspaceTask` rebound is verified by inspection and the shared resolver's unit tests only — `landWorkspaceTask` is only ever *mocked* (project-engine.test.ts), never executed. Covering it needs a multi-repo git fixture and a full land run. The **other three are genuinely exercised** by pre-existing merger-ai.test.ts (lines 676/716/777/895 assert `moveTask("FN-1","todo",…)` through a real git repo) and pass unchanged — real wiring proof for those. - **3 of the 9 task-agent-sync tests passed before the conversion too**, vacuously — the literal handler early-returned and cleared nothing. They assert nothing about the old code; they are guardrails against the conversion over-clearing. - **No renamed workflow was run against a live engine.** All evidence is unit-level. ## Call sites outside this slice — NOT converted, byte-identical They keep the legacy defaults: `scheduler.ts:1273`, `agent-heartbeat.ts:1169/3642`, `self-healing.ts:11600/11665` (all `evaluateParkedAgentTaskLink`), and `merger.ts:6585` (the sibling terminal guard). Each is its own Phase C/D surface. ## Behavior changes (not a pure refactor) For a **renamed** workflow: agent links now actually get cleared on terminal moves (they never were); lease rebounds land in the resolved hold column; finalize-blocked rebounds land in the resolved hold column and their operator-facing task-log lines name the real column; already-finalized cards short-circuit cleanly instead of throwing. For **builtin:coding** and any unresolvable workflow: byte-identical. Every new parameter defaults to the legacy set, and both merger-ai resolvers fail *soft* to legacy ids in opposite directions — the terminal guard keeps `done`/`archived` (losing it sends a finished card into the merge path), the rebound keeps `todo` (abandoning it strands the card in the merge lane with no owner). ## Verification - Merge gate **green**: 299 + 10 + 71 tests - Slice suites **green**: 100 tests across 8 files (new + all pre-existing neighbours) - Existing merger suites **green**: 82 tests across 5 files, unchanged - `tsc --noEmit` clean, `pnpm lint` clean No changeset: `@fusion/engine` is private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
5d0f1ef631 |
Phase B slice B1: lifecycle column roles in the U6 policy modules (4 guards, red-green) (#2479)
**Stacked on #2469** → #2468 → #2467. Base is `feature/workflow-capacity-ground-truth`. This is **slice B1 of Phase B, not all of Phase B.** Sizing escalation sent separately; the census is below. ## Why this is a slice Measured census of code lines referencing a lifecycle column literal (comments excluded): | Unit | Files | Sites | |---|---|---:| | U4 | `self-healing.ts` | 203 | | U5 | `executor.ts` 171, `scheduler.ts` 55, `replan-target.ts` 20, `merger-ai.ts` 5, `hold-release.ts` 4, `mesh-lease-manager.ts` 4, `task-agent-sync.ts` 3 | 262 | | U6 | `moves.ts` 34, `default-workflow-hooks.ts` 13, `board-config.ts` 9, `blocker-fanout.ts` 6, `task-priority.ts` 5, `dependency-blocked-todo-report.ts` 2, `stale-paused-todo.ts` 1 | 70 | | | **Total** | **535** | The plan's "~207" counts the guard category only. Under the phase's non-negotiable rule — a test that **fails before** conversion, per guard — that is ~200 red-green cycles. Doing it as one sweep would reproduce exactly the failure this phase exists to prevent: converted guards nobody proved still fire. `moves.ts` and `default-workflow-hooks.ts` stay **parked** per the dispatch constraint (move-path convergence and the pool-id sentinel are on an operator decision). ## Guards converted (4), each red-green Every case below was written **first** and observed failing against the literal implementation. | Module | Guard | Before → After | |---|---|---| | `stale-paused-todo.ts` | stall detection | `column !== "todo"` → resolved **hold** column | | `blocker-fanout.ts` | active | `ACTIVE_COLUMNS.has(col)` → `!terminalColumns.has(col)` | | `blocker-fanout.ts` | hold-wait metric | `col === "todo"` → resolved **hold** column | | `task-priority.ts` | unblock active | `UNBLOCK_ACTIVE_COLUMNS` **deleted**, folded into the terminal set | Three of the seven new cases are **regression floors** that pass before and after. One of them earned its keep immediately: it failed on my own fixture (`activeCount` vs the public `totalCount`), catching a bad test rather than bad code — which is the point of asserting the default path alongside the renamed one. ### The `task-priority` finding `UNBLOCK_ACTIVE_COLUMNS` and `DONE_COLUMNS` encoded **one concept twice**, two lines apart, and disagreed for any custom column: dependency counting treated a `drafting` card as unmet (correct) while the active check treated it as inactive (wrong), zeroing the blocker's unblock weight. The enumeration wasn't just legacy-shaped — it contradicted its own neighbour. ## ⚠️ Behavior change, not a pure refactor Inverting active from enumeration to exclusion means **a card in a column that is neither terminal nor in the legacy enum now counts as active where it previously did not.** That is the plan's stated intent, but it is a real change for any project already using a custom column — **Coding (Ideas)' `ideas` column is the in-tree case.** Fan-out counts and unblock weights for such cards will rise. ## Verification - Four affected suites green (45 tests), each conversion observed red→green. - `pnpm lint`, `tsc --noEmit` (core) green. **Not verified / not done, stated plainly:** - **Call sites are not wired.** These modules now *accept* resolved roles; every parameter still defaults to the legacy set, so at the call sites the vocabulary is unchanged. A caller that cannot resolve a workflow keeps literal behavior. Threading `resolveLifecycleColumns` through `reads.ts` and `self-healing.ts` is follow-on work — until then the guards are *convertible*, not *converted end-to-end*. - `dependency-blocked-todo-report.ts` and `board-config.ts` are untouched in this slice. - 19 core-suite failures exist on this branch; all confirmed **pre-existing** by stashing and re-running on a clean tree (`duplicate-guard`, `log-severity-spam-contract`, `settings-parity`, `task-delete-caller-attribution`, `settings-defaults`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Supersedes #2470**, which GitHub force-closed when its base branch was deleted by the merge of #2469 and refuses to reopen. Same head branch, same commits (rebased onto `main`), now based on `main` directly. The two P1 review threads on #2470 were resolved there — one of them with a correction noting the threading half landed in code that was subsequently deleted as a dead feature in #2477. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Dependency and blocker reports now correctly recognize custom hold, active, and terminal workflow columns. * Blockers in renamed terminal columns are no longer incorrectly reported as active. * Stale paused-task badges and self-healing now work with workflow-specific hold columns. * Mixed boards with different workflow column names are handled consistently. * Existing default workflow behavior remains compatible, including fallback handling when workflow details cannot be resolved. * **Enhancements** * Reporting and task-priority calculations now support configurable single or multiple hold and terminal columns. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
553cc3b517 |
Phase A3: the in-txn capacity invariant does NOT hold — root-caused, ratcheted, not fixed (#2469)
**Stacked on #2468**, which is stacked on #2467. Base is `feature/workflow-move-path-convergence`. **Answer: the A2 observation was real. The documented invariant is broken.** Not a false alarm, not a harness artifact. `workflow-capacity.ts` states enforcement "runs INSIDE `moveTaskInternal`'s transaction and is **NEVER bypassable** (not a guard — runs regardless of bypassGuards/recoveryRehome/moveSource)". It does not hold for default-workflow tasks, for two independent reasons. ## R1 — Pool-id sentinel mismatch (the defect) | Site | Sentinel for "no workflow selection" | |---|---| | `moves.ts:319` (in-txn check, **asks**) | `?? "builtin:coding"` | | `countActiveInCapacitySlotAsyncImpl` (**answers**) | `?? DEFAULT_WORKFLOW_POOL_ID` → `"__default-workflow__"` | | `hold-release.ts:116` (sweep, second enforcement point) | `?? DEFAULT_WORKFLOW_POOL_ID` ✅ | The check asks for occupants of a pool that no occupant is ever bucketed into, so the count comes back `0` and the limit can never bind. The sweep is correct, so the two enforcement points **disagree about pool identity** — precisely what the module docstring says is impossible ("the two enforcement points can never disagree on what a limit *is*, only on the live count"). Note the shape of the bug: it is not a missing check. The check runs, queries correctly, and returns a confidently wrong answer. ## R2 — The `useWorkflow` gate The whole block sits inside `if (useWorkflow && workflowIr && fromColumn !== toColumn)` (`moves.ts:921`), and `useWorkflow` reads the raw `experimentalFeatures.workflowColumns` key nothing in production sets. **On the live path the check cannot run at all**, so R1 is latent today and becomes reachable the moment A2 converges onto the flag-ON side. ## How this was established, not guessed Three of my assumptions failed earlier in this program, so this one is pinned by a **discriminating experiment** rather than a code reading: | Case | Path | Selection | Result | |---|---|---|---| | DEFECT (R2) | inline (live) | none | accepted — check cannot run | | DEFECT (R1) | hooks | none | accepted — sentinels disagree | | **DISCRIMINATOR** | hooks | explicit `builtin:coding` | **refused, `capacity-exhausted`** | The third case changes nothing but sentinel agreement. That rules out "capacity is simply not wired" and isolates the cause to the mismatch. Both-path forcing reuses A2's `assertPathActive` probe. Without it this suite would silently run one path twice and report a tautology — the failure mode that produced sixteen false passes across A2's two harness bugs. ## The deliverable: an invariant ratchet The three cases above assert today's wrong behavior, so on their own they would let the defect live forever. A fourth case states the invariant **as written** and is marked `it.fails`: - **today** — the body fails, so `it.fails` passes; CI stays green while honestly recording the breach; - **when fixed** — the body passes, `it.fails` *fails*, forcing whoever lands the fix to flip it and the two `DEFECT:` expectations. That is "a test that fails if the invariant is broken" in the only shape that does not park a permanently-red test in CI. ## Blast radius (step 4) — why I did not fix it The fix is one line: make `moves.ts:319` use `DEFAULT_WORKFLOW_POOL_ID`, matching the sweep. The consequences are not one line. **Today: zero.** `useWorkflow` is false everywhere, so the corrected check still cannot run on the live path. The sentinel fix is safe to land in isolation. **At A2 convergence: every default-workflow task move into `in-progress` becomes capacity-checked against `maxConcurrent` (default 2), for the first time.** Affected movers: - **The graph column boundary** catches `capacity-exhausted` and *parks the run*. Runs that previously proceeded would begin suspending — this is a scheduling behavior change across every project, not an error path. - **`executor.ts`, `project-engine.ts`, `pr-comment-handler.ts`** each move tasks into `in-progress` and would begin seeing a rejection they have never seen. - **Operator drags and the promote route** would start refusing beyond `maxConcurrent`. Scheduler-side admission (`maxConcurrent`) is a separate, still-live control, so this is not "capacity is unenforced today" — it is "the store-level check the graph and promote paths are written against returns 0 and never binds". **Recommendation:** land the sentinel fix on its own (provably inert today), with the ratchet flipped in the same commit, *before* A2 convergence — so convergence does not simultaneously switch paths and switch on a previously-dead enforcement. ## Verification 4 cases green against real PostgreSQL (3 passed + 1 expected-fail), path flip proven live on every case. `pnpm lint` and `tsc --noEmit` green. ## Follow-up: both "not verified" items are now answered Recorded here rather than left as open questions, since this is where anyone investigating capacity will look. **1. Does the sync/SQLite counter carry the same mismatch? — YES, identically, but it is unreachable.** `countActiveInCapacitySlotSyncImpl` (`project-store-ops.ts:767`) buckets rows the same way as the async one: ```ts const effectiveWorkflowId = row.wid ?? TaskStore.DEFAULT_WORKFLOW_POOL_ID; ``` So it disagrees with `moves.ts:319` in exactly the same way. **However** its only caller is the public `TaskStore.countActiveInCapacitySlotSync` wrapper, which has no in-repo caller at all — it is dead API surface. The mismatch is real but currently unreachable, which makes it a landmine for whoever wires it up rather than an active defect. Fixing the sentinel should fix both call sites together. **2. Are custom workflows with an explicit numeric `limit` affected? — NO, and this is already proven by the discriminator above.** The mismatch fires **only when the selection row is absent** — that is what the `??` fallback is for. A custom workflow necessarily *has* a selection row; that is what makes it custom. The DISCRIMINATOR case adds an explicit selection and the rejection appears, which is exactly the custom-workflow shape. So the defect is scoped to **no-selection (default-workflow) tasks only**. The explicit-numeric-`limit` question turns out to be orthogonal: `resolveColumnBudgetKey` returning `col:${columnId}` decides *which columns share a budget*, not which pool id is passed to the counter. It does not interact with the sentinel at all. Net effect on blast radius: **narrower than first stated.** Only default-workflow (no-selection) tasks slip the limit today. Custom-workflow tasks are already enforced — meaning the fix does not switch enforcement on for them, it only closes the gap for the default workflow. 🤖 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 workflow column capacity enforcement during transactions. * Documented scenarios where capacity limits are bypassed, including tasks without workflow selection. * Verified that explicit workflow selection correctly rejects moves when capacity is exhausted. * Added a tracked failing test for the expected invariant once enforcement is corrected. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
b941d3cba5 |
Phase A2 (steps 1-2): differential characterization of the two move paths (#2468)
**Stacked on #2467** (base is `feature/workflow-owned-lifecycle`, not
`main`).
Phase A2 steps 1 and 2. **Step 3 — make one path authoritative and
delete the other — is NOT done.** It is blocked on a measured
divergence, escalated to the operator. This PR is the evidence that
decision needs.
## The setup
`moves.ts` branches on `useWorkflow =
isWorkflowColumnsCompatibilityFlagEnabled(settings)`, which reads the
raw `experimentalFeatures.workflowColumns` key. Nothing in production
writes it, so the **inline branch is LIVE** and
**`default-workflow-hooks.ts` is DEAD**.
Because only one implementation runs, equivalence cannot be observed by
running the suite normally — the dead path is never entered. Every case
here forces both paths explicitly through one shared fixture and
compares a 19-field observation, not "it moved".
## Step 1-2 result: side-effect equivalence is PROVEN
Eight behaviors, field-by-field identical across both paths:
| Behavior | Verdict |
|---|---|
| `in-progress → todo` user reopen field clears | equivalent |
| Engine-source reopen does not set `userPaused` | equivalent |
| `preserveStatus` keeps status/error | equivalent |
| `preservePause` keeps an operator park (FN-7851) | equivalent |
| Timing / `cumulativeActiveMs` across exit and re-entry | equivalent |
| `preserveResumeState` step progress | equivalent |
| `preserveWorktree` | equivalent |
| Default worktree clear on reopen | equivalent |
### Why this is a proof and not a green suite
Two independent guards, both of which caught a real silent failure in
this PR's own development:
- **The forcing mechanism is self-checked.** `assertPathActive` probes
an undeclared target column — whose rejection message differs per path —
before every case. The first version of this suite wrote the flag with
`updateSettings` instead of `updateGlobalSettings`
(`experimentalFeatures` is global-scoped, which is exactly why
`moves.ts` reads it through `getSettingsFast()`), and reported **nine
passing "equivalence" cases while running the inline path twice**. The
check then caught a second failure: `updateGlobalSettings` *merges*, so
resetting with `{}` left a previous `true` in place and leaked the hooks
path into seven cases that believed they were on inline.
- **The suite is mutation-tested.** Deleting `task.blockedBy =
undefined` from `applyResetOnEntryEffects` fails the reopen case, naming
the field. Restored before commit.
Timestamps are compared by presence rather than value — the two runs
happen at different wall-clock instants by construction — but a path
that forgets to stamp `executionCompletedAt`, or wrongly clears
`firstExecutionAt`, still fails.
## ⚠️ Read this before writing any both-paths test
**A differential harness that cannot prove which path it is on will
report a tautology, confidently, and in green.** This suite hit that
twice in one afternoon:
1. **Global-scoped key written to project scope.** The first version set
the flag with `updateSettings`. `experimentalFeatures` is
**global**-scoped — which is exactly why `moves.ts` reads it through
`getSettingsFast()` (merged global + project). The write was silently
accepted and never reached `useWorkflow`. Result: **nine passing
"equivalence" cases while running the inline path twice.**
2. **Merge-on-write leaking a stale `true`.** `updateGlobalSettings`
*merges*, so resetting with `{ experimentalFeatures: {} }` left the
previous `workflowColumns: true` in place. Result: the hooks path leaked
into **seven cases that believed they were on inline.**
Neither failure produced a red test. Both were caught only by
`assertPathActive` — a per-case probe that moves a task to an undeclared
column and asserts on the rejection *message*, which differs per path
(`Valid targets: …` inline vs `Unknown column for this workflow` on
hooks).
This is the same failure class as a spy passing on a refused payload
(see #2467): **the observation confirms the assumption instead of the
behavior.** The rule that generalizes:
> When a test forces a code path, assert that the path is active using a
signal only that path can produce — before every case, not once in
setup. A forcing mechanism that can fail silently makes every assertion
downstream worthless.
Any future work touching both move paths needs this probe or it will get
a confident wrong answer.
## Why step 3 is blocked
### Divergence: rejection type and message
| | Inline (live) | Hooks (dead) |
|---|---|---|
| Validates against | legacy `VALID_TRANSITIONS` | the task's own
workflow |
| Throws | bare `Error` | `TransitionRejectionError` with a
machine-readable `rejection` |
| Message | `Valid targets: …` | `Unknown column for this workflow` |
Both reject, so neither is "broken" — but they are not interchangeable.
Making either authoritative changes what every catch site observes,
including the flag-OFF characterization suite that pins the bare-Error
contract and the callers that branch on `rejection.code`.
### Unproven, recorded as an honest negative: in-transaction capacity
The capacity block sits inside `if (useWorkflow && workflowIr &&
fromColumn !== toColumn)`, so it **cannot** run on the live path. The
natural inference is "convergence turns store-level capacity rejection
on for every project at once" — a serious blast radius, since
`capacity-exhausted` is what the graph column boundary parks on and what
the promote route surfaces to operators.
**That inference did not survive measurement.** With `maxConcurrent: 1`
and an already-occupied wip column, the second move was **accepted on
both paths**. Something further in — `resolveColumnCapacity`'s limit
resolution, or what `countActiveInCapacitySlotAsync` counts as an
occupant — keeps the check from firing even flag-on. This suite does not
establish which.
So the capacity blast radius is **unquantified, not absent**. The test
pins today's observed behavior so the investigation starts from a fact
rather than from the code reading; if a future change makes it reject,
that failure is the signal to reopen the question.
## Verification
- 10/10 green against real PostgreSQL, with the path flip proven live by
`assertPathActive` on every case.
- Mutation-tested (see above).
- `pnpm lint` and `tsc --noEmit` (core) green.
**Not verified:** whether the capacity gate would activate under some
other configuration; the plugin column-gate and post-commit plugin-hook
divergences (also inside the `useWorkflow` gate) are identified
structurally but not characterized here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Workflow lifecycle updates now provide more consistent task state
notifications and workflow activity handling.
* Task lists display resolved workflow column names and lifecycle
details more reliably.
* **Bug Fixes**
* Workflow-based task promotion and board views no longer depend on an
obsolete feature setting.
* Improved recovery for tasks left in transitional states.
* Preserved task status, pause, progress, timing, and worktree behavior
across workflow transitions.
* **Reliability**
* Added stronger validation and durable handling for workflow events and
follow-up processing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4158cf1ab7 |
Phase A: workflow-owned lifecycle foundation (U1, U2, U3) (#2467)
Phase A (Foundation) of
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`.
Three units, one commit each. No operator-visible behavior change.
## U1 — Lifecycle-column resolution seam
`resolveLifecycleColumns(ir)` returns `{ intake, hold, wip, review,
complete, archived }` — the first column carrying each trait,
`undefined` for a role no column carries.
`resolveTaskLifecycleColumns(store, taskId, cache?)` is the store-aware
form; the cache is caller-owned so a sweep reads one IR per workflow
rather than one per card.
A v1/column-less IR resolves to `undefined` for the **whole struct**
rather than a struct of undefined roles. A caller must be able to
distinguish "this workflow declares no hold column" (a real shape to
honor) from "no column vocabulary at all" (skip and log) — only the
second licenses conservative fallback.
Nothing consumes the seam yet; Phases B–D convert the ~207 hardcoded
column literals onto it.
## U2 — Delete the pre-cutover parity machinery (delete-only)
**`workflow-columns-settings.ts`** — `isWorkflowColumnsEnabled` had the
body `return true`. Six live call sites branched on it, so every
flag-OFF arm was dead code that read as a supported configuration.
Deleted; surviving side inlined at self-healing's transitionPending
sweep, the scheduler's per-column capacity diagnostic, merge-trait's
policy resolver, the board-workflows payload, two task-workflow routes,
and the CLI TUI's column enrichment.
**`workflow-parity.ts`** — asserted the default workflow's adjacency
*equals* the legacy `VALID_TRANSITIONS`. U11 deliberately breaks that
equality by merging Todo into Planning, so this is not a stale assertion
to update; it is a contract against the target state. Its emitter
(`workflow-parity-observer.ts`) is already a tombstone, so
`getWorkflowParitySummary` and `computeWorkflowColumnsGraduationReport`
aggregated run-audit rows nothing writes and had no caller outside
`TaskStore`. Both store methods go with it.
`flagEnabled` stays on the board-workflows **wire** as a constant `true`
— shipped dashboard clients still branch on it, and changing the
response shape is not a deletion. U10 retires the field once no client
reads it.
The `legacy-tombstones` ratchet is extended to both files plus seven
symbols, each with the reason it is gone.
### ⚠️ Finding: the third listed deletion was NOT dead
The plan also lists "the flag-off inline move path" in
`task-store/moves.ts`. It is **not** deleted, per U2's execution note
("any behavior change found while removing a branch means the branch was
not dead").
That path is gated on `isWorkflowColumnsCompatibilityFlagEnabled`
(`store.ts:38`) — a **different** function from the always-true public
helper. It reads the raw `experimentalFeatures.workflowColumns` setting,
which nothing in production sets (`settings-schema.ts:396` — "no default
flags are emitted"; zero non-test writers; the operator's own
`~/.fusion/settings.json` has no such key). So `useWorkflow` is false
for effectively every real project: the flag-OFF inline side effects are
the **live** default move path and the flag-ON `default-workflow-hooks`
path is the dead one. The code says so itself at `moves.ts:638`.
Deleting that branch would swap every project onto an untravelled code
path — a behavior change, not a deletion.
**Carry this into Phases B and C, stated plainly so the plan's error is
not repeated:**
> **The inline move path in `moves.ts` is LIVE.
`default-workflow-hooks.ts` (the trait-hook path) is DEAD.** KTD-6
asserted the inverse. Until the convergence unit lands, **nothing may
assume trait hooks run** — a guard, sweep, or subscriber written against
`applyDefaultWorkflowMoveEffects` would never fire in production and
would still pass its tests.
Convergence is **not** attempted here. It is its own unit (Phase A2)
with a proper equivalence proof, per operator decision.
### U3's emit point is on the LIVE path — the seam is not born dead
Worth stating explicitly because it is the failure mode that would make
every later subscriber silently never fire: the `TaskTransitioned` emit
is **not** inside the `if (useWorkflow)` branch. That block closes at
`moves.ts:1212`; the emit sits at `:1214`, beside the existing
`store.emit("task:moved", …)`, on the unconditional post-commit path. It
therefore fires on **both** the live inline path and the dead hooks
path, and the convergence unit inherits the obligation to keep it firing
on whichever path survives — same events, same order, same payloads.
The graph-side emitters (`NodeEntered`, `RunSuspended`) carry the same
risk from a different direction: the bus refuses an invalid payload
*silently* by design, so an emitter regression would stop the event with
no test failure. They are asserted end-to-end through the real bus —
"did a subscriber actually receive it", not "was emit called" — because
a spy passes on a refused payload. The `moveTaskInternalImpl` emit does
**not** yet have that end-to-end assertion against a real store move;
that proof belongs to the convergence unit, which has to build the
both-paths fixture anyway.
## U3 — Post-commit event seam with a transactional outbox
**The bus is not a queue, not a transaction participant, and not a
delivery guarantee.** Durable follow-on work uses the transactional
outbox — a `workflow_work_items` row written *inside* the transition
transaction (the shape `createCompletionHandoffWorkflowWork` already
uses). "Emit after commit, let a subscriber enqueue the work" has a
crash window where a process dies between commit and subscriber, leaving
no event *and* no work-item row, so required work is skipped permanently
with nothing to recover from. Post-commit subscribers therefore carry
only losable reactions.
Emission is consequently lossy and isolated by design: a throwing or
rejecting subscriber is caught and logged, cannot roll back the
transition, and cannot stop the others. Deliveries append to one serial
chain, so two transitions on a task deliver in commit order.
The ids/outcomes-only rule is **mechanised, not documented** —
run-audit's equivalent lives only in prose and has been violated
repeatedly. A payload carrying an object body or a prose string is
refused at the emit boundary and never reaches a subscriber or log sink.
It degrades rather than throws: the emitter is post-commit, so a shape
bug must not become a lifecycle failure.
Emit points: `TaskTransitioned` from the single post-commit point in
`moveTaskInternalImpl`; `NodeEntered` and `RunSuspended` from the graph
column boundary, the latter *after* the durable continuation is
persisted so an observed suspension implies a resumable run.
`registerWorkflowEventSubscribers` (engine) is empty on purpose —
U7/U8/U10 move real reactions onto it, each with the characterization
test proving the reaction was non-authoritative first.
## Verification
- `pnpm test:gate` — green (2/10, 16/299, 1/71).
- `pnpm lint`, `pnpm build`, `tsc --noEmit` on core and engine — green.
- U1: 20 tests in `workflow-lifecycle-traits.test.ts`, including the
fully-renamed-workflow case (fails if the resolver falls back to a
literal) and a shared-cache read-count assertion.
- U2: `legacy-tombstones.test.ts` green with the extended ratchet;
`board-workflows`, `merge-trait`, `workflow-graph-executor-parity`, and
move-hook suites green with no expectation edits.
- U3: 20 bus-invariant unit tests (isolation, ordering, the allowed-key
and required-key halves of the ids-only rule, lossiness) plus 3
end-to-end emitter-delivery tests; 5 outbox tests against a **real
PostgreSQL** work-item table (crash survival, rollback, at-least-once
redelivery on lease expiry, idempotent handler → one effect,
dropped-subscriber vs. durable work). A hand-written fake of the lease
predicate would only prove the fake redelivers.
**Not verified:** the `moveTaskInternalImpl` emit is confirmed on the
unconditional post-commit path by structure and by the surrounding
tests, but is *not* yet asserted end-to-end against a real store move on
both flag settings — that is Phase A2's fixture. The engine subscriber
registry ships empty by design, so no production subscriber exercises
the bus end-to-end yet. `settings-defaults.test.ts` has one pre-existing
failure on `main` (a logger-prefix mismatch in the
`mergeIntegrationWorktree=cwd-main` warning) — confirmed present on a
clean tree, unrelated to this branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Workflow lifecycle columns are now derived from workflow definitions,
supporting renamed and custom workflows.
* Added post-commit lifecycle events for task transitions, node entry,
and run suspend/resume with validated payloads.
* Follow-on processing for lifecycle emissions is now more robust
(rollback-safe, at-least-once delivery, idempotent handling).
* **Bug Fixes**
* Workflow board responses, task enrichment, and promotion no longer
depend on workflow-columns feature-flag gating.
* Subscriber failures no longer impact committed workflow transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
d873ee8160 |
docs: correct KTD-6 — the "flag-off" move path is live, the hooks path is dead (#2466)
Docs only. Corrects a factual error in the program plan merged in #2463, found during Phase A execution. ## What was wrong KTD-6 listed three deletions as "provably dead." Two are. The third — the flag-off inline move path in `task-store/moves.ts` — is the **live** path: - It is gated on `isWorkflowColumnsCompatibilityFlagEnabled` (`store.ts:38`), which reads the **raw** `experimentalFeatures.workflowColumns` key. - That is a *different* function from the always-true public `isWorkflowColumnsEnabled`, which is what the plan reasoned from. - No production code writes that key, so the flag reads false — the inline branch runs for essentially every project, and `default-workflow-hooks.ts` is the dead one. - `moves.ts:637` already said so: *"this 'flag-OFF' branch is the DEFAULT move path for nearly every project — the strict compat flag reads false because nothing sets it."* Deleting it would have swapped every project onto an untravelled code path and called it a cleanup. ## Changes - **KTD-6** carries the correction inline, with the two consequences that bind the rest of the program. - **U2** rescoped to the two proven-dead deletions; `moves.ts` is out of scope for it. - **New U2b** converges the two implementations with a per-behavior equivalence proof, and **blocks Phase B** — nothing downstream may assume the trait-hook path runs until it lands. - **U3's emit point must attach to the live inline path.** Wired into the dead hooks path, the event seam would never fire — and because subscribers are non-authoritative by design, *nothing would fail a test*. - Risk table gains: *a "dead" branch turns out to be live*. ## How it was caught The Phase A worker escalated instead of following the instruction, because U2 carried a delete-only Execution note: *any behavior change found while removing a branch means the branch was not dead — stop and treat it as a finding*. I verified the claim independently (flag function, absent writers, live `settings.json`, source comment) before accepting it. That note stays on every deletion unit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Clarified terminology in the workflow lifecycle migration plan, distinguishing the live inline move path from the compat-flag behavior. * Expanded the convergence phase with explicit behavior-equivalence requirements, pinned observable expectations, and removal of compat-flag branching. * Updated the migration diagram and phase ordering to reflect the new gating structure, revised scope boundaries, and refreshed risk mitigations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8b039a543e |
fix(desktop): advance Pi runtime pin to 0.82.1 for packaging PR lane (#2465)
## Summary - Advance the matched Pi runtime pin (`pi-ai`, `pi-coding-agent`, `pi-agent-core`, `pi-tui`) from **0.82.0 → 0.82.1** so electron-builder's production-dependency walk accepts `pi-agent-core`'s `pi-ai@^0.82.1` requirement. - Fixes the Desktop packaging PR-lane failure: `Production dependency @earendil-works/pi-ai not found for package @earendil-works/pi-agent-core` (required `^0.82.1`). - Keep the workspace override guard; update pin-policy fixtures and CLI package-config expectations. - Tighten the advisory packaging step-order test so it asserts against the real `electron-builder --dir` step (not a missing release-only step name that previously passed via `indexOf === -1`). - Run `pnpm dedupe` so the packaging lane's lockfile dedupe early-warning is clean. ## Context #2439 pinned the full Pi closure at 0.82.0 and made recent main-based packaging runs green. This advances to the current upstream patch so deploy + electron-builder stay aligned with `pi-agent-core@0.82.1`'s declared dependency range. ## Test plan - [x] `node scripts/check-pi-versions-pinned.mjs` - [x] `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs` - [x] `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/package-config.test.ts` - [x] `pnpm --filter @fusion/desktop exec vitest run src/__tests__/release-workflow.test.ts` - [x] `pnpm dedupe --check` - [ ] GitHub: Desktop packaging (should run full packaging walk — lockfile/package.json touched) - [ ] GitHub: PR Checks (Lint, Typecheck, Build, Gate) |
||
|
|
3c5d07ab01 |
docs: workflow-owned lifecycle program plan + column-placement contract (#2463)
Docs only — no code. Companion to #2462. ## `docs/solutions/architecture-patterns/workflow-node-column-placement-and-graph-entry-contract.md` Why a workflow node's `column` is a lifecycle contract rather than a display choice: it decides **who can drive the node**, **whether the card holds a WIP slot**, and **whether anything can move it onward**. Contents: - The graph **entry contract** (`resolveColumnResumeNode`, shipped in #2462) with the resume table. - The **plan-in-place chain** — triage → finalize → continuation seed → drain → resume → capacity suspend → release — annotated with the check each link performs. Notably `todo`, not `triage`: an intake column has no releaser, so a card parked there waits for a human. - Why the pre-release gate must be narrow (column match **and** enablement). - The measured failure table from three reverted placement attempts. - Why removing a column is a lifecycle-vocabulary refactor, not a workflow edit: **82 guards** that silently stop matching, **43 writes** to a column that no longer exists, **59 dashboard literals**. A guard that never fires doesn't fail a test — it disables a recovery path. ## `docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md` The program that finishes the job, in four movements: 1. Resolve lifecycle columns from the workflow instead of ~207 string literals. 2. Move every lane — planning, execution, review, merge — behind graph nodes; lane services keep substrate only (storage, leases, timers, supervision, capacity, recovery, audit). 3. A **post-commit event seam**: transitions commit transactionally, *then* emit; subscribers react and may enqueue durable work items, but no subscriber performs a transition. Enforced by test — dropping every subscriber must change no lifecycle outcome. 4. Only then merge Todo into a single Planning column. Phased so each phase lands green independently, with the IR change deliberately **last** (KTD-7). Changing the workflow first makes the suite green over dead guards — that's how the earlier attempts hid their own breakage. The merge lane **adopts** the existing design in `docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md` (slices S02–S08, still `draft-stack-handoff`) rather than authoring a competing one, with a note to re-validate against current `main` since it was drafted seven weeks ago. Scale is stated honestly: ~48k lines across the four lane services, with the executor unit explicitly landing across several commits rather than one sweep. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
99c9f14ee0 |
feat: run Plan Review in the planning lane with a Plan Review badge (#2462)
## What Plan Review, planning, and the replan loop move from the implementation column into the **planning lane** (`todo`), so a task under specification never holds a WIP slot. The card crosses into `in-progress` exactly once, at `parse`, released by the scheduler. Operators also finally see a **Plan Review** badge while the gate runs — it was previously invisible on the default workflow. ## The part that made it possible Moving the node is ten lines. It was attempted three times and reverted each time, because a graph run with no durable continuation replayed from `start` and dragged an in-progress card *backward* out of the WIP column, firing `abort-on-exit` and stranding it in a pre-WIP column with no releaser. So this PR adds the graph **entry contract** — `resolveColumnResumeNode`: | Card is in | Resumes at | |---|---| | `triage` | `start` | | `todo` | `plan` | | `in-progress` | `parse` — never re-plans, never moves backward | | `in-review` | first review node — gates are not skipped | `ir.columns` is ordered and that order is the lifecycle order; rework and failure edges are excluded so the entry point is always the main path. The proof it's the right fix: **`executor-task-done-invariant` passes unmodified** after failing every previous attempt. ## Also in here - **Release gate narrowed twice.** `isUnplannedForExecution` applies its pre-release plan-review gate only when the node's column equals the card's column *and* the group is enabled for the task. The enablement check fixes a real deadlock — a task with Plan Review toggled off was held forever waiting for evidence nothing would ever write. - **Badge cleanup.** Gate badge reads "Plan Review" instead of the ambiguous "Reviewing" and no longer hides behind a lane restriction; the status badge stops duplicating it; `planning` renders as "Planning" instead of the raw engine token. - **Coding (Ideas)** renames its planner column to "Planning" (id `todo` unchanged) and loses its private planning-node re-home — the graph it clones is already plan-in-place. - **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose column its workflow no longer declares. Written for a follow-up, kept because it makes any column edit survivable. ## Test changes Scheduler and release fixtures now model a card whose Plan Review passed — the state every real card is in when the capacity sweep sees it. A held unreviewed card is the gate working, and that path stays owned by `pre-release-plan-review.test.ts`. New `workflow-graph-entry-contract.test.ts` covers the invariant at every lifecycle position, plus the gap-column and remediation-node cases. ## Verification Gate 299 + 70 + 10, dashboard badge suites 672, engine workflow/entry/executor suites 147, core 122. Lint and typecheck clean. Full engine suite sits at the pre-existing baseline (notifier / plugin-runner / notification-service, untouched by this). ## Follow-up Removing the Todo column entirely is a separate ~207-site lifecycle-vocabulary refactor — planned in `docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md` (companion docs PR). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Plan Review now runs in the Planning lane before implementation begins. * Cards resume from their current workflow column without replaying earlier steps. * Added automatic recovery for cards stranded in outdated workflow columns. * **Improvements** * Renamed the Coding (Ideas) planner column to “Planning.” * Refined Plan Review gating to respect enabled settings and the card’s current column. * Updated planning and Plan Review badges for clearer, consistent labels across cards and lists. * **Bug Fixes** * Improved workflow transitions and release behavior around planning, review, and execution. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5ae6332563 |
refactor: collapse dead SQLite dual-path code; keep migration-only readers (#2454)
# Remove dead SQLite dual-path code; keep migration-only readers ## Summary PostgreSQL cutover left hundreds of production dual-path branches (`backendMode ? PG : SQLite/store.db`) whose SQLite arms only hit throwing `Database`/`ArchiveDatabase`/`CentralDatabase` stubs. This change mechanically collapses those unreachable arms so production authority is AsyncDataLayer/PostgreSQL only, while preserving the six authorized read-only migration/recovery `DatabaseSync` seams. ## Dual-path mass removed | Metric | Before | After | |---|---|---| | `if (…backendMode)` (non-test) | ~328 | ~70 | | `store.db` / `this.db` refs in core (non-test) | ~570+ | ~375 (mostly pure legacy MissionStore/eval/insight SQLite classes + thin getters) | | Net diff | — | **~6.7k lines removed** across 41 files | Remaining `backendMode` checks are intentional (incomplete-PG sync safe-defaults, settings-sync disabled-on-PG, symbol-lock PG-only gates, “requires PostgreSQL” config versioning throws), not live SQLite authority. ## Subsystems cleaned - **Core TaskStore / task-store/***: collapsed if/else and early-return dual-path across reads, moves, lifecycle, mutations, workflow, archive, branch/PR, artifacts, comments, audit, project ops, etc. `initImpl` is PostgreSQL-only (SQLite startup tail deleted). - **Satellite stores**: automation, agent, routine, plugin, secrets, approval-request, central-core dual-path arms collapsed. - **Plugins**: reports async methods, compound-engineering pipeline + session stores, CLI Printing Press store — SQLite fallbacks removed; PG required. - **Engine**: no functional dual-path change beyond whitespace (settings-sync / peer-exchange PG-disabled behavior kept). ## Six migration-only readers retained (allowlist unchanged) 1. `packages/core/src/postgres/sqlite-migrator.ts` 2. `packages/core/src/project-identity.ts` 3. `packages/core/src/sqlite-validation.ts` 4. `packages/core/src/postgres/startup-factory.ts` 5. `packages/cli/src/commands/db.ts` 6. `scripts/lib/start-local-project.mjs` Plus low-level `sqlite-adapter` and migrator/startup-import tests. Inventory ratchet still requires exactly these six `new DatabaseSync(` production sites, all `readOnly: true`. ## Not treated as SQLite - `.fusion/project.json`, `task.json`, `agent-log.jsonl` file storage - AsyncDataLayer / Drizzle PG paths - Incomplete-PG sync safe-default stubs (still return empty/false/null under backend without consulting SQLite) ## Verification - `sqlite-production-reader-inventory.test.ts` — 15/15 pass - `incomplete-pg-ports.pg.test.ts` — 6/6 pass - Targeted PG tests (create-task, move, handoff, runtime-persistence, agent, mission, insight, central-core) — green - `tsc --noEmit` for `@fusion/core`, `@fusion/engine`, `@fusion/dashboard` — green - `scripts/check-no-getdatabase.mjs` — clean <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Improved end-to-end consistency by making PostgreSQL/async persistence the standard across core task/workflow, automation, agents, plugins, routines, secrets, approvals, central operations, and session storage. * Unified scheduling, settings, configuration revision writes, run/workflow selection, queues/leases/transitions, and audit/lifecycle updates around consistent async transaction behavior. * **Bug Fixes** * Fixed edge cases for archived/deleted reads, unarchive/recovery flows, not-found handling, and task/artifact/document/log/comment operations, including more reliable emissions and hydration across search/list and lifecycle operations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
52d64fa66e |
fix(engine): project CE steps after review handoff (#2464)
## Summary - reconcile successful graph-native workflow results with pending task checklist steps even when review handoff already moved the card into the merge column - preserve terminal, paused, and no-redundant-move behavior - cover the real Compound Engineering post-review-handoff state with a regression test ## Root cause Compound Engineering runs `review-handoff` before `merge`. Review handoff moves the task to `in-review`, which is also the merge column. `ensureWorkflowMergeBoundaryTask()` returned immediately for cards already in that column, before projecting successful `workflowStepResults` onto legacy `Task.steps[]`. The merger then saw `0/N` and rejected approved work with `task has incomplete steps`. ## Verification - RED: regression test failed before the fix because `store.updateTask` was never called - GREEN: `executor-graph-boundary.test.ts` — 6 passed - relevant non-PostgreSQL set — 31 passed, 5 PostgreSQL tests explicitly skipped - `@fusion/engine` typecheck passed - changeset format passed - `git diff --check` passed ## Baseline note `ce-workflow-step-executor.test.ts` currently has three failures on clean `origin/main` after FN-8601 foreach-proof hardening. The same failures reproduce without this patch and are not regressions from this change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved reconciliation after review handoff by projecting completed step results onto the legacy checklist when reaching the merge column. * Prevented tasks from being marked approved with incomplete step counts (including “0/N” style states). * Reduced unnecessary merge failures and deadlock/pause scenarios when merge-column progress was already recorded. * **Tests** * Added coverage for execute-and-merge workflows, ensuring merge-boundary resolution updates pending steps without moving the task. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
d6c7e7dc74 |
chore(deps): bump actions/cache from 4 to 6 (#2441)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 6. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/actions/cache/releases">actions/cache's releases</a>.</em></p> <blockquote> <h2>v6.0.0</h2> <h2>What's Changed</h2> <ul> <li>Update packages, migrate to ESM by <a href="https://github.com/Samirat"><code>@Samirat</code></a> in <a href="https://redirect.github.com/actions/cache/pull/1760">actions/cache#1760</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v5...v6.0.0">https://github.com/actions/cache/compare/v5...v6.0.0</a></p> <h2>v5.1.0</h2> <h2>What's Changed</h2> <ul> <li>Bump <code>@actions/cache</code> to v5.1.0 - handle read-only cache access by <a href="https://github.com/jasongin"><code>@jasongin</code></a> in <a href="https://redirect.github.com/actions/cache/pull/1775">actions/cache#1775</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v5...v5.1.0">https://github.com/actions/cache/compare/v5...v5.1.0</a></p> <h2>v5.0.5</h2> <h2>What's Changed</h2> <ul> <li>Update ts-http-runtime dependency by <a href="https://github.com/yacaovsnc"><code>@yacaovsnc</code></a> in <a href="https://redirect.github.com/actions/cache/pull/1747">actions/cache#1747</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v5...v5.0.5">https://github.com/actions/cache/compare/v5...v5.0.5</a></p> <h2>v5.0.4</h2> <h2>What's Changed</h2> <ul> <li>Add release instructions and update maintainer docs by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/cache/pull/1696">actions/cache#1696</a></li> <li>Potential fix for code scanning alert no. 52: Workflow does not contain permissions by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/cache/pull/1697">actions/cache#1697</a></li> <li>Fix workflow permissions and cleanup workflow names / formatting by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/cache/pull/1699">actions/cache#1699</a></li> <li>docs: Update examples to use the latest version by <a href="https://github.com/XZTDean"><code>@XZTDean</code></a> in <a href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li> <li>Fix proxy integration tests by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/cache/pull/1701">actions/cache#1701</a></li> <li>Fix cache key in examples.md for bun.lock by <a href="https://github.com/RyPeck"><code>@RyPeck</code></a> in <a href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li> <li>Update dependencies & patch security vulnerabilities by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/cache/pull/1738">actions/cache#1738</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/XZTDean"><code>@XZTDean</code></a> made their first contribution in <a href="https://redirect.github.com/actions/cache/pull/1690">actions/cache#1690</a></li> <li><a href="https://github.com/RyPeck"><code>@RyPeck</code></a> made their first contribution in <a href="https://redirect.github.com/actions/cache/pull/1722">actions/cache#1722</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v5...v5.0.4">https://github.com/actions/cache/compare/v5...v5.0.4</a></p> <h2>v5.0.3</h2> <h2>What's Changed</h2> <ul> <li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li> <li>Bump <code>@actions/core</code> to v2.0.3</li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/cache/compare/v5...v5.0.3">https://github.com/actions/cache/compare/v5...v5.0.3</a></p> <h2>v.5.0.2</h2> <h1>v5.0.2</h1> <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/actions/cache/blob/main/RELEASES.md">actions/cache's changelog</a>.</em></p> <blockquote> <h1>Releases</h1> <h2>How to prepare a release</h2> <blockquote> <p>[!NOTE] Relevant for maintainers with write access only.</p> </blockquote> <ol> <li>Switch to a new branch from <code>main</code>.</li> <li>Run <code>npm test</code> to ensure all tests are passing.</li> <li>Update the version in <a href="https://github.com/actions/cache/blob/main/package.json"><code>https://github.com/actions/cache/blob/main/package.json</code></a>.</li> <li>Run <code>npm run build</code> to update the compiled files.</li> <li>Update this <a href="https://github.com/actions/cache/blob/main/RELEASES.md"><code>https://github.com/actions/cache/blob/main/RELEASES.md</code></a> with the new version and changes in the <code>## Changelog</code> section.</li> <li>Run <code>licensed cache</code> to update the license report.</li> <li>Run <code>licensed status</code> and resolve any warnings by updating the <a href="https://github.com/actions/cache/blob/main/.licensed.yml"><code>https://github.com/actions/cache/blob/main/.licensed.yml</code></a> file with the exceptions.</li> <li>Commit your changes and push your branch upstream.</li> <li>Open a pull request against <code>main</code> and get it reviewed and merged.</li> <li>Draft a new release <a href="https://github.com/actions/cache/releases">https://github.com/actions/cache/releases</a> use the same version number used in <code>package.json</code> <ol> <li>Create a new tag with the version number.</li> <li>Auto generate release notes and update them to match the changes you made in <code>RELEASES.md</code>.</li> <li>Toggle the set as the latest release option.</li> <li>Publish the release.</li> </ol> </li> <li>Navigate to <a href="https://github.com/actions/cache/actions/workflows/release-new-action-version.yml">https://github.com/actions/cache/actions/workflows/release-new-action-version.yml</a> <ol> <li>There should be a workflow run queued with the same version number.</li> <li>Approve the run to publish the new version and update the major tags for this action.</li> </ol> </li> </ol> <h2>Changelog</h2> <h3>6.1.0</h3> <ul> <li>Bump <code>@actions/cache</code> to v6.1.0 to pick up <a href="https://redirect.github.com/actions/toolkit/pull/2435">actions/toolkit#2435 Handle cache write error due to read-only token</a></li> <li>Switch redundant "Cache save failed" warning to debug log in save-only</li> </ul> <h3>6.0.0</h3> <ul> <li>Updated <code>@actions/cache</code> to ^6.0.1, <code>@actions/core</code> to ^3.0.1, <code>@actions/exec</code> to ^3.0.0, <code>@actions/io</code> to ^3.0.2</li> <li>Migrated to ESM module system</li> <li>Upgraded Jest to v30 and test infrastructure to be ESM compatible</li> </ul> <h3>5.0.4</h3> <ul> <li>Bump <code>minimatch</code> to v3.1.5 (fixes ReDoS via globstar patterns)</li> <li>Bump <code>undici</code> to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)</li> <li>Bump <code>fast-xml-parser</code> to v5.5.6</li> </ul> <h3>5.0.3</h3> <ul> <li>Bump <code>@actions/cache</code> to v5.0.5 (Resolves: <a href="https://github.com/actions/cache/security/dependabot/33">https://github.com/actions/cache/security/dependabot/33</a>)</li> <li>Bump <code>@actions/core</code> to v2.0.3</li> </ul> <h3>5.0.2</h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
88cc152d12 |
chore(deps): bump actions/setup-node from 4 to 7 (#2440)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/actions/setup-node/releases">actions/setup-node's releases</a>.</em></p> <blockquote> <h2>v7.0.0</h2> <h2>What's Changed</h2> <h3>Enhancements:</h3> <ul> <li>Add cache-primary-key and cache-matched-key as outputs by <a href="https://github.com/gowridurgad"><code>@gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1577">actions/setup-node#1577</a></li> <li>Migrate to ESM and upgrade dependencies by <a href="https://github.com/gowridurgad"><code>@gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1574">actions/setup-node#1574</a></li> </ul> <h3>Bug fixes:</h3> <ul> <li>Remove dummy NODE_AUTH_TOKEN export by <a href="https://github.com/gowridurgad"><code>@gowridurgad</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1558">actions/setup-node#1558</a></li> <li>Only use <code>mirrorToken</code> in <code>getManifest</code> if it's provided by <a href="https://github.com/deiga"><code>@deiga</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li> </ul> <h3>Documentation updates:</h3> <ul> <li>Add documentation for publishing to npm with Trusted Publisher (OIDC) by <a href="https://github.com/chiranjib-swain"><code>@chiranjib-swain</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li> <li>docs: Update restore-only cache documentation by <a href="https://github.com/priya-kinthali"><code>@priya-kinthali</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1550">actions/setup-node#1550</a></li> <li>docs: Update caching recommendations to mitigate cache poisoning risks by <a href="https://github.com/chiranjib-swain"><code>@chiranjib-swain</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1567">actions/setup-node#1567</a></li> </ul> <h3>Dependency update:</h3> <ul> <li>Upgrade <code>@actions/cache</code> to 5.1.0, log cache write denied by <a href="https://github.com/jasongin"><code>@jasongin</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/chiranjib-swain"><code>@chiranjib-swain</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1536">actions/setup-node#1536</a></li> <li><a href="https://github.com/deiga"><code>@deiga</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1548">actions/setup-node#1548</a></li> <li><a href="https://github.com/jasongin"><code>@jasongin</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1569">actions/setup-node#1569</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6...v7.0.0">https://github.com/actions/setup-node/compare/v6...v7.0.0</a></p> <h2>v6.5.0</h2> <h2>What's Changed</h2> <ul> <li>Update <code>@actions/cache</code> to 5.1.0 and add security overrides for undici and fast-xml-parser by <a href="https://github.com/HarithaVattikuti"><code>@HarithaVattikuti</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1579">actions/setup-node#1579</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0">https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0</a></p> <h2>v6.4.0</h2> <h2>What's Changed</h2> <h3>Dependency updates:</h3> <ul> <li>Upgrade <a href="https://github.com/actions"><code>@actions</code></a> dependencies by <a href="https://github.com/Copilot"><code>@Copilot</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li> <li>Update Node.js versions in versions.yml and bump package to v6.4.0 by <a href="https://github.com/priya-kinthali"><code>@priya-kinthali</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1533">actions/setup-node#1533</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/Copilot"><code>@Copilot</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1525">actions/setup-node#1525</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v6...v6.4.0">https://github.com/actions/setup-node/compare/v6...v6.4.0</a></p> <h2>v6.3.0</h2> <h2>What's Changed</h2> <h3>Enhancements:</h3> <ul> <li>Support parsing <code>devEngines</code> field by <a href="https://github.com/susnux"><code>@susnux</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1283">actions/setup-node#1283</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |