fb0863f660c19cbbe8796b0b53a69072d424a85b
1397 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fb0863f660 |
FN-8753: enable installed voice input in project settings
Make Voice Input available only after its local model and runtime are ready. - Bundle the optional sherpa runtime with the published CLI. - Gate the project setting on model installation and stable runtime status codes. - Add localized recovery guidance, documentation, and coverage. Files changed: .changeset/fn-8753-voice-input-enable.md | 7 ++++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 8 +++-- packages/cli/package.json | 3 ++ packages/cli/src/__tests__/package-config.test.ts | 16 +++++++++ .../settings/__tests__/VoiceInputSection.test.tsx | 36 ++++++++++++++++---- .../settings/sections/VoiceInputSection.tsx | 30 ++++++++++++----- packages/dashboard/package.json | 2 +- .../routes/__tests__/register-voice-routes.test.ts | 39 ++++++++++++++++++++++ .../dashboard/src/stt/__tests__/voice-stt.test.ts | 27 ++++++++++++--- packages/dashboard/src/stt/parakeet-service.ts | 26 ++++++++++----- packages/i18n/locales/en/app.json | 6 ++++ pnpm-lock.yaml | 6 +++- 13 files changed, 175 insertions(+), 33 deletions(-) Fusion-Task-Id: FN-8753 Fusion-Task-Lineage: db92b148-37f0-45d2-b616-ed2e3f2d54f6 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
6e416b044d |
chore(deps): bump actions/download-artifact from 4 to 8 (#3304)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/actions/download-artifact/releases">actions/download-artifact's releases</a>.</em></p> <blockquote> <h2>v8.0.0</h2> <h2>v8 - What's new</h2> <blockquote> <p>[!IMPORTANT] actions/download-artifact@v8 has been migrated to an ESM module. This should be transparent to the caller but forks might need to make significant changes.</p> </blockquote> <blockquote> <p>[!IMPORTANT] Hash mismatches will now error by default. Users can override this behavior with a setting change (see below).</p> </blockquote> <h3>Direct downloads</h3> <p>To support direct uploads in <code>actions/upload-artifact</code>, the action will no longer attempt to unzip all downloaded files. Instead, the action checks the <code>Content-Type</code> header ahead of unzipping and skips non-zipped files. Callers wishing to download a zipped file as-is can also set the new <code>skip-decompress</code> parameter to <code>true</code>.</p> <h3>Enforced checks (breaking)</h3> <p>A previous release introduced digest checks on the download. If a download hash didn't match the expected hash from the server, the action would log a warning. Callers can now configure the behavior on mismatch with the <code>digest-mismatch</code> parameter. To be secure by default, we are now defaulting the behavior to <code>error</code> which will fail the workflow run.</p> <h3>ESM</h3> <p>To support new versions of the @actions/* packages, we've upgraded the package to ESM.</p> <h2>What's Changed</h2> <ul> <li>Don't attempt to un-zip non-zipped downloads by <a href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in <a href="https://redirect.github.com/actions/download-artifact/pull/460">actions/download-artifact#460</a></li> <li>Add a setting to specify what to do on hash mismatch and default it to <code>error</code> by <a href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in <a href="https://redirect.github.com/actions/download-artifact/pull/461">actions/download-artifact#461</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/download-artifact/compare/v7...v8.0.0">https://github.com/actions/download-artifact/compare/v7...v8.0.0</a></p> <h2>v7.0.0</h2> <h2>v7 - What's new</h2> <blockquote> <p>[!IMPORTANT] actions/download-artifact@v7 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. v6 had preliminary support for Node 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>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/download-artifact/pull/440">actions/download-artifact#440</a></li> <li>Download Artifact Node24 support by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li> <li>fix: update <code>@actions/artifact</code> to fix Node.js 24 punycode deprecation by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li> <li>prepare release v7.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/download-artifact/pull/452">actions/download-artifact#452</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/patrikpolyak"><code>@patrikpolyak</code></a> made their first contribution in <a href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li> <li><a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> made their first contribution in <a href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0">https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0</a></p> <h2>v6.0.0</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
0583f1684b |
chore(deps): bump ink from 7.0.5 to 7.1.1 (#3306)
Bumps [ink](https://github.com/vadimdemedes/ink) from 7.0.5 to 7.1.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vadimdemedes/ink/releases">ink's releases</a>.</em></p> <blockquote> <h2>v7.1.1</h2> <ul> <li>Fix: Preserve last <code><Static></code> line erased after a full-clear frame (<a href="https://redirect.github.com/vadimdemedes/ink/issues/974">#974</a>) e51dfdd</li> <li>Make <code>measureElement()</code> also return position coordinates (<a href="https://redirect.github.com/vadimdemedes/ink/issues/968">#968</a>) c073b27</li> </ul> <hr /> <p><a href="https://github.com/vadimdemedes/ink/compare/v7.1.0...v7.1.1">https://github.com/vadimdemedes/ink/compare/v7.1.0...v7.1.1</a></p> <h2>v7.1.0</h2> <ul> <li>Add <a href="https://github.com/vadimdemedes/ink#suspendterminalcallback"><code>suspendTerminal()</code></a> to hand the terminal to a child process (<a href="https://redirect.github.com/vadimdemedes/ink/issues/972">#972</a>) 9e8ed1f</li> </ul> <hr /> <p><a href="https://github.com/vadimdemedes/ink/compare/v7.0.6...v7.1.0">https://github.com/vadimdemedes/ink/compare/v7.0.6...v7.1.0</a></p> <h2>v7.0.6</h2> <ul> <li>Fix stale frames on Windows when output exactly fills the terminal (<a href="https://redirect.github.com/vadimdemedes/ink/issues/971">#971</a>) 2c08d55</li> </ul> <hr /> <p><a href="https://github.com/vadimdemedes/ink/compare/v7.0.5...v7.0.6">https://github.com/vadimdemedes/ink/compare/v7.0.5...v7.0.6</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
a3bb584bff |
chore(release): v0.74.0
Version bump via changesets. |
||
|
|
bc34b724ac |
chore(release): v0.74.0-beta.9
Version bump via changesets. |
||
|
|
0ae6c396d0 |
chore(release): v0.74.0-beta.8
Version bump via changesets. |
||
|
|
546a19aef6 |
chore(release): v0.74.0-beta.7
Version bump via changesets. |
||
|
|
006cc40454 |
FN-8685: add durable cross-process task deletion consumers
Deliver durable, replay-safe cross-process task deletion observation. - Add PostgreSQL lifecycle consumer cursors, leases, acknowledgements, retention, and recovery. - Start named consumers in dashboard, serve, and engine runtime paths. - Preserve delete integration metadata while suppressing replayed GitHub and GitLab side effects. - Cover outbox identity, observed delivery, fencing, and reconciliation behavior. Files changed: ...fn-8685-cross-process-task-deleted-observers.md | 7 + .../fn-8685-task-deleted-outbox-consumers.md | 7 + docs/architecture.md | 8 +- ...tgres-cross-process-task-deleted-observation.md | 8 +- docs/storage.md | 10 +- packages/cli/src/commands/dashboard.ts | 9 +- packages/cli/src/commands/serve.ts | 9 +- packages/cli/src/project-context.ts | 9 +- .../task-deleted-outbox-consumer.pg.test.ts | 157 ++++++++ ...-deleted-observed-dispatch-side-effects.test.ts | 36 ++ .../task-lifecycle-consumer-identity.test.ts | 22 ++ packages/core/src/index.ts | 11 + .../0041_fn_8685_task_lifecycle_consumers.sql | 88 +++++ packages/core/src/postgres/schema-applier.ts | 16 +- packages/core/src/postgres/schema/project.ts | 45 +++ packages/core/src/postgres/startup-factory.ts | 4 + packages/core/src/store.ts | 54 ++- .../__tests__/lifecycle-outbox-writer.test.ts | 4 +- .../core/src/task-store/archive-lifecycle-2.ts | 1 + packages/core/src/task-store/lifecycle-ops.ts | 13 +- packages/core/src/task-store/lifecycle-outbox.ts | 2 + packages/core/src/task-store/project-store-ops.ts | 4 +- .../src/task-store/task-deleted-outbox-consumer.ts | 333 +++++++++++++++++ .../task-store/task-lifecycle-consumer-identity.ts | 32 ++ .../task-store/task-lifecycle-consumer-registry.ts | 396 +++++++++++++++++++++ .../task-store/task-lifecycle-event-retention.ts | 104 ++++++ packages/core/src/task-store/task-mutation-ops.ts | 1 + packages/dashboard/src/github-tracking-state.ts | 12 +- packages/dashboard/src/gitlab-delete-close.ts | 3 + packages/dashboard/src/gitlab-split-close.ts | 7 +- packages/dashboard/src/project-store-resolver.ts | 9 +- packages/engine/src/project-manager.ts | 4 +- packages/engine/src/project-runtime.ts | 2 +- packages/engine/src/runtimes/in-process-runtime.ts | 17 +- packages/engine/src/self-healing.ts | 27 ++ 35 files changed, 1439 insertions(+), 32 deletions(-) Fusion-Task-Id: FN-8685 Fusion-Task-Lineage: 63eca9ac-d2af-44b0-ba79-388a950148d3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
1e83dcceec |
chore(release): v0.74.0-beta.6
Version bump via changesets. |
||
|
|
56e16d9dea |
test(cli): pin the board glyph's terminal-lane resolve (extract seam + pin) (#3238)
## What Pins the CLI board glyph's terminal-lane resolve — **the last flagged site in the repo-wide resolver audit.** Two commits: a behaviour-preserving extraction, then the test. ## I was wrong to flag this as unpinnable In #3236 I recorded this site as not pinnable, reasoning that *"extracting a pure helper and testing it would look like coverage and would not be."* That is true of a helper that **receives** the lane set — such a test passes with the resolve blinded, which is exactly the `reads.ts` trap the audit note records. It is **not** true of one that **resolves** it. Building `resolveReliabilityLanes` in #3237 made the distinction obvious: the seam has to contain the resolve, and then blinding fails a test of it. So the flag was too broad, and correcting it closes the site rather than leaving a permanent excuse. That is the same failure mode I corrected in someone else's note earlier today — a caution that hardens into a reason not to look. ## Measured ``` converted: Tests 5 passed (5) blinded: Tests 2 failed | 3 passed (5) ``` The two failures are the **renamed complete** and **renamed archive** lanes. The three survivors are the default-vocabulary control, the active-lane negative, and the degrade path — all of which should survive. ``` task-list-board-columns + bin: 82 passed typecheck clean; lint clean; fnxc-future-dates: none added ``` ## Why the sibling file did not cover it `task-list-board-columns.test.ts` pins `boardColumnsForDisplay`, which decides **which** lanes print. That function takes no lane set, so it cannot fail when this resolve is blinded — and its own header says so honestly. Two tests about the same command, one of which cannot see the other's bug. ## What breaks without the conversion On a board whose complete lane is `shipped`, a finished lane renders `●` — the same glyph as active work. The board says work is in flight when it shipped. Cosmetic next to the blank-board bug this area already fixed, but wrong in the direction an operator reads at a glance. ## Also pinned Two contracts the surrounding comments assert but nothing tested: - **Cards come from the TASKS, not a resolved IR** — a card must never depend on resolution succeeding to be *visible*. Asserted with an unreadable workflow list. - **A failed resolve degrades to the legacy pair**, with an unresolved custom lane rendering as active — the documented fail-open direction. Plus the paired negative: an ACTIVE lane keeps the active glyph under both vocabularies, so widening the terminal set cannot mark the whole board finished. ## Audit complete Every `resolveProjectColumnsForRoles` call site in the repository — `engine`, `core`, `dashboard`, `cli` — has now been blinded individually, and every uncovered one is either pinned or has a recorded reason it cannot be. Nothing is left flagged. |
||
|
|
6646c1b95d |
docs: regenerate synced skill tool tables (unblocks all four full-suite shards)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d6079970e8 |
fix(self-healing): 18 recovery rebounds hardcoded todo and THREW on a renamed board (#3150, first slice) (#3152)
First slice of #3150. `self-healing.ts` held **26** `moveTask` calls with a legacy literal target; this converts the **18 `todo` rebounds**. ## Why this is worse than a guard, and documented already `task-store/moves.ts` records it from a previous incident: > `moveTaskInternal` **REJECTS** a target the workflow does not declare (`TransitionRejectionError: unknown-column`) … completion handoff did not silently no-op — it **THREW**. Every one of these 18 is a **recovery**. On a renamed board they threw instead of rebounding, so the strand each sweep exists to clear survived *and* the sweep reported failure. The reliability layer meant to be the backstop was the layer that broke. ## Why the census never saw it It counts **comparisons** against legacy ids. A move target is an **argument**. That is the third blind spot of the same instrument, and all three have now produced real defects found by hand: | blind spot | found this session | |---|---| | definitions | `GITHUB_TRACKING_EDITABLE_COLUMNS` — tracking unreachable on renamed boards (#3149) | | collections | swept: 30 sites, 29 already correct, 1 defect (the above) | | **targets** | **this** — 26 in one file, 31 tree-wide | ## Why 18 sites at once is safe `resolveReboundTargetForTask` **degrades to `"todo"`** when no workflow resolves, and `self-healing.ts` already used it at line 745. On every board we ship, the resolved answer *is* `todo` — so default behaviour is unchanged **by construction**, not by inspection. The control case pins exactly that, and it is the reason this can land as one change rather than eighteen. ## Scope, and what I deliberately did not touch Converted: the 18 `todo` rebounds. **Not** converted: the `done`, `archived` and `in-review` targets. They need different helpers and genuine reasoning about which lane a completion or an archive belongs in — converting them by analogy is exactly the half-conversion this program keeps paying for. Sites with no resolver in scope are unchanged. The audit behind the split is in the commit: of 26 sites, 5 had resolved lanes in scope, 4 had an IR, 17 had nothing — and `lanesOfReclaim` returns **Sets**, which is the wrong arity for a target (a move takes exactly one column, per the `moves.ts` note). ## Verification | | result | |---|---| | engine `tsc` | **0 errors** | | **all 43 self-healing suites** | **843 passed** | | census `--strict` | exit 0, **unchanged** — invisible to it | | `check-inert-sync-lanes` | exit 0 | | differential | restoring the literal → **1 failed \| 1 passed**, renamed case only | The new test drives a **public entry point** (`reconcileInReviewUnmetDependencies`, the FN-6793 contract) rather than calling the helper directly, so it covers the producer path too. One harness note worth keeping: the first version of the test failed **upstream** of the target, because the sweep selects rows via `resolveProjectColumnsForRoles` — a *project-level* resolver reading `listWorkflowDefinitions`, not the task's own selection. Without that mocked, the renamed card was never considered and the failure looked like the fix not working. That distinction (project-level vocabulary vs per-task IR) will bite the next slices too. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Tasks now move to workflow-specific rebound, completion, and archive columns instead of fixed default destinations. * Retrying and recovering tasks works correctly on boards with renamed lifecycle columns. * Added safe fallback behavior for workflows without custom lifecycle settings. * **Tests** * Added coverage to prevent legacy hardcoded task destinations and verify renamed-column recovery scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7d9d097acf |
fix(cli): the TUI board fell back to the LEGACY workflow — it rendered a triage lane the default no longer has (#3178)
Found by following an unexplained number rather than by a sweep: while re-verifying #3141 the resolver reported `intake: "todo"` where `BUILTIN_CODING_WORKFLOW_IR` resolves `intake: "triage"`. That divergence is correct and intentional inside core — and wrong here. ## The defect `dashboard.ts` resolved a task's columns as `def?.ir ?? BUILTIN_CODING_WORKFLOW_IR`, and its card-chip fields the same way. That constant is the **legacy** monolithic IR (`builtin:legacy-coding`); the catalog's actual default is `resolveDefaultWorkflowIr()`. Post-U11 they differ **by a whole column**: ``` default todo, in-progress, in-review, done, archived (planning merged into todo) legacy triage, todo, in-progress, in-review, done, archived ``` So a task with **no workflow selection row** was rendered against a six-column board including `triage` — a lane the real default no longer declares. ## The same drift is already documented as fixed elsewhere `builtin-workflows.ts` records it: > `prepareWorkflowMovePolicyPreflightImpl` resolved the default through the catalog while `resolveTaskWorkflowIrForMove` used the raw constant, so a task with NO selection row produced two different workflow signatures and every flag-ON move threw *"workflow move policy preflight is stale"*. Both sides (and the sync resolver) now call this helper so the default cannot drift again. This surface was missed, and it is the **last non-test consumer of the legacy constant outside core**. ## Test scope, stated because it is narrow Driving the TUI end-to-end needs a rendered terminal and a live store. That harness does not exist here, and building one to assert a fallback would be testing the harness. So the test pins the two facts that make the bug possible and the fix meaningful: 1. **the two IRs genuinely disagree, about `triage` specifically** — if a future change re-merges them, this reports it rather than leaving the fix silently pointless; 2. **the source no longer reaches for the legacy constant.** (2) is a source assertion, weaker than driving the code. It is used for the same reason as the `FloatingWindow` aria-label scan: the defect is a **value at a call site**, there is no single render that reaches both sites, and a per-site render test would pin the one someone bothered to write. Both assertions are anti-vacuity guarded — the IR comparison fails if either side stops resolving to a v2 column set. ## Verification | | result | |---|---| | cli `tsc` | **0 errors** | | new test | **2 passed** | | mutation — restore `?? BUILTIN_CODING_WORKFLOW_IR` | **1 failed / 2** | | census `--strict`, `check-fnxc-future-dates` | exit 0 (this class is invisible to the census — an argument, not a comparison) | Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
25b3c06d2d |
fix(plugins): compound-engineering pipelines stalled forever on a renamed board (#3022)
Closes #3020 — which I filed **instead of** fixing, on a rationale that turned out to be wrong. I said the plugin had no scaffolding for faking `CePipelineStore` + `taskStore` together. It does: `_harness.ts` already builds a real `PluginContext` over a live PostgreSQL layer. The gap was **two missing readers on its task-store stub**, not missing infrastructure. I checked the harness only after filing. ## The defect `TERMINAL_COLUMNS` is `{in-review, done}`, and the reconciler advances a pipeline only when **every** current-stage board task is in that set. On a board whose review and completion lanes are renamed that's false for every task, permanently: - the pipeline never advances a stage - it never creates its outbound task - it sits `running` indefinitely Nothing errors, so it reads as work that hasn't finished. Unlike the display defects in this family (#3014, #3017), the CE flow actually **stops**. ## Shape The decision is extracted to an exported `isStageTerminalColumn` because it *is* the whole decision. Left private it could only be reached through a pipeline-state + links + board-tasks fixture, and the half that needed proving is that a renamed board resolves to its own lanes through this store. It uses `resolveReviewColumns` rather than re-deriving the union — that helper is the documented review **set** (`mergeOrchestration ∪ mergeBlocker ∪ humanReview`), so a board splitting those across a merge lane and a human lane is covered without this site drifting from it. ## Two things my first attempt got wrong **The fixture spelled traits in camelCase** — `{ trait: "humanReview" }`. Trait **ids** are kebab-case (`human-review`, `merge-blocker`, `wip`); the camelCase names are the resolved **flags**. Those columns therefore resolved to *no roles at all*, silently, because an unknown trait isn't an error. `complete` is spelled identically in both vocabularies, which is exactly what made the first run look like *"complete works, review is broken"* rather than *"the fixture is wrong"* — I nearly went debugging the production union. **The harness extension is additive** and inert until a test seeds it, so all 24 existing plugin suites see the previous shape. ## Measured | check | result | |---|---| | new suite | **4/4** | | reverting to the literal-only gate | fails **exactly 2** — the renamed-terminal case, and a board declaring a NON-terminal column named `done` — while the legacy control and the WIP/intake negative still pass | | plugin suite | **24 files, 184 tests green** | | `tsc` + all five gates | clean | That second row is the one that matters: the `done`-without-`complete` board is the only shape where a real resolution and a legacy fallback disagree, so it's what separates the fix from a lucky agreement. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f8155cafd7 |
fix(cli): the node-override guard saw only the FIRST wip lane (#3023)
Follow-up to #3019, which merged with an incomplete fix. I found this while sitting down to write the test that PR was missing. ## The guard still never fired, one lane over #3019 wired `fn_task_update`'s guard like this: ```ts const nodeOverrideLifecycle = await resolveTaskLifecycleColumns(store, task.id); wipColumns: nodeOverrideLifecycle?.wip ? new Set([nodeOverrideLifecycle.wip]) : undefined, ``` `resolveTaskLifecycleColumns` → `resolveLifecycleColumns`, whose per-role accessor is **first match** (`workflow-lifecycle-traits.ts:353`): ```ts const first = (flag) => resolved.find((c) => c.flags[flag] === true)?.id; ``` The guard's contract is **every** column carrying the trait — its own resolver uses `columnsWithFlag(ir, "countsTowardWip")`. So on a board with a build lane beside a verify lane, a task sitting in the **second** wip lane still slipped the mid-flight check, and an operator could still repoint the node of a running task. That is the defect #3019 set out to close. Interchangeable on any single-wip-lane board, which is exactly why it read as correct — the same arity trap #2975 removed from the surfacing family. ## The fix Use `resolveNodeOverrideLanes`, the guard's own resolver, which `task-update.ts` and `branch-and-pr-entities.ts` already call. All three callers now resolve identically and the V1/unresolvable fallback lives in one place. Needed a one-line re-export from `@fusion/core`. **Mutation:** forcing the resolver to first-match (`.slice(0, 1)`) fails the new case, 1 of 32. The new test names **two** wip lanes, because that is the only shape that separates the two resolutions — a single-wip-lane test passes against both, which is why #3019's gap was invisible and why I would have written a useless test if I had not read the implementation first. ## A gate constraint worth recording My first version passed the resolved object straight through: ```ts validateNodeOverrideChange(task, normalizedNodeId ?? null, overrideLanes) ``` Identical at runtime, and it turned the lane-wiring gate **red**: `check-lane-wiring` matches an object-literal argument and cannot see through a variable, so the correct call reads as UNWIRED. #3019's header records hitting the same constraint — and it is what pushed that PR toward resolving the lanes inline, which is where the first-match bug entered. So the gate's shape requirement steered a correct instinct into a subtly wrong implementation. The fix here spells both keys explicitly, satisfying the gate without the bespoke resolution. Worth someone deciding whether the census should follow a variable to its initializer — but that is a change to a shared ratchet, and I have noted it at the call site rather than making it. **Verified:** 32/32 core guard suite, `tsc` 0 errors for both packages, lane-wiring gate exit 0, FNXC gate exit 0, lint clean. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5659ccace9 |
test(cli): pin the node-override error contract on a renamed board, which is what #3019 actually changed (#3024)
## What #3019 actually changed, pinned — and a correction to my own claim I described #3019 as closing a hole where an operator could re-route a running task on a renamed board. **That was wrong.** `TaskStore.updateTask` runs the same guard with its own resolved lanes (`resolveNodeOverrideLanes`) and throws, so the change was refused either way. This test is how I found out: I wrote it to cover #3019's wiring and it passed against a tree with that wiring removed. A test that passes with the change reverted is not a test, so I went looking for what was really refusing — and it was the store. ## But the two paths *are* distinguishable, which my correction then got wrong in the other direction In correcting myself on #3019 I said the paths were externally indistinguishable and no test could separate them. Also wrong. Measured both ways: | | `details.error` | | --- | --- | | pre-check fires (wired) | `"task-in-progress"` — machine-readable reason code | | pre-check misses (unwired) | `"Cannot change node override for KB-001 while it is in progress…"` — the store's thrown prose | So on a **legacy** board a caller could branch on `task-in-progress`; on a **renamed** board it silently got a sentence instead. That is a real API inconsistency, visible only to whoever was parsing it — the kind of thing nobody notices until it breaks. That is what these cases pin, and it is the honest description of #3019's value: an error-contract fix, not a security fix. ## Revert proof With #3019's wiring removed: ``` Expected: "task-in-progress" Received: "Cannot change node override for KB-001 while it is in progress. …" Tests 1 failed | 1 passed (2) ``` Verified by actually reverting, not by reading the source — which is the discipline that caught both of my wrong claims above. The paired case ("still allows the override once the card leaves that wip lane") passes both ways by design; it guards against over-refusal, so I am not counting it as coverage of the contract. ## Also closes the gap I named in #3019 That PR shipped with `check-lane-wiring` as its only regression proof, and I said a behavioural test was owed. The two are complementary and fail for different reasons: **the ratchet** fails if the argument stops being passed; **this** fails if it is passed and the contract still degrades. ## Verification (measured) - **2 passed / 0 failed** - `tsc --noEmit` clean; `eslint` clean (one pre-existing warning, no errors) - `check-fnxc-future-dates`, `lifecycle-column-census --strict`, `check-lane-wiring` — green Tests only; no product file touched. No changeset. ## Note on the harness, for whoever writes the next one of these Seeding a card into a renamed lane has two traps, both inherited from `merge-blocker-renamed-review-lane.test.ts` and both recorded in this file's header: the real API is `createWorkflowDefinition` + `selectTaskWorkflow` (the plausible `saveWorkflowDefinition?.()` does not exist and the optional call swallows it silently), and moving a card takes `moveTask`, not `updateTask({ column })`. Both are guarded here by asserting the card really is in `building` before the subject runs. |
||
|
|
6f936f2de7 |
fix(cli): the node-override guard never fired on a renamed board, so mid-flight changes were allowed (#3019)
## The node-override guard never fired on a renamed board
`fn_task_update` called the guard with no options:
```ts
const validation = validateNodeOverrideChange(task, normalizedNodeId ?? null);
```
so `wipColumns` fell back to its documented default of
`{"in-progress"}`. On a board whose WIP lane is named anything else,
`wipColumns.has(task.column)` is false, the mid-flight check passes, and
**an operator can change the node override on a running task** —
precisely what that guard exists to refuse, in its own words:
> "Is this task executing right now?" — keyed on the literal, a renamed
board let an operator change the node override MID-FLIGHT on a running
task, which is exactly what this guard exists to refuse.
That note is attached to the `wipColumns` option added for this purpose.
The CLI simply never passed it.
## Two assumptions in the guard's own docs that did not hold
```
Both callers supply them. An omitted set keeps the legacy id, which is what a caller
without cheap IR access (a CLI tool, a route with only a task row) still gets.
```
1. **"Both callers"** — this is a *third* one, and it was in
`check-lane-wiring`'s known-unwired baseline the whole time.
2. **"a CLI tool … without cheap IR access"** — this handler is async
and has already awaited `store.getTask`, so one more resolve costs
exactly what `resolveTaskLifecycleColumns` already costs elsewhere **in
this same file** (the linked-lineage label at ~1239). The assumption was
reasonable in general and wrong here.
Passed present-but-conditionally-valued rather than as a conditional
argument: an omitted set still keeps the documented legacy default, and
only that shape is visible to `lane-wiring-census`, which matches an
object-literal argument and cannot see a ternary.
## Coverage — stated rather than implied
**There is no new unit test.** The regression guard is the ratchet
itself, and it is a real revert-proof: with the wiring removed,
```
[check-lane-wiring] call sites not passing a resolved lane argument INCREASED:
packages/cli/src/extension.ts: 1 unwired now, baseline allows 0
```
Verified by actually reverting it, not by assuming. Baseline re-recorded
19 → 18 in the same commit, so the allowance cannot be regrown into.
A behavioural test would need a custom workflow definition persisted
*and* selected inside the integration harness to get a card resting in a
renamed WIP lane. That is worth doing and I would take it as follow-up
harness work — but it is not part of this fix, and I would rather name
the gap than let "85 passed" imply coverage I did not write.
## Verification (measured)
- **85 passed** across `extension.test.ts`,
`extension-experiment-finalize.test.ts`,
`task-list-board-columns.test.ts`
- `tsc --noEmit`, `eslint` — clean
- `check-lane-wiring` (18, none added), `lifecycle-column-census
--strict`, `check-inert-flag-seams`, `check-fnxc-future-dates`,
`check:changesets` — green
Changeset included (`patch`): `packages/cli` is the published
`@runfusion/fusion` and this changes guard behaviour operators rely on.
|
||
|
|
72f5f8e51a |
fix(gate): the FNXC stamp gate never validated the hour, so 25:30 passed (#2995)
`check-fnxc-future-dates.mjs` validates the **date** portion of a stamp
and never looks at the clock time:
```js
const STAMP = /FNXC:[A-Za-z0-9_-]+\s+(\d{4}-\d{2}-\d{2})/g;
…
for (const match of source.matchAll(STAMP)) if (match[1] > today) hits += 1;
```
The capture stops before the hour, so a stamp may carry **any** `hh:mm`
and pass. Found while pre-flighting #2992, whose new comments read
`2026-07-30-25:30`.
## It is not one typo
Four stamps **already on `main`** carry a clock time that cannot exist:
```
packages/cli/src/__tests__/task-list-board-columns.test.ts:2 -24:40
packages/cli/src/commands/task.ts:29 -24:40
packages/cli/src/commands/task.ts:636 -24:40
scripts/check-lane-wiring.mjs:18 -24:00
```
Three separate authors, so this is the gate's blind spot rather than one
person's slip — and #2992 adds two more, which is how I noticed.
AGENTS.md specifies `yyyy-MM-dd-hh:mm`. The stamp's whole purpose is to
make the FNXC record a readable chronology of *why* code exists; a
timestamp that cannot exist quietly costs it that, and nothing was going
to catch it.
## The fix
Hours `00-23`, minutes `00-59`, counted per file **alongside** the
future-dated population rather than as a separate gate — same defect
class (a stamp that does not describe a real moment), and one ratchet is
cheaper to keep honest than two.
**Mutations, both directions:**
| stamp | result |
|---|---|
| `2026-07-30-25:00` | **flagged** |
| `2026-07-30-23:75` | **flagged** |
| clean tree | `475 known future-dated stamp(s), none added`, exit 0 |
## On the four existing stamps
Normalized by clamping the impossible hour to `23`, minutes preserved,
so relative ordering within each file survives. **That is a
normalization with a stated rule, not a claim about the true minute** —
`-24:40` most plausibly meant "just past midnight", but writing
`2026-07-31-00:40` would be future-dated against today's local calendar
and fail the very gate this PR extends. Clamping keeps every stamp real,
ordered, and non-future; the exact minute was already unrecoverable.
**Verified:** FNXC gate exit 0, lane-wiring gate exit 0,
`task-list-board-columns` 5/5, lint clean.
Comment-only changes to the CLI files (stamp text inside FNXC blocks),
so no behaviour change and no changeset.
Noted separately on #2992 so its two new stamps get corrected there
rather than landing and immediately failing this gate.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a57f6699b3 |
fix(cli): fn task list never printed cards in renamed columns (#2986)
## `fn task list` never printed cards in renamed columns
```ts
for (const col of COLUMNS) { // the legacy six ids
const colTasks = tasks.filter((t) => t.column === col);
```
A task in a workflow-defined column matches **no iteration**, so it is
not printed. This is not a wrong label or a wrong glyph — **the card is
absent**, and the output reads as a shorter, healthy board rather than
as a bug. On a fully renamed board the command prints nothing but the
header. `COLUMNS` also still contains `triage`, which U11 (#2515)
deleted.
## Found where the previous author left it
The `DELIBERATE-LITERAL` note directly above this loop is correct about
its own glyph, and it named the deeper bug rather than hiding it:
> NOT claimed as trait-resolved, and the deeper bug is left alone:
because the loop iterates the legacy enum, a card in a workflow-renamed
column is not rendered AT ALL. That is the R8/U10 surface change […] and
a far bigger fix than this glyph.
It also predicted the coupling: *"If this ever iterates
workflow-resolved columns, that difference becomes live and the right
answer is a trait lookup, not this."* So both move together — once the
loop can yield a custom id, the terminal test **must** stop being an id
comparison. Fixing only the loop would leave a renamed done-lane
rendering as active work.
## Two deliberate choices
**Lanes come from the tasks, not from a resolved IR.** A board can span
several workflows and therefore has no single column list, and a card
must never depend on a resolution succeeding in order to be *visible*.
Legacy ids keep their familiar order and labels; anything else follows
alphabetically, so output is deterministic.
**Terminal lanes are resolved**, via
`resolveProjectColumnsForRoles(TERMINAL_ROLES)` — that is a display
question with a real answer, and this function is async with a store in
hand. Best-effort: a failed resolve falls back to the legacy pair rather
than failing the command, and an unresolved custom lane renders as
*active*. Showing a finished card with the wrong glyph is a far smaller
error than the blank board this replaces.
## Coverage, and its limit stated plainly
The lane-selection decision is extracted to an exported seam and tested
there. It is **not** end-to-end: `runTaskList` resolves a real project
context and ends in `process.exit`, so driving it would need the
mock-the-world shell `docs/testing.md` tells us to avoid when a narrower
seam exists. The call site is held by the compiler instead — the loop's
only source of lanes is that function. I have written this in the test
file rather than leaving it implied, because "5 passed" on a helper
could otherwise read as proof of the command's behaviour.
Reverted — the seam returning `[...COLUMNS]`, which is exactly what the
loop did — **all 5 cases fail**:
```
AssertionError: expected [ 'triage', 'todo', …(4) ] to deeply equal [ 'backlog', 'building', 'checking' ]
AssertionError: expected [ 'triage', 'todo', …(4) ] to deeply equal [ 'todo', 'shipped' ]
Tests 5 failed (5)
```
## Verification (measured)
- **86 passed / 3 files** — new suite plus `bin.test.ts` and
`pr-merge-review-lane.test.ts`
- `tsc --noEmit`, `eslint` — clean
- `lifecycle-column-census --strict`, `check-lane-wiring` (26, none
added), `check-fnxc-future-dates` — green
- `pnpm check:changesets` — clean; changeset included (`patch`), since
`packages/cli` is the published `@runfusion/fusion` and this is
user-facing
|
||
|
|
be79fe0db6 |
fix(cli): PR merges silently never ran on a renamed board — the blocker was asked about in-review (#2976)
## PR merges silently never ran on a renamed board
`processPullRequestMergeTask` called its injected blocker with the task
alone:
```ts
if (getTaskMergeBlocker(task)) return "skipped";
```
So `options.reviewColumns` was undefined and the blocker's identity
check fell back to `task.column === "in-review"`. On a board whose merge
lane is named anything else it returns:
```
task is in 'checking', must be in 'in-review'
```
…which is truthy, so this function returns `"skipped"`. **Silently and
permanently** — nothing logs, nothing fails, the PR simply never merges.
`daemon.ts`, `serve.ts` and `dashboard.ts` all drain PR merges through
here, making this a third instance of the #2963/#2964 class ("merge
entry points unwired — merging was impossible on a renamed board").
Found via the baseline #2966 shipped:
`packages/cli/src/commands/task-lifecycle.ts` was a known-unwired call
site in it.
## Narrow resolution, deliberately
`resolveReviewColumns` is the **broad** set, and its own FNXC note warns
that a caller which admits on it *and then moves the card* will act on
cards the engine does not consider in review. This function merges and
moves to the complete lane — a state-changing admission — so it uses
`resolveMergeOrchestrationColumn`, the single lane the engine acts on.
That matches how `moves.ts` wires the same call.
Degradation is unchanged in both directions: `resolveWorkflowIrForTask`
substitutes the default IR rather than throwing, so a default board
resolves `in-review` and behaves identically; a v1-upgraded IR resolves
every role empty and keeps the documented legacy literal (covered by a
test).
## One shape choice worth flagging
The option is always **passed** and conditionally **valued**:
```ts
getTaskMergeBlocker(task, { reviewColumns: mergeLane ? new Set([mergeLane]) : undefined })
```
rather than making the whole argument conditional. These are identical
at runtime — the blocker treats an undefined `reviewColumns` exactly as
it treats absent options — but **only this shape is visible to
`lane-wiring-census.mjs`**, which matches an object-literal argument and
cannot see a ternary. I wrote the ternary first, and the gate still
reported the site as unwired; wiring a gate cannot check is how this
defect survived in the first place.
The gate then confirmed the fix and asked for the baseline in the same
commit:
```
[check-lane-wiring] unwired call sites decreased:
packages/cli/src/commands/task-lifecycle.ts: 1 -> 0
```
Baseline re-recorded 9 → 8 in this commit, so the allowance cannot be
regrown into.
## Revert proof
**There was no test for this function at all** — that is why it went
unnoticed. Restoring only `task-lifecycle.ts`:
```
AssertionError: expected "vi.fn()" to be called with arguments: [ ObjectContaining{…}, …(1) ]
AssertionError: expected 'skipped' not to be 'skipped'
AssertionError: expected "vi.fn()" to be called with arguments: [ ObjectContaining{…}, undefined ]
Tests 3 failed | 1 passed (4)
```
The one case that passes both ways is "still skips a card that is not in
any merge lane" — it guards against over-admission rather than proving
the fix, and I am not claiming it as coverage of the defect.
## Verification (measured)
- new suite **4/4**; with `pr-automerge-cleanup` **9 passed / 2 files**
- `tsc --noEmit`, `eslint` — clean
- `check-lane-wiring` (8, none added), `lifecycle-column-census
--strict`, `check-sql-column-literals`, `check-fnxc-future-dates` —
green
**Changeset added** (`patch`). `packages/cli` is the published
`@runfusion/fusion` and this changes user-facing merge behaviour, so
AGENTS.md requires one. My first pass hedged and left it to a maintainer
— that was wrong, the rule is not discretionary, and it is now in the
branch.
|
||
|
|
dd930c8d7d |
fix(cli): qualify cross-fork PR heads (#2377)
## Summary - resolve the repository receiving pushes through `git remote get-url --push origin` - qualify pull-request head branches with the fork owner when the push owner differs from upstream - preserve the existing unqualified head for same-repository workflows ## Root cause Fusion correctly resolved the PR target from origin's fetch URL, but assumed the pushed branch lived in that same repository. With an upstream fetch URL and a fork push URL, GitHub requires `fork-owner:branch`; the unqualified branch is rejected. ## Validation - CLI task lifecycle tests: 48 passed - `@fusion/core` typecheck - `@runfusion/fusion` typecheck - strict changeset validation <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Pull requests created from branches pushed to contributor forks now correctly qualify the PR head with the fork owner when the push remote differs from the upstream owner. * Improved PR head handling across both group/shared-branch and per-task pull request creation paths. * **Tests** * Updated and expanded lifecycle tests to cover “origin push to fork” scenarios using push URL–based repo resolution. * **Documentation** * Added a patch release note for the fork-aware PR head fix. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: v <v@v.speedport.ip> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com> |
||
|
|
189f237a07 |
fix(security): make the approval and permission gates actually enforce (#2457)
## Why
An AI agent autonomously deleted a live task (FN-8609) via
`fn_task_delete`. Investigating that led to two audits, which found the
gate machinery is well-built and its *enforcement* well-tested — but the
*decision boundary* was unguarded and the tools that matter never
reached the gate at all.
The headline defect: all ~76 tools in `packages/cli/src/extension.ts`
are delivered through `hostExtensionPaths` → `DefaultResourceLoader`,
which never passes through `wrapToolsWithActionGate`. `fn_task_delete`
*was* classified as gated in `gating-classifications.ts` — that
classification was dead code. Roughly 15 destructive tools were ungated
even for an operator who had locked their policy all the way down.
Second: `POST /api/approvals/:id/decision` took the approver identity
from the request body, validated for shape only, with no check that the
decider was human or differed from the requester. Combined with a bash
tool whose command string was never inspected (only `params.cwd`), an
agent could read the daemon token from `~/.fusion/settings.json`, find
its own pending request, and approve it.
## Scope
This does **not** tighten defaults. The `unrestricted` preset is
byte-for-byte unchanged — `git diff` on `agent-permission-policy.ts` is
empty — and regression tests assert that an out-of-the-box install
behaves exactly as before. The bug was never "the default is too
permissive"; it was "strict policy doesn't enforce." This makes turning
security up actually work.
The one deliberate exception: the containment that stops an agent
escalating its *own* privileges (reading the daemon token / credentials,
calling the approvals API to self-approve) applies at every preset
including `unrestricted`. That is a privilege-escalation boundary rather
than a permission preference — if it only engaged under strict policy it
would not have prevented the incident that prompted this.
## What changed
8 bisectable commits:
- **Approval lifecycle** — self-approval blocked via server-derived
deciders; same-verdict replay 409s; decide re-reads and re-validates
inside the transaction; expiry TTLs; `markCompleted` ownership check;
session identity registry in core.
- **Engine gates enforce for real** — unclassified tools resolve to a
policy-governed category instead of hardcoded `allow`; missing-policy
fail-open closed; bash containment floor + exact-command approval
binding.
- **Dashboard decision routes** — stop trusting client-supplied actors
(decision, bypass-review, worktrunk → 403 on forged actors).
- **`fn serve` authenticated by default** — auto-mints a token following
the existing `fn dashboard` precedent; `--no-auth` opts out.
- **Sibling entry points closed** — user-sourced hard-cancel moves, ACP
execute-once approvals, plugin task-store gating.
- **pi-extension principal resolution** — the extension resolves the
acting principal and can withhold or policy-gate the previously ungated
destructive tools.
- **Root-cause bonus fix** — `findLatestByDedupeKey` was broken in
PostgreSQL backend mode (already-parsed jsonb fed through a string-only
parser), so approved-grant redemption **never matched in production**,
minting duplicate requests. This explains the live DB state of 17
approved / 0 completed. *(Also cherry-picked to `main` as `a9b30013bb`,
since it is an active production defect on its own.)*
- **Review follow-ups** (`627f1b1fa8`) — operator-configured
provisioning privilege and a configurable grant TTL; see below.
## Review follow-ups
**Provisioning privilege is operator-configured, not role-derived.**
`isCallerPrivileged` had gone from `caller.reportsTo == null` (every
top-level agent privileged — permanent escalation by creating a
manager-less agent) to `caller.role === "ceo"`, which swapped an
implicit rule for a magic string: any agent config can claim that role,
while an operator who genuinely wants a privileged agent had no
supported way to say so. Privilege now derives solely from
`agentProvisioning.trustedAgentIds` / `trustedRoles` and fails closed
when settings are unresolvable.
It is also no longer forwarded to `resolveAgentProvisioningPolicy` as
`isPrivileged`, because that flag short-circuits ahead of
`alwaysApproveDelete` — a trusted caller was bypassing delete approval
entirely. The policy applies the same trusted rules itself, in the right
order. The function now governs only the org-chart escape hatch (acting
outside your own direct reports).
**Grant TTL defaults to 1 hour and is configurable.** Approval →
redemption is not instantaneous: an operator approving from their phone,
an engine restart, a queued lane, or a task waiting on a worktree all
routinely exceeded 15 minutes, after which the grant expired and the
agent silently re-requested. One hour remains far short of the
"redeemable forever" hazard the TTL exists to bound. Override via
`FUSION_APPROVAL_GRANT_TTL_MS` or `configureApprovalRequestTtls()`;
invalid overrides are ignored rather than widening the window to
infinity or collapsing it to zero.
## Behavior changes requiring operator review before rollout
1. `fn serve` requires a bearer token by default (`--no-auth` opts out);
unauthenticated clients get 401.
2. Agents can no longer run withheld destructive tools
(`fn_task_delete`, `fn_task_bypass_review`,
mission/milestone/slice/feature/workflow deletes, `experiment_finalize`,
`skills_install`). Operators keep them via CLI/dashboard. **This is the
incident fix.**
3. Agents get provisioning privilege only when the operator lists them
in `agentProvisioning.trustedAgentIds` / `trustedRoles`; the
provisioning gate is now live in production. Previously-implicit
privilege (top-level position, or a `ceo` role) no longer grants
anything on its own.
4. Decision replay 409s (was 200); pending approvals expire after 24h,
approved grants after 1h (configurable); bash approvals bind per exact
command.
5. Forged/body actors on decision, bypass-review, worktrunk routes →
403; `archive-all-done` requires `{confirm:true}` (external scripts
affected).
6. `fn_secret_get` approvals grant exactly one reveal (previously
granted nothing and looped forever); ACP approvals are execute-once
(previously infinite reuse).
7. Bash containment denies token/credential/approvals-API commands in
all agent sessions at every preset.
## Verification
Independently re-run against the branch, not just self-reported:
- 5 typechecks (core, engine, cli, dashboard `tsconfig.json` +
`tsconfig.app.json`) — clean
- `pnpm lint` — clean
- `pnpm test:gate` — 379 passed
- `pnpm build --force` — green (a plain `pnpm build` skips packages as
unchanged and does **not** compile the branch)
- `pnpm check:changesets` — clean
- ~650 file-scoped tests including new negative-path suites for the
decision boundary, which previously had **zero** test coverage
`packages/engine/src/__tests__/plugin-runner.test.ts` fails 56/80 —
**verified pre-existing**, reproducing identically at base commit
`93a403af67` on `main`. Not in the merge gate.
### A mutation check that failed to fail
Worth recording, because it nearly shipped an untested security fix. The
first mutation check on the provisioning change reintroduced the `ceo`
hardcode and **all 17 tests still passed** — the tests asserted through
the policy path, which can no longer observe `isCallerPrivileged` at
all, precisely because `isPrivileged` is no longer forwarded there.
Org-chart cases that do exercise the function were added; the hardcode
now fails exactly 1 of 19, and restoring is green. A green mutation run
is only meaningful if the test can actually see the code under test.
## Known limitations (stated, not papered over)
- The bash containment floor is string-matching: a cost-raiser, not a
sandbox. Quoting, encoding, `$HOME`, symlinks, or an interpreter
one-liner can evade it. The durable protection is the decision route
refusing agent-originated deciders — the filter is the belt, not the
braces.
- Approval expiry is lazy (evaluated at decide/complete/redeem), not
swept, so an expired pending row stays visible in lists until touched.
- The extension's require-approval path returns a pending message but
cannot suspend a pi session mid-turn; engine-side pause hooks cover
engine lanes only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Security**
* Hardened approval and permission gating with server-side decider
attribution, self-approval blocking, ownership checks, replay/race
protection, and status/TTL enforcement.
* Added fail-closed behavior for sensitive/unclassified tools and
sandbox provisioning approvals.
* Blocked credential/approval access via bash containment; plugin
destructive task operations now require explicit permission.
* **New Features**
* `fn serve` now defaults to bearer-token auth, with `--no-auth` as the
explicit opt-out.
* **Bug Fixes**
* Improved task move-source attribution (`moveSource: "user"`) and
tightened dashboard archive/bypass confirmation and operator attribution
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b1bd571682 |
batch-sql-ratchet: the census / gate-ratchet family — collection branch, fold here (#2941)
## Family branch for consolidation directive item 4 `batch-sql-ratchet` did not exist and ~10 open PRs are waiting for a collection point, so this establishes it. **Fold your census/ratchet commit here and close your own PR as superseded.** ```bash git fetch origin batch-sql-ratchet git checkout -B batch-sql-ratchet origin/batch-sql-ratchet git cherry-pick <your-sha> # verify scoped, not full suite: pnpm --filter @fusion/core exec vitest run src/__tests__/archived-column-gate-parity.test.ts --silent=passed-only --reporter=dot git push origin HEAD:batch-sql-ratchet ``` **Candidates I can see open right now** (owners: please fold + close): | PR | branch | |---|---| | #2938 | `fix/comments-ops-sentinel` | | #2935 | `fix/task-artifacts-sentinels` | | #2933 | `chore/commit-tightened-census-baseline` | | #2931 | `fix/async-comments-sentinels` | | #2928 | `fix/audit-ops-sentinel-marker` | | #2925 | `live-task-column-lanes` | | #2923 | `fix/task-id-integrity-sentinel` | | #2921 | `fix/plugin-store-migration-marker` | | #2894 | `gate/sql-literals-match-census-placement` | That is **10 → 1** once folded. I have not cherry-picked anyone else's commits — folding someone's work without them verifying it is how a batch lands broken. --- ## What is in it so far (mine, from #2924) **Clears a live main red:** `archived-column-gate-parity` fails on `origin/main` today. ``` AssertionError: TypeScript encoding changed. async-comments-attachments.ts: 8 → 5 ``` #2886 fixed a real bug — archived-document guards failing in *opposite* directions on a renamed lane — by replacing three `column === "archived"` comparisons with `isArchivedLane(column, archivedColumns)`. The AST scan counts raw comparisons, so the tally dropped. **What I did not do is record it as three sites converted**, because measured, it is not: ``` grep -rn "archivedColumns:" packages/core/src packages/engine/src --include="*.ts" | grep -v __tests__ → (no matches) ``` No caller passes it. The parameter defaults to `LEGACY_ARCHIVED_LANES = new Set(["archived"])`, so every call resolves to the literal it replaced — byte-identical behaviour, resolved branch dead. That matters for this guard's whole argument: its header warns that converting the TypeScript half while the Drizzle and raw-`sql` halves still compare the string is a split brain *"no test would catch, because every builtin workflow spells the column `archived` so the two halves agree by accident on every board we ship."* **There is no split brain today precisely because the resolved half is unwired** — it becomes one the moment a caller threads real lanes in without the SQL sides moving. Recorded inline so `5` cannot be read as "3 sites done"; flagged on #2886. Verified not a split brain: the Drizzle and raw-sql inventories are unchanged and both pass — worth stating because those assertions run *after* the TypeScript one, so a plain red says nothing about them. Scoped edit to `AUDITED_TS_SITES` by line range: these paths appear in more than one inventory here, and an unscoped replace would quietly edit the raw-sql side too, making the parity guard agree with itself (the trap I hit in #2817). Guard still bites: appending a real `task.column === "archived"` to an audited file fails it. Core **4852 passed / 0 failed**, lint clean, test-only. Closing #2924 as superseded by this. 🤖 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 task delegation messages when workflow pickup cannot be confirmed. * Delegation results now clearly indicate when a task has not been verified for pickup. * **Quality Improvements** * Added validation checks to catch future-dated markers and inconsistent SQL-column usage. * Refined workflow checks to distinguish stale configuration from incomplete configuration. * **Documentation** * Updated lifecycle conversion guidance with more accurate audit findings and limitations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b85e5f90e1 |
fix(create): two task-CREATE destinations named a lane the board does not have (#2843)
Both files sat at **census-zero** and both wrote real cards into columns
no workflow declares. The census scores `===` comparisons, so a lane
literal passed as a **call argument** is invisible to it — one of the
four census-blind classes. These are the only two explicit-`column`
creates in production:
```
packages/dashboard/src/routes/register-gitlab.ts:108 column: "triage"
packages/cli/src/extension.ts:5243 column: "todo"
```
## The two defects
**`register-gitlab.ts` — `column: "triage"`, a column U11 DELETED.**
This one is broken on *every* board, not only renamed ones: the default
lineage is now `todo | in-progress | in-review | done | archived`.
`createTask` already resolves the intake column of the workflow it
selects (`resolvedEntryColumn`), and an explicit `column` **overrides**
that resolution — which is why the stale literal survived U11. Nothing
rejects the write and nothing logs it: the route answers `201` with a
task id and the imported card is simply not on the board. Same shape as
the `task-update.ts` triage defect fixed earlier in this program.
Fix: omit `column` and let `createTask` resolve intake.
**`extension.ts` `fn_delegate_task` — `column: "todo"`.** On a workflow
whose ready lane is named anything else, the delegated card goes to an
undeclared column: written, reported to the caller as delegated, never
visible to the agent it was delegated to.
Fix: resolve the selected workflow's `hold` lane. **Deliberately not**
"omit the column like the GitLab route" — the tool's own contract is
*"the task goes to the ready-to-work lane and the target agent picks it
up on its next heartbeat"*, so inheriting intake resolution would park a
delegated card in a manual-intake lane waiting for a human. That would
be a behaviour change; `hold` is the role that names the lane the
literal meant.
## New helper: `resolveWorkflowColumnForRole(store, role, workflowId?)`
The **write**-shaped counterpart to `resolveProjectColumnsForRoles`. The
read helper unions in the legacy ids because an extra id in a query set
is inert; here the same trick is a silent wrong write (post-U12 an
undeclared column is a `TransitionRejectionError` on move, a phantom
lane on create), so it returns one column from one workflow, or
`undefined`.
**A contract I got wrong twice, now pinned by a test.** `undefined`
means *"this workflow declares no such column"* and nothing else.
`resolveWorkflowIrById` never throws and never returns nothing — an
unregistered builtin id, a missing definition row and a failing read all
resolve to the default coding IR (branded via `markFellBack`). So an
unreadable workflow yields the **built-in** hold lane, not `undefined`,
and both call sites' `?? "todo"` fallbacks are narrower than they look.
Two of my first test cases asserted the opposite and failed; the
behaviour is the resolver's, and the write it produces is identical to
the caller's own legacy fallback either way.
## Revert proofs (measured, not asserted)
| revert | failure |
|---|---|
| `column: holdColumn` -> `column: "todo"` | `extension.test.ts`:
`expected 'todo' to be 'queued'` |
| omitted column -> `column: "triage"` | `routes-gitlab.test.ts`:
`expected 'triage' to be undefined` |
The two neighbouring `fn_delegate_task` cases stay green under the first
revert, because the built-in board and the test's `linearWorkflowIr`
both call the lane `todo` — which is exactly why this literal survived
every previous pass.
The GitLab case asserts **absence** of the key rather than a resolved
id: the store there is a fake whose `createTask` echoes its input, so
asserting a resolved value would be testing the fake. Absence is the
property that hands the decision to the real `createTask`.
## Census
| file | before | after |
|---|---|---|
| `packages/dashboard/src/routes/register-gitlab.ts` | 1 | 0 |
| `packages/cli/src/extension.ts` | 1 | 0 |
Baseline tightened. It also picks up
`packages/core/src/task-store/moves.ts` 2 -> 0, which was **already true
on main** — not from this diff.
## Noted, deliberately not changed
- `validateAssignableAgentId`'s synthetic probe a few lines above still
uses `{ id: "<new>", column: "todo" }`. It feeds `isImplementationTask`,
whose `IMPLEMENTATION_TASK_COLUMNS` set an earlier worker documented as
deliberately-not-converted (converting it makes the routing policy async
— an agent-admission behaviour change). On a renamed board the probe is
now *stricter* than the real destination, which is the safe direction
and matches the pre-existing behaviour.
- The third site from this bucket, `workflow-node-handlers.ts:455`
(`transitionTask({ column: "in-review" })` on the `review-handoff`
seam), is a **hard** failure rather than a silent one — `transitionTask`
routes through `moveTask`, which post-U12 throws
`TransitionRejectionError` for an undeclared destination, so the
workflow walk dies at the handoff on any renamed review lane. It is
engine (`batch-engine`) and fixing it properly touches `executor.ts`,
which #2820 is also editing. Left for that batch rather than opened as a
conflicting edit.
## Verification
- `pnpm test:gate` — 161 / 487 / 13 / 71 passed
- `pnpm lint` — clean
- `tsc --noEmit -p tsconfig.json` for `@fusion/core`,
`@runfusion/fusion`, `@fusion/dashboard` — clean
- `node scripts/lifecycle-column-census.mjs --strict` — exit 0
- targeted: `project-lane-vocabulary.test.ts` 14/14,
`routes-gitlab.test.ts` 8/8, `extension.test.ts -t fn_delegate_task` 9/9
🤖 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**
* GitLab-imported cards now appear in the workflow’s configured intake
lane.
* Delegated tasks now move to the workflow’s configured hold lane,
including workflows with custom lane names or separate intake and hold
lanes.
* Delegation reports the task’s final lane and provides an error when it
cannot be moved successfully.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6fc98fd6c7 |
the third census-invisible class: 51 hardcoded moveTask destinations, measured — and duplicates never archived on a renamed board (#2808)
A third census-invisible class, measured — plus the two worst instances
fixed.
## The shape
```ts
if (task.column !== "in-review") { … return; } // the census counts THIS
await this.store.moveTask(taskId, "in-progress"); // and cannot see THIS
```
The census is an AST scan for **comparisons**. A `moveTask` destination
is a **call argument**, so no backlog entry ever points at one.
Converting the guard alone is *worse than converting neither*: the
handler starts admitting work on a renamed board and then tries to move
the card into a lane that board may not declare.
This bit twice in one week — #2797 (`branch-worktree` requeued into a
lane that may not exist) and #2807 (a GitHub "changes requested" review
dropped, then a move to a hardcoded `in-progress`). Both times it was
found only because the guard *next to it* happened to be under
conversion. So I went looking.
## Measured
Across `core`/`engine`/`dashboard`/`cli`/`plugins`, excluding
`__tests__`/`*.test.*` and comment lines:
| | count |
| --- | ---: |
| hardcoded `moveTask` destinations in production | **51** |
| …passing `recoveryRehome: true` — **deliberate**, not defects | 22 |
| …plain, rejected on a board that does not declare the target | **29**
|
**The 22 must not be "fixed".** `moves.ts` exempts them on purpose
(#1411): a card stranded in an undeclared column has to stay rescuable
to a legacy safe-landing column, or it can never be recovered at all. A
sweep that converts them deletes the rescue path. That distinction is
the reason this is 29 and not 51, and it is why I measured before
writing.
## Why this got sharper recently
The `workflowHasColumn(workflowIr, toColumn)` rejection used to sit
inside a block gated on `isWorkflowColumnsCompatibilityFlagEnabled` — a
settings key **nothing in production writes** — so it never executed and
the legacy `VALID_TRANSITIONS` table decided instead. U12 hoisted it out
of that dead branch and it is now live, proven on a real store by
`live-move-path-undeclared-target.test.ts`:
```
moveTask(card in "todo" -> "triage") now REJECTS: /Unknown column for this workflow/
```
That changed the failure mode of all 29 from *"silently lands the card
in an undeclared column"* to *"throws"*.
**29 is not a crash count.** Whether a throw surfaces or disappears
depends on whether the caller catches, which is per-site and I did
**not** measure it — the doc says so explicitly rather than letting the
number imply severity it hasn't earned.
## Fixed here: 9 of the 29
`duplicate-intake` and `duplicate-guard` both archive a duplicate. On a
renamed archive lane the move is rejected, so **the duplicate is never
archived and keeps sitting on the operator's board as live work** — and
in `duplicate-guard` the row has already been stamped
`deterministicDuplicateOf`, so it is *marked* a duplicate while
occupying an active lane. Half-applied, which is the same trap as
#2797's branch clear.
Both now resolve the `archived`-trait column from the task's own
workflow through one shared helper, unioned with the legacy id.
**`cli/commands/task-lifecycle`** — `finalizePullRequestMerge` and
`finalizeNoOpMergeTask` both move the card to a hardcoded `"done"`, and
both run `updateTask({ status: null, mergeRetries: 0 })` *first*. On a
rejection the merge has already landed and the bookkeeping is already
cleared while the card never reaches its complete lane: the operator
sees a merged branch, a card still sitting in review, and a reset retry
counter. Same half-applied shape as #2797's branch clear. Both now route
through one resolver so they cannot drift.
**`contamination` / `foreign-only-contamination` (×2) /
`restart-recovery-coordinator`** — four recovery requeues to a hardcoded
`"todo"`, none of them a `recoveryRehome` escape. On a board without
that column the move is rejected and **the recovery never completes** —
the card stays contaminated or stranded, which is precisely the state
these paths exist to clear.
**Consolidation.** `resolveReboundTargetForTask` and
`resolveArchiveTargetForTask` now live beside
`resolveTaskLifecycleColumns` in `workflow-lifecycle-traits`, already
the store-dependent resolution seam. My first pass put the archive
helper inside `duplicate-intake` and had `duplicate-guard` import it
from there — wrong home, and it would have grown a copy per caller as
more sites converted. Seven call sites now share two definitions.
**Plain (non-`recoveryRehome`) destinations: 29 → 21.**
**Coverage on the CLI pair is scoped, and I'd rather say so than imply
more:** the test covers the *resolver*, not the two call sites. Both
enclosing functions are private and reachable only through
`processPullRequest`, which needs a live GitHub surface — exporting them
purely to test wiring is a worse trade than stating what is covered.
Three cases: renamed lane resolves, no-workflow falls back to the legacy
id (which also pins that a default board is byte-identical), and a
throwing lookup falls back.
## Revert result (measured)
| conversion | reverted → |
| --- | --- |
| duplicate archive destination | new case fails — `moveTask` called
with `"archived"` on a board whose archive lane is `boxed` |
| CLI complete-lane resolver | replacing the body with a bare `return
"done"` fails the renamed case |
| both move-target resolvers | replacing either body with a bare return
of its legacy id fails 5 cases across the resolver suite and
`duplicate-guard` |
Each resolver has a **non-vacuous companion** asserting it does *not*
return the legacy id on a renamed board — without it, a resolver
returning any string would pass. The fallback cases are load-bearing
rather than padding: `resolveWorkflowIrForTask` degrades to the built-in
IR rather than throwing, and the built-in rebound/archive lanes *are*
`todo`/`archived`, so those cases also pin that a default board is
byte-identical.
The pre-existing case asserting the legacy `"archived"` passes both
ways, which is exactly why it could not detect this and why the new one
supplies a workflow.
## Ownership note
`packages/core` was `batch-core`'s territory and `packages/cli` was
`batch-cli-plugins`'. Both batches have landed, and this is
newly-discovered work in the class documented here rather than leftover
conversion backlog. Four sites, two shared helpers — happy for either
half to move if those owners would rather carry it.
## Verification
- `pnpm test:gate` — 161 + 487 + 13 + 71, green
- `duplicate-guard` + `duplicate-intake` — 40 passed
- `tsc` on core and engine — clean
- `pnpm lint`, `check:changesets`, census `--strict` — all clean (run
explicitly; a clean `pnpm lint` alone is not evidence the CI Lint check
passes)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Duplicate tasks are now archived to each workflow’s configured archive
lane.
- Completed tasks are moved to the workflow-specific completion lane,
with a safe fallback for older workflows.
- Recovery and requeue actions now use each workflow’s configured
rebound lane instead of assuming a fixed destination.
- **Documentation**
- Added guidance on avoiding failures caused by hardcoded workflow
destinations and incomplete lifecycle conversions.
- **Tests**
- Added coverage for renamed workflow lanes, fallback behavior,
duplicate archiving, and recovery destinations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
29186a96da |
fix(tests): new CLI red from #2775 — the test pinned a decision its own PR superseded (#2801)
## New red on main #2775 landed and put one failure on `main`, in a test that PR itself added: ``` pr-create-review-lane-resolved.test.ts > refuses WITHOUT naming a phantom lane when the workflow declares no review lane AssertionError: expected undefined to be defined ``` ## Two review rounds pushed `pr.ts` in opposite directions; the test is from the losing one | round | decision | |---|---| | **1** (greptile P2) | a resolved workflow with no review-trait column is an **answer** — do not invent `'in-review'`, say *"no review lane"*. **This test was written against that.** | | **2** (greptile) | refusing on an empty set rejects **every v1 workflow**, because `synthesizeDefaultColumns` upgrades a v1 graph by emitting every column with `traits: []` — so a v1 board whose `in-review` column plainly exists resolves to an empty review set. | **Round 2 shipped** (`pr.ts:206-207`) and is right: an empty set is indistinguishable from a v1 upgrade, so it means *unexpressed* rather than *absent* and takes the same legacy fallback as an unreadable workflow. Both rounds are extensively documented in `pr.ts` — the code is deliberate and I have not touched it. The consequence is simply that **there is no "no review lane" message in the shipped code at all**, so `errors.find((e) => e.includes("no review lane"))` returned `undefined`. The test could never have passed against what merged. ## The fix Re-pointed at the contract that actually shipped: the filtered board takes the legacy `'in-review'` fallback, and the refusal must **not** name the renamed lanes (`signoff`, `waiting-on-a-human`) that this board no longer declares — which preserves the anti-phantom-lane intent the test was named for. ## Flagged, not guessed The round-1 behaviour is **not recoverable** without a way to distinguish *"v2 board that declares no review lane"* from *"v1 board whose traits were synthesised empty"*. The IR does not currently carry that signal, so emitting a distinct message would re-break every pre-v2 project — the exact regression round 2 caught. Recorded in the test rather than invented. ## Evidence Mutations, both caught: | mutation | result | |---|---| | fallback names lanes the board lacks | **1 failed** | | the review-lane gate removed entirely | **2 failed** | Full CLI package **1684 passed / 106 skipped (126 files)** — was 1 failed. Gate **732 green** · lint clean. Test-only; `pr.ts` restored clean after the mutations. ## How this was found Pre-flighting the open batch PRs against current `main` rather than their branch heads, after batch-engine's previous landing put 32 failures on main that were only caught post-merge. #2785 and #2783 both came back clean (commented on each); re-running `main` itself after the newest landings surfaced this one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4184fde08d |
batch-cli-plugins: 7 guards — 3 were a foreign enum, and fn pr create refused every card on a renamed board (#2775)
`batch-cli-plugins` — the u7 worker's mega-batch: `packages/cli` + `plugins` + anything left. ## The batch is 7 guards, and 3 of them are not guards at all The census's per-file list gives this batch seven sites. Reading them, **three are a foreign vocabulary the census matches on the string alone**: | file | site | verdict | |---|---|---| | `plugins/fusion-plugin-reports/store/report-store.ts` | `next === "archived"` ×2 | **not a column** — `next` is a `ReportStatus` | | `plugins/fusion-plugin-reports/store/report-types.ts` | `to === "failed" \|\| to === "archived"` | **not a column** — same enum, its own terminal states | The reports plugin has its own status lineage (`draft → generating → review_* → approved → published`, plus `failed`/`archived`) that shares two spellings with the lifecycle vocabulary. A report is not on a board and has no workflow, so resolving an IR there would answer a question nobody asked. All three are marked `DELIBERATE-LITERAL` with the reason at the site. **This cuts the other way from #2763.** That PR establishes the census total as a *floor* (25 membership predicates it structurally cannot see). This is the opposite error in the same number: a foreign enum inflating it. The total is neither a ceiling nor a floor — it is an estimate with error in both directions, and the per-file list is worth reading before trusting a file's count. ## Converted (census before → after, per file) | file | before | after | |---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | **0** | | `plugins/…/even-realities-glasses/notifications/diff.ts` | 1 | **0** | | `plugins/…/reports/store/report-store.ts` | 2 | **0** (deliberate) | | `plugins/…/reports/store/report-types.ts` | 1 | **0** (deliberate) | ### `fn pr create` refused every card on a renamed board The live defect in this batch. The gate was `task.column !== "in-review"`, and its error told the operator to move the task to a column their board does not have: ``` Error: Task must be in 'in-review' column to create a PR (current: signoff) ``` There is no way to satisfy that short of renaming the workflow back. Now resolved through core's `resolveReviewColumns`, and the message names the lanes that actually exist. **The SET, not `lifecycle.review`.** A board may declare more than one review lane, and a card parked in a `humanReview`-only lane is still a card you can open a PR from. A single-id answer keeps refusing those — the same narrowing #2728's review caught in the CLI retry gate, which is why the test pins both lanes. ## Skipped, with the reason **`plugins/fusion-plugin-even-cards` (2 guards) — blocked on packaging, not on analysis.** The defect is real: `boardToDeck` filters with `column !== "archived" && column !== "done"`, so on a renamed board every finished card stays in the deck, fills `maxCards`, and pushes the active cards off the display. The wearer sees a board that never finishes anything. I implemented the fix and **reverted it**: this plugin is not in `pnpm-workspace.yaml` and depends only on `@fusion/plugin-sdk` — it has no `@fusion/core` dependency, so the route cannot reach `resolveTaskLifecycleColumns`. Adding one is a packaging change, which this program's rules put out of scope. Shipping only the injected parameter without a caller was the alternative, and that is precisely the decorative conversion #2759 documents: the census would drop by 2 and the deck would keep the bug. Flagged for whoever owns the plugin's dependency surface. The glasses plugin next door *does* depend on `@fusion/core`, so this is a one-plugin problem, not a plugin-wide one. ## Honest note on the glasses conversion `diff.ts`'s completion branch is **currently unreachable** — the only production caller (`notifier.ts`) passes `alsoNotifyOnDone: false`. So that conversion changes nothing at runtime today. It is converted rather than marked deliberate because the literal is not deliberate: it is wrong, and would ship the bug the day someone turns the flag on. Stated here rather than left for a reviewer to discover. ## Verification - new CLI suite **4 passed**; `pr-command` + `pr-automerge-cleanup` + `bin-pr-router` **35 passed** - glasses plugin **181 passed (19 files)** · reports plugin **110 passed (23 files)** - `pnpm test:gate` — **158 / 10 / 487 / 71** · `pnpm lint` clean · `--strict` exits 0 **Revert proof, measured.** Restoring `if (task.column !== "in-review")` fails 3 of the 4 new cases (`process.exit:1` on both renamed lanes, and the refusal message reverts to naming `in-review`). The unresolvable-workflow case keeps passing — it is the legacy path — so the negative cases alone do not pin the fix and all four are required. ## Handoff to `batch-engine` `packages/engine/src/project-engine.ts` **5 → 0** is finished, green, and pushed as `handoff/project-engine-lanes-for-batch-engine` (`34dbb35209`) for the capacity worker to cherry-pick — it is engine-owned, not mine to land. It fixes two live defects: a card that **had merged** reported as a failed merge to `fn task merge` and the dashboard button (`merged: finalTask?.column === "done"`), and the three post-finalize `column === "done" && mergeConfirmed` fast-path checks, which on a renamed board sent an already-landed card down the bounce path — re-queued, retry-counted, and in the capped branch parked `failed` with its merge sitting on main. Plus `hasAutoHealableVerificationBufferFailure`, which returned false for every card on a renamed board, so a buffer-overflow verification failure was never auto-healed. 8 new tests, revert-proven (restoring the literal fails 4 of 8), gate green. --- ## Completion pass (u7) — the batch is now closed Two workers converged on this branch. I rebased onto the first-landed commit rather than force-pushing over it, took its wording wherever the conclusion was identical, and added what was missing. ### What this pass added 1. **`even-cards` (2 sites)** — the only in-scope file the first pass left open. Marked DELIBERATE-LITERAL: the package depends on `@fusion/plugin-sdk` only, and the SDK does not re-export the lifecycle role helpers, so there is no IR, no store, and no trait flags to resolve *from*. Fixing it properly means the SDK exposing role flags on the task shape it hands plugins — a structural change, out of scope, and recorded at the site as the correct home. Live consequence is cosmetic: a finished card on a renamed board shows as active in the glasses deck. 2. **A red test in the `fn pr create` conversion.** The incoming version rendered `Task must be in 'in-review' to create a PR`, dropping the word `column`. `task.test.ts:3422` pins `must be in 'in-review' column`, so that hunk failed `runTaskPrCreate > exits with error when task not in in-review column`. Restoring the word makes the single-lane message **byte-identical** to the pre-conversion one, which is what a vocabulary conversion should be — the guard's own test now passes unmodified. Marked at the site so it is not "simplified" back. 3. **Duplicate imports** — the two independent conversions each added `resolveWorkflowIrForTask`/`resolveReviewColumns`, which does not compile. Deduped in its own commit. ### Census Measured with `--json` on `origin/main` and on this branch. | file | before | after | action | |---|---|---|---| | `packages/cli/src/commands/pr.ts` | 1 | 0 | converted | | `plugins/fusion-plugin-reports/src/store/report-types.ts` | 1 | 0 | marked | | `plugins/fusion-plugin-reports/src/store/report-store.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-cards/src/cards/board-cards.ts` | 2 | 0 | marked | | `plugins/fusion-plugin-even-realities-glasses/.../diff.ts` | 1 | 0 | marked | Backlog **415 → 408** (−7, exactly the in-scope count). Deliberate **40 → 46** (+6 marked); 6 + 1 converted = 7. `--strict` exits 0. **Nothing remains in `cli` + `plugins` + everything-else — there is no follow-up batch behind this one.** ### One note on the `even-realities-glasses` site Worth recording beyond "cannot resolve": its only production caller (`notifier.ts:80`) passes `alsoNotifyOnDone: false`, so that arm is **unreachable today**. Converting it could not have changed observed behaviour either way. ### Verification (measured, on the merged branch) - `pnpm --filter @runfusion/fusion exec tsc --noEmit` → exit 0 - `pnpm lint` → 0 errors - CLI `task.test.ts` → 144 passed, including the `runTaskPrCreate` guard test - `@fusion-plugin-examples/reports` → 110 passed; `even-realities-glasses` → 181 passed **Pre-existing failures, not from this change:** the 5 `runTaskImportFromGitHub` / `runTaskImportGitHubInteractive` tests fail identically on `origin/main` — verified by stashing this diff and re-running (5 failed / 144 passed both ways). --- ## Census audit (unowned follow-on) After closing the batch scope I audited whether the **392** column-backlog number is inflated by foreign vocabularies — the class this batch found in the reports plugin, where `"archived"` is a `ReportStatus` rather than a board lane. If that class were widespread, every remaining batch would be chasing sites that must not be converted. **It is not. The number is real.** A receiver-level pass over all 392 column-category sites found exactly **3** false positives, all in `plugins/fusion-plugin-reports` (`next`, a `ReportStatus`), all now marked in this PR. What was checked and cleared: - **Property-reached foreign enums** (`step.status`, `feature.status`, `mission.status`) — already correctly bucketed into the separate `status` category (185), not the column backlog. Verified against `merge-queue-ops.ts`: 11 lifecycle-spelled literals in the file, census counts **1**, and that 1 is the genuine `.column` guard. - **Bare step-status variables** (`status`, `currentStatus`, `liveStatus` compared to `"done"`/`"skipped"`) — likewise excluded. - **Every other receiver in the backlog** — `to`, `from`, `column`, `fromColumn`, `toColumn`, `latestColumn`, `state`, `preArchiveColumn`. All resolve to genuine task columns. `executor.ts`'s 15 sites were spot-checked line by line: all 15 are real. The gap the classifier genuinely cannot close is a foreign enum held in a **bare variable** — the receiver name carries no type information, so `next === "archived"` is indistinguishable from a lifecycle guard by AST alone. That is why the reports sites need a marker rather than a classifier fix, and it is now documented in `lifecycle-column-census-ast.mjs`'s header alongside the measured scope, so the remaining batches do not re-run this hunt. Census tests: **43 passed**. The change is comment-only. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
59b5e61fa2 |
fix(tests): the last CLI reds — import assertions still required the column U11 removed (#2788)
## What was red All 5 failures in a full `@runfusion/fusion` run on `origin/main` (`5 failed / 1673 passed`), in `src/commands/__tests__/task.test.ts`: ``` - "column": "triage", ``` ## The product change is intentional and documented **#2603 (U11)** removed the hardcoded `column: "triage"` from the GitHub/GitLab import writes so `createTaskImpl` resolves the **workflow's** intake column instead. Passing `column` would override that resolution and, post-U11, name a lane the default workflow no longer declares. `task.ts` still carries the note at three sites: > `createTaskImpl` resolves the WORKFLOW'S intake column, and `input.column` would override it. Hard-coding `"triage"` created the card in a column the default [workflow does not declare]. Six `toHaveBeenCalledWith` assertions still required the removed literal, so a correct product change surfaced as five CLI failures. ## Scoped deliberately Only the **six assertion-side** occurrences are removed. The other **16** `column: "triage"` literals in this file are mock *return* values and `makeTask` fixtures, and they stay — what a created task comes *back* as is a different question from what the import *asks for*, and blanking them would weaken unrelated cases. ## Evidence - Full CLI package: **1678 passed / 106 skipped, 125 files green** (was 5 failed). - **Mutation:** reintroduce `column: "triage"` into the import write → **2 failed**. The assertions still pin the invariant rather than having been loosened into always-true — the thing worth checking when a fix is "delete an expectation". - Gate **732 green** · `pnpm lint` clean. Test-only (mutation reverted; `git diff` clean). ## Ownership `packages/cli` belongs to the **batch-cli-plugins** owner (u7) under the mega-batch split. This is fix-forward on a red rather than a conversion, confined to one test file, and touches no production code. With #2779 and #2786 this leaves engine, core and CLI at **0 failures** on main. The remaining known reds are the 123 dashboard failures documented in #2784. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2e4905fa0e |
refactor: one definition of "which columns are review" — three copies deleted onto core's resolver (#2751)
**#2730 added `resolveReviewColumns` to core. This deletes the three copies that predated it.** Measured on `origin/main` before this change — three in-tree definitions, **none of which agreed**: | site | definition | |---|---| | `core/workflow-lifecycle-traits.ts` (#2730, authoritative) | mergeOrchestration ∪ mergeBlocker ∪ humanReview — **all** columns | | `dashboard/routes/register-task-workflow-routes.ts` | mergeBlocker ∪ humanReview ∪ **first** mergeOrchestration | | `cli/src/extension.ts` | mergeBlocker ∪ humanReview ∪ **first** mergeOrchestration | | `cli/src/commands/task.ts` | all three, full union | **Both `.slice(0, 1)` variants are mine**, from #2723's review round: I narrowed to core's then-single `.review` because the reviewer was right that a superset let the dashboard act on a lane the engine did not own. #2730 answered that question authoritatively in the other direction, so the narrowing is obsolete. Worse, and the part that makes this urgent rather than tidy: **the two CLI copies had already drifted apart inside #2728.** `fn_task_retry` refused a card in a second merge lane that `fn task retry` accepted — two surfaces, one operator action, two answers, from two copies of one definition written days apart by me. All three now call core. The dashboard keeps its thin store→IR wrapper (its callers hold a store and a task id, not an IR) but the **body** is core's. ## One assertion inverted, deliberately My #2723 case asserted that a **second** `mergeOrchestration` column is **refused**. Core says every merge lane is review, so the behaviour legitimately changed and the assertion flips with it. **Kept rather than deleted**, because the invariant under test — *the routes agree with core* — is unchanged. Deleting the case would have hidden that its answer moved; inverting it records which decision moved and why. A test whose expectation quietly disappears is indistinguishable from a test that was wrong. ## A footgun found while rebasing The shipped signature is `isInReviewMissingWorktreeSessionStartFailure(task, isReviewColumn?: boolean)` — the merged version takes the **answer**, not the lanes. My branch had passed a `ReadonlySet`, and because the parameter is `boolean | undefined` with a `??` default, **a truthy object makes it answer `true` for every column**. TypeScript stops typed callers; my test only reached it through an `as never` cast, which is how I found it. All three production call sites correctly pass `retryReviewColumns.has(task.column)` — now asserted structurally so a fourth surface cannot omit it. The boolean is arguably the better shape, and I'd keep it: there is nothing left for the callee to re-derive, so it cannot disagree with the caller's own membership test. ## The ratchet No surface may reintroduce a local review union (`columnsWithFlag(…, "mergeBlocker" | "humanReview")`). Those three copies appeared because each was added **in good faith, in a different review round, by someone reading only their own call site** — which no amount of care prevents and a ratchet does. ## Verification census **553** · `pnpm test:gate` **487 / 10 / 71** · `tsc` clean in cli and dashboard · `pnpm lint` clean · 10/10 in each touched suite. **Pre-existing, not mine:** `register-task-workflow-routes.move-bypassguards.test.ts` fails on `origin/main` (400 vs 200) — already reported on #2723. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc363543a2 |
fix(cli): fn task retry now CRASHES on a renamed board — #2728 converted the classifier and left the target (#2752)
## This is a live regression on `main`, not a conversion `#2728` converted the retry **classifier** and left all three re-queue **targets** on the literal `"todo"`. That pairing is **strictly worse than the bug it fixed**: - **Before:** `fn task retry` silently did nothing on a renamed board. - **After (main today):** it correctly decides to retry, then throws. ``` TransitionRejectionError: Invalid transition: 'checking' → 'todo'. Unknown column for this workflow. ``` `todo` is not a column that board declares. Reproduced against main's exact code — reverting this fix fails **2 of 4** cases with that error. I flagged this on #2728 before it landed; posting it as a fix rather than a comment now that it is merged. ## Why the census did not catch it The census counts **comparisons**. A move **target** contains no comparison, so all three sites are invisible to it — `packages/cli/src/commands/task.ts` reads **0 guards** on main while the crash is live. That is the clearest case in this program so far that **the census measures conversion progress, not correctness**. A classifier and the target it feeds have to move together, and no automated signal will say so. ## The fix The target resolves from the task's own workflow: ```ts const retryHoldColumn = (await resolveTaskLifecycleColumns(context.store, id))?.hold ?? "todo"; ``` Failing soft to `"todo"` when the workflow cannot be resolved, matching every other fallback in this file. Three call sites, all three converted. ## Revert proof | state | result | |---|---| | main today (classifier converted, target literal) | **2 failed** / 2 passed — `Invalid transition: 'checking' → 'todo'` | | with this fix | **4 passed** | ## The fixture is derived, not hand-built The renamed workflow is `BUILTIN_CODING_WORKFLOW_IR` with **only its column ids renamed**, so the sole difference between the two runs is vocabulary. Hand-building a graph tested the fixture's shape as much as the code — the IR validator rejects an undeclared back-edge, and once declared as `kind: "rework"` the transition table still did not match the default board's. The suite also asserts the rename landed (`checking` present, `in-review` absent), so a surviving literal cannot pass by accident. Real store, real persisted workflow, driven through the real `runTaskRetry` — not the predicate. A unit test of the classifier goes green on the half-fix; only driving the whole command surfaces the crash. ## Relationship to #2736 This replaces it. #2736's other contents (active-task count, near-duplicate filter, archived-lineage label, node-override guards, the missing-worktree classifier) are now redundant with #2728, so they are dropped rather than re-litigated. What survives is this fix, its test, and the **changeset for the published CLI** that #2728 did not include. I will close #2736 once this is reviewed. ## Verification - new PG suite **4 passed** · `task-retry.test.ts` **7 passed** across 2 files - `pnpm test:gate` — **10 / 158 / 487 / 71** · `pnpm lint` clean · CLI `tsc --noEmit` clean · `check:changesets` passes · `--strict` exits 0 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e50967279 |
fix: restore current main regression invariants (#2755)
## Summary - align the renamed-review CLI regression with the classifier’s resolved-boolean contract - keep GitHub tracking controls expanded across same-task detail and sparse SSE updates - strengthen the sticky tracking regression to wait for the sparse update ## Test plan - `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/cli-active-count-lanes.test.ts` - `pnpm --filter @fusion/engine exec vitest run src/__tests__/restart-recovery-coordinator.test.ts` - `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx` - `pnpm --filter @fusion/dashboard typecheck` - `pnpm --filter @runfusion/fusion typecheck` - `pnpm check:changesets` |
||
|
|
72d42652e5 |
fleet: CLI surface 16 → 0 — 'active=0' on a busy board, and a retry gate that disagreed with the dashboard (#2728)
**Claimed on #2714 before starting.** `packages/cli/src/commands/task.ts` (8) + `dashboard.ts` (8) — **16 → 0**. ## The finding that matters: `active=0` on a busy board The same four-line aggregation appears **four times** in `dashboard.ts` — the TUI stats refresh, the serve summary, the status line, the agent-stats pass. Each compared the default lineage's two ids, so on a renamed board every one reported `active=0` while the board was plainly busy. **This is worse than an inert internal guard.** A recovery path that silently stops firing is invisible until something breaks. A stats line that says zero is **read, believed, and acted on** — *"nothing is running, so I can restart the engine."* The four copies are now one helper, and that is the other half of the fix: four independent copies of a lifecycle decision is how they drift, and these were identical **by accident, not by construction**. One IR read per *workflow*, asserted by call count — because the returned number is identical either way, so only counting the work can see it. ## The retry gate exists twice, and #2713 converted one of them After #2713, `POST /tasks/:id/retry` accepted a renamed board's stalled review card while `fn task retry` refused it with *"not in a retryable state"* — **one operator action answering differently depending on the surface**. The rule, stated at the site: **converting one copy of a duplicated gate creates a disagreement that is harder to diagnose than the original inert guard.** Grep the classifier by name before calling a lane converted. ## The rest - **`fn task set-node` / `clear-node`** rewrote the node override of an *actively executing* card, because the "is in progress" check never matched. That guard exists because the rewrite races the run. - **The duplicate-guard candidate filter** kept completed cards in the comparison set on a renamed board, so a new task was reported as a duplicate of work that had already landed — the opposite of useful. - **The duplicate-lineage `(archived)` marker** never printed, so the operator could not tell a live duplicate from a filed one. ## Two DELIBERATE-LITERALs, with reasons The board-render glyph compares `col` taken from the legacy `COLUMNS` enum **that loop iterates** — the literal matches its own receiver by construction. The real defect is already named in the code above it: a card in a renamed column **is not rendered at all**, which is the R8/U10 surface change, not this glyph. Converting it would hide that behind a trait lookup while the loop still cannot see the card. ## Pre-existing, not mine 5 failures in `commands/__tests__/task.test.ts` (GitHub import) **fail on `origin/main`** — verified by stashing this change and re-running. Someone owns that; it should not ride in here. ## Verification census **16 → 0** · `pnpm test:gate` **10 / 71** · `pnpm smoke:boot` **PASS** · `tsc -p packages/cli` clean · `pnpm lint` clean · `task-retry` 3/3 · 4 new cases with **2 red on revert**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dbb53eebaa |
test(cli): four suites broke on mocks that predate the PG cutover and U11 (12 red → 0) (#2702)
## What was red
`project.test.ts` (8), `extension.test.ts` (2),
`project-lock-retry.test.ts` (1), `extension-workflow-tools.test.ts` (1)
— **12 failures** on clean main, all from full-suite shard 4/4. **No
product defect in any of them.**
### Cause 1 — an incomplete mock that the fail-soft catch disguised (9
cases)
`getTaskCounts` now **enriches** each row before counting, so a renamed
wip column still counts as running work (`FNXC:WorkflowLifecycleColumns
2026-07-30-12:20`). But `resolveWorkflowIrForTask` and
`enrichRunningAgentTaskShape` were absent from the `@fusion/core` mocks.
Calling an undefined function threw, and `getTaskCounts`'s
**deliberately fail-soft** catch converted that into `{ byColumn: {},
runningAgentCount: 0 }`.
So the failures presented as "task counts are zero" — indistinguishable
from a real counting bug. Worth flagging beyond this PR:
`project-lock-retry.test.ts` exists specifically to prove a transient
lock does *not* "silently masquerade as zero tasks" (FN-7731/FN-7740),
and an incomplete mock produced that exact symptom by a different route.
**That catch will hide the next real enrichment failure the same way.**
### Cause 2 — column-vocabulary drift (3 cases)
A column-less `createTask` used to land in `triage`; post-U11 it lands
in `todo`, the merged Planning column. Three assertions were pinned to
the old landing column, or to `COLUMN_LABELS` values **the product never
prints** — the stale mock said `"Triage"` / `"To Do"` where core says
`"Planning"` / `"Todo"`. A label assertion could pass against a string
that exists only in the mock. The mock's labels are now copied from the
real table.
## Measured
| Check | Result |
|---|---|
| the four files | 12 failed → **0** (107 passed) |
| whole `@runfusion/fusion` package | **122 of 126 files green, 1656
passed** |
| `pnpm test:gate` | **726 passed** |
| `pnpm lint`, CLI `tsc --noEmit` | clean |
**Census unaffected — 721 both with and against my diff**, verified by
reverting the four files and re-measuring rather than assuming test
files are unscanned. (I also caught that `--strict` *writes* the
baseline locally; that write is reverted, so this PR does not touch the
baseline.)
## Not touched
`task.test.ts`'s **5 failures**. That file is claimed by
`feature/tool-permission-gates`, and its 5 are exactly the ones shard
4/4 reports — so shard 4/4 goes 17 → 5, with the remainder belonging to
that PR.
## Two judgement calls recorded in-file rather than made silently
**The `extension-workflow-tools` guard is RE-PINNED, not deleted.** It
asserted "a task created on `builtin:coding` lands in `triage`" — a
byte-identical regression guard that fired because the change was
*intended* (U11's merge). Deleting it would remove the only check that
this landing column stays stable; leaving it pinned a column the product
no longer declares. Re-pinning to `todo` keeps it doing its job.
**The broad-listing assertion now names the group that actually leads
the output.** It asserted a *second* group header inside a listing that
truncates to a text budget. That only ever passed because `triage` sorts
before `todo` in `COLUMNS`, so its group fitted before truncation —
fixture ordering masquerading as a bounding assertion. Per-column
coverage of every group is still asserted by the three filtered cases,
which do not truncate. I also seeded those 8 tasks in an explicit third
column so that case still covers a three-way filter instead of
collapsing to two groups.
|
||
|
|
50ebf3c543 |
TAKING cli/project.ts (fn project reported 0 running agents) + two test fixes — dashboard conversions WITHDRAWN in favour of #2626 and #2636 (#2631)
Three app-cluster conversions plus the evidence that they behave on a renamed AND a merged board. ## Per-file guard counts | file | before | after | note | |---|---|---|---| | `packages/cli/src/commands/project.ts` | 0 | 0 | not a comparison site — see below | | `packages/dashboard/app/components/TaskContextMenu.tsx` | 2 | 2 | **count does not move — deliberate, see below** | | `packages/dashboard/app/components/Column.tsx` | 2 | 2 | **count does not move — deliberate, see below** | **Read this before scoring the PR against the bar.** You said a claim that does not move your number is not done, so I am telling you up front that *this PR does not move it*, and why. Both dashboard conversions are **fallback-preserving**: ```ts const isIntakeColumn = columnFlags ? columnFlags.intake === true : column === "triage"; ``` The literal survives as the no-flags branch, so the grep still counts it. That is the shape the sibling code already uses (`isPreExecutionHoldColumn`, same file, converted earlier in the program), and dropping the fallback would make an unresolved-column render *lose* the affordance a second way. What changes is the **behaviour when flags exist** — which is what the mutation results below measure. If you want these to zero out the count, the fallback has to go, and that is a separate decision about whether an unresolved column should fail open or closed. Say the word and I will do it as a follow-up; I did not make that call unilaterally because it is not reversible from a rendering standpoint. `cli/project.ts` was never a comparison site at all — it fed **raw rows** to `isRunningAgentTaskShape`, so the helper's own internal legacy fallback kicked in and `fn project` reported **0 running agents** on any renamed board. Fixed by resolving the IR per task before counting. Nothing to subtract. ## Two of the three had a test that looked like coverage and was not - **`Column.tsx`** — the quick-create gate is `workflowMode || isIntakeColumn`. Every pre-existing intake case in `Column.test.tsx` *also* passes `workflowMode`, so the `||` short-circuited and **none of them ever reached the trait lookup**. Added cases that omit `workflowMode`, the only path where the conversion changes the answer. - **`TaskContextMenu.tsx`** — the intake suppression was asserted only for the legacy `triage` id, the one board shape where a broken conversion still returns the right answer. Mutation-verified rather than asserted: | mutation | result | |---|---| | `isIntakeColumn` → `column === "triage"` | **2 of 88 fail** (exactly the renamed and merged cases) | | menu suppression → `task.column !== "triage"` | **1 of 12 fail** | ## A pre-existing red I fixed on the way past `uses VALID_TRANSITIONS and in-review back-to-progress labels` was **already failing on origin/main**. #2521 correctly moved the "Back to X" label onto the host's `columnLabel` function; this file's stub is `(column) => column`, so the hardcoded `"Back to In Progress"` expectation was left over from the pre-#2521 hardcode and nothing had updated it. Matching the raw id would have made it pass while proving nothing, so instead that one case gets a display-like label function — the assertion now fails both if the "Back to" prefix regresses **and** if the label stops routing through `columnLabel`. Strengthened, not relaxed. Counts against completion criterion #2. ## Verification - `Column.test.tsx` + `TaskContextMenu.test.tsx`: **100 passed** - `tsc -p tsconfig.app.json` (the root config does not cover `app/`) and the CLI typecheck: clean - `pnpm test:gate`: green 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eb874f3da3 |
convert(cli/commands/task.ts): triage guard 1 → 0 (+ a live main regression in the lifecycle E2E release path) (#2627)
**Batched push, gate open.** One conversion; the two E2E commits are held back and the reason is below — it is the more important half of this PR body. ## Conversion | File | triage column comparisons before | after | |---|---|---| | `packages/cli/src/commands/task.ts` | **1** | **0** | `pnpm test:gate` green, `pnpm lint` clean. All four non-terminal columns rendered the **same** glyph, so the four id comparisons were only ever asking "is this column terminal?". Naming `triage` made it a lifecycle-vocabulary site for no behavioural reason — the merged Planning column dropped that id, so the comparison silently stopped matching while the output stayed correct **by accident** (the fallthrough gave it the same glyph). Behaviour-identical **only** because the loop iterates the legacy `COLUMNS` constant (`types/board.ts:27` — exactly the six ids), so `col` can never be a custom id. Stated because the forms **diverge** outside that set: the old chain fell through to the terminal glyph for an unrecognised id, the new form returns the non-terminal one. If this ever iterates workflow-resolved columns that difference becomes live, and the right answer is a trait lookup, not this. **Deeper bug deliberately untouched, for U12:** because the loop iterates the legacy enum, a card in a workflow-renamed column **is not rendered at all**. That is R8's surface change, far bigger than this glyph. *(Note: this file is not on the 45-guard list, so it will not move your count. Flagging so the numbers reconcile.)* --- ## ⚠️ Live regression on origin/main — the lifecycle E2E release path While rebasing to push, the flagship lifecycle E2E went red. **I verified it on `origin/main` alone, with none of my commits: 2 failed / 18 passed.** ``` scenario 1 — DEFAULT vocabulary → AssertionError: expected [] to include 'FN-E2E-1' (r.sweep.released is EMPTY) scenario 2 — RENAMED vocabulary → audit trail differential broken: renamed produced [{end},{review}], default produced [] ``` Both are pre-existing tests I have never touched. **The capacity release sweep is releasing nothing.** **Likely cause, from reading rather than bisecting** — so treat it as a lead, not a verdict: `moves.ts:1059` now resolves a capacity pool id and enforces `enforcePooledColumnCapacity` **inside the move transaction** (#2488 "bind the in-transaction capacity gate", made user-visible by #2499 "make the capacity gate actually bind for real projects"). The E2E drives with `settings = { experimentalFeatures: { workflowGraphExecutor: true } }` — **no `maxConcurrent`** — while the fixture's wip column declares `{ trait: "wip", config: { limitSetting: "maxConcurrent", countPending: true } }`. If the resolved limit is finite and the pooled count meets it, the hold→wip move is rejected on capacity and the sweep correctly reports nothing released. If that is right, it is a **test-harness/production interaction, not a product break** — but it means the program's primary end-to-end evidence for the capacity boundary is currently inert on main, which matters for completion criterion 3. It needs the capacity worker's eyes, since #2488/#2499 are theirs and I would be guessing at the intended pool/limit contract. ## Why my two E2E commits are held They add scenario 3 (merged intake+hold board) and scenario 6 (REVISE → rework), both of which **depend on the same release leg**. On current main they fail for main's reason, taking the file from 2 failures to 4. Pushing them would add red to the count you are tracking and obscure whose regression it is. Both are complete, mutation-attributed, and green against the commit I wrote them on: | Scenario | Proves | Mutation that fails it | |---|---|---| | 3 — merged intake+hold | capacity release works from a dual-role column | `isHeldTask` treating intake/hold as exclusive → exactly its 2 tests | | 6 — REVISE → rework | `InReview → InProgress` on renamed *and* merged boards | disabling rework re-entry → exactly its 2 tests | They go out in the next batch the moment the release path is green. ## Also not shipped, twice attempted, deleted both times Safeguard 2 (`autoMerge:false` terminal-until-human) still has **no** graph-level E2E. Attempt 1 passed and then survived mutating `merge-gate` to ignore `task.autoMerge` — the card was parking on the review column's `merge-blocker` trait, not the gate. Attempt 2 removed that trait to isolate the gate, and then the *control* case parked too, so the flag still was not the discriminator. A fixture that can isolate it needs a merge path mirroring the builtin (`merge-gate → merge node → end`) rather than a direct edge to `end` — a real redesign, not a speculative edit. The enforcement that actually holds today is `allowInReviewMergeProcessing` in `project-engine` (unit-mutation verified, NEW=9; gated via #2526). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d5f1ce7abd |
U11 [writes]: stop CREATING cards into a column the workflow no longer declares (9 -> 0, engine+cli) (#2603)
**Taking: `engine/triage.ts`, `engine/pr-comment-handler.ts`, `engine/eval-followups.ts`, `cli/commands/task.ts`, `cli/extension.ts`** (write class — no collision with the comparison backlog). ## A class the census does not count The 48-guard work list tracks `=== "triage"` **comparisons**. These are `column: "triage"` **writes** — and post-#2515 every one creates a card directly into the state STALL 3 was about, except **manufactured continuously** rather than left behind by the upgrade. ## Why they bite `createTaskImpl` resolves the column as: ```ts column: input.column || options?.resolvedEntryColumn || fallbackIntakeColumn || "triage" ``` `input.column` **wins**, so an explicit `column: "triage"` overrides the workflow's resolved intake column entirely. `store-create-intake-column.test.ts` already pins that a create with **no** column lands in the default workflow's intake (now `todo`) — these callers opted out of it. The sharpest is `triage.ts`'s `fn_task_create` agent tool: it passed `workflowId: params.workflow_id` **and** `column: "triage"` in the same call. The caller chose a workflow and the column ignored it — a Coding (Ideas) create landed in `triage` instead of `ideas`. ## Counts **Comparison guards: unchanged by this PR.** This is the write class; conflating the two would misreport convergence toward the zero bar. | file | `column: "triage"` writes before | after | |---|---:|---:| | `packages/engine/src/triage.ts` | 1 | **0** | | `packages/engine/src/pr-comment-handler.ts` | 1 | **0** | | `packages/engine/src/eval-followups.ts` | 1 | **0** | | `packages/cli/src/commands/task.ts` | 3 | **0** | | `packages/cli/src/extension.ts` | 3 | **0** | | **total** | **9** | **0** | ## A test that pinned the defect `pr-comment-handler.test.ts` asserted `column: "triage"` in the createTask call — so it would have **failed the fix and passed the bug**. Rewritten to assert the invariant (the caller passes no column, so the workflow's intake wins) plus an explicit `Object.hasOwn(arg, "column") === false`, which is what actually catches a reintroduction. ## Interaction with #2591 My merged #2591 rescues these cards once created — they sit on a legacy planner id their workflow doesn't declare and are still in planning stage. So this isn't a *visible* stall today; the rescue absorbs it. **That's the reason to fix it rather than leave it:** a self-healing path silently absorbing a steady stream of malformed creates is exactly how the underlying defect stays invisible. ## Deliberately not touched - `{ id: "start", kind: "start", column: "triage" }` in the builtin coding / PR / lead-generation IRs — workflow-internal **node declarations** for workflows that still legitimately declare a `triage` column, not lifecycle writes. - Left for their owners: `core/task-store/project-store-ops.ts:210`, `core/task-store/update-task-deps.ts:111` (main worker), `dashboard/src/routes/register-gitlab.ts:108` (u12). Same defect, same one-line shape. ## Verification - 304 engine/CLI tests green across the affected suites - merge gate green (482 + 132 + 10), engine + CLI tsc clean, lint clean No changeset: `@fusion/engine` and `@fusion/core` are private; the CLI change is a bug fix with no user-facing API change — happy to add one if you'd rather it appear in release notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
9a8fc409ff |
fix: persist manual task pauses (#2536)
## Summary - persist an explicit `userPaused` latch when operators pause tasks through CLI, MCP, dashboard task routes, or mission stop - keep automatic/internal pauses distinct (`userPaused` remains false unless explicitly requested) - clear the latch on unpause - route the flag through in-memory and PostgreSQL task stores - add contract coverage across core, CLI, MCP, dashboard task routes, and mission stop ## Why A manually paused task could lose the reason for its pause across dashboard/runtime restart. Startup recovery then treated it like an internally interrupted task and reclaimed it, restarting automation against the operator’s intent. Manual pauses must survive restart and remain non-runnable until explicitly unpaused. ## Verification - core pause durability tests: 2 passed - CLI task/extension tests: 150 passed; PostgreSQL integration lane remains active in CI - dashboard route tests: 261 passed - `@fusion/core`, `@runfusion/fusion`, and `@fusion/dashboard` typechecks passed - full workspace build passed with pnpm 10.33.0 - changeset validation and `git diff --check` passed - live aggregate runtime verification also confirmed `paused=true,userPaused=true` survived a normal dashboard restart with zero active tasks <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Manual task pauses now persist across application restarts and recovery. - Pauses initiated via the CLI, dashboard, MCP tools, and mission stop controls are recorded as explicit user actions. - Automatically paused tasks remain eligible for recovery. - Unpausing clears the durable manual-pause state. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
3badc244a7 |
U12 part 2: bind the three U5 reconciliation guards — USER-VISIBLE (and one path that couldn't run under PostgreSQL at all) (#2512)
## U12 part 2 — the three U5 reconciliation guards now actually fire
USER-VISIBLE. Taken on standing authority; here is exactly what changed
for operators.
All three read the RAW `experimentalFeatures.workflowColumns` key via
`store.workflowColumnsFlagOn()`. Nothing in production writes it, so all
three have been inert since the workflow-columns cutover.
| Guard | Before (every real project) | After |
|---|---|---|
| Workflow edit removing an **occupied** column | Save succeeded; cards
left in a column the workflow no longer declares | Save fails with
`OccupiedColumnsError` unless `rehomeTo` is supplied |
| Workflow **delete** | Occupant capture returned `[]`; cards sat in the
deleted workflow's columns until the next engine start | Cards move to
the default workflow's entry column as part of the delete |
| Workflow **switch** | Never reconciled; the `reconciliation` field in
the declared return type was never populated | Card in an undeclared
column moves to the resolved target; a declared column is preserved |
Both consumers already handle the new outcomes and needed no change:
`register-workflow-routes.ts` maps `OccupiedColumnsError` to a
structured 409 carrying per-column occupant counts, and
`fn_workflow_update` returns a retryable structured result. The
dashboard editor's `rehomeTo` retry flow becomes reachable for the first
time. I only updated two stale "flag-ON" comments there — that code was
correct all along and simply never fired.
### What an operator actually sees (USER-VISIBLE — read this bit)
Four changes to what the board and the API do. Nothing here is silent.
1. **Editing a workflow to remove a column that has cards in it now
FAILS.** Previously the save succeeded and the cards were left in a
column their workflow no longer declared. The dashboard shows the
existing 409 with per-column occupant counts and prompts for a re-home
target; retrying with `rehomeTo` moves the cards and saves. Removing an
EMPTY column is unaffected.
2. **Deleting a workflow moves its cards immediately** to the default
workflow's entry column, instead of leaving them until the next engine
start.
3. **Switching a task's workflow moves the card** when the new workflow
does not declare its current column. A card whose column IS declared
stays exactly where it is. The API response now carries the
`reconciliation` summary it always promised.
4. **A switch whose re-home would be REJECTED is now refused before
anything is written.** If the destination column is at its WIP limit,
the switch fails with a structured 409 (`workflow-switch-rehome-failed`)
naming the task, both columns and the reason — and **nothing changes**:
the task keeps its current workflow AND its current column. Retry after
making room. Previously this combination committed the selection and
then silently reported a move that never happened, leaving selection and
column disagreeing.
**Can a torn card still happen? Yes, in one narrow case, and here is how
you recover.** If the destination fills in the window between the
pre-flight and the move, the selection is already committed and the card
ends up in a column its new workflow does not declare. That case is not
silent: it writes a `task:workflow-switch-torn` run-audit row, and the
error carries `selectionCommitted: true` with both columns. Recovery:
make room in the destination and move the card there, or switch the task
back — and if neither happens, the R7 startup sweep
`reconcileUndeclaredTaskColumns` re-homes it on the next engine start.
The card is never lost; it is visible in a lane the board may not draw
until one of those runs.
The one thing to watch after merge: (1) converts a previously-silent
success into a visible failure, so an operator mid-edit on a busy
workflow will start seeing a 409 they never saw before. That is the
point — the alternative was stranding their cards — but it is the change
most likely to generate a "this used to work" report.
### The thing that made this more than a gate removal
Un-gating the switch guard surfaced that
`selectTaskWorkflowAndReconcileImpl` read the task through
`store.readTaskFromDb` — the **synchronous SQLite** reader, which throws
under PostgreSQL:
```
TaskStore.db: SQLite Database is not available in backend mode
```
The flag returned before that line, so the gate was hiding a path that
**could not execute at all in the production backend**, not merely a
disabled feature. Ported to the async `readTaskRow`. Found by the new
tests, not by reading the code.
### Review round 2 (both findings real, both fixed)
**Torn write with no alarm — fixed by ORDERING, not by a louder
message.** My first attempt only made the error loud, which left the
torn state intact. The real fix is that the deterministic rejection
cause (destination at its WIP limit) is now checked BEFORE
`selectTaskWorkflow` commits, by resolving the target IR straight from
`workflowId` instead of through the task's selection. Nothing commits on
that path.
For the residual race the failure is loud AND recorded: `rehomeOccupant`
now returns `{ moved, error? }` (additive; sweep callers ignore it), the
switch writes a `task:workflow-switch-torn` run-audit row, and throws
`WorkflowSwitchRehomeFailedError` with `committed: true`. Consumers
translate it: the dashboard route returns a structured 409 with
`selectionCommitted`, and `fn_task_set_workflow` returns the same fields
— no more generic "something went wrong".
**Fabricated column for a deleted task.** My first fix fell back to
`fromColumn` when the final read found no row, so a task soft-deleted
mid-switch was reported as having its old column *preserved*. Absent now
reads as absent (the optional `reconciliation` is omitted). Extracted as
the pure `buildSwitchReconciliation` seam because the window is not
reachable through the public call — `selectTaskWorkflow` rejects an
already-deleted task up front — so it is a genuine race, and I test the
decision directly rather than asserting it from reading the code.
### Revert-proof, measured
New `workflow-reconciliation-production-shape.pg.test.ts` — 6 cases,
with the flag **never written**, which is the configuration every real
project has. Each flip reverted individually:
- re-gate the edit guard → **2 failures** (OccupiedColumnsError case;
rehomeTo re-home case)
- re-gate the delete capture → **1 failure** (card stays in
`custom-hold`)
- restore the switch early return → **2 failures** (`reconciliation`
undefined; card does not move)
- all three in place → **6/6 green**
Round-2 fixes, also measured:
- restore the `fromColumn` fallback → the "row is gone" case fails
(reports `preserved: true` for a deleted task)
- drop the `!outcome.moved` throw → the capacity-blocked case fails
(resolves instead of raising)
- **move the capacity pre-flight back AFTER the commit → the case fails
on the SELECTION assertion** (expected `WF-002`, received `WF-001`),
i.e. it proves the ordering, not the wording
The pre-existing coverage in `workflow-authoritative-reads.pg.test.ts`
reached the occupied-column guard by **writing the flag ON itself** —
same pattern as the ListView/Board suites in part 1. Its flag write is
removed; it now runs in the production shape.
### Where I nearly got this wrong
My first revert harness was buggy and I briefly concluded the delete
re-home was **redundant** — I had probed the stored column and seen
`triage` with what I thought was the flip reverted. It wasn't.
`workflow-ops.ts` contains two identical `const occupantTaskIds = await
store.listWorkflowOccupantTaskIds(id, false)` lines (field-reconcile
block, delete path), so my first-match edit reverted the wrong one.
Re-run anchored on surrounding context, the delete case fails as
predicted. Recorded in the test header as a caution. I also chased and
**refuted** a scarier hypothesis along the way — that an unrelated
`updateTask` coerces a custom column back to `triage`. It does not; the
column survives.
### Deliberately NOT in this PR
The v1-IR rollback-compat persistence (`downgradeIrToV1IfPure`) on the
workflow UPDATE path. It shared the same `flagOn` variable, which is how
it surfaced: **one flag read was feeding two unrelated decisions, so the
flag has more decision sites than call sites** — my earlier 9-site
inventory undercounted. It chooses the stored *shape* of the graph
rather than gating a guard, so it is a persistence-format change with a
different blast radius. It now reads the flag explicitly, behaviour
unchanged, for a follow-up.
The `moves.ts` group remains U2b's.
### Verification
`pnpm test:gate` (307 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17
steps), both typechecks green. Full `packages/core` PostgreSQL suite:
**1042 passed, 3 failed** — `central-archive-secrets.test.ts`
(log-prefix assertion) and
`workflow-settings-project-identity.pg.test.ts` (×2, project-id
resolution). I confirmed the identical 3 failures on a stashed clean
tree: pre-existing, unrelated. No Fusion instance booted.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Workflow edits now prevent removal of occupied columns unless cards
are moved to a specified destination.
* Cards are automatically re-homed when workflows are deleted or
switched.
* Workflow switches now check destination capacity before committing and
provide clear conflict details when re-homing fails.
* Reconciliation results now indicate whether cards were moved or
preserved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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=" |
||
|
|
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 -->
|
||
|
|
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) |
||
|
|
3f33cb000f |
feat: per-origin workflow selection + feedback-derived refinement titles
Two task origins had no workflow picker in front of the operator and always inherited the project default: `fn task create` (CLI + the `fn_task_create` agent tool) and refinement tasks. Add a Project General setting for each, where blank/unset means "Selected workflow" (the operator's current Board lane, falling back to the project default) and a concrete id pins that origin. Because the Board lane lives in browser localStorage, non-browser callers could not resolve "Selected workflow" at all. `boardSelectedWorkflowId` mirrors the lane into project settings so they can. Note this makes the mirrored lane project-scoped: two operators on one project share it, last switch wins. The Board never reads it back, so the only effect is which workflow a newly created task inherits. Resolution is `TaskStore.resolveOriginWorkflowOverrideId(origin)`: pinned setting -> mirrored lane -> `undefined` to inherit each caller's existing default-workflow path unchanged. A deleted or fragment id degrades to inherit rather than throwing, so a stale settings value can never break task creation. An explicit `workflow_id` argument to `fn_task_create` still wins. Separately, a refinement is now titled by the operator's own feedback via the shared `deriveFallbackTaskTitle`, not `Refinement: <parent title>`. Ten refinements of one task previously rendered ten identical titles, so the board could not tell them apart while the text saying what each one asked for sat in the description. Provenance moves to a `Refines <id>` card chip alongside the existing detail-view parent link and dependency edge. Verified: merge gate (299 tests), lint, full build, and typecheck for core, CLI, and dashboard all pass. New coverage: origin resolution across both origins and the full precedence ladder, the two settings pickers, the board-lane mirror, refinement titling (including sibling distinctness), and the card chip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
256c64a7bd |
chore(release): v0.74.0-beta.5
Version bump via changesets. |
||
|
|
0022621d22 |
chore(release): v0.74.0-beta.4
Version bump via changesets. |
||
|
|
ab87d0d803 |
fix(api): return 404 for missing tasks, and make task deletions attributable
Three related fixes, all originating from a `[api:error] Request failed` log line showing a 500 on `GET /api/tasks/FN-8610/runtime-fallback`. 1. Missing/deleted tasks now return 404 instead of 500. `getTaskImpl` signalled a miss with a bare `Error`, and route catches only mapped errno `ENOENT` to 404 — a leftover from the file-backed storage era. In Postgres mode nothing sets an errno code, so every unknown/missing/soft-deleted/wrong-project read returned 500. Adds a typed `TaskNotFoundError` (message byte-identical) plus a shared `task-lookup-error` mapper applied across the task, session-diff, git/GitHub, workflow and file-workspace route registrars. The same bare throw existed on both archive-lifecycle delete paths, so `DELETE /tasks/:id` was affected too. 2. 5xx logs now carry the origin stack. `rethrowAsApiError` constructed a fresh `ApiError` from the message and discarded the original, so the `FNXC:ApiErrorDiagnostics` contract logged the rethrow site rather than the throw site — the reported log entry had no stack at all. Threads `cause` through the error factories and walks the chain (bounded, cycle-guarded). 3. Task deletions are attributable, and non-operator deletes notify. `task:deleted` audit rows recorded `agentId: "system"` for every HTTP delete, making an operator click indistinguishable from a script or an agent; the calling agent's task id was accepted by the store and then never persisted. Adds a `callerKind` union recorded in audit metadata, tags every delete call site, and stamps a self-reported `x-fusion-client` header from the dashboard client. When the caller is `agent-tool` or `api-unattributed`, a best-effort notice is sent to the operator mailbox; operator and engine deletes stay silent. `x-fusion-client` is attribution, not authentication — anything can send it. No delete-blocking, gating or permission logic is added here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
106c61e6ee |
fix(agent-tools): close the fn_delegate_task Deny bypass and the store's window clamp
Follow-up to
|
||
|
|
13a2b2a9da |
fix(agent-tools): hide fn_task_create under Deny and widen the dedupe window
Operator report: with project policy "Ephemeral agent follow-up tasks = Deny", an executing agent filed ten follow-up tasks — five parallel fn_task_create calls it reported as timed out, then five sequential retries. Two defects: 1. Deny was advisory. fn_task_create was registered for every session and only refused inside execute(), so the model still saw the tool, planned around it, and retried it. The pi extension's isEphemeralCallerAgent also failed OPEN whenever the caller id did not resolve to an agent row — which is the normal shape of an ephemeral task-worker — so on that lane Deny was a no-op. 2. The deterministic content-fingerprint duplicate window was 60s, which only covered concurrent in-flight creates. A retry two minutes later saw nothing and filed a second task. Fixes: isAgentTaskCreateToolAvailable() withholds the tool from ephemeral sessions under Deny in both engine lanes (outer execution session, per-step workflow session); isEphemeralCallerAgent fails closed on an unresolvable caller id; the fingerprint window goes 60s -> 10m (clamp ceiling 5m -> 1h). upon_validation keeps the tool, and permanent-agent and human/chat callers are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
99b80ad748 |
feat(dashboard): add opt-in auto-update and harden restart supervision
Add the `autoUpdateAndRestart` global setting (default off, Settings -> General next to Release channel). When enabled, the dashboard host installs available updates on the selected channel by itself and requests the supervised in-place restart. Supervised hosts only: without a parent to respawn, installing would leave a running process whose code no longer matches its own install. Fix two ways the restart affordance could silently do nothing: - The supervisor now stamps FUSION_SUPERVISOR_PID and supervision is only counted when that pid is the real parent. FUSION_RESTART_SUPERVISED is inherited by every process Fusion spawns, so `fn dashboard` launched from an agent terminal skipped its own supervisor while still advertising restart support -- a restart request then killed it for good. - Settings and the update banner probe /system/info on mount and treat capability as advisory: the button always issues the request and shows the server's actual refusal instead of sitting disabled after a failed probe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |