b095ddeb696ffa415dbd9ede812227719d811721
213 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f2c729bf77 |
FN-8994: add workspace package graph validation
Prevent cold workspace installs from failing on missing or unglobbed local packages. - Validate workspace-protocol dependencies and overrides against glob-covered packages. - Add static-gate coverage and regression tests for missing plugin packages. - Document the workspace package graph check. Files changed: docs/testing.md | 4 +- package.json | 9 +- scripts/__tests__/check-workspace-package-graph.test.mjs | 103 +++++++++++++++ scripts/check-workspace-package-graph.mjs | 138 +++++++++++++++++++++ 4 files changed, 249 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-8994 Fusion-Task-Lineage: 17dbe062-79aa-4417-ae38-43df881e9fa4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f1fe399184 |
FN-8991: add runtime skill-loader drift gate
Enforce the intentional Claude-to-Grok runtime skill-loader clone relationship across verification lanes. - Add an exact rename-diff validator with fixture and live-loader coverage. - Run the validator in pretest, fast verification, and static merge-gate checks. - Document the loader duplication contract and expanded static-validator inventory. Files changed: AGENTS.md | 2 + docs/testing.md | 4 +- package.json | 7 +- .../check-runtime-skill-loader-drift.test.mjs | 113 +++++++++++++++++++++ scripts/__tests__/run-static-gate-checks.test.mjs | 1 + scripts/__tests__/verify-fast.test.mjs | 1 + scripts/check-runtime-skill-loader-drift.mjs | 113 +++++++++++++++++++++ 7 files changed, 236 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-8991 Fusion-Task-Lineage: a3b67752-0043-4336-9a2a-ff391672b31f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
25e292d0e6 |
FN-8954: preserve CLI liveness during startup
Ensure CLI startup operations settle before process exit on supported Node runtimes. - Keep awaited QMD probes and ephemeral port selection ref'd until completion. - Add CLI process regressions for init persistence and exit code 13. - Declare the Node 22.4 runtime floor and extend boot smoke coverage. Files changed: .changeset/fn-8954-cli-exit-13.md | 7 ++ docs/testing.md | 4 +- package.json | 3 + packages/cli/agent-browser.mjs | 6 ++ packages/cli/bin.mjs | 7 ++ packages/cli/package.json | 3 + packages/cli/src/__tests__/ci-workflow.test.ts | 9 +++ packages/cli/src/__tests__/cli-exit-code.test.ts | 82 ++++++++++++++++++++++ packages/cli/src/__tests__/package-config.test.ts | 12 ++++ packages/cli/src/bin.ts | 6 ++ .../__tests__/postgres/embedded-free-port.test.ts | 37 ++++++++++ packages/core/src/memory/memory-backend.ts | 17 +++-- packages/core/src/postgres/embedded-lifecycle.ts | 16 +++-- scripts/boot-smoke.mjs | 62 +++++++++++----- 14 files changed, 244 insertions(+), 27 deletions(-) Fusion-Task-Id: FN-8954 Fusion-Task-Lineage: 05303d07-2662-48d5-a442-6d43fa0a4493 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
71f77e7ac5 |
Bump typescript-eslint from 8.58.1 to 8.66.0 (#3368)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.58.1 to 8.66.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/typescript-eslint/typescript-eslint/releases">typescript-eslint's releases</a>.</em></p> <blockquote> <h2>v8.66.0</h2> <h2>8.66.0 (2026-08-03)</h2> <h3>🚀 Features</h3> <ul> <li><strong>typescript-estree:</strong> handle import.defer() as ImportExpression (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12609">#12609</a>)</li> </ul> <h3>🩹 Fixes</h3> <ul> <li><strong>eslint-plugin:</strong> [no-unnecessary-type-conversion] ignore shadowed built-ins (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12590">#12590</a>)</li> <li><strong>eslint-plugin:</strong> [prefer-nullish-coalescing] handle shadowed Boolean calls (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12591">#12591</a>)</li> <li><strong>eslint-plugin:</strong> [no-useless-default-assignment] don't report defaults used by other overloads (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12607">#12607</a>)</li> <li><strong>eslint-plugin:</strong> [no-unnecessary-type-parameters] check MappedType key remapping (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12588">#12588</a>)</li> <li><strong>eslint-plugin:</strong> [class-literal-property-style] preserve type annotations and don't drop decorators (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12617">#12617</a>)</li> <li><strong>website:</strong> list <code>onUnsupportedTypeScriptVersion</code> in parser options (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12585">#12585</a>)</li> </ul> <h3>❤️ Thank You</h3> <ul> <li>Evyatar Daud <a href="https://github.com/StyleShit"><code>@StyleShit</code></a></li> <li>Kirk Waiblinger <a href="https://github.com/kirkwaiblinger"><code>@kirkwaiblinger</code></a></li> <li>Steffen Schroeder</li> <li>송재욱</li> </ul> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.0">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>v8.65.0</h2> <h2>8.65.0 (2026-07-20)</h2> <h3>🚀 Features</h3> <ul> <li>add warning when TS 7 is detected (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12529">#12529</a>)</li> <li><strong>eslint-plugin:</strong> [no-restricted-imports] deprecate extension rule (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12527">#12527</a>, <a href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/19562">#19562</a>, <a href="https://redirect.github.com/typescript-eslint/typescript-eslint/issues/11889">#11889</a>)</li> <li><strong>eslint-plugin:</strong> [no-shadow] specialized error on enum declaration and member shadowing (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12578">#12578</a>)</li> <li><strong>parser:</strong> add onUnsupportedTypeScriptVersion option to error on unsupported TypeScript versions (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12465">#12465</a>)</li> <li><strong>typescript-estree:</strong> throw for invalid import defer syntax (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12552">#12552</a>)</li> </ul> <h3>🩹 Fixes</h3> <ul> <li><strong>eslint-plugin:</strong> [prefer-string-starts-ends-with] handle escaped $ ending regex literals (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12515">#12515</a>)</li> <li><strong>eslint-plugin:</strong> [unbound-method] report unbound methods accessed via member expression on union types (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12448">#12448</a>)</li> <li><strong>eslint-plugin:</strong> [no-unnecessary-parameter-property-assignment] don't flag computed assignments with a variable key (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12568">#12568</a>)</li> </ul> <h3>❤️ Thank You</h3> <ul> <li>Evyatar Daud <a href="https://github.com/StyleShit"><code>@StyleShit</code></a></li> <li>Josh Goldberg ✨</li> <li>Kirk Waiblinger <a href="https://github.com/kirkwaiblinger"><code>@kirkwaiblinger</code></a></li> <li>Serhii Leniv <a href="https://github.com/Serhii-Leniv"><code>@Serhii-Leniv</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md">typescript-eslint's changelog</a>.</em></p> <blockquote> <h2>8.66.0 (2026-08-03)</h2> <p>This was a version bump only for typescript-eslint to align it with other projects, there were no code changes.</p> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.66.0">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>8.65.0 (2026-07-20)</h2> <h3>🚀 Features</h3> <ul> <li>add warning when TS 7 is detected (<a href="https://redirect.github.com/typescript-eslint/typescript-eslint/pull/12529">#12529</a>)</li> </ul> <h3>❤️ Thank You</h3> <ul> <li>Josh Goldberg ✨</li> <li>Kirk Waiblinger <a href="https://github.com/kirkwaiblinger"><code>@kirkwaiblinger</code></a></li> </ul> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.65.0">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>8.64.0 (2026-07-13)</h2> <p>This was a version bump only for typescript-eslint to align it with other projects, there were no code changes.</p> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.64.0">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>8.63.0 (2026-07-06)</h2> <p>This was a version bump only for typescript-eslint to align it with other projects, there were no code changes.</p> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.63.0">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>8.62.1 (2026-06-29)</h2> <p>This was a version bump only for typescript-eslint to align it with other projects, there were no code changes.</p> <p>See <a href="https://github.com/typescript-eslint/typescript-eslint/releases/tag/v8.62.1">GitHub Releases</a> for more information.</p> <p>You can read about our <a href="https://typescript-eslint.io/users/versioning">versioning strategy</a> and <a href="https://typescript-eslint.io/users/releases">releases</a> on our website.</p> <h2>8.62.0 (2026-06-22)</h2> <h3>🚀 Features</h3> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
3d6a908b95 |
FN-8898: document inert prerebase settings
Clarify that legacy prerebase settings are inert on the production merge path. - Mark retained prerebase configuration and audit events as legacy-only. - Add a static validator and tests preventing new prerebase callers. - Update merge architecture, testing, and settings documentation. Files changed: AGENTS.md | 2 +- docs/architecture.md | 3 +- docs/settings-reference.md | 6 +- docs/testing.md | 2 +- package.json | 6 +- packages/core/src/types/settings/settings-scope.ts | 32 +++-- .../src/errors/transient-merge-error-classifier.ts | 12 +- packages/engine/src/merge/merger-auto-prerebase.ts | 12 +- packages/engine/src/util/run-audit.ts | 2 + scripts/__tests__/check-prerebase-inert.test.mjs | 73 +++++++++++ scripts/__tests__/run-static-gate-checks.test.mjs | 1 + scripts/__tests__/verify-fast.test.mjs | 1 + scripts/check-prerebase-inert.mjs | 146 +++++++++++++++++++++ scripts/lib/source-projection.mjs | 87 ++++++++++++ 14 files changed, 359 insertions(+), 26 deletions(-) Fusion-Task-Id: FN-8898 Fusion-Task-Lineage: 9cfd836d-17c2-44a0-a076-56fef0917935 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
03f84d406c |
chore(release): v0.76.0-beta.0
Version bump via changesets. |
||
|
|
64413f4e8d |
chore(release): v0.75.1
Version bump via changesets. |
||
|
|
cb500eeb4c |
chore(release): v0.75.1-beta.2
Version bump via changesets. |
||
|
|
68e964383f |
chore(release): v0.75.1-beta.1
Version bump via changesets. |
||
|
|
6a5e1b0145 |
chore(release): v0.75.1-beta.0
Version bump via changesets. |
||
|
|
5dbfef7b64 |
chore(release): v0.75.0
Version bump via changesets. |
||
|
|
b1d5e0fec3 |
chore(release): v0.75.0-beta.2
Version bump via changesets. |
||
|
|
07dccbe2bd |
FN-8783: parallelize static merge-gate validators
Run independent static merge-gate policy validators concurrently without weakening gate ordering. - Add a fail-closed concurrent static-validator runner with coverage for inventory and failures. - Preserve curated engine, PostgreSQL, unit, and CI-shape gate contracts. - Document the gate composition and warm-cache performance policy. Files changed: docs/testing.md | 13 ++- package.json | 3 +- packages/cli/src/__tests__/ci-workflow.test.ts | 21 ++-- packages/engine/vitest.config.ts | 36 +++++-- .../__tests__/engine-vitest-gate-policy.test.mjs | 90 +++++++++++++---- scripts/__tests__/run-static-gate-checks.test.mjs | 100 +++++++++++++++++++ scripts/run-static-gate-checks.mjs | 106 +++++++++++++++++++++ 7 files changed, 332 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-8783 Fusion-Task-Lineage: d5d3c9e1-b3c4-45ff-a3e7-f9555585cd70 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
9e4a0817db |
feat: restart the development engine on source changes (#3329)
## Summary Add an opt-in source-development loop that restarts the dashboard and engine when runtime TypeScript or JSON changes. Use `pnpm dev:watch`; `pnpm dev:hmr` now combines Vite UI HMR with the same supervised API/engine restart path. The watcher filters tests, fixtures, generated declarations, build output, and task state. It coalesces bursts with a two-second maximum wait, waits for the child to acknowledge its IPC listener, and rebuilds runtime dist artifacts before a source-triggered respawn. ## Safety model - Close scheduler, triage, heartbeat, mission, routine, self-healing, and merge admission before checking for active work. - Let already-running agents reach a safe boundary; do not mutate durable pause settings. - Enter the existing graceful exit-code-86 shutdown and supervised respawn path. - Retry failed liveness reads and declined restart requests instead of dropping the pending change. - Keep ordinary `pnpm dev` behavior unchanged; inherited watch state does not break nested non-dashboard development commands. A development restart intentionally replaces the dashboard process, so transient dashboard connections and project dev-server children reconnect or restart with it. Agent work is the protected boundary. ## Validation - `pnpm lint` - `pnpm test:gate` (753 tests passed across engine, core, PostgreSQL gate, and CI-shape suites) - Focused CLI watcher/restart/supervision suites: 40 tests passed - Focused engine drain/manager suites: 52 tests passed - `pnpm --filter @runfusion/fusion typecheck` - `pnpm --filter @fusion/engine typecheck` - `pnpm verify:fast` (13 steps passed, including CLI build and real health boot smoke) - Manual unsupported-command probe confirms explicit `--watch` fails clearly outside the dashboard command ## Post-Deploy Monitoring & Validation - Watch for `[fusion:dev] source changed`, `source restart deferred`, `active work drained`, and `restart requested` logs during the first watched development session. - Healthy behavior is one exit-86 respawn per edit batch, no interrupted active agents, refreshed dist artifacts, and a healthy dashboard after respawn. - Investigate repeated restart loops, watcher attachment warnings, declined restart retries, or liveness-read failures. - Immediate mitigation is to use ordinary `pnpm dev` without `--watch`; no production runtime behavior or durable setting needs rollback. - Validation owner: Fusion maintainers during the first source edit after merge. --- [](https://github.com/EveryInc/compound-engineering-plugin) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `pnpm dev:watch` to automatically restart development runtime processes when source files change. * Development restarts now wait for active work to finish, preventing new work from starting during the transition. * Enhanced `pnpm dev:hmr` with graceful runtime source restarts while keeping the dashboard available. * Rapid source changes are grouped to avoid unnecessary restarts. * **Documentation** * Updated development setup and contribution guides with the new watch workflow. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
993a2f9d86 |
chore(release): v0.75.0-beta.1
Version bump via changesets. |
||
|
|
77c698a249 |
chore(release): v0.75.0-beta.0
Version bump via changesets. |
||
|
|
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. |
||
|
|
1e83dcceec |
chore(release): v0.74.0-beta.6
Version bump via changesets. |
||
|
|
4e2f52ce8f |
feat(gate): ratchet move-target literals at zero — #3150's population had nothing holding it (#3246)
Closes the gap I flagged when re-measuring #3150: that population is at **0**, and nothing was holding it there. ## Why this surface has no gate today The lifecycle census parses **comparisons**. A move destination is a call **argument**: ```ts await store.moveTask(id, "in-review"); // never counted by anything ``` #3150 measured 31 of these across four files. They are now 0 — I verified that on current main before writing this — but the comparison backlog drifted **787 → 854** during the window its own ratchet was unwired, and this population never had one. ## The failure mode is louder than the guards' A wrong lane **guard** silently answers "no". A wrong move **target** is rejected by `moveTaskInternal` with `TransitionRejectionError: unknown-column` — so on a board that renamed its review lane, every task finishing implementation **threw** instead of reaching review. Loud at runtime, invisible to any test on the default board. ## AST, not grep — and that is measured, not stylistic | scan | result | |---|---| | comment-naive grep of `self-healing.ts` | 1 hit — **JSDoc prose**: `* could call moveTask("in-review")` | | #3150's own SQL survey by grep | 37 hits against **12** real sites (25 comments) | Comments are not AST nodes, so that false-positive class cannot occur here in either direction. ## Controls — all four run, because a gate that only reports 0 proves nothing | probe | expected | got | |---|---|---| | real `moveTask(id, "in-review")` injected | fail | **exit 1**, names the file | | identical call as JSDoc prose | pass | **exit 0** (AST ignores comments) | | legacy target + leading `DELIBERATE-LITERAL` | pass | **exit 0** (marker honored) | | probe removed | pass | **exit 0** | The third is the #1411 `recoveryRehome` safe-landing path, where the legacy id genuinely *is* the target. Marker must be **leading** — the census already learned that an inline marker attaches to the wrong node and is silently ignored. ## Ratchet semantics match the census Fails on a **drop** as well as a rise. A stale allowance is a hole a re-added target can return through while the gate stays green — exactly what let the comparison baseline drift. ## Measured | check | result | |---|---| | this gate | scans **1816** files, reports **0** | | `check:lifecycle-columns` / `check:sql-column-literals` | 0 / 0 | | `check:fnxc-future-dates` / `check:lane-wiring` | 0 / 0 | | eslint / `pnpm test:gate` | clean / exit 0 | Wired into `pr-checks.yml` beside the sibling ratchets, named to match ("Move-target ratchet"). ## Scope Gate only — **no production code touched**, and no conversions in this PR. The population was already empty; this makes "31 → 0" an invariant instead of a snapshot, which is the caveat I attached when recommending #3150 for closure. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Quality Improvements** * Added automated validation for task-movement configuration values. * Pull request checks now detect unexpected changes in tracked values. * Added baseline tracking with strict validation to identify both additions and removals. * Added support for explicitly documenting intentional exceptions. * Improved reporting for file-discovery and source-reading failures. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bc37185026 |
gate: enforce the quarantine deletion ratchet — nothing ran it, and it could not fail (#3167)
## A policy with nothing enforcing it AGENTS.md states the deletion ratchet plainly: > A quarantined test is **DELETED after 14 days** (`quarantinedAt` + 2 weeks) unless rescued. Nothing enforced it, and it failed in two independent ways: 1. **`check:quarantine-ledger` omitted `--strict`.** The script only exits non-zero with that flag (`check-quarantine-ledger.mjs:197`: `return args.strict && (summary.expired > 0 || summary.near > 0) ? 1 : 0`). Without it, it is a report that always exits 0. 2. **No workflow ran it.** I audited all 12 `check:*` scripts against `pr-checks.yml` and `full-suite.yml`: this is the only one appearing in neither. Either alone would have made it toothless. Together, a quarantined test could sit past its deletion date indefinitely with every gate green. ## This exact shape is already documented in the file I edited The comment above the lifecycle-column ratchet in `pr-checks.yml` says: > `pnpm census:lifecycle-columns` (no `--strict`) and nothing ran it, so three PRs lowered counts without re-recording and left allowances the deleted guards could return through while this gate stayed green. Script supports enforcement → package script omits the flag → no workflow runs it. Same three steps, different ratchet. That precedent is why I went looking. ## What changed - `check:quarantine-ledger` now passes `--strict` - wired into `pr-checks.yml` beside its siblings It fires **5 days before** the deadline, not after, so the response is still delete-or-rescue rather than an overdue entry. That window is the script author's design; I did not invent it. ## Mutation evidence Against a temp ledger, real one restored after: | Ledger state | Result | |---|---| | today (1 entry, 13 days remaining) | exit 0 | | entry inside the 5-day near window | **exit 1** | | entry 6 days past deadline | **exit 1**, reports `EXPIRED (6 days overdue)` | | real ledger restored | exit 0 | Without `--strict` all four exit 0 — which is the state on `main`. ## Honest note on what this will do This is a **deadline ratchet**: it fires on a timer by design. The current entry (`useTasks-hydration-freshness.test.ts`, deadline 2026-08-13) will trip it on **2026-08-08** unless someone deletes or rescues it first. That is the intended behaviour and the whole point — AGENTS.md is explicit that rescue "requires evidence the test catches real regressions plus a root-cause fix — not stabilization passes." A gate that never fires enforces nothing. ## Census before / after ``` before: COLUMN guards (the backlog): 13 after: COLUMN guards (the backlog): 13 ``` Unchanged — this touches no lifecycle guard. It is gate wiring. ## Verification `test:gate` exit 0 · `check:quarantine-ledger --strict` exit 0 on the real ledger · both failure arms mutation-verified · ledger file restored byte-for-byte. |
||
|
|
f818bdce2c |
gate: run the inert-sync-lane ratchet in test:gate — it was wired to nothing (#3136)
## A gate that nothing runs `check:inert-sync-lanes` has existed since #3062 and been hardened three times (#3068, #3079, and #3122 pending). It is invoked by **nothing** — not `test:gate`, not any workflow. It is a `package.json` script that only runs if a human types it. Here is the round trip that cost, on `main`: | | | |---|---| | **#3108** | flagged `triage.ts:765`: *"the obvious next move is to convert the third with the same helper. **That would be wrong twice over.**"* Both blockers named, backed by `sync-workflow-ir-second-blocker.test.ts`. | | **#3114** | converted exactly that arm, replaced the warning with its own note, reported census 45 → 44. | | **#3126** | reverted it — after someone ran the ratchet by hand and found `main` red. | Hours apart. All three green in CI. **No behaviour changed on any board at any point.** The ratchet flagged it correctly the entire time; nothing ran it, so a caught defect cost three PRs instead of one failed check. This is not a criticism of #3114's author. A written warning at the exact line, with a test behind it, was overwritten within hours by a well-intentioned change — that is simply what unenforced prose does under fleet pressure. It is the fourth time this class has landed (#3051, the `resolveMoveFanoutColumnsSync` family, #3114, and my own `--triage` draft that failed to zero). ## What this does One line: adds `node scripts/check-inert-sync-lane-conversions.mjs` to `test:gate`, beside the cheap AST guards it belongs with (`check-no-nohup`, `check-capacity-pool-id`, `check-mock-completeness`). Single-pass parse; no measurable cost. ## Scope reduced from the earlier version This branch previously also restored the triage literal. **#3126 landed that first**, so I rebuilt it as the wiring alone rather than carry a half-redundant diff. #3127 is closed with nothing lost. ## Census before / after ``` before: COLUMN guards (the backlog): 18 after: COLUMN guards (the backlog): 18 ``` Unchanged — this converts nothing. It makes an existing check actually run. ## Verification `test:gate` exit 0 **with the ratchet inside it**, against current `main` (now green on the ratchet since #3126). Diff is one line of `package.json`. ## Related **#3122** restores 13 guards laundered through `mergeParkedColumns` — worth landing after this so the ratchet enters CI at full sensitivity. **#3135** attaches lanes at the last two live emitters. **#3131** corrects a 44% under-report in `--triage`. |
||
|
|
bdedb6cf1a |
gate: fail the build on a NEW inert sync-lane conversion (#3062)
## Claim, and why it turned into a gate
I claimed the largest unclaimed unflagged cluster, `executor.ts` (4
guards at 3557/3581/3632/3642). All four are conditions of a
**synchronous** `store.on("task:moved", …)` listener — the same class as
`scheduler.ts`. Converting them needs either an `await` in a sync
prologue or the sync resolver, and the sync resolver is inert.
`executor.ts` already says so, at line 10459, dated 2026-07-30:
> **THE SYNCHRONOUS RESOLVER IS A NO-OP IN PRODUCTION.** … every
sync-resolved conversion resolves the DEFAULT workflow and answers with
the legacy ids no matter what board the task is on. That makes a sync
conversion cosmetic: the census counts it as converted, `--strict` goes
down by one, and the guard behaves exactly as the literal did. **Worse
than leaving the literal, because the number says the site is done.**
The next day, #3051 did exactly that to ten `scheduler.ts` arms. Census
fell by ten; nothing changed on any board (refuted live in #3058).
So the finding was already written down, in the file a converter would
be reading, in capitals — and the fleet phase produced the defect
anyway. **A comment cannot fail a build.** Converting `executor.ts`'s
four the only available way would have made me the third instance. I
flagged them and built the guard instead.
## What the check does
Per file: finds functions reaching `resolveTaskWorkflowIrSync`, the
locals assigned from them, and the `===`/`!==` guards consuming those
roles. Baselined per file; **fails on a rise.**
Not zero, deliberately. The existing sync guards are real and documented
— the scheduler's listeners genuinely cannot `await` today and their
authors said so. Demanding zero forces a revert or a day-one exemption
marker. What must not happen is *more* literals quietly becoming
inert-resolved.
Complements `check-inert-flag-seams.mjs`, which catches the opposite
shape (a lane parameter **no** caller supplies). This catches a
parameter that **is** supplied, from a source that always answers the
same thing — which passes that check cleanly.
## Why the shape is invisible
The obvious reading is wrong, and it is what makes this survive review.
The helper does **not** receive `undefined` and fall through to `??
"in-review"`. It receives a **real IR that resolves real traits** — the
default board's — so it answers with full confidence and the `??` arms
beside it are dead code.
```
tsc passes the value is a string, correctly typed
tests pass on the default board the constant answer IS the right answer
the census DROPS it counts comparisons against literals, and the literal really is gone
```
## Mutation evidence — including one against this check itself
| Mutant | Result |
|---|---|
| baseline | exit 0, 20 guards in `scheduler.ts` |
| convert one more literal to a sync-resolved lane (the #3051 move) |
**exit 1, 20 → 21** |
| convert the same literal to an **async**-resolved lane | exit 0 —
correctly silent |
The first draft **failed its own mutation test**: it matched only the
local-variable spelling (`const parked = resolveX(...)` then
`parked.review`), which is what #3051 used, and the inline spelling
`resolveX(store, id).review` walked straight past it while being exactly
as inert. A ratchet one rewrite evades is worse than none, because the
green result reads as proof. Both spellings now count.
## Limits, stated so nobody over-trusts it
Sources are matched **within a file by function name**, so a helper
imported from another module is not followed — this finds the dominant
local-helper shape and will miss a cross-module one
(`resolvePlannerLanes`, consumed in `executor.ts`/`triage.ts`, is
currently outside its reach). It proves a guard consumes a sync-resolved
answer, not that the answer is wrong for every caller. Tests are
excluded. Treat a report as a pointer to investigate.
## Census before / after
```
before: COLUMN guards (the backlog): 104
after: COLUMN guards (the backlog): 104
```
Unchanged by design — this converts nothing. It stops the count from
moving for the wrong reason.
Worth recording alongside it, measured across the current backlog: **21
of 104 already carry an explicit flag note**, **51 are
`self-healing.ts`** (concurrently claimed by **#3055, #3050 and #3049**
— three PRs, one file, still worth de-conflicting), and **28 are
genuinely unclaimed and unflagged**, the largest being these
`executor.ts` four. The cluster-sized work is close to exhausted; what
is left is scattered and mostly blocked, which is the pressure that
produced #3051.
## Verification
`test:gate` exit 0 · `pnpm lint` clean · lifecycle-column census exit 0
· `pnpm check:inert-sync-lanes` exit 0. No production file touched.
|
||
|
|
a460a9bbc0 |
fix(plugins,dashboard): the dependency graph drew every card with the LEGACY lane vocabulary (#3029)
## The third producer of unflagged cards — the one a host-side fix could not reach #3025 fixed the two producers that go through `renderTaskCard`. `GraphTaskNode` is a third: it imports `TaskCard` **directly** through the plugin's interop shim, so that fix bypassed it and every role helper inside a graph card kept reading the legacy ids. The same component also called the stuck predicate without its flags: ```ts const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs); // no columnFlags ``` so `isWipColumnRole` fell back to the literal and **no card in the graph could ever be stuck on a renamed board**. Because `isStuck` gates `isActive`, a wedged card rendered with the **active** styling — the graph reported *"running"* about a task that had not moved in hours, while the main board showed the same card as stuck. That asymmetry between two views of one task is the defect, and it is what the new test pins. ## One cause, so one fix Both symptoms came from the same gap: `PluginDashboardViewContext` exposed `tasks` and nothing about the board's vocabulary. It now carries `columnFlagsByTaskId` — the same per-task map `renderTaskCard` already uses, **two lines away in the same object literal**. ## I filed this twice as blocked on a public-API change. It was not. ``` packages/dashboard @fusion/dashboard private: true packages/plugin-sdk @fusion/plugin-sdk private: true plugins/fusion-plugin-dependency-graph @fusion-plugin-examples/dependency-graph private: true ``` No published surface anywhere in the path — three in-repo private packages and a hand-written `.d.ts`. **#3026 landed the general form of that mistake while I was still making it**: a deferral's stated blocker is a claim, and mine decayed unchecked until I finally measured it. ## Two type decisions worth reviewing - **`Partial<TraitFlags>`** in the plugin-facing type, not the dashboard's `ExecutorColumnFlags` — that module's own header restricts it to `@fusion/core` and `react` imports so external plugin builds can consume it. Same runtime object either way. - **`MainContentProps.columnFlagsByTaskId` widened** from `{complete, archived, intake, hold}` to the flags the map really carries. It is built from `workflow.columns.find(...).flags`, so the four-flag declaration was a narrower view than the value — and `countsTowardWip`, which every wip predicate needs, was invisible through it. That narrow type is why threading this looked impossible at first. Absent still means legacy, matching how the host treats remote rows and off-board columns: the degraded answer is the documented literal, never *"this board has no wip lane"*. ## Revert proof Dropping the 4th argument: ``` AssertionError: expected 'graph-task-node graph-task-node--acti…' not to contain 'graph-task-node--active' Tests 1 failed | 26 passed (27) ``` The paired case (a fresh legacy `in-progress` card still reads active) passes both ways by design — it guards against over-detection, so I am not counting it as coverage. The gate agrees independently: `plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx: 1 -> 0`, baseline re-recorded 16 → 15 in the same commit. ## Verification (measured) - plugin suite — **185 passed / 20 files** - dashboard `dashboard/` + `plugins/` suites — **48 passed / 6 files** - `tsc --noEmit` clean in both packages; `pnpm lint` clean - `lifecycle-column-census --strict`, `check-lane-wiring` (15, none added), `check-sql-column-literals`, `check-inert-flag-seams`, `check-fnxc-future-dates` — green No changeset: all three packages are `private: true`. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
19deb42170 |
gate: ratchet call sites that never receive the lane answer (#2966)
**This is the gap that let three defects reach `main` in one day.** `unwired-lane-parameter.mjs` catches a parameter that reaches **no** caller. It is deliberately satisfied by a mention *anywhere*, so **partial** wiring is invisible to it: | | | | --- | --- | | #2956 | `getInReviewStallReason` wired at **0 of 4** call sites while both siblings were wired | | #2963 | both merge entry points unwired — merging was **impossible** on a renamed board | | #2964 | merge-confirmed finalization unwired — **already-landed work parked `failed`** | Every one was a fix that added an optional parameter without the call-site sweep that has to follow it. The existing guard was green throughout, correctly by its own contract. ## A census, not a guard — and that distinction is the whole design Auditing the sites this finds showed **four of seven were legitimately unwired**: `skipColumnIdentityCheck` callers have already proven lane identity by a stronger means, a sentinel-column caller wants the identity check satisfied by construction, and a dead export has no caller to wire at all. A check that failed on those is ~57% false positives. The sibling guard's own header says why that is worse than a miss — *"it teaches people to disable the check"* — and I agree, so this does not do it. Instead it ratchets like the lifecycle census: **36 known unwired sites across 20 files**, allowed to shrink and not to grow. A new unwired caller raises the count and fails; wiring one lowers it and re-records. The recurrence — adding a caller that forgets the lane answer — is precisely what gets caught, and the legitimate sites cost one baseline line each instead of a permanently red gate. ## Detection is AST-based, deliberately It finds exported functions accepting a lane-named argument — directly *or* as an options-bag member — then finds call sites passing none of them. Not regex: the ad-hoc scan I used during the audit produced false negatives on multi-line calls, which is exactly how a caller gets missed in the first place. Using a heuristic to police a defect caused by a heuristic seemed like a poor trade. ## Verified to fail on the recurrence A ratchet that cannot fail is worse than none, so this was measured rather than assumed. Injecting one new unwired caller into `self-healing.ts`: ``` [check-lane-wiring] call sites not passing a resolved lane argument INCREASED: packages/engine/src/self-healing.ts: 9 unwired now, baseline allows 8 ``` exit 1, naming the file and the delta. ## Placement Runs as a named `check:lane-wiring` step in `pr-checks.yml` beside the lifecycle, SQL, inert-seam and FNXC ratchets — same convention, same failure ergonomics, ~1s. Note the baseline records today's state, which still includes the #2963/#2964 sites because those fixes have not merged yet. When they land the count drops and the baseline is re-recorded downward — the ratchet working as intended rather than a conflict. ## Verification `pnpm test:gate` 161 + 487 + 13 + 71; `tsc` engine clean; lint, lifecycle census `--strict`, FNXC gate, and the new check all clean. |
||
|
|
b8e5d42b7a |
chore(gate): move the FNXC date ratchet beside its three siblings (#2952)
The half that #2948 and #2950 did not cover. Both of those fixed today's redness; **#2949** landed the un-redding first, so both are now conflicting and redundant. This is the placement, which is what made today's failure so expensive. ## Why it hurt `check-fnxc-future-dates` was wired into `pretest` **and** `test:gate`, with no `check:*` script and no `pr-checks.yml` step. So a baseline frozen below the tree it froze did not produce "one CI step is red" — it produced: - `pnpm test:gate` → exit 1, merge gate down for everyone - `pnpm test` → refuses to run before a single test executes ## The precedent All three sibling ratchets are dedicated `pr-checks.yml` steps. `lifecycle-column-census` always has been; `check-sql-column-literals` and `check-inert-flag-seams` moved there in #2941. The census's own header states the reason, and it is the one that matters here: > a permanently-red gate is a bigger hole than a stale allowance, because it gets ignored and then nothing is guarded at all ## The change ``` check:fnxc-future-dates script, beside check:inert-flag-seams "FNXC stamp dates" step in pr-checks.yml, after the other three removed from pretest / pretest:full / test:gate ``` **Enforcement where it matters is unchanged** — `pr-checks.yml` is the blocking gate, so a newly added future-dated stamp still cannot merge. What changes is that a baseline mismatch stops halting work unrelated to it. ## Deliberately not touching The drop behaviour. This gate **already** tightens on a drop rather than failing — the #2888 pattern, already correct here. I checked rather than assuming it needed the same fix its siblings did. ## Verification `pnpm check:fnxc-future-dates` exit 0 · `pnpm test:gate` green (now without this check in it) · lint 0 · step confirmed adjacent to the other three ratchets. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added automated validation for FNXC stamp dates to lint checks. * Updated test and validation scripts to run the date check through a dedicated command. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5 (1M context) <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> |
||
|
|
ef50244234 |
feat(gate): freeze the SQL column-literal surface — 30 sites, none may be added (#2841)
Instruments a surface no existing check can see. Follows #2839, and **corrects the count I reported there** (12 → 14). ## Why it was invisible The lifecycle census parses TypeScript **comparisons**; a legacy id inside a SQL string is string data. The inert-seam gate reasons about parameters and call sites. Neither has ever looked here. **What it cost:** `cleanupStaleMergeQueueRowsImpl` filtered on `t.column != 'in-review'`, so on a renamed board every queued card looked stale, its `merge_queue` row was deleted, and the card became **unleaseable**. The operator found it reviewing #2819 — in SQL I had already read past during that same work. The quieter half is analytics: five sites count `"column" = 'done'`, so throughput, cycle time, and team dashboards report **zero completed work** on a renamed board. Nothing errors, which is why nobody files it. ## What this does, and does not do It does **not** fix the sites. `resolveProjectColumnsForRoles` is the mechanism and its migration has an owner (#2839). This freezes the population so the surface cannot grow underneath that migration: a new file or a higher count fails, **and a lower count fails too** — so the baseline ratchets down as sites migrate rather than leaving slots to silently regrow into. That is the same rot as an allow-list entry for a deleted function, which this repo already hit once. AST-based, deliberately: a line grep for the same pattern reports **37** hits, **25 of them prose** quoting `column === "done"` in explanatory notes. A guard that is 68% false positives trains its readers to skip it — a lesson this program has already paid for. ## Two corrections found by mutation-testing my own gate **1. Clause fragments were missed.** Requiring a SQL keyword *in the same literal* skipped `team-analytics.ts`, which builds `["assignedAgentId IS NOT NULL", `"column" = 'done'`, ...]` and joins them into a `WHERE` later. That fragment is as vocabulary-bound as any full query but contains no keyword. Fixing it took the population **12 → 14**, so the number I put on #2839 was low. **2. My first mutation test proved a direction it had not.** I replaced the first textual occurrence in a file — which was inside a **comment** — and read the unchanged count as the scanner being broken. The scanner was right; my test was wrong. All three directions are now driven against real SQL: | mutation | result | |---|---| | add a full query with a legacy comparison | `3 SQL column literal(s), baseline allows 2` | | add a bare clause **fragment** (no keyword) | caught — same failure | | migrate one away (count drops) | `1 site(s) now, baseline still allows 2 — re-record it` | | restore | exit 0 | I am flagging that second one because it is the exact failure mode this program keeps finding: a green result read as evidence when the experiment was invalid. ## Verification `pnpm test:gate` green with the new check in it · lint 0 · single AST pass. Wired into `test:gate` and both `pretest` hooks. 🤖 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** * Added automated checks to detect increases in legacy SQL column literals. * Added baseline tracking to ensure known SQL literal counts do not regress. * **Tests** * Expanded pre-test and gated verification steps with SQL literal and mock completeness checks. * Updated test validation workflows to enforce the new safeguards. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ba40942a10 |
batch-dashboard-app: 75 → 2 across packages/dashboard/app — the last two are deliberate, not missed (#2772)
**Batch branch is live: `batch-dashboard-app`.** Push conversions here as commits rather than opening per-file PRs — that is the CI-run bottleneck this model removes. **One-line ownership note for you to arbitrate:** you have addressed me as U11, U12 and U7 at different points, so the `u12 worker -> batch-dashboard-app` mapping is ambiguous from my side. I claimed it because `dashboard/app` is where I have done the most work this session (TaskContextMenu, Column, TaskCard, TaskDetailModal, columnRoles, taskActivity) and I know which of its guards are load-bearing fallbacks. **If another worker is the intended owner, say so and I will hand the branch over rather than both of us pushing to it** — two workers on one shared branch is exactly what silently discarded a reviewed fix in #2645 today. ## The work order (measured at branch point, tests excluded) **75 guards across 32 files.** Largest: `TaskContextMenu.tsx` 9 · `Column.tsx` 7 · `ListView.tsx` 6 · `TaskDetailModal.tsx` 4 · then a long tail of 3s, 2s and 1s. Full per-file list is in the committed work order so feeders can claim without re-measuring. ## Two rules this surface keeps tripping on **1. A literal after `??`, or in the `else` of a `flags ?` ternary, is a DEGRADED-MODE answer — not an unconverted guard.** Two real states reach it: the **pre-load window** (board renders before the workflows fetch resolves) and a card stranded on an id its workflow no longer declares. In both, `columnFlagsById` has no entry at all. Deleting the fallback does not remove a decision — it substitutes "no role" silently, and affordances vanish during first paint. Those sites reach 0 by **marking**, not deleting. Expect `TaskContextMenu.tsx` and the `utils` files to be **mostly marks**. A "9 → 0" that deleted 9 fallbacks is a regression wearing a green census. **2. A marker excuses ONLY the construct it is attached to** — the statement or function holding the literal, not a sibling declaration. This has cost three passes, two of them mine; my first attempt on `reliability-metrics.ts` scored **1 of 6**. **Verify by the count moving, not by the comment existing.** With the ratchet gate-blocking, a mis-marked batch either wedges the gate or locks the miss into a re-recorded baseline. ## Status Opening commit is the work order only — **0 of 75 converted so far.** I am near the end of my context, so I am establishing the branch and the shared list rather than starting conversions I cannot finish cleanly. Feeders can begin immediately; I will keep the branch rebased. My other PR **#2762** (`live-agent-count.ts` 6 → 0) is green and unconflicted — per your rule it should land rather than fold into a batch, and it is `packages/core` so it belongs to batch-core anyway. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Task UI now resolves workflow “column roles” per task to drive diffs/merge details, routing/steering, progress/runtime visibility, and review badges. * Right-dock/overflow views and dev-server now use per-task column traits for “executing” behavior and dependency-based “Up Next” eligibility. * **Bug Fixes** * Fixed bulk action selection/delete/archive eligibility and prevented cross-workflow role leakage. * Made in-review/stale-paused-review, stuck, and effective executor/validator model logic role-aware. * **Tests** * Added regression coverage for degraded-flag behavior and ensured resolved-flag props aren’t ignored. * Added a static check to fail builds on inert optional flag seams. * **Documentation** * Updated batch work-order and mega-batch branch guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- ## Late addition: the seam gate was masking a real offender `scripts/check-inert-flag-seams.mjs` matched call sites by NAME, so two same-named functions in different modules were conflated. I had documented that as a known false-positive source and moved on — reports mentioning `sortTasksForDisplayColumn` are noise, read past them. That annotation was the damage. Core's `sortTasksForDisplayColumn` genuinely never receives its `columnFlags` argument outside its own tests. The dashboard's separate function of the same name (`app/components/taskSorting.ts`), called with up to five arguments from `Lane`/`Board`/`ListView`, was raising the arg-count max and clearing core's seam. The offender was behind a row everyone had been told to skip. The gate now records the module each callee is imported from and matches it against the seam's declaring module. **Measured, by reverting the change:** the scan prints `17 seams, all supplied` and emits **no row** for the function. With the change, it is reported. Both directions watched. Reported on #2783 rather than fixed from outside — core owns it, and "wire the flags" vs "drop the parameter and let the literal stay counted" is their judgment call. TEMPORARY allow-list entry carries it meanwhile; the existing staleness check fails the moment the site becomes supplied, so the entry cannot outlive the fix. Two known limits remain, both inherent to name matching and both documented in the script: the one-supplier floor, and the `__tests__` exclusion (hence the two permanent `ALLOWED` entries). ## And the one-supplier floor, closed the same way I wrote in the section above that the floor "hasn't cost anything yet." That is verbatim the reasoning that kept the imported-shadow bug alive, so I closed it instead of leaving the note. `best < arity` asked only whether SOME caller supplied the argument. One correct call site cleared the seam while every sibling took the legacy fallback — the `isTaskStuck` defect class, where two of three sites omitted the flags and the gate stayed green because the third was right. Review caught that one. A partially-supplied seam is the harder of the two: wholly-unsupplied is uniformly wrong, this works on the board you tested and degrades on the column you did not. **Measured:** dropping the flags argument at `Column.tsx`'s supplied call site produces `supplied by 5/6 call sites; omitted at packages/dashboard/app/components/Column.tsx:1 (of 2)`; restoring returns `all supplied at every call site`. Red and green both watched. Two real omissions found, both on `isNearDuplicateCanonicalInactive`: - **`TaskDetailModal.tsx`** — deliberate, and it **corrects a note I left at that site**. The old note said hoisting the flags state was "the actual fix." It is not, for this call: the flags in scope describe the *modal's* task, and the canonical is a **different task** on a column this component never resolves. Passing them would type-check, read as a conversion, and answer about the wrong task — exactly what `column-role-degraded-flags.test.ts` exists to catch. Supplying it correctly needs a fetch, which is a data change and out of scope. - **`core/task-store/branch-group-ops.ts`** — genuinely wireable (the impl is async and already holds `store` and `canonicalId`). Reported on #2783, not edited from outside. Exemptions for this class are keyed by **call site** (`<file>::<function>`), not by function name. A name-level entry would waive every site of a partially-supplied seam, which is backwards — its other sites are correct and are the reason the omission is worth reporting. Both entries carry the same staleness check as the name-level list and cannot outlive their fix. Remaining known limit, now the only one: the `__tests__` exclusion, which makes a test-only export read as having no callers. That is what the two permanent `ALLOWED` entries are. ## The `__tests__` exclusion, and two allow-list entries built on false reasons Named as the "last remaining limit" above, so it got closed too. The scan now reads test files for call sites — but counts them **separately**, and a test never clears a seam. That direction is the dangerous one: counting test callers as suppliers would have re-hidden core's `sortTasksForDisplayColumn`, whose only suppliers are its own tests. Measured by lifting its exemption: still reported. Both permanent allow-list entries claimed the scanner couldn't see their callers. **Both reasons were false**, and reading tests is what proved it: - **`evaluateMergeBlockerGuard`** — zero callers in tests either. Its only reference in the repo is its own declaration; never registered as a trait hook; the `evaluateDefaultWorkflowGuards` reader its file header credits does not exist. The `lifecycleColumns` conversion went onto dead code, and its note describes a crossing the guard cannot make. Reported on #2783, including the two things I am explicitly *not* concluding (no `"guard"` hook is registered in production; whether that is residue or a dropped registration needs core's intent). - **`isRecoverableMissingWorktreeReviewFailure`** — 5 test call sites. It wraps `...WithProgress`/`...NoProgress`, the live pair called from `self-healing.ts`, both supplying `reviewColumns`. Entry kept, true reason recorded. ### A wrong turn, recorded because it is the failure mode this PR is about I first classified no-production-caller seams as *informational* when they weren't re-exported from a package index, reasoning that a public export might be called externally. That silently downgraded `sortTasksForDisplayColumn` — a confirmed real offender — from failing to a footnote. Publication status has nothing to do with whether there is production behaviour to be wrong. Reverted to the simple rule: no production caller means inert, and it fails. It is worth stating plainly because it is the exact shape of everything else in this PR: a change that made the gate read *cleaner* while making it catch *less*, and it type-checked, passed every test, and would have reviewed fine. ### Where that leaves the check Every blind spot named in this PR has now been closed, and **each one produced a real defect within minutes of closing it** — imported shadows, the one-supplier floor, the `__tests__` exclusion. Four verified findings went to core, one to engine. I would not read the remaining ~240 guards' green gates as evidence that they are clean; I would read them as untested. ## Two guards for one question, one of them worse Having hardened the script, I checked its older twin rather than assuming it was fine. `resolved-flags-seams-have-suppliers.test.ts` carried its own copy of the trailing-flags-parameter check — written before the script existed — with **all three** holes the script has since closed. **Measured on one reintroduced defect** (dropping the flags argument at `Column.tsx`'s supplied `isNearDuplicateCanonicalInactive` call): | | result | |---|---| | `scripts/check-inert-flag-seams.mjs` | `supplied by 5/6 call sites; omitted at .../Column.tsx:1 (of 2)` | | this test's arity half | **3 passed** | Deleted the arity half. Redundancy between a strong and a weak check isn't redundancy — it's a green result available to whoever runs the weak one, and there was no signal at the call site telling you which you were looking at. The **props-shape half stays**: it has no twin in the script, and I confirmed it still fires by reintroducing the original `PrPanel` defect (outer component stops destructuring `taskColumnFlags`) — it reports `PrPanel declares taskColumnFlags but never takes it`. Dashboard app suite: **113 files / 3921 tests** (was 3922 — the deleted case is the difference). ## The gate started catching defects as they landed Syncing with main brought in three fresh conversions from other workers. The hardened check flagged all three immediately — the first time these guards have fired on someone else's landed code rather than on my own. - **`TaskCard`** — `getRunningOptionalGateBadge(task)` omitted flags while *both* `ListView` sites supplied. Fixed, and `taskColumnFlags` added to the `useMemo` deps: no `exhaustive-deps` rule here, so a memo that reads flags without listing them keeps the first-paint `undefined` answer and reproduces the bug through staleness instead of omission. - **`TaskTokenStatsPanel`** — `getTotalAgentActiveMs` omitted while `TaskCard` supplied, so the same runtime number came from the real column on a card and from legacy ids in the detail modal. Now takes `columnFlags`, supplied from `detailColumnFlags` — correct here because the panel renders the modal's **own** task, unlike the near-duplicate canonical above. - **`ListView` ×2** — passed `columnFlagsById.get(task.column)`, the cross-workflow **union**. A task whose own workflow doesn't declare that column gets a *neighbour workflow's* traits. The landed comment justified it as "this list already owns `columnFlagsById`" — exactly the reasoning `column-role-degraded-flags.test.ts` exists to reject. It failed on merge and is how I found this. Also: the `getTotalAgentActiveMs` exemption I was carrying **self-retired**. Main wired the seam, the staleness check failed the entry, and I removed it. That mechanism has now paid for itself once. ### Pre-existing, NOT from this PR: `App.test.tsx` is red on main `app/components/__tests__/App.test.tsx` fails **10 of 141** identically with my changes, with my changes stashed, and with main's own `App.tsx` restored. Not mine, and not in the merge gate. **Bisected on clean `main` checkouts, so this is measured rather than inferred:** | commit | date | result | |---|---|---| | `main~400` (`41d60f0355`) | 2026-07-25 | **140 passed** (140 tests) | | `main~275` (`74d6513fae`) | 2026-07-27 | 3 failed / 141 | | `main~210` (`d2ce1ba8b5`) | 2026-07-29 | 10 failed / 141 | | `main` (`6fc98fd6c7`) | 2026-07-30 | 10 failed / 141 | So it is **not one regression** — it degraded in two stages across 2026-07-25 → 07-29, and the test file itself changed in that window (140 → 141 tests). Three commits touched it there: `73b2a32e2b`, `f26cbedf4f`, `f157bf7460`. That window overlaps the workflow-owned lifecycle migration, which is suggestive but not something I confirmed. The failures are render-level, not assertion-level — `Unable to find an element with the text: + New Task`, `Unable to find role="dialog"`, `Unable to find ... Back nav task`. The board appears to render nothing. That reads like a real regression or a harness mismatch after the lifecycle migration, not a flake, so I have deliberately **not** quarantined it — quarantine is for flakes, and using it here would hide the signal. Flagging for whoever owns `App.tsx`. My suites: `app/__tests__` **113 files / 3921 tests** green, `tsc` 0, lint 0, census `--strict` 0, seam gate 0. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2771408bba |
ci: enforce the lifecycle-column ratchet — it has never actually run (#2654)
**The ratchet was advisory.** `scripts/lifecycle-column-census.mjs` existed only as `pnpm census:lifecycle-columns` — without `--strict` — and **no workflow invoked it**. Nothing has ever compared the tree to the baseline. Every "the baseline ratchet holds them" assumption in this program rested on a check that does not run. That explains both classes of hole: **1. Three PRs lowered counts without re-recording,** leaving allowances the deleted guards could return through while every check stayed green. I've tightened them across #2593 and earlier PRs, but nothing stops the next one. **2. #2621 GREW the count while its own title claimed "count 0 → 0".** It added `column === "triage"` and `column === "todo"` at `register-task-workflow-routes.ts:2681`, taking that file to **23 against an allowance of 22**. It landed unchallenged. This is the failure mode the ratchet exists to prevent, and it happened *inside this program*, in a PR that asserted the opposite. ## The change Adds `check:lifecycle-columns` (the census with `--strict`) to the `pr-checks.yml` lint job, next to `check:changesets` and `check:routes-modular` — the established pattern. **~1.8s over ~1950 files**, so this is not a slow-test addition. ## Proven to fail, in both directions A guard that reports success without checking anything is worse than no guard, so: | injected defect | result | |---|---| | `const __probe = (c: string) => c === "triage"` added to `moves.ts` | `count ROSE — moves.ts: 39 -> 40`, exit 1 | | run against main's current baseline | exit 1 on `mission-feature-sync.ts: allows 5, tree has 0` | Both reverted; exit 0 restored. Note the second row: **this check is RED on main right now**, which is the point. ## Merge order **Stacked on #2593**, which carries the `DELIBERATE-LITERAL` marker for the #2621 site (a v1 IR declares no roles, so no trait can answer that question) plus the baseline re-record. Standalone on main this PR is red — correctly. **Merge #2593 first**, then this. I stacked rather than duplicating those two edits because I already caused one conflict today by appending related content from two branches, and #2651 merged a correction ahead of the section it corrected. Same-content edits in two PRs is the same mistake. ## Census Unchanged by this PR: **776 total, triage 5, reviewed 16** — it adds no guards and converts none. It only makes the numbers enforceable. ## For the fleet This should land before the 776-guard fleet launches. The brief says "the baseline ratchet must shrink by exactly the converted count" — until now nothing verified that claim, so a batch worker could report a shrink that did not happen, or grow the count while converting, and CI would agree. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e211d1870 |
TAKING scripts/: parse instead of grep — an AST classifier for the lifecycle-column bar, cross-checked by a second implementation (#2633)
The program's completion bar is "`column === "triage"` reaches zero".
This measures what that bar actually covers, and checks the measurement
in so it cannot drift.
## The number, measured by the checked-in tool
```
lifecycle-column-census: scanned 1956 source files
COLUMN guards (the backlog): 1031
ROLE comparisons (not guards): 10
DELIBERATE-LITERAL (reviewed): 4
by column id:
313 done
217 in-review
201 in-progress
177 archived
83 todo
40 triage
top files:
151 packages/engine/src/executor.ts
136 packages/engine/src/self-healing.ts
50 packages/dashboard/app/components/TaskCard.tsx
44 packages/core/src/task-store/moves.ts
34 packages/dashboard/app/components/TaskDetailModal.tsx
```
**`triage` is under 4% of the class.** Every one of those 1031 sites is
the same defect: a lifecycle decision made by column NAME, which stops
matching the moment a board renames a column. The bar can be met in full
while 991 identical guards remain — and two files hold a quarter of
them.
## The tracked count is wrong in three directions at once
Each of these cost real work this week, which is why this is a PR and
not a comment.
1. **Vocabulary.** It measures one of six legacy ids.
2. **Receiver.** It is anchored on locals named
`column`/`toColumn`/`fromColumn`, so it never saw the three real guards
in `executor.ts` written against `from` and `originColumn`. One of those
meant completed-but-stranded work was never recovered on a renamed
board, with nothing else owning that state (converted in #2628).
3. **Collision.** `role === "triage"`, `agentType === "triage"`,
`entry.agent === "triage"` compare an **AGENT ROLE**. The planner *lane*
is named `triage` and keeps that name — U11 removed the *column*. Ten
such sites were counted as backlog, and the "obvious" fix (renaming the
role) silently empties the planner's prompt template and mis-binds its
model markers.
A count that is too high and too low simultaneously sends work to the
wrong files while hiding the files that need it. So the census reports
**three separate numbers** and never nets them.
## Proven to fail on the original defect
Not asserted — exercised:
```
$ # reintroduce `task.column === "triage" || task.column === "todo"` into live-agent-count.ts
$ node scripts/lifecycle-column-census.mjs --strict; echo "exit=$?"
packages/core/src/live-agent-count.ts: 10 -> 12
exit=1
$ # restore the file
$ node scripts/lifecycle-column-census.mjs --strict >/dev/null; echo "exit=$?"
exit=0
```
The CLI also exits 1 when its own file list comes back empty — a guard
that reports success without checking anything is worse than no guard.
## 12 regression cases, split by what they defend
Must catch: all six ids; a guard on a local named `from`/`originColumn`
(verbatim the executor.ts shape); single quotes; negation; several
comparisons on one line.
Must **not** catch: role comparisons; comment prose (two tracked
"guards" in `replan-target.ts` were prose about a filter that lives in
another file); a trailing `// … === "triage"` on a code line; sites
carrying a `DELIBERATE-LITERAL` marker.
Plus: **one marker cannot launder a distant guard in the same file** —
that is how allowlists rot.
## Report-only, deliberately
`--strict` compares per-file counts against
`scripts/lib/lifecycle-column-census-baseline.json` and fails when any
file's count **rises**. It is **not** wired into the merge gate: a
thousand-site backlog cannot be a blocking check the day it is first
measured, and a guard nobody can pass is a guard everyone disables.
Owners tightening their own area re-record the baseline in the PR that
lowers it. This is the ratchet shape the `DELIBERATE-LITERAL` markers
scattered through the program already anticipate.
## Stated limitation
Classification is by receiver **name**, so a future field named `agent`
that holds a column would be misclassified as a role comparison.
Recorded at the site, and it is precisely why the two classes are
reported separately instead of netted into one figure.
## Verification
- 12/12 new cases
(`packages/engine/src/__tests__/lifecycle-column-census.test.ts`)
- `pnpm test:gate` **71/71**; `pnpm lint` clean
- `pnpm census:lifecycle-columns`, `--json`, and `--strict` all
exercised end to end
- documented in `docs/testing.md`; no production code touched
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3da5358b33 |
test(U9): add a core unit-gate so dependency gating and FN-5819 block merges (#2569)
**U9, PR10.** Two `package.json` lines. No test or production changes —
this only decides *when* existing tests run.
## The gap
Two U9 safeguards are well covered but sat in **no blocking gate**.
Their proof lives in `packages/core/src/__tests__/task-merge.test.ts`,
and core's only gate job is `test:pg-gate` (two PG tests). A regression
in either surfaced in non-blocking full-suite — after the merge.
## What's now gated, each verified by mutation delta
**`task-merge.test.ts`**
| Invariant | Mutation | NEW failures |
|---|---|---|
| Safeguard 3 — dependency gating | `getTaskCompletionBlocker` drops the
unresolved-dependency reason | **5** |
| FN-5819 — exception bounded to a live group | drop `group.status ===
"open"` | **1** |
| FN-5819 — exception bounded to shared members | widen
`isSharedBranchGroupMemberIntegration` to every task | **4** |
Both FN-5819 directions matter. This is the **only** scoped exception to
`autoMerge:false`, so its *narrowness* is the invariant — not merely its
existence. A test that only proves the exception works would pass while
the exception swallowed every task.
**`legacy-adoption.test.ts`**
| Invariant | Mutation | NEW failures |
|---|---|---|
| FN-8492 — orphaned pending results REWRITTEN to failed, never DELETED
| delete instead of rewrite | **2** |
That one matters because deletion *silently satisfies* the merge gate:
the gate blocks on pending/failed results, not on an enabled step with
no result, so deleting lets a task merge with its review skipped.
## Implementation
Adds `packages/core` → `test:unit-gate`, a curated **non-PG allow-list**
mirroring `engine-core`'s discipline (explicit membership, not a glob),
run as a third parallel job in the root `test:gate` block alongside the
engine and PG jobs.
**Gate fires — verified, not assumed:**
- drop the dependency reason → `pnpm test:gate` **exits 1**
- drop the FN-5819 open-group bound → **exits 1**
- restored → **exits 0**
## Cost: no measurable increase
| | Runs |
|---|---|
| baseline | 13.07s, 14.95s |
| with the job | 12.20s, 12.58s |
It runs in parallel with the existing jobs and finishes well inside
them, so the delta sits inside run-to-run variance. **I am not claiming
a speedup** — the honest reading is "no measurable cost", and the
variance band here is wider than the change.
## Reversible call made rather than asked
A new `test:unit-gate` script rather than widening `test:pg-gate` or
adding a glob. `test:pg-gate` carries PG setup these pure unit tests do
not need, and a glob would admit all of core by default — which
AGENTS.md explicitly forbids ("tests never graduate into the gate by
default"). Membership stays explicit so the next addition has to state
its evidence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7871b28766 |
fix(core): bind the in-transaction capacity gate — one shared pool-id convention (NOT user-visible yet — see R2) (#2488)
## The bug `moves.ts` asked `countActiveInCapacitySlotAsync` for occupants of pool `"builtin:coding"`, while the counter buckets selection-less rows under `DEFAULT_WORKFLOW_POOL_ID` (`"__default-workflow__"`). Nothing ever landed in the pool being asked about, so the count came back **0** and a finite limit could never bind. ## Root fix, not a literal swap A shared *constant* would not have prevented this: **`DEFAULT_WORKFLOW_ID` was already imported in `moves.ts` and the code still wrote a literal.** So both sides now call a shared **function**, `resolveCapacityPoolId` — "which pool does a selection-less task belong to" has exactly one answer and no call site is in a position to disagree with it. The one variable serving two masters is split: a capacity **pool key** (a bucketing sentinel that must not collide with a workflow id) and a **workflow id** (telemetry, must stay a real id). The emitted `TaskTransitioned` payload is byte-identical. ## Checked, not assumed: no second copy `scheduler.ts:2514` and `:2536` do carry `?? "builtin:coding"` — but as an **IR resolution key** (`resolveWorkflowIrById`), where a real workflow id is required and the pool sentinel would not resolve at all. Same literal, different concept, correctly used. A blanket replace would have broken it. ## Something did depend on the gate being dead — exactly one thing `move-path-equivalence.pg.test.ts` → *"UNPROVEN: in-transaction column capacity did NOT reject on EITHER path in this fixture"*. It left the cause open — > something further in (`resolveColumnCapacity`'s limit resolution, or what `countActiveInCapacitySlotAsync` counts as an occupant — a task with no session/agent may not count) keeps the check from firing … This suite does not establish which. — and predicted its own obsolescence (*"if a future change makes this reject, that is the capacity gate coming alive"*). **Neither guess was right; it was the pool id.** Updated to assert the divergence with the answer recorded — **not weakened**. Its fixture also had to start each phase from an empty wip column: once the gate binds, the inline phase's leftovers trip the cap on the *holder* move before the contended move under test runs. `schema-applier.test.ts` failed only in the full-suite run and passes in isolation both with and without the fix — cross-file contamination, not mine. ## Before / after — measured, both directions `maxConcurrent: 1`, real PG store, real `moveTask`: | | flagOFF / no selection | flagOFF / selection | flagON / no selection | flagON / selection | |---|---|---|---|---| | **before** | ADMITTED | ADMITTED | **ADMITTED** ← the bug | REJECTED | | **after** | ADMITTED | ADMITTED | **REJECTED** | REJECTED | The E2E acceptance row asserts **held at cap 1 and admitted at cap 2 on the same fixture**, so it cannot pass by simply never admitting anything. **With the fix reverted that row fails**; the `admitted` case still passes, as it should. The Phase A3 ratchet's two flipped assertions also fail with the fix reverted. Ratchet flipped exactly as its author specified: `DEFECT (R1)` becomes a rejection, and `it.fails` on the invariant becomes a plain `it`. ## ⚠️ This is NOT user-visible yet — please read before merging The premise this was approved on ("once it binds, cards that currently slip through will start being held") **does not hold for this change alone.** The whole capacity block sits inside `if (useWorkflow && workflowIr && fromColumn !== toColumn)`, and `useWorkflow` is `experimentalFeatures.workflowColumns === true` — absent from `DEFAULT_GLOBAL_SETTINGS`, with **no writer anywhere outside tests**. That is Phase A3's R2, still live and now retitled `DEFECT (R2, STILL LIVE)` with the measured matrix recorded in it. So on merge: nothing changes for any real project. Making it actually bind means **also** removing the `useWorkflow` condition — a materially larger, genuinely user-visible change that I have not made unilaterally. Escalated for a decision; if that lands, the changeset here should be re-categorised. ## Review follow-up (48e79ffd9): the convention was still duplicated — swept and ratcheted The first pass added the resolver and routed the transactional gate + counters, but **hold-release still derived the pool independently**. Swept the repo: six sites name the sentinel, **five derive the convention** and now call `resolveCapacityPoolId` (`hold-release.ts:116/118/442/576`, `task-store-helpers.ts:290`). The sixth, `scheduler.ts:1558`, names the default pool as a literal in a capacity *diagnostic* — no selection input, nothing to disagree with — so it keeps the constant. **Does this change hold-release behavior? No, and it was never releasing against the wrong pool.** hold-release computed `x ?? DEFAULT_WORKFLOW_POOL_ID`, which is exactly what the counter buckets under; `moves.ts` (`?? "builtin:coding"`) was the sole disagreeing site, and the first commit moved *it* into agreement with hold-release, not the reverse. `resolveCapacityPoolId(x)` **is** `x ?? DEFAULT_WORKFLOW_POOL_ID`, so every routed site computes an identical value for every input. **No second user-visible change rides along with this PR** — the only behavior delta remains the gate binding on the flag-ON path, which per R2 is still not the path production takes. Evidence: hold-release + capacity suites **43/43 identical before and after**. **The resolver is now the only way to compute a pool id, not merely the newest way.** `scripts/check-capacity-pool-id.mjs` fails on any inline `?? DEFAULT_WORKFLOW_POOL_ID` outside `workflow-capacity.ts`, wired into **both `pretest` and the blocking `test:gate`**. A review note would not have sufficed: the original defect landed in a file that *already imported* the canonical constant. Verified both ways — clean run scans 1124 files and passes; reintroducing the old hold-release expression exits 1 and names the line. ## Review follow-up (a5b675503): the ratchet was rebuilt because it would not have caught the bug The first ratchet matched one spelling (`?? DEFAULT_WORKFLOW_POOL_ID`) and the real defect used another (`?? "builtin:coding"`). **Verified: reintroducing the original defect and running the old checker exits 0.** A guard that reports success without checking is worse than no guard — it stops anyone looking. Rebuilt on the TypeScript AST with two rules. **Rule 1 (sink):** a value reaching a capacity counter's `workflowId` must come from `resolveCapacityPoolId`, or a local initialized from it — so it fires on the original defect regardless of which literal was used, on one line or twenty. **Rule 2 (sentinel):** no `??` onto the sentinel at any qualification depth or as its raw value; multiline is one AST node and caught by construction. `?? "builtin:coding"` is deliberately *not* banned outright — it is the legitimate default for a *workflow* id in ~8 places, and is only a bug when it reaches a capacity pool. **Fails closed three ways** that previously reported success without inspecting: unreadable file, unparseable file, and an empty file listing (the old script would have printed a green tick off a broken glob). **Acceptance was not "passes on main".** Each form was reintroduced into the real source and confirmed to fail: the original defect in `moves.ts`, a multiline fallback, and a deeply qualified sentinel. All are pinned in `capacity-pool-id-check.test.ts` (12 cases: 7 must-catch starting with the reduced actual pre-fix `moves.ts`, 4 must-not-flag, 1 fail-closed) so the guard cannot silently narrow again. Also added to `pretest:full`, which had omitted it. ### Follow-up (0be8df6ea): a dead rule found by fixing a test title Splitting the mislabelled fail-closed test surfaced more than a mislabel: **`ts.createSourceFile` is error-tolerant and does not throw on malformed syntax**, so the `try/catch` behind the `unparseable` rule was unreachable and that rule could never fire. The earlier "fails closed three ways" claim was overstated — the guard advertised a capability it did not have. Detection now reads `sf.parseDiagnostics`; a partial AST can silently lack the `??` nodes and sink calls the rules look for, so "did not parse" must not read as "inspected and clean". Mutation-verified: reverting the detection fails that case and only that case. Test-file exclusion also moved to the repo's `{test,spec}.{ts,tsx}` guideline shape — a `.spec.ts` under `packages/<pkg>/src/` was being scanned as production source. Verified both ways: the `.spec.ts` is skipped, and the identical content in a non-test file is still caught, so the exclusion is scoped rather than a hole. ## Verification - engine + core `tsc --noEmit` clean - `pnpm test:gate` green (299 + 10 + 71) - E2E 20/20; capacity + move-path suites 14/14 - full core PG: **1037 passed / 3 failed** — all three reproduce with the fix stashed (pre-existing) - engine-default: **279 failed** vs **280 at baseline** with the fix stashed — pre-existing red lane, no regression - hold-release + capacity suites: **43/43 identical before and after** the resolver routing - `check-capacity-pool-id` ratchet: 14/14 regression cases; clean over 1124 files; exits 1 on the original defect, a multiline fallback, and a deeply qualified sentinel reintroduced into real source 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed capacity-limit accounting when workflow selection is missing by consistently deriving the correct capacity pool id. * Made capacity enforcement align across move and hold/release paths, rejecting over-limit moves with `capacity-exhausted`. * **Tests** * Updated PostgreSQL and added an E2E scenario to verify the corrected in-transaction gating behavior at `maxConcurrent` limits of 1 and 2. * **Chores** * Added an automated guard to detect inconsistent capacity pool id fallback patterns in code. * **Public API** * Exposed `resolveCapacityPoolId` for consistent capacity pool id derivation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
256c64a7bd |
chore(release): v0.74.0-beta.5
Version bump via changesets. |
||
|
|
0022621d22 |
chore(release): v0.74.0-beta.4
Version bump via changesets. |
||
|
|
bf317f6340 |
chore(release): v0.74.0-beta.3
Version bump via changesets. |
||
|
|
2560944663 |
chore(release): v0.74.0-beta.2
Version bump via changesets. |
||
|
|
a0496c175c |
chore(release): v0.74.0-beta.1
Version bump via changesets. |
||
|
|
2a2b157cb9 |
FN-8585: fix dashboard composer test source reads
Stabilize dashboard composer tests when Vitest launches from the workspace root. - Resolve dashboard test source fixtures relative to the app directory. - Migrate affected component tests away from cwd-relative CSS reads. - Enforce the fixture convention in test hooks and document it. Files changed: docs/testing.md | 4 +++ package.json | 6 ++-- .../__tests__/AuthTokenRecoveryDialog.test.tsx | 3 +- .../components/__tests__/ChatView.mobile.test.tsx | 5 +-- .../__tests__/EngineControlMenu.test.tsx | 11 ++---- .../components/__tests__/FloatingWindow.test.tsx | 5 +-- .../app/components/__tests__/ListView.test.tsx | 5 +-- .../__tests__/MissionInterviewModal.test.tsx | 3 +- .../app/components/__tests__/MobileNavBar.test.tsx | 3 +- .../app/components/__tests__/NewTaskModal.test.tsx | 3 +- .../__tests__/PlanningModeModal.initial.test.tsx | 3 +- .../PlanningModeModal.ui-interactions.test.tsx | 7 ++-- .../components/__tests__/PrCreateModal.test.tsx | 3 +- .../__tests__/QuickChat.persist.test.tsx | 3 +- .../components/__tests__/QuickEntryBox.test.tsx | 3 +- .../components/__tests__/ReportActionMenu.test.tsx | 9 ++--- .../app/components/__tests__/ReportModal.test.tsx | 3 +- .../__tests__/ShadcnColorPicker.test.tsx | 3 +- .../components/__tests__/TerminalModal.test.tsx | 3 +- .../components/__tests__/ThemeDropdown.test.tsx | 9 ++--- .../__tests__/WorkflowNodeEditor.test.tsx | 5 +-- .../WorkflowOptionalStepsDropdown.test.tsx | 3 +- .../components/__tests__/WorkflowSwitcher.test.tsx | 5 +-- .../app/components/__tests__/board-mobile.test.tsx | 4 +-- .../__tests__/CommandCenterControls.test.tsx | 6 ++-- .../__tests__/SystemControlsArea.test.tsx | 5 +-- .../__tests__/SystemStatsArea.test.tsx | 3 +- .../command-center/areas/__tests__/areas.test.tsx | 3 +- .../__tests__/KeyboardShortcutsSection.test.tsx | 3 +- .../app/test/__tests__/cssFixture.test.ts | 35 +++++++++++++++++++ packages/dashboard/app/test/cssFixture.ts | 12 +++++++ .../check-no-cwd-relative-dashboard-test-reads.mjs | 39 ++++++++++++++++++++++ 32 files changed, 162 insertions(+), 55 deletions(-) Fusion-Task-Id: FN-8585 Fusion-Task-Lineage: 83a35fb6-a29d-4e97-b282-1054c68b8cc9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
58d55d6439 |
chore(release): v0.74.0-beta.0
Version bump via changesets. |
||
|
|
127b640b3f |
chore(release): v0.73.0
Version bump via changesets. |
||
|
|
593f38249c |
chore(release): v0.73.0-beta.6
Version bump via changesets. |
||
|
|
26628b356b |
chore(release): v0.73.0-beta.5
Version bump via changesets. |
||
|
|
a45d82d09b |
chore(release): v0.73.0-beta.4
Version bump via changesets. |
||
|
|
2cbb80c501 |
chore(release): v0.73.0-beta.3
Version bump via changesets. |
||
|
|
9002fca9de |
FN-8497: reduce merge gate wall time
Keep merge-gate coverage focused while running its independent test lanes concurrently. - Limit PostgreSQL gate coverage to lifecycle and transactional-handoff canaries. - Run engine and PostgreSQL gate lanes concurrently while preserving failure propagation. - Enforce canary coverage policy and refresh velocity documentation and history. Files changed: docs/test-velocity-baseline.md | 16 +-- docs/testing.md | 5 +- package.json | 2 +- packages/core/package.json | 2 +- .../__tests__/engine-vitest-gate-policy.test.mjs | 79 +++++++++++++- scripts/test-velocity-history.json | 115 +++++++++++++++++++++ 6 files changed, 204 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-8497 Fusion-Task-Lineage: 8777959c-6d8c-4686-a975-d91af2c169ea Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
88e343e331 |
chore(release): v0.73.0-beta.2
Version bump via changesets. |
||
|
|
dcc249c674 |
chore(release): v0.73.0-beta.1
Version bump via changesets. |