ee57f8a3b915ded39df1485627b74ae95a799f93
366 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ee57f8a3b9 |
fix: print the dashboard token with the dev tunnel URL
`pnpm dev --tunnel` published a bare URL under a "public, unauthenticated"
header. That label was wrong for the flag's own default target: --tunnel with
no port aims at the dashboard, which is bearer-token gated, so the recipient
of a shared link got a 401 with no token to supply.
resolveDevTunnelAuth() now classifies the target and the banner says what is
actually true of it:
token dashboard with auth on — prints the token and a ?token= link,
resolved from FUSION_DASHBOARD_TOKEN, FUSION_DAEMON_TOKEN,
then ~/.fusion/settings.json
token-pending first run, token not minted yet — defers to the dashboard's
own startup banner
no-auth --no-auth is on; the dashboard really is open
foreign a non-dashboard port; Fusion has no auth to lend it, the only
genuinely ungated case
Auth resolves at banner time rather than flag-parse time so a token the dev
child mints on a first authenticated run is already readable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7423555c46 |
feat(dev): pnpm dev --tunnel publishes the dev server over a quick tunnel
Operator case: someone works inside a remote Fusion (a container, a shared box), starts a dev server there, and needs to view it from their own browser. The dev server binds inside that machine, so without a tunnel the only options are port publishing or a VPN — both needing cooperation from whoever owns the host. pnpm dev --tunnel # tunnels the dashboard port (PORT, default 4040) pnpm dev --tunnel=5173 # tunnels a Vite dev server instead pnpm dev --tunnel dashboard # tunnel the default port AND run the dashboard FUSION_DEV_TUNNEL=1 pnpm dev Cloudflare QUICK tunnels are usable here precisely because a dev server is HTTP: no account, no domain, no card. The TCP endpoints that SSH would have needed require a card (ngrok) or a domain plus Zero Trust (Cloudflare) — that asymmetry is why this exists for HTTP only, and it is recorded in the module header so the next person does not retry the SSH variant. Design decisions: - Tunnel failure is NON-FATAL. A missing cloudflared or a tunnel that never publishes a URL logs and is skipped; losing a preview URL must never cost the operator their dev loop. - Watch-mode restarts reuse the existing tunnel. A fresh quick tunnel hands out a different hostname each time, which would invalidate an already-shared link. - `--tunnel` consumes a following token only when it is numeric, so `--tunnel dashboard` forwards `dashboard` to the dev command rather than tunnelling port NaN. That is the bug this flag shape invites, so it is tested. Verified end to end in a container: a dev server bound to 127.0.0.1 inside it was fetched from the public internet through the tunnel (200, correct body). Also confirmed that tunnelling the DASHBOARD port does not weaken auth — unauthenticated requests through the tunnel return 401 for /api/tasks, /api/settings and /api/artifacts, with only /api/health open by design. Adding two fields to parseDevWrapperArgs' return broke two existing strict toEqual assertions; those were updated rather than loosened to toMatchObject. 27 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b99b365de |
FN-9141: rescue plugin-runner tests and enforce quarantine lockstep
Rescue the plugin-runner suite before deletion while making quarantine records mechanically consistent. - preserve logger assertions across worker-reused mock cleanup with a stable hoisted logger - remove the rescued suite from the quarantine ledger and Vitest exclusion - enforce ledger-to-exclude lockstep and cover missing or dangling quarantine entries - document the reproduction evidence, rescue disposition, and strict checker behavior Files changed: .../suite-only-flakes-observed-register.md | 14 +- docs/testing.md | 17 +- .../engine/src/__tests__/plugin-runner.test.ts | 37 ++-- packages/engine/vitest.config.ts | 14 +- scripts/__tests__/check-quarantine-ledger.test.mjs | 217 +++++++++--------- scripts/__tests__/ci-test-shard-timings.test.mjs | 5 +- scripts/check-quarantine-ledger.mjs | 245 +++++++++++++-------- scripts/lib/test-quarantine.json | 10 +- 8 files changed, 314 insertions(+), 245 deletions(-) Fusion-Task-Id: FN-9141 Fusion-Task-Lineage: 5b0549bf-3cc6-495e-bf99-a30a2dffb029 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
beb8ae67db |
FN-9125: document flake findings and quarantine plugin runner
Classify the suite-only failures by actual PostgreSQL dependency and preserve unresolved evidence for follow-up. - Record non-reproduction results and assign PostgreSQL investigations to focused follow-up tasks. - Quarantine the independent in-memory plugin runner test under the deletion ratchet. - Document evidence requirements for future PostgreSQL flake diagnosis. Files changed: .../suite-only-flakes-observed-register.md | 32 ++++++++++++++++++++-- docs/testing.md | 4 +++ packages/engine/vitest.config.ts | 10 +++++++ scripts/lib/test-quarantine.json | 8 +++++- 4 files changed, 51 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-9125 Fusion-Task-Lineage: 1dc80163-a0dc-4241-bab1-75a2cafb9abe Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
53acaf96c3 |
FN-9123: Repair script test contract drift
Repair script-test contracts to match the current repository policies and live workflow coverage. - Align static-gate and pretest mirrors with authoritative validator chains. - Update the merger-rule floor and parameterized flake registration. - Repoint workflow reliability evidence to the surviving dispatch test. Files changed: .../test-failures/suite-only-flakes-observed-register.md | 4 +++- scripts/__tests__/agents-md-invariants.test.mjs | 10 ++++++++-- scripts/__tests__/engine-vitest-gate-policy.test.mjs | 6 ++++++ scripts/__tests__/run-static-gate-checks.test.mjs | 7 +++++++ scripts/__tests__/verify-fast.test.mjs | 6 ++++++ scripts/lib/workflow-reliability-release-check.json | 4 ++-- 6 files changed, 32 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-9123 Fusion-Task-Lineage: 1a3dad24-69e3-423e-a159-56fd3f1085a3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
81b1860f95 |
fix(ci): correct future FNXC stamps (#3440)
## Summary - replaces newly added future-dated FNXC metadata with the actual UTC change time - tightens the FNXC future-date baseline to zero known exceptions ## Test plan - `pnpm check:fnxc-future-dates` - `pnpm check:lifecycle-columns` - `pnpm check:changesets` - `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/staged-plugin-core-imports.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/engine typecheck` - `pnpm lint` |
||
|
|
374ae08d56 |
test(engine): repoint source-scan contracts to the peeled module layout
Full-suite repair, engine source-scan cluster. The package code organization waves moved ~30 engine modules into subdirectories (plugins/, execution/, scheduling/, healing/, worktree/, executor/ peels); the log-severity manifest, prompt carve-out, emit-surface, failure-lane, and worktree-invariant scanners now read the moved locations, verified per file via git log --follow. Two scans caught real drift rather than moves: the lifecycle census had 12 unexamined column guards (resolved with DELIBERATE-LITERAL markers for the mailbox archived tab, the FN-9059 lease-owner terminality check, and the FN-9056 legacy done fallback — baseline re-recorded with zero absorbed debt), and planning-claim gained a genuine second writer in self-healing's FN-8998 transport-failure recovery, admitted to the allowlist with its CAS-guarded justification. 9 files / 119 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1d3f6c198c |
FN-9096: route CLI models through installed runtimes
Route every CLI-provider selection through an explicit installed-runtime policy. - Centralize CLI provider classifications, runtime hints, fallback behavior, and actionable missing-runtime errors. - Validate routing coverage statically and add conformance and integration tests for CLI runtime paths. - Document runtime routing behavior and add a published CLI changeset. Files changed: .changeset/fn-9096-cli-runtime-routing.md | 7 + docs/settings-reference.md | 29 +++ docs/testing.md | 6 +- package.json | 6 +- .../src/__tests__/cli-provider-routing.test.ts | 74 ++++++++ .../__tests__/cli-runtime-routing-check.test.ts | 25 +++ .../cli-runtime-routing-conformance.test.ts | 210 +++++++++++++++++++++ .../__tests__/hermes-runtime-integration.test.ts | 28 +++ .../engine/src/agents/agent-session-helpers.ts | 166 ++++------------ packages/engine/src/agents/cli-provider-routing.ts | 174 +++++++++++++++++ scripts/check-cli-runtime-routing.mjs | 26 +++ scripts/lib/cli-runtime-routing-check.mjs | 84 +++++++++ 12 files changed, 701 insertions(+), 134 deletions(-) Fusion-Task-Id: FN-9096 Fusion-Task-Lineage: f9f6a434-b28d-4ebb-816a-53ca75efc2c4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
9c83812351 |
fix(ci): unblock check:lane-wiring for the task-recommendations read
check:lane-wiring runs in CI's Lint gate and has been failing on main since FN-9037 landed `listTaskRecommendations`, so it blocks every PR in the repo, not just the one that hit it. Recorded rather than rewired, because this is the false-positive shape the escape hatch exists for. The guard catches a callee silently falling back to a LEGACY COLUMN LITERAL when a caller omits the lane; `listTaskRecommendationsImpl` falls back to `resolveProjectColumnsForRoles(store, ["complete"])`, which reads the board's own lanes. Real callers already pass a resolved set — the dashboard route resolves `completeColumns` before calling — so the fallback only serves the pass-through wrapper. Resolving again in the wrapper would duplicate that query on every call for no behavioural difference. The reason lives at the call site as well as in the baseline, since a bare count in a JSON file is exactly the kind of entry that later reads as unexplained debt. Baseline diff verified to be a single added entry (store.ts: 1) — nothing else raised or lowered. Verified: check:lane-wiring exits 0, core typecheck clean, eslint clean, FNXC date check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
210c22c485 |
fix(ci): classify workflowRole as role vocabulary (#3408)
## Summary - Classify workflow work-item `workflowRole` comparisons as role vocabulary in the lifecycle-column census. - Add a regression test so triage role comparisons cannot raise a phantom lifecycle-column guard. ## Test Plan - `node --test scripts/__tests__/lifecycle-census*.test.mjs` - `corepack pnpm check:lifecycle-columns` - `corepack pnpm lint` - `corepack pnpm check:changesets --strict` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved classification of workflow role comparisons, including `workflowRole === "triage"`, so they are recognized separately from lifecycle-column comparisons. * Ensured workflow role values are correctly identified as role vocabulary rather than lifecycle-column values. * **Tests** * Added automated coverage to verify accurate workflow role and column identification across comparison patterns. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
6cc15fd73d |
fix(ci): restore clean-main CLI and lifecycle gates (#3420)
## Summary - Complete the isolated `@fusion/core` mock used by the experiment-finalize extension suite - Classify three intentional physical/synthetic lifecycle literals introduced on current main - Re-record the strict lifecycle census baseline with zero unexamined guards ## Test plan - `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/extension-experiment-finalize.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/core exec vitest run src/__tests__/task-intake-owner-resolver.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/engine exec vitest run --project engine-default src/__tests__/mission-feature-sync-lanes.test.ts --silent=passed-only --reporter=dot` - `pnpm check:lifecycle-columns` - `node scripts/check-mock-completeness.mjs` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved mission reconciliation previews for task links, specification alignment, and lifecycle updates. - Prevented stale or superseded validation runs from overwriting current feature status or ownership. - Improved blocked-feature diagnostics and archived-task handling across workflow configurations. - **Documentation** - Clarified validation, assignment checks, and mission synchronization behavior. - **Tests** - Expanded coverage for reconciliation previews and validator ownership scenarios. - **Chores** - Updated lifecycle baseline data for known archived-task cases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
8cba8d3e92 |
fix(core): make TaskStore.emit override assignable to EventEmitter<TaskStoreEvents> signature (#3407)
## Problem
Dashboard typecheck fails with **TS2416** in `@fusion/core`'s
`TaskStore`:
```
Property 'emit' in type 'TaskStore' is not assignable to the same property in base type 'EventEmitter<TaskStoreEvents>'.
```
The `override emit<E extends string | symbol>(event, ...args)` generic
conflicts with the base class's generic `emit<K>(eventName: keyof
TaskStoreEvents | K, ...)`. This breaks the dashboard typecheck / CI
merge gate.
## Fix
Change the override to:
```ts
override emit(event: unknown, ...args: any[]): boolean {
return EventEmitter.prototype.emit.call(this, event as string, ...args);
}
```
`event: unknown` remains assignable to the base's generic signature
while still forwarding non-typed runtime keys (`agent:log`,
`settings:updated`, …). Internal `EventEmitter.prototype.emit` calls
cast `event as string`. Behavior-preserving.
## Verification
- `@fusion/dashboard` `tsc --noEmit` → **PASS** (previously failed with
TS2416)
- `eslint` on touched file → clean
- Single-file change (`packages/core/src/store.ts`, +6/−3)
## Scope
No behavior change, no changesets required.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved task event handling to support a broader range of event
identifiers.
* Preserved cached-lane information for single-argument task update
events.
* Maintained support for custom and arbitrary event names without
disrupting existing behavior.
* Improved classification of workflow roles, session purposes, and
outcome-related status checks in lifecycle analysis, producing more
accurate findings and reducing misleading results.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
878db6dca7 |
FN-8951: repair script-test governance drift
Keep test-shard timing governance aligned with the current workspace and workflow seams. - Add a safe timing-snapshot pruning mode with coverage. - Align Todo plugin Vitest isolation and Docker dependency manifests. - Refresh workflow reliability evidence and remove deleted test timings. Files changed: Dockerfile | 7 ++- docs/testing.md | 9 ++- plugins/fusion-plugin-todos/vitest.config.ts | 26 ++++++-- scripts/__tests__/ci-test-shard-timings.test.mjs | 71 ++++++++++++++++++++++ scripts/ci-test-shard.mjs | 65 ++++++++++++++++++-- .../lib/workflow-reliability-release-check.json | 22 +++---- scripts/test-timings.json | 21 ------- 7 files changed, 175 insertions(+), 46 deletions(-) Fusion-Task-Id: FN-8951 Fusion-Task-Lineage: fbf7e79f-4cb2-43e2-9982-09f3f94de70d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
0fbeba50d1 |
FN-8937: rescue project engine test quarantine
Rescue the project engine suite by making subprocess watchdog behavior deterministic. - Capture real timer APIs for subprocess watchdogs and isolate failure ownership. - Mock integration-branch resolution to prevent host git during lifecycle tests. - Add watchdog regression coverage and remove the expired quarantine exclusion. Files changed: docs/testing.md | 3 + packages/core/src/__test-utils__/vitest-setup.ts | 74 ++++++++++- .../__tests__/subprocess-guard-fake-timers.test.ts | 140 +++++++++++++++++++++ .../engine/src/__tests__/project-engine.test.ts | 63 +++++++--- packages/engine/vitest.config.ts | 12 +- scripts/lib/test-quarantine.json | 8 +- 6 files changed, 265 insertions(+), 35 deletions(-) Fusion-Task-Id: FN-8937 Fusion-Task-Lineage: 9fe166b5-b101-4683-bb2b-4855ee73df10 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
e7a873c505 |
FN-8936: stabilize Planning Mode handoff tests
Stabilize live Proceed-action handoffs and re-admit the Planning Mode flow suite. - Settle hydration and re-query the Proceed action before direct-create test clicks. - Remove the Planning Mode test quarantine and record its rescue in the testing ledger. Files changed: .../suite-only-flakes-observed-register.md | 4 ++++ docs/testing.md | 3 +++ .../PlanningModeModal.planning-flow.test.tsx | 20 ++++++++++++++++---- packages/dashboard/vitest.config.ts | 5 ----- scripts/lib/test-quarantine.json | 5 ----- 5 files changed, 23 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8936 Fusion-Task-Lineage: ed869b67-9394-458b-879c-54da0d7d327e Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
1cf86baa1c |
refactor: package code organization wave 18 (executor pure peels) (#3317)
## Summary
Wave 18 continues the package code-organization program after wave 17
domain folders (U4 Slice A from
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`).
### What changed
Peel **pure, behavior-preserving** helpers out of
`packages/engine/src/executor.ts` into domain modules under
`packages/engine/src/executor/`, with **stable re-exports** from
`executor.ts` so deep imports and `vi.mock("../executor.js")` keep
working.
| New module | Symbols |
|------------|---------|
| `executor/task-done-refusal.ts` | `evaluateTaskDoneRefusal`,
`determineRevisionResetStart`, skip-bypass refusal helper |
| `executor/workflow-feedback-paths.ts` |
`extractReferencedPathsFromWorkflowFeedback`,
`isAlwaysAllowedScopeLeakPath`, `workflowPathMatchesDeclaredScope` |
| `executor/workflow-step-verdict.ts` |
`FUSION_WORKFLOW_STEP_CONVENTIONS_PREAMBLE`, `parseWorkflowStepVerdict`
/ `parseWorkflowStepOutput`, step outcome types |
| `executor/await-input-parse.ts` | `parseAwaitInputSentinel`,
`parseAwaitInputQuestionToolCall` |
| `executor/no-commit-eligibility.ts` | `getNoCommitEligibilityReason`
(+ prompt heuristics) |
`executor.ts` live LOC ~**22817 → ~22427** (first pure-peel batch; more
peels needed to approach the 2k cap).
### Shims
- `old path` `executor.ts` public exports → `new path` `executor/*.ts` →
delete-when consumer deep-imports are re-pointed (not this PR)
### Test plan
- [x] `@fusion/engine` typecheck
- [x] Oracle: task-done refusal, skip-bypass, workflow malformed
verdict, scope-leak allowlist, executor-step-session, executor-prompt
- [x] `vitest --project=engine-core` (merge-gate curated suite)
- [ ] CI merge gate
**Stack:** wave17 (merged) → **this PR**
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved recognition of workflow outcomes from structured and
conversational responses.
* Added support for extracting questions from await-input responses and
tool calls.
* Improved workflow feedback handling for referenced files and declared
scope patterns.
* Added clearer guidance for task execution, approvals, verification,
and available tools.
* **Bug Fixes**
* Prevented completion when required review approvals are missing or
revisions remain pending.
* Improved handling of workflows that legitimately require no code
changes.
* Added clearer refusal messages and more reliable revision restarts.
* Sanitized repository paths in Git remediation instructions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
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> |
||
|
|
cf171bc4b2 |
FN-8900: rescue deterministic Kimi K3 catalog test
Rescue Kimi K3 route coverage with a deterministic bundled-catalog registry seam. - Use pi-ai's real Kimi catalog without live registry refresh. - Restore route merge and deduplication coverage and remove the paired quarantine records. - Document the measured refresh stall and preserve the existing timeout budget. Files changed: docs/testing.md | 3 +- packages/dashboard/package.json | 1 + .../src/__tests__/_kimi-model-catalog-fixture.ts | 40 +++++++++ ...ister-model-routes-kimi-k3-supplemental.test.ts | 75 ++++++----------- packages/dashboard/vitest.config.ts | 19 ++--- pnpm-lock.yaml | 98 +++++++++++++++++----- scripts/lib/test-quarantine.json | 5 -- 7 files changed, 153 insertions(+), 88 deletions(-) Fusion-Task-Id: FN-8900 Fusion-Task-Lineage: 6d4986ed-bc93-479f-85fb-510d17ced4b5 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
b2f8b0d3fe |
test(quarantine): delete 27 permanently-broken quarantined tests per operator directive
Operator directed deletion of tests that test pre-refactor behavior no longer in the codebase (removed APIs, mock shape drift, stale assertions from the 2026-08-05 full-suite quarantine wave, run 30982276306). All 27 entries were permanently red — not flaky — testing APIs removed during the PG cutover and workflow peel refactors (getBuiltinWorkflow, resolveWorkflowIrForTaskWithProvenance, layer.db.select mock shapes, vi.mock hoist errors, stale serialization/count literals). Kept 3 actionable entries that catch real issues: - register-model-routes-kimi-k3-supplemental (real CI flake, rescue feature ready) - project-engine.test.ts (catches real 60s→120s assertion drift) - PlanningModeModal.planning-flow (second-sighting real race) Vitest config exclusions and quarantine ledger updated in lockstep. |
||
|
|
de38ead4c9 |
fix(ci): restore main full-suite after path peel and suite drift (#3334)
## Summary Restores the non-blocking full suite on `main` after consistent shard failures (latest red: [run 30982276306](https://github.com/Runfusion/Fusion/actions/runs/30982276306); all four shards failed on `@fusion/core`, `@fusion/engine`, and `@fusion/plugin-sdk`). ### Fixes - **Path / import drift** after code-organization peels: update static-guard and integration tests to new module locations (`central/`, `board/`, `execution/`, `merge/`, `worktree/`, `plugins/`, `types/*` barrels, etc.). - **Inventory re-pins**: - SQLite production `DatabaseSync` allowlist (`central/project-identity.ts`, `db/sqlite-validation.ts`) - Engine blocking-shellout allowlist regenerated from live source (33 audited sites) - Core log-severity manifest paths for peeled modules - **Partial protocol assert update** for `isPlanReviewSatisfied` (file also quarantined until full rescue) ### Quarantine (deletion ratchet) Remaining behavioral reds quarantined on sight — no timeout/retry/assertion appeasement: - **14 core** files (incomplete unit fakes for `layer.db.select`, ledger/census drift, 15s wedge timeout, serialization protocol drift) - **13 engine** files (mock-hoist errors, fake-store/census/behavior drift under suite) Paired updates: `scripts/lib/test-quarantine.json` + package vitest excludes. Deletion clock starts `2026-08-05`. ### Local verification - Path-fixed core scanners: 173 passed - Path-fixed engine scanners: 58 passed - `@fusion/plugin-sdk` full: 16 passed - PG smokes: mission-autopilot, research-execution, satellite, transition-pending, workflow-sync ## Test plan - [ ] CI PR checks green (lint/typecheck/build/gate) - [ ] Full suite on merge to main: all 4 shards green or only intentional non-blocking signal - [ ] Confirm quarantined files appear in ledger + vitest excludes and are not executed <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Updated test coverage to reflect reorganized source locations and module paths. * Refreshed static checks, allowlists, and source-based assertions without changing tested behavior. * **Chores** * Quarantined failing core and engine test suites with documented tracking details. * Updated test configuration and quarantine records to improve suite stability and reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
5532019fd3 |
FN-8816: make planning storage failures non-fatal
Keep Planning Mode running when browser storage writes fail. - Retry failed project-scoped planning persistence after targeted eviction. - Cover storage failure recovery and planning draft hand-off behavior. - Quarantine the recurring planning-flow flake and add a patch changeset. Files changed: .changeset/fn-8816-planning-storage-recovery.md | 7 + .../app/hooks/__tests__/modalPersistence.test.ts | 159 ++++++++++++++++++++- packages/dashboard/app/hooks/modalPersistence.ts | 22 ++- packages/dashboard/vitest.config.ts | 5 + scripts/lib/test-quarantine.json | 5 + 5 files changed, 195 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-8816 Fusion-Task-Lineage: 929c3d96-3a28-49fa-8018-710fc75e3fcc Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
4f4aef7173 |
FN-8811: preserve explicit shared-member review holds
Keep shared branch-group integration moving unless an operator explicitly holds the task. - Track auto-merge provenance and distinguish explicit user holds from inherited mission policy. - Preserve manual holds across workflow recovery, merge coordination, API updates, and dashboard status. - Add regression coverage, document the behavior, and quarantine the observed flaky test. Files changed: .changeset/fn-8811-shared-member-review-hold.md | 7 ++ docs/architecture.md | 4 +- docs/dashboard-guide.md | 1 + .../mission-store.sync-auto-merge.test.ts | 7 +- .../__tests__/postgres/mission-store.pg.test.ts | 1 + .../__tests__/postgres/store-movement.pg.test.ts | 20 ++++ packages/core/src/__tests__/task-merge.test.ts | 14 +++ .../core/src/async-stores/async-mission-store.ts | 6 +- packages/core/src/index.gate.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/merge/task-merge.ts | 20 +++- packages/core/src/missions/mission-store.ts | 6 +- packages/core/src/task-store/serialization.ts | 2 +- packages/core/src/task-store/task-creation.ts | 8 +- packages/core/src/types/task/task-core.ts | 12 ++- .../components/__tests__/TaskDetailModal.test.tsx | 63 ++++++++++++ .../dashboard/src/__tests__/routes-tasks.test.ts | 47 +++++++++ .../src/routes/register-task-workflow-routes.ts | 15 ++- ...cutor-live-branch-group-auto-merge-hold.test.ts | 87 +++++++++++++++++ .../src/__tests__/group-merge-coordinator.test.ts | 99 ++++++++++++++++++- .../engine/src/__tests__/project-engine.test.ts | 57 ++++++++++- .../self-healing-paused-abort-recovery.test.ts | 52 +++++++++- packages/engine/src/__tests__/self-healing.test.ts | 106 +++++++++++++++++++++ .../workflow-graph-executor-handlers.test.ts | 23 +++++ packages/engine/src/executor.ts | 37 ++++++- packages/engine/src/project-engine.ts | 25 +++-- packages/engine/src/self-healing.ts | 71 ++++++++++++-- .../src/workflow-node-runners/merge-runner.ts | 24 ++++- .../src/workflows/workflow-graph-executor.ts | 4 + .../src/workflows/workflow-graph-task-runner.ts | 6 ++ .../engine/src/workflows/workflow-node-handlers.ts | 5 +- packages/engine/vitest.config.ts | 11 ++- scripts/lib/test-quarantine.json | 5 + 33 files changed, 789 insertions(+), 58 deletions(-) Fusion-Task-Id: FN-8811 Fusion-Task-Lineage: 5c1609bf-3132-4988-a254-fedec6c0e33d 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 --> |
||
|
|
2249b9bc20 |
FN-8774: retain Kimi K3 quarantine through deadline
Keep the Kimi K3 dashboard route test quarantined until the mandated deletion date. - Preserve the /api/models supplemental test and paired Vitest exclusion through 2026-08-15. - Record the explicit retention deadline in the quarantine ledger. Files changed: packages/dashboard/vitest.config.ts | 5 +++++ scripts/lib/test-quarantine.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8774 Fusion-Task-Lineage: 8ef704e4-f682-4a97-af1a-2070ca43d8a1 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
cb57093d03 |
refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary Wave 17 organizes Fusion into **domain folders** (stacks on #2397). ### Layout - **core/types/** — board, task, agents, settings, merge, workflow, mesh, … - **core/src/** — agents, ai, async-stores, workflows, tasks, config, db, … - **dashboard/app/api/** — client, tasks, agents, git, missions, planning, … - **engine/src/** — agents, auth, execution, merge, missions, overseer, worktree, … Root keepers retained for large entrypoints (`store.ts`, `executor.ts`, `merger.ts`, …). Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy) stay stable. ## Test plan - [x] `@fusion/core` typecheck - [x] `@fusion/engine` typecheck (pre-existing playwright-core noise only) - [ ] CI merge gate **Stack:** #2394 → #2397 → **this PR** |
||
|
|
19e9f5bc88 |
chore(release): require interactive confirm; drop authorized/--yes skips
Remove the typed authorization phrase and the --yes/-y auto-confirm path so every real release must confirm y/N in an interactive terminal. Reject --yes with a clear error so old muscle memory cannot skip the proceed prompt. |
||
|
|
cfc63fc8f5 |
FN-8724: stabilize hydration freshness tests
Make the useTasks hydration freshness coverage deterministic and restore it to the dashboard suite. - Control the system clock for hydration fixtures and flush async updates without advancing time. - Remove the rescued test from the dashboard exclusion list and quarantine ledger. Files changed: .../__tests__/useTasks-hydration-freshness.test.ts | 30 ++++++++++++++-------- packages/dashboard/vitest.config.ts | 8 ------ scripts/lib/test-quarantine.json | 5 ---- 3 files changed, 19 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-8724 Fusion-Task-Lineage: 1d764e2c-0975-4d26-92c6-187a6a94caee Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
6d176a9372 |
FN-8721: reconcile census, delegation routing, and archive repair
Align lifecycle census coverage while routing delegated work to workflow-ready lanes and safely repairing archived tasks. - Route delegated tasks through the selected workflow's hold or entry column. - Scope soft-deleted archive repairs by project and protect them with compare-and-set updates. - Refresh lifecycle-column census detection, baseline, documentation, and coverage. Files changed: docs/testing.md | 21 +++--- .../u15-engine-dashboard-consumers.test.ts | 31 ++++++++- .../core/src/task-store/archive-lifecycle-2.ts | 5 ++ .../core/src/task-store/async-archive-lineage.ts | 5 ++ packages/core/src/task-store/async-persistence.ts | 11 ++++ packages/core/src/task-store/async-self-healing.ts | 76 +++++++++++++++------- .../src/__tests__/agent-tools-delegation.test.ts | 43 +++++++++++- .../__tests__/lifecycle-column-census-ast.test.ts | 20 ++++++ .../src/__tests__/lifecycle-column-census.test.ts | 29 ++++++--- packages/engine/src/agent-tools.ts | 29 +++++++-- scripts/lib/lifecycle-column-census-ast.mjs | 21 +++++- scripts/lib/lifecycle-column-census-baseline.json | 17 ++--- scripts/lifecycle-column-census.mjs | 3 +- 13 files changed, 245 insertions(+), 66 deletions(-) Fusion-Task-Id: FN-8721 Fusion-Task-Lineage: ff78481f-5ecb-4d9b-b21a-a095682372ed Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ebe514c3e4 |
FN-8677: propagate task update lanes before conversion
Propagate cache-warmed workflow lanes through task updates so synchronous engine consumers support renamed boards. - Add task lane cache and attach resolved lanes to task:updated metadata. - Update scheduler, triage, and notification consumers to use carried lanes with bridge-safe fallbacks. - Cover lane propagation and renamed-lane event behavior with core and engine tests. Files changed: .changeset/fn-8677-manual-merge-hold-lanes.md | 7 ++ .changeset/task-updated-carries-lanes.md | 7 ++ ...orkflow-ir-readers-always-return-the-default.md | 22 +++++ .../sync-workflow-ir-second-blocker.test.ts | 43 +++----- .../core/src/__tests__/task-lane-cache.test.ts | 30 ++++++ .../task-updated-lanes-emit-surfaces.test.ts | 92 ++++++++++++++++++ .../__tests__/task-updated-lanes-payload.test.ts | 42 ++++++++ packages/core/src/index.ts | 1 + packages/core/src/store.ts | 36 ++++++- packages/core/src/task-lane-cache.ts | 63 ++++++++++++ .../core/src/task-store/archive-lifecycle-2.ts | 3 + packages/core/src/task-store/moves.ts | 1 + packages/core/src/task-store/task-artifacts-ops.ts | 1 + packages/core/src/task-store/task-update.ts | 1 + packages/core/src/task-store/update-task-deps.ts | 4 +- .../core/src/task-store/workflow-definitions.ts | 71 +++++--------- .../__tests__/scheduler-task-updated-lanes.test.ts | 108 +++++++++++++++++++++ .../task-updated-lanes-bridge-compat.test.ts | 94 ++++++++++++++++++ ...task-updated-lanes-engine-emit-surfaces.test.ts | 101 +++++++++++++++++++ .../src/__tests__/triage-pause-abort.test.ts | 22 +++++ .../src/__tests__/triage-planning-wake.test.ts | 25 +++++ .../notification-renamed-lifecycle-columns.test.ts | 84 +++++++++++++++- .../__tests__/task-wedge-notification.test.ts | 19 ++++ .../src/notification/notification-service.ts | 56 ++++------- packages/engine/src/scheduler.ts | 62 +++--------- packages/engine/src/triage.ts | 105 ++++++-------------- scripts/lib/inert-sync-lane-baseline.json | 5 +- 27 files changed, 858 insertions(+), 247 deletions(-) Fusion-Task-Id: FN-8677 Fusion-Task-Lineage: d8fef9db-0f88-4dfd-9813-be25e10e3588 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
5596d915ab |
FN-8647: quarantine flaky Kimi K3 catalog test
Quarantine the timing-sensitive Kimi K3 SDK catalog test without changing timeout budgets. - Reuse the native model registry once per test file. - Add the observed CI timeout to the dashboard quarantine ledger and config. - Document validation and timeout-budget preservation requirements. Files changed: docs/testing.md | 8 ++++++++ ...ister-model-routes-kimi-k3-supplemental.test.ts | 23 ++++++++++++++++++++-- packages/dashboard/vitest.config.ts | 8 ++++++++ scripts/lib/test-quarantine.json | 5 +++++ 4 files changed, 42 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8647 Fusion-Task-Lineage: 31e79677-d923-4003-a8e8-082159334e65 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
897cce9d94 |
FN-8656: resolve scheduler lanes for renamed holds
Resolve scheduler lane lookup against workflow-defined hold and terminal columns. - Use asynchronous workflow lane resolution after the synchronous event prologue - Preserve legacy lane fallback and recognize all terminal workflow columns - Update scheduler regression coverage, sync-lane guardrails, and release notes Files changed: .changeset/fn-8656-scheduler-renamed-hold-lanes.md | 7 ++ .../sync-workflow-ir-callsite-allowlist.test.ts | 10 +- .../scheduler-renamed-hold-events.test.ts | 20 ++-- ...ow-scheduler-parked-columns-live-e2e.pg.test.ts | 13 ++- ...-sync-role-conversion-inert-live-e2e.pg.test.ts | 8 +- packages/engine/src/scheduler.ts | 121 +++++++++------------ scripts/check-inert-sync-lane-conversions.mjs | 5 + scripts/lib/inert-sync-lane-baseline.json | 3 +- 8 files changed, 92 insertions(+), 95 deletions(-) Fusion-Task-Id: FN-8656 Fusion-Task-Lineage: 389a95a1-289f-4dde-86b3-1e450f8d43db Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
012729cf2b |
chore: tighten lifecycle-column census baseline after slot-accounting fix
The active-worktree slot-accounting fix removed two deliberate scheduler literals (done/archived: 3 -> 2); re-record so the ratchet follows the count down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
577dcb6c0c |
FN-8643: correct TaskWedgeNotifications FNXC timestamp
Correct the TaskWedgeNotifications migration-baseline stamp and remove its obsolete future-date allowance. - Set the baseline FNXC annotation to its intended non-future timestamp. - Remove the resolved migration from the future-date baseline allowlist. Files changed: packages/core/src/postgres/migrations/0000_initial.sql | 2 +- scripts/lib/fnxc-future-dates-baseline.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) Fusion-Task-Id: FN-8643 Fusion-Task-Lineage: 0f27e9e6-0581-4e38-8005-e93f94ad4f78 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
e56116d8e9 |
chore(fnxc): retire the stale scheduler allowance left by the rollover re-record (#3283)
**The date gate passes on `main` — but not because this was fixed.** ## What actually happened UTC rolled over to `2026-08-01`. The gate compares against the later of local and UTC, so two `2026-08-01` stamps in `scheduler.ts` became valid on their own. That is the ratchet's normal drop path and is fine. `#3278` then re-recorded the baseline "after the UTC rollover", which set `scheduler.ts` to **allow 1** — exactly enough to absorb the one stamp that did *not* age out: ``` FNXC:ConcurrencyAdmission 2026-08-06-09:00 ← six days out, wrong on any calendar ``` So the gate reports `123 known future-dated stamp(s), none added` and exits 0, with a stamp inside it that will not be valid until next week. ## Why this is the failure the gate exists to catch A blanket re-record cannot distinguish **aged out** from **still wrong**, so it launders the second past the first. The sibling ratchet states the rule outright: > Do NOT re-record the baseline to clear this — that is the same false green one layer up. This is that, one layer up again: not a guard cleared by a baseline, but a *baseline refresh* clearing a guard as a side effect. ## The fix - stamp repointed to `2026-08-01` — today in UTC, which is the calendar the gate actually compares against - **allowance removed**, not left at 1, so the entry cannot be regrown into **Mutation-verified**: with the allowance gone, restoring `2026-08-06` exits **1**. Before this change the same stamp exited **0**. That is the whole point — the ratchet can now see it. ## One thing worth carrying forward A six-days-out stamp is not a timezone slip. Neither the old `date -u` guidance nor the current local-date guidance in AGENTS.md would have prevented it, and CI-only checking cannot catch it before merge. This is the concrete case for running the date check at author time, which I have flagged but not landed since it changes the gate's contract. ## Verification - `check-fnxc-future-dates` — exit 0, allowance removed - `scheduler` suites — **148 pass** - `tsc --noEmit` (engine) — 0 errors Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
208c32c970 |
chore(fnxc): re-record the future-dates baseline after the UTC rollover (92 → 64 files) (#3278)
## What Re-records the future-dates baseline after the UTC rollover. **92 → 64 file entries.** Tooling only, no source changes. The gate's own instruction is *"If a count went DOWN, re-record the baseline in the same commit."* UTC is now `2026-08-01`, so every stamp dated `2026-08-01` is dated **today** rather than after it, and **28 files no longer carry any future-dated stamp at all.** ## Why pruning matters more than the number Those 28 files kept a non-zero allowance they no longer need, and **an allowance is a hole a new violation can hide in.** Four separate commits landed a future-dated stamp last evening — in that environment, a stale allowance on a file is exactly where the fifth would go unnoticed. With the entries pruned, the next one in any of those files is caught on the first run instead of being absorbed silently. This is the same direction as #3168 (*"tightens the allowance 1 → 0"*) and #3211, just triggered by the clock rather than by a fix. ## What this is not - **Not a correction to anyone's stamp** — no source file is touched. - **Not a loosening** — no entry increases and no file is added. - The **123 stamps still dated beyond today** (e.g. `2026-08-06`) keep their existing allowances untouched. ``` baseline file entries 92 -> 64 check-fnxc-future-dates rc=0 lint clean ``` ## Related, deliberately not included `scheduler.ts:2258` carries `FNXC:WorkflowScheduling 2026-08-01-01:05` — dated today, one hour ahead of the current clock. I repointed it while preparing this change and then reverted: after the rollover it is no longer a gate violation, and mixing a cosmetic timestamp edit into a baseline re-record would make both harder to review. Noting it so the residual I flagged when closing #3270 does not get lost — it is now an accuracy nit rather than a gate concern. |
||
|
|
78d411cfe2 |
fix: main is RED on two gates — record the new fallback, repoint six future-dated stamps (#3261)
`9094d1640e` (globalPause gates every graph node entry) reddened **two** lifecycle gates on main. Both are fixed here, in separate commits. ## 1. The census ratchet went 0 → 2 `isTerminalColumnTask` in `scheduler.ts`: ```ts const flags = columnFlagsForTask(task); if (flags) return flags.complete === true || flags.archived === true; return task.column === "done" || task.column === "archived"; // ← counted ``` **The code is correct.** It resolves traits first and falls back only when the workflow is unreadable. The census counts fallback literals on purpose — *"a fallback literal is still a literal and should go when the trait path becomes unconditional"* — and reports them beside the backlog as already-converted. Its own remedy for a legitimate one is a `DELIBERATE-LITERAL` marker at the site. Recorded rather than converted because **there is nothing to convert to**: a task whose workflow cannot be read has no resolved lane, and treating it as non-terminal would count a finished card's retained worktree against live capacity — the opposite of what the surrounding fix does. Marker sits in the declaration's **leading** comments; an inline one attaches to the wrong node and is silently ignored, which cost a miscount once before. Baseline re-recorded in the same commit, since the census tracks deliberate counts and reports a marker addition as `RECLASSIFIED`. ## 2. The stamp gate was red as well Six files stamped `2026-08-01-00:2x` while UTC was `2026-07-31`: ``` workflow-column-boundary.ts 2 workflow-graph-task-runner.ts 1 workflow-column-boundary-hooks.ts 1 in-process-runtime.ts 5 (allows 4) workflow-column-boundary-capacity.test 1 ``` This checkout is UTC-7, so "just after midnight local" is tomorrow in UTC — the case AGENTS.md documents, which passes `pnpm lint` locally *because* the local clock agrees with what was written. Second occurrence today; I fixed the same shape on #3208 for another worker. Repointed to `2026-07-31-22:2x`, preserving relative order. **Zero non-comment lines changed** — 8 lines across 6 files, verified by diffing out FNXC lines. ## Measured | check | before | after | |---|---|---| | `census --strict` | **1** | **0** | | backlog | **2** | **0** (DELIBERATE-LITERAL 148 → 150) | | `check-fnxc-future-dates` | **1** | **0** | | `pnpm test:gate` | 0 | 0 | | `census-reclassification-message` | 2 failed | **1 failed** | That last row is deliberate: the remaining failure is the expired-premise case #3260 fixes, and I have not touched it. The capacity test from `9094d1640e` still passes 9/9. ## Why this landed at all Both gates run in `pr-checks.yml`, so a PR carrying either would have gone red. Worth someone checking how it merged — a stale merge base would explain it, and if so the same hole is open for the next merge. |
||
|
|
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> |
||
|
|
301bd8ed1e |
fix(census): detect membership and switch column guards, which could land silently (#3247)
## What
The census prints **"a new guard cannot land silently"** next to a zero.
That claim was true only for the guard form it happened to parse. This
closes the two it could not see. No product change.
The comparison walk visits `BinaryExpression` only, so neither of these
was visible:
```ts
["done", "archived"].includes(task.column)
switch (task.column) { case "todo": ... }
```
Both are lifecycle-column guards by any reading.
## How I found it
By applying this program's own rule — **break the guard on purpose** —
to the guard itself. I staged a probe file with five guard forms and
measured which moved the count:
| form | counted before |
|---|---|
| `t.column === "todo"` | ✅ |
| `t.column !== "in-review"` | ✅ |
| `["done","archived"].includes(t.column)` | ❌ |
| `switch (t.column) { case "triage": }` | ❌ |
| SQL string `"column" = 'done'` | ❌ (separate gate owns this) |
A worker converting a `===` chain into an array membership would have
scored the conversion **and kept the guard**.
*(The first probe run was itself invalid — the file was untracked and
the census enumerates git-tracked files, so the scanned count stayed at
1961 and nothing was measured. Staging it moved the scan to 1962.
Checking the scanned count is what caught that.)*
## The near-miss worth reading
My first implementation counted **unless** the receiver looked like a
role or status — mirroring the `===` walk. On the real tree it reported
**7 column guards**, and I nearly published that as a hidden backlog.
Six were false: `switch (eventName)`, `switch (state)`, `switch (event)`
— event and state enums routinely carry `case "done"` / `case
"archived"`. Landing it would have injected six phantom guards into a
backlog the ratchet treats as zero, and `--strict` would then have
**failed every other worker's PR**.
So the new walks require a **positive** column signal instead. That
regression is pinned by a test asserting all three receivers stay
uncounted.
## Measured
```
real repo, before and after: COLUMN guards 0, STATUS 185 (no false positives)
staged probe: 2 detected before -> 4 after
new tests: 6/6 pass; 3 FAIL with the extension reverted
existing lifecycle-census test: 9/9 still green
lint clean; census --strict passes; fnxc-future-dates: none added
```
## Known limit, stated rather than left to be discovered
The positive signal is the receiver **name**, so `switch (column.id)` —
a `Column` object rather than a task's column — is **not** counted. That
is a real guard shape and it is deliberately out of scope: widening to
reach it is exactly what produced the six false positives, so it needs
its own discrimination rather than a looser regex. Flagged here so the
next person extends it deliberately instead of assuming coverage.
## Why this and not another conversion PR
The conversion queue has been genuinely empty for several cycles —
census 0, 116 resolver sites unchanged across four commits, every site
blinded and pinned. The remaining risk in this program was never another
literal; it was that **the instrument defining "done" could not see two
of the shapes it claims to protect against**. A zero from a detector
with blind spots is the exact failure this phase has spent its time
documenting.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added lifecycle-column guard detection for array membership checks and
`switch` cases.
* Recognizes supported column receiver names and classifies findings
consistently with existing guards.
* Ignores status, event, and state receivers, and avoids duplicate trait
fallback findings.
* **Tests**
* Added coverage for membership checks, `indexOf`, `switch` guards, and
deliberate-literal suppression.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
cfcbba6f81 |
fix(census): 4 RED ratchet tests on main, and the report said nothing at zero (#3218)
Two problems, both caused by the backlog actually shrinking. ## 1. Four failing tests on main **Pre-existing, not introduced here** — running this file on clean `origin/main` gives `49 passed / 4 failed` with identical messages. I checked that before touching anything, because the failures surfaced while I was editing the same file. The ratchet cases build their fixture like this: ```ts Object.entries(baseline.byFile).find(([, c]) => c > 1) // needs a file with MORE THAN ONE guard ``` After the tail reclassification no such entry exists. `find` returns undefined → `byFile[undefined] = NaN` → the baseline is corrupt → every case fails with `expected … to contain 'TIGHTENED'`, a message that points squarely at the CLI when the **fixture** is at fault. That misdirection is why this sat red. The ratchet doesn't care *which* file it tightens, only that an allowance exceeds the measured count. So `inflate` now takes any entry, and synthesises one against a real scanned file when the backlog is empty. `deflate` is the harder half: a RISE needs an allowance **below** the real count, and once every measured count is 0 the only value below is negative. The empty case uses `-1`. That is not a realistic baseline value and the comment says so — it is the sole way to exercise the `measured > allowed` comparison against a tree with nothing left to count, which is the tree this suite now runs on. Same class as the unbounded-slice rot in #3207: **census self-tests coupled to the size of a shrinking backlog.** That is now twice, so it is a pattern rather than an accident. ## 2. The report went silent at the finish line The verdict was two inline branches and neither fired at zero — `CONVERSION QUEUE EMPTY` required `totals.column > 0`. So the one state the entire fleet phase was working toward printed **nothing**, which reads as a broken scan rather than the protected end state. Extracted to a pure `describeBacklogState({ columnGuards, unexaminedGuards })` returning lines, so the caller stays a dumb printer: ``` BACKLOG ZERO: no lifecycle-column guard remains. This is the protected end state, not an empty scan — `--strict` fails on any RISE, so a new guard cannot land silently. Use the role helpers (resolveLifecycleColumns / columnHasRole). ``` Pure **specifically** so the zero state is testable before the tree reaches zero. While it was inline, only the *current* backlog state was observable — and a message nobody can test before they need it is the one that is wrong when they do. ## Evidence | check | result | |---|---| | census test file | **53 passed** (was 49 passed / 4 failed) | | behaviour on today's tree | **unchanged** — identical `CONVERSION QUEUE EMPTY` block | | empty-baseline probe | exits 1, `column-guard count ROSE` | | forced zero verdict | prints `BACKLOG ZERO … not an empty scan` | | `--strict` / `check-fnxc-future-dates` / eslint | 0 / 0 / clean | | `pnpm test:gate` | exit 0 (744 tests) | Four new tests pin all three states, including that the unexamined branch must **not** claim the queue is empty while real work is outstanding. ## Census No guard converted — this is tooling and test repair. Backlog unchanged at 1, which #3215 takes to 0. |
||
|
|
0bdc9bf4fb |
fix(dashboard): archived tasks stayed in the research picker on a renamed board (#3215)
## The defect The enrich-mode task picker filtered with `task.column !== "archived"`. On a board whose archive lane is renamed, that matched nothing — so filed-away tasks stayed in the picker and an operator could attach research findings to work they had deliberately archived. ## Census before / after | | before | after | |---|---|---| | COLUMN guards (backlog) | 10 | **9** | | `ResearchTaskActionModal.tsx` | 1 | **0 — converted** | Baseline re-recorded in the same commit; `--strict` green. ## This site was declined twice, and I wrote the second wrong estimate #3213 left it counted, correctly, on the note that was here — which was mine. Both prior cost estimates were wrong, so this corrects my own work: 1. **"Needs a data-fetch change"** — reasoned about `columnFlagsByTaskId`, a per-**task** map built from board-resident rows. Right that such a map can't help (archived rows are exactly what a board map omits), but this guard asks a per-**column** question, so it never needed one. 2. **"Needs prop threading, MainContent → ResearchView → here"** — right that the answer is column-keyed, wrong about where it lives. `ListView` builds `columnFlagsById` *inline*, which made it look like the owner. The data is `useBoardWorkflows`, a hook already called from `App`, `Board`, and `HeaderWorkflowSwitcherSlot`. **Measured cost: one file.** The modal already takes `projectId`, and `ResearchView` renders it only when a finding is open (`open` hardcoded beside `if (!finding) return null`) — so the hook cannot fetch for a closed modal, which was the one real objection to calling it here. Union across workflows keyed by column id, first declaration wins — the same convention `ListView` uses, so the two cannot disagree about a shared id. `isArchivedColumnRole` fail-softs to the legacy id when a column has no flags, so an unresolved workflow behaves exactly as the literal did. ## Tests — the invariant, not the repro Per the surface-enumeration rule, four cases: renamed archive lane, legacy id, unresolved workflow (fail-soft), and a second workflow's archive lane through the cross-workflow union. A repro-only test would pass on the legacy board and prove nothing about the case the guard exists for. **Anti-vacuity control:** | | renamed lane | union | legacy id | fail-soft | |---|---|---|---|---| | pre-fix literal | **FAIL** | **FAIL** | pass | pass | | converted | pass | pass | pass | pass | The legacy and fail-soft cases hold in both directions **on purpose** — they pin that this conversion did not change the pre-resolution answer. Flagging that so 4/4 isn't read as four independent proofs. ## Measured | check | result | |---|---| | `census --strict` / `check-fnxc-future-dates` | exit 0 / exit 0 | | `eslint` | clean | | `tsc -p tsconfig.app.json` (the config that actually covers `app/`) | exit 0 | | new tests | 4/4 | | `pnpm test:gate` | exit 0 (744 tests) | ## Note on process My first attempt at the control silently did nothing — the revert script threw a `SyntaxError`, so the "pre-fix" run was the fixed code and reported 4/4. Caught it because the error printed. The table above is from the re-run. |
||
|
|
c66b434b7b |
fix(self-healing): a renamed hold lane re-logged the same overlap blocker on every sweep (#3216)
## The defect `clearStaleBlockedBy` keeps a per-task memo of which overlap blocker it already logged, so a sweep running every few seconds doesn't repeat the same line forever. The memo was retained only while the card sat in a column matching the literal `todo` — so on a renamed board it was dropped on **every** sweep and `still blocked by file scope overlap with <id>` was re-logged each time. ## Census before / after | | before | after | |---|---|---| | COLUMN guards (backlog) | 9 | **8** | | `packages/engine/src/self-healing.ts` | 1 | **0 — converted** | Baseline re-recorded in the same commit; `--strict` green. (Counts follow #3215, which took 10 → 9.) ## The stated blocker was not real The note here declined the conversion because the lane prefetch is keyed on `candidates`, *"which this closure helps build"*. Measured — it does not: ``` 6033| for (const task of blockedTasks) candidates.set(task.id, task); 6034| for (const task of queuedDependencyTasks) candidates.set(task.id, task); 6036| for (const [taskId, lastLoggedBlockerId] of this.preservedQueuedOverlapLogged) { <- only CLEARS memos ``` `candidates` is fully populated two statements earlier, and this loop only clears memo entries. So the prefetch was hoistable; it now sits above the loop. That is a pure move of a read-only computation with no conditional between the two positions. Reaching the lane clause already proves the id is a candidate — `!candidates.has(taskId)` is the first arm of the same `||` chain, so short-circuit means the lane question is only asked for ids the prefetch covered (`referencedIds.add(task.id)` runs for every candidate). `lanesOf` still falls back to the legacy set, so an unresolvable workflow answers exactly as the literal did. This is the second inherited "too expensive" estimate to fail on inspection this session (see #3215). Both were written in good faith and both were checkable in a few minutes. ## One thing typecheck caught that review would not have `memoTask?.column !== "todo"` was **also** the undefined check, and tsc narrowed the later clauses on it. Replacing it without that arm compiled clean to the eye but broke narrowing — `TS18048: 'memoTask' is possibly 'undefined'` on the next line. `|| !memoTask` is now explicit rather than implied. ## Evidence The test drives the sweep **twice**, because a single pass cannot observe a dedup memo at all. | | pre-fix literal | converted | |---|---|---| | `still blocked by file scope overlap` log lines | **2 — FAILS** | **1 — passes** | Failure message against the pre-fix code: `expected [ [ 'FN-DEPENDENT', …(1) ], …(1) ] to have a length of 1 but got 2`. Worth correcting the record: the note called the cost *"a duplicate log line, not a wrong lifecycle decision"*. The lifecycle half is right — but it is a duplicate on **every sweep**, so it is recurring log spam, not a one-off. That is a bigger cost than the note implies, though still not a correctness bug. | check | result | |---|---| | `census --strict` / `check-fnxc-future-dates` | exit 0 / exit 0 | | `eslint` / engine `tsc --noEmit` | clean / exit 0 | | self-healing + overlap suites | 15 / 21 / 6 passed | | `pnpm test:gate` | exit 0 (744 tests) | Reused the existing `RENAMED_BOARD_IR` harness in that file rather than building a new one. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved cleanup of stale workflow blockers, including renamed workflow lanes. - Prevented duplicate overlap warnings during repeated cleanup. - More reliably preserves valid queued overlaps while ignoring missing or inactive tasks. - **Tests** - Added regression coverage for repeated stale-blocker cleanup and duplicate warning prevention. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
aa1655ccd9 |
fleet: reclassify the census tail — 10 → 2 guards, all reasoning already in the code (#3213)
## Census before / after
```
before after
COLUMN guards (backlog) 10 2
DELIBERATE-LITERAL 138 148
```
Baseline re-recorded in the same commit; `--strict` green.
## This converts nothing — the tail was never backlog
All ten remaining guards already carried an explicit in-code decision.
**None carried the `DELIBERATE-LITERAL` marker the census reads**, so
each re-appeared to every fleet pass as if unexamined. That is the whole
defect this fixes.
| site | the reasoning already at the site |
| --- | --- |
| `audit-ops.ts`, `moves.ts` | the degraded fallback arm of an
**already-converted** site; the live arm uses the resolved lane set |
| `scheduler.ts` ×2 | *"LEFT COUNTED"* — an await behind the
`tracked.has` re-entrance guard lets two updates double-start a monitor;
the sibling is the measured-expensive `task:updated` emit path (26 sites
against 7) |
| `notification-service.ts` | this method and its only caller are
**sync**, reached from a listener the store invokes as `(task: Task):
void`; resolving makes the chain async and reorders notification
classification against every other `task:updated` handler |
| `lifecycle-ops.ts` | *"Recorded rather than converted"* — dead code |
| `task-id-integrity.ts` | sync, no store-scoped read; converting alone
would disagree with `getLiveTaskColumn` |
| `triage.ts` | *"LEFT COUNTED until then"* — wants a non-sync-resolved
lane answer |
## Marker placement is load-bearing, and I got it wrong twice
The census reads a node's **leading** comments. A marker in a nearby
block comment attaches to the wrong node and is **silently ignored** —
it reads as reviewed while the count still lists the site.
- `task-id-integrity.ts` — my first marker went into the block comment
above the `const`; the literal is in the `return`. Count stayed at 1
until I moved it.
- `ResearchTaskActionModal.tsx` — marker added, **measured that it did
not register**, reverted.
Every edit was verified by re-running the census, not assumed. That is
the only reason the count actually moved.
## Two sites deliberately left counted
- **`ResearchTaskActionModal.tsx`** — the literal sits mid-expression
inside a `.then()` chain, so no marker can attach. The census's own
guidance is to hoist it into a named helper; the site's note asks for
that to be someone's deliberate change rather than a drive-by, so it
stays counted and honest.
- **`self-healing.ts`** — the memo closure I converted and reverted in
#3049. Its note: a renamed board costs a duplicate log line, not a wrong
lifecycle decision.
## Correction I owe on the measurement itself
For many turns I reported "zero unclaimed guards". That came from a bug
in **my own** query — `byFile` is an array of `[file, count]` pairs and
I had switched to `Object.entries()`, which yields `[index, pair]`, so
`n > 0` was always false and the filter returned zero regardless of
state. It agreed with reality while open PRs held every file, which is
why it went unnoticed; it was still wrong, and a constant zero against a
falling backlog should have prompted me to check it sooner.
## Verification (measured)
- engine `self-healing` + `scheduler` suites — **1003 passed / 56
files**
- core `task-id` / `moves` suites — green
- `tsc --noEmit` clean in core, engine and dashboard; `eslint` clean
- `pnpm test:gate` — green
- `lifecycle-column-census --strict`, `check-lane-wiring`,
`check-sql-column-literals`, `check-fnxc-future-dates` — green
No changeset: `@fusion/core`, `@fusion/engine` and `@fusion/dashboard`
are private, and no runtime behaviour changes.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Clarified internal annotations for archived, in-progress, and
in-review workflow states.
* Documented fallback behavior and timing safeguards across lifecycle,
scheduling, notification, and triage flows.
* **Chores**
* Updated internal lifecycle tracking baselines to reflect current
annotations and state coverage.
* **Bug Fixes**
* No user-visible behavior changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
8d393422ac |
chore(fnxc): tighten the future-dates baseline — merge-queue-ops-2 4 -> 3 (#3211)
One-line baseline tightening, produced by the gate's own auto-tighten path. `check-fnxc-future-dates` deliberately auto-tightens rather than failing on a drop, because its population moves with the calendar and a drop has **no author** — the counterpart asymmetry to `check-inert-sync-lane-conversions`, where a drop *does* have an author and must fail. Any gate run regenerates this; `main`'s committed baseline had simply not caught up. **Why this isn't churn:** left loose, the baseline permits 4 future stamps in a file that now has 3. That slack silently absorbs one genuine future-dated stamp — precisely the failure this gate exists to catch, and one the fleet hit four times in a single day (`scheduler.ts`, a scheduler PG test, `task-update.ts` twice by different authors), each a real time on the wrong day that passed locally and reddened `main` for everyone else. Verified: both `check-fnxc-future-dates` and `check-inert-sync-lane-conversions` green on the tightened baseline. No changeset: tooling baseline, no published-package surface. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
215f09d88f |
fix(census): the bare command could not say the conversion queue is EMPTY — and a test fix for main (#3207)
## Why this exists
The fleet instruction is *"claim the largest unclaimed census file
cluster (`node scripts/lifecycle-column-census.mjs`)"*. That command
cannot answer it. The availability verdict lived **only** behind
`--claims`, which shells to `gh`:
```
line 342: if (claims && !json) {
```
So a worker following the instruction literally sees per-file counts,
reads a nonzero backlog as a work queue, and picks a file whose guard is
already documented as deferred. Counts alone cannot separate *work left*
from *debt left*.
**Measured cost:** the queue reached **zero unexamined guards** while
dispatch continued. I re-audited the last three candidates —
`merge-queue-ops-2`, `lifecycle-ops`, `notification-service` — and all
three were already documented. Only one was reclassifiable, and by
**deletion** rather than conversion (#3205).
## What the bare command prints now
```
COLUMN guards (the backlog): 11
CONVERSION QUEUE EMPTY: all 11 remaining column guard(s) carry a documented deferral note.
There is no unexamined guard to claim. A nonzero backlog above is DEBT, not a work queue.
Re-read the note at a site before converting it; run --claims to also check open-PR ownership.
```
Or, when work does exist: `N unexamined guard(s) remain (no deferral
note) — run --triage to list them by file.`
**Local signals only**, so it is honest offline. It reports what it can
prove — no *unexamined* guard remains — and explicitly does **not**
claim the files are unclaimed, because only `--claims` sees open PRs. No
count, no exit code, `--strict`/`--json` untouched.
## Three commits, deliberately separated
1. **`refactor`** — move `FLAG_MARKERS` + the 40-line window into the
lib as `hasDeferralNote()`, verbatim. It was a private const plus an
inline `.slice()` in the CLI, so the rule deciding where the fleet is
sent had **no test in either direction**. Proven identical on the real
tree: `11 documented / 0 unexamined` before and after.
2. **`feat`** — the verdict + 6 tests.
3. **`fix`** — an unrelated pre-existing failure (below).
## The test fix — this one is turning main red
`attributes a remaining file to the open PR that touches it` asserted
over `out.slice(out.indexOf("UNCLAIMED:"))`, which runs to **end of
output** and so also covers the `SYNC-RESOLVED` section printed
afterward. That section legitimately lists `scheduler.ts`.
Latent until `topRemainingFile()` returned `scheduler.ts` — which
happened as the backlog shrank, **a state every conversion moves
toward**. Confirmed pre-existing: clean `origin/main` runs `42 passed /
1 failed` with the identical message.
## Evidence
| check | result |
|---|---|
| `hasDeferralNote` tests | both directions, boundary exact at 40 above
/ not below, 5 real phrasings |
| verdict control (by hand) | one tracked undocumented guard → **11 →
12**, verdict flips to `1 unexamined`; removed → restored |
| test-fix anti-vacuity | claim split broken → **FAILS**; restored →
passes |
| census file | **49 passed** (was 42 passed / 1 failed) |
| `census --strict` / `check:fnxc-future-dates` | exit 0 / exit 0 |
| `pnpm test:gate` | **exit 0** (732 tests) |
The verdict control was **invalid on the first attempt** — my probe file
was untracked and `git ls-files` never scanned it, so the verdict did
not flip and nothing was proven. Recording that because a control that
silently proves nothing is the exact failure this PR is about.
## Census before / after
No guard converted here; this is tooling. Backlog unchanged at 11, all
deferred.
|
||
|
|
230be28576 |
fix(core): the merge-queue enqueue guard was not debt — the code it guarded had no callers (#3205)
## The deferral note was right about the mechanism and wrong about the
remedy
`merge-queue-ops-2.ts` sat in the census as deferred debt behind this
note:
> Converting it properly means either making this path async or pushing
the trait read into SQL, both of which are store-architecture changes
rather than call-site conversions.
That is correct as far as it goes — the guard runs inside
`store.db.transactionImmediate`, so the only synchronous resolver
available (`resolveTaskWorkflowIrSync`) returns the DEFAULT workflow
under PostgreSQL and a "conversion" would be inert.
But it assumed the code needed converting. Measured across the tree:
```
=== every call site of .enqueueMergeQueueSyncInternal( ===
packages/core/src/store.ts:1775: public enqueueMergeQueueSyncInternal(...) <- the declaration itself
```
**Zero callers.** Every other occurrence of the name is a comment. The
live path is `enqueueMergeQueueAsync` (`task-artifacts-ops.ts:117`), and
that file already documented the deletion:
> Merge-queue enqueue is PostgreSQL-only via enqueueMergeQueueAsync …
The SQLite `enqueueMergeQueueSyncInternal` arm is deleted.
The arm was deleted; its declaration was not. The guard was unreachable
on the shipped backend.
## Change
- Deleted `enqueueMergeQueueSyncInternalImpl` (-85 lines) and its
`store.enqueueMergeQueueSyncInternal` entry point.
- Dropped the six imports that became unused
(`MergeQueueTaskNotFoundError`, `MergeQueueInvalidColumnError`,
`MergeQueueEntry`, `MergeQueueEnqueueOptions`, `normalizeTaskPriority`,
`MergeQueueRow`).
- Refreshed the three comments naming the removed symbol, so none points
at a deleted identifier. The
`handoffMergeQueueFailureInjectorForTesting` hook those comments sit on
is a **different** member and is untouched — it only mentioned the sync
arm as context.
## Census before / after
| | before | after |
|---|---|---|
| `packages/core/src/task-store/merge-queue-ops-2.ts` | 1 | **0 (entry
removed)** |
Baseline tightened by exactly one entry. **The 0 here is a deletion, not
a conversion** — recorded in the file's own FNXC note so the next worker
does not read it as a converted seam. This is the failure mode the
census warns about ("a count of 0 is the WORST case, not the best"), so
it is stated at the site rather than left to inference.
## Measured
| check | result |
|---|---|
| `census --strict` | exit 0 |
| `@fusion/core tsc --noEmit` | exit 0 |
| `eslint` (4 changed files) | clean |
| core merge-queue tests | **110 passed / 6 files**, incl.
`postgres/merge-queue-renamed-review-column.pg.test.ts` |
| `pnpm test:gate` | exit 0 (**732 tests**) |
No changeset: `@fusion/core` is private and this removes unreachable
code with no user-visible behavior.
## Flagged, not guessed
The other four deferral-note files remain deferred. I only reclassified
this one because its call-site count is a fact I could measure, not a
judgement. Whether `lifecycle-ops.ts:667` is likewise dead (it sits in
the legacy-SQLite polling-replica path) is a separate question I have
not measured, so I have not touched it.
|
||
|
|
a319e35a67 |
fix(dashboard): the card's completion timestamp reads the resolved complete lane (census 13 → 12) (#3146)
`TaskCard.tsx` 1 → 0. **Census 13 → 12**, baseline re-recorded in-commit. ## The defect `getInReviewCompletionMs` gated on `task.column === "done"`, so on a board whose completion lane is renamed, a finished card rendered its execution time **without the completion half** — the `Completed <when>` part of the indicator's `title` / `aria-label` never appeared. Nobody reported it because the card does not look broken. It looks like a card whose completion time was never recorded. ## The recorded blocker had expired, and I trusted it twice The note on that helper read: > Module-scope, takes only a `Task`, and has no flags to consult. Converting it means either threading resolved flags through a pure duration helper or resolving a workflow inside it. True when written (2026-07-30). False within a day, and the evidence is in the same file: - `taskColumnFlags` is a **prop of this component**, destructured and already consumed by `isWipColumnRole` / `isReviewColumnRole`. - The **sibling duration helpers were threaded for exactly this purpose** — `getTotalAgentActiveMs` carries the note *"THREADED SO THE CONVERSION IS NOT INERT"*. - This helper has **one caller**, inside the component, where the flags are in scope. The threading the note called prohibitive was already done; only this helper was left behind. I read that note twice this week and took it at face value both times — and what finally prompted the check was main landing `taskRevert 2 → 0 — **the recorded blocker named the wrong variable**` (#3129), someone else finding the same class of decay in a note I had also accepted. This program's own learnings say a deferral's stated blocker is a claim that ages like any measurement. I had applied every other entry in that document this week except that one. ## A dependency-array bug the conversion would have introduced The memo now reads `taskColumnFlags`, so it joins the dependency array. Flags arrive **asynchronously** — the board resolves workflows after first paint — so a card rendered before they load and re-rendered after would otherwise keep the pre-flag answer, since none of the memo's other inputs changed. This repo has **no `react-hooks/exhaustive-deps` rule**, so nothing would have flagged the omission. ## Two wrong probes before a correct one, both caught by controls and mutation Recording these because the fix was right from the start and my instruments were not: 1. **`textContent` matched nothing.** The completion time lands in `title`/`aria-label`, never in visible text. The **control failed too** — the signature of a broken probe rather than a broken fix. 2. **`innerHTML` on the whole card matched always.** The lifecycle-dates footer renders its own `Completed <date>` line, and *that* path already resolves the complete lane correctly. The probe was reading a different, already-converted feature. **Mutation exposed it: reverting the fix left all six green.** The final assertion queries `.card-time-indicator` and reads its `title`, which is the only form that can tell the two apart. ## Verification | | result | |---|---| | suite | **6 passed** | | mutation (restore `=== "done"`) | **1 failed \| 5 passed** — the renamed case only, control still green | | dashboard `tsc -p tsconfig.app.json` | **0 errors** | | census `--strict` | exit 0, baseline re-recorded in-commit | Flags stay optional with the legacy id as fallback (`isCompleteColumnRole`), so any caller without resolved flags behaves exactly as before. ## Note on `check-fnxc-future-dates` It fails on this branch, but **not because of it** — `scheduler.ts` and one PG test carry future stamps on `main` itself. #3139 fixes that. None of my files appear in the report. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a6d67844b8 |
fix(triage): the "unconvertible" site was convertible — the blocker was two test harnesses (#3191)
#3141 measured this site as unconvertible, and I twice reported the cause as a production constraint. It was not. This is the instrumented answer to the probe I recommended there and then ran myself. ## The isolation | configuration | result | |---|---| | flag only, no conversion | **8 passed** → the orphan arm is *not* the cause | | flag + conversion | **5 failed** → the conversion is | | same, with a realistic mock store | **8 passed** → the mock was the cause | `triage-stuck-requeue-preserve-draft.test.ts` defined neither `getTaskWorkflowSelection` nor its async twin — exactly like `triage.test.ts` did before #3189. Both made `resolveWorkflowIrForTaskWithProvenance` **throw** and take its catch branch: the *"could not ask"* shape, which a production store never presents. So the 5 failures I deferred as a possible semantics change were the same harness gap in a second file — confirmed, not argued. ## What changes **`selectionAbsent`** marks the determinate case: the store *answered* "no selection", so the workflow is the default and its IR is in hand. Added as a **separate field, not a third `source` value** — `source === "default"` is compared in **31 places** in `self-healing.ts` meaning "be conservative", and a new enum value would silently stop matching every one of them while still compiling and still passing on a default board. **`recoverApprovedTask`** now accepts a legacy `triage` row *explicitly* (its workflow does not declare that column) instead of depending on `resolvePlannerLanes` **failing** and falling back to legacy ids. Correctness resting on a resolver's failure mode is what this removes. ## Measured | | result | |---|---| | broad suite (triage / self-healing / recovery / planning) | **77 files, 1302 tests passed** | | the three directly affected suites, post-rebase | **245 passed** | | the flag is load-bearing | conversion **without** it: **18 failed \| 221 passed** | | `census --strict`, `check-fnxc-future-dates` | exit 0 | **The inert-sync-lane count is unchanged at 7 for `triage.ts`.** This site was never among the counted guards, so this is **not** a ratchet reduction — stating that rather than letting a conversion imply one. It removes a real inert dependency the ratchet cannot see, which is the blind-spot class this phase has been mapping. ## Why this took four attempts I described this blocker at four levels: merged intake/hold, orphan-arm scoping, identity verification (filed as **#3187**, closed as wrong), and finally the harness. **The two I instrumented held; the two I reasoned to did not.** The fix here is the probe I wrote down for someone else — which is where it should have started. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
24c565540e |
gate: a sync lane handed to a wrapper is still inert — 13 scheduler guards were invisible (#3181)
## The fourth shape: a sync lane handed to a wrapper #3169 taught `unwrapForSyncCall` to walk await, parenthesized, conditional and binary expressions. It still stops at the **call boundary**, so a source call sitting in an *argument* position stays invisible: ```ts const parked = mergeParkedColumns(resolveTaskParkedColumnsSync(store, id), lanes); ``` That prefers the event payload and falls back to the sync answer whenever `lanes` is absent. The callee is `mergeParkedColumns`, not a source — so the walker never looked inside, and **the entire `scheduler.ts` file read as clean**. ``` main today: 9 (triage 7, executor 2, scheduler 0) this PR: 22 (scheduler 13, triage 7, executor 2) ``` Thirteen guards. And `check:inert-sync-lanes` has run in `test:gate` since #3136, so CI is currently enforcing a ratchet that reports a file it cannot see into as fully converted. The green is official, which makes it worse than the version nobody ran. ## Is the fallback still reachable? Yes, which is why these are not retired. #3135 attached lanes at every *live* emitter, but absence remains reachable three ways: the two `lifecycle-ops.ts` emitters on the SQLite-only polling path, any future emitter added without lanes, and the three forwarders (`project-manager.ts`, `remote-node-runtime.ts`, `child-process-runtime.ts`) that reconstruct the event object field-by-field rather than forwarding it. A rarely-exercised fallback is still a fallback. Counting it as clean is how the ledger stops meaning anything. ## The change One line inside your walker, plus its note: ```js if (ts.isCallExpression(n)) { for (const a of n.arguments) walk(a); } ``` Every shape #3169 added is preserved. Still a name match, not dataflow — the limits section still applies. ## Mutation evidence — all three shapes, one tree | Mutant | Result | |---|---| | baseline (22) | exit 0 | | **argument position** (this PR) | **exit 1**, 13 → 14 | | conditional (#3169's) | exit 1, 13 → 14 | | inline (#3062's) | exit 1, 13 → 14 | `scheduler.ts` restored clean after each run. ## Baseline 9 → 22 **Detection, not regression.** No production file changes in this PR. 22 is the exact union I measured before #3169 merged (13 + 7 + 2) and posted on both PRs at the time — it landing unchanged is the confirmation that the two fixes were additive rather than overlapping. ## Census before / after ``` before: COLUMN guards (the backlog): 12 after: COLUMN guards (the backlog): 12 ``` Unchanged — this converts nothing. It restores 13 guards to a ledger that had silently dropped them. ## Supersedes #3122 #3122 carried this fix as a standalone rewrite of `syncLaneLocals` and conflicted with #3169 the moment it landed. This is the six-line version I offered there; #3122 is closed. ## Verification `test:gate` exit 0 · `check:inert-sync-lanes` exit 0 at the re-recorded baseline · plus `fnxc-future-dates`, `lifecycle-columns`, `quarantine-ledger`, `inert-flag-seams`, `lane-wiring`, `sql-column-literals` — all exit 0. Gate script + baseline only. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved detection of synchronous operations nested within wrapper arguments. * Updated synchronization checks to report all currently identified findings, including additional scheduler-related cases. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
fa62c951cf |
fix(gate): the inert ratchet went quiet exactly when the code improved (conditional initializer) (#3169)
Found by dogfooding my own change: I wrote `executor.ts` in the payload-first/sync-fallback shape while adopting #3140's better fallback, **predicted in a comment that the guards would stay counted**, and the gate reported **zero**. The prediction was wrong in the direction that matters — the gate under-reports. ## The gap `syncLaneLocals` registered a local only when its initializer **was** a call expression: ```ts const sync = payload ? undefined : localSync(store, id); return column === sync?.hold; // inert, and counted as nothing ``` Conditionals and `??`/`||` chains are now unwrapped, so a sync call in any branch registers the local. Still a **name** match, not dataflow — the file's LIMITS section still applies. ## Why this shape matters more than the inline one already guarded **The missed shape is the one authors are steered toward.** Falling back to the sync resolver is *better* than falling back to legacy literals — it is best-effort under legacy SQLite, whereas a literal can never be right on a renamed board. So writing the guard well is what made it invisible. A ratchet that goes quiet exactly when the code improves is worse than none: it rewards the worse degraded path with a tidier number. ## Known remaining gap, stated in the test rather than implied Only **one hop** is followed. The two-hop form is still uncounted: ```ts const sync = payload ? undefined : localSync(store, id); const lanes = { hold: payload?.hold ?? sync?.hold ?? "todo" }; if (from !== lanes.hold) … // still invisible ``` `executor.ts` is written that way today, which is why it reads 0 while the sync call is still present. Closing it needs propagation through object-literal construction — a larger change than this one, and I would rather ship the one-hop fix with the gap documented than imply full coverage. ## Verification | | result | |---|---| | gate on `main` | **exit 0**, output unchanged (11 = triage 7 + executor 4) | | test suite | **5 pass** | | new case against the **unfixed** gate | **fails** — `the conditional-initializer shape must be counted` | The regression case drives a real file through the scanned tree rather than calling a helper, because the bug was in which nodes the scan **visits**. A helper-level assertion would have been written against the same wrong mental model that produced the gap — which is how the inline-spelling hole in this same file survived its first draft. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved detection of sync-lane conversions in conditional expressions, fallback logic, awaited and parenthesized values, and object-literal relays. * Corrected matching for identifiers containing special characters. * Updated validation results to include two additional findings that were previously missed. * **Tests** * Added integration coverage for conditional initializers, chained object-literal conversions, and special-character identifiers. * Ensured temporary test files are cleaned up automatically. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2cb5cab595 |
chore(core): mark the mission-store dead-sync-path literal DELIBERATE (census 13→12) (#3179)
Comment-only. `tsc` 0 errors, `census --strict` and `check-fnxc-future-dates` exit 0. ## Claimed with the new tool First use of `scripts/check-file-claimed.mjs` (#3175) to pick work instead of guessing: ``` CLAIMED packages/engine/src/scheduler.ts #3177, #3142 CLAIMED packages/core/src/task-store/audit-ops.ts #3165 UNCLAIMED packages/core/src/mission-store.ts UNCLAIMED packages/core/src/task-store/task-id-integrity.ts ``` Two of the four files I would have reached for were already taken — by PRs whose branch names give no hint they touch those paths. That is the collision this phase paid for five times, answered in one command. I took `mission-store.ts`; `task-id-integrity.ts` is still free. ## Census 13 → 12 Reclassification, not conversion — the line is unchanged. ## Verified the blocker rather than deferring to it The site carries an audited note: the sync `MissionStore` reaches `this.db.prepare`, and `getMissionStoreImpl` returns the `AsyncDataLayer`-backed `AsyncMissionStore` under PostgreSQL, so the class is unreachable in the shipped backend. I checked that independently instead of accepting it — `async-mission-store.ts:168` states the same routing from the other side. **That check exists because of #3129**, where a note I had accepted as settled ("blocked on a per-neighbour flag map that does not exist") turned out to name the wrong variable, and the file was convertible all along. I had publicly argued it should stay counted. So the rule I am applying: a documented blocker gets marked only after its named obstacle is confirmed from a second source. Here it held; on `taskRevert.ts` it did not. ## Related, and still open `merge-queue-ops-2.ts` carries a note of the same shape that does **not** survive this check — it names two ways to convert (make the path async, push the trait read down) and misses the one that worked twice this phase: thread the resolved lanes in from a caller that already awaited them, as #3112 and #3118 did for `executor.ts`. Its sibling `taskStillInReview(projectId, reviewColumns)` already takes lanes from its caller. Worth a real look rather than a marker. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |