## 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 -->
## Summary
Planning can no longer approve or execute against evidence from a
superseded dependency episode. Dependency mutations, approval decisions,
recovery, and execution admission now share serialized lifecycle rules,
so stale planner work cannot restore an invalid approval or release an
unplanned task.
Review also converges instead of discovering one blocker per round.
Planning performs a repository-grounded completeness pass up front; Plan
Review batches all independently discoverable blockers and carries an
episode-scoped decision ledger across revisions; code review traces
changed invariants through production consumers and tests. Repeated
feedback still advances the safety budget, while provider failures and
superseded episodes stay outside the remediation ledger.
The dashboard now exposes manual approval only for the intended
exhausted-review state, and refusal/recovery audit events make rejected
lifecycle transitions diagnosable without leaking prompt content.
## Validation
- `pnpm verify:fast` — scoped typechecks/builds, CLI build, and boot
smoke passed.
- Focused Core and Engine regression suites — 511 tests passed.
- `pnpm lint`, strict changeset validation, Core/Engine typechecks, and
package builds passed.
Fixes#3325.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Improved Plan Review approvals, rejections, and replan-cap handling
across task workflows.
* Added cumulative feedback and attempt tracking across repeated
planning reviews.
* Added safer recovery for stalled planning handoffs and interrupted
approval updates.
* **Bug Fixes**
* Prevented stale approvals and unplanned execution after dependency
changes.
* Improved concurrent approval handling, retryability, and
refusal-record deduplication.
* Refined dashboard approval indicators and responsive approval views.
* **Quality Improvements**
* Strengthened planning and code-review completeness checks and
blocking-finding coverage.
* Preserved review history while clearly marking outdated approvals.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Approving or rejecting a plan now releases the task instead of leaving
it permanently blocked as `awaiting-approval`.
The workflow routes use the task store's durable clear semantics, and
approval records the current plan fingerprint while safely clearing a
stale fingerprint when the plan cannot be read. Real PostgreSQL route
coverage proves both actions clear the scheduler hold and persist the
expected fingerprint state.
Fixes#3322.
## Validation
- `pnpm --filter @fusion/dashboard exec vitest run
src/__tests__/plan-approval-status.pg.test.ts --silent=passed-only
--reporter=dot`
- `pnpm --filter @fusion/dashboard exec vitest run
src/__tests__/routes-github.test.ts --silent=passed-only --reporter=dot
-t 'POST /tasks/:id/(approve-plan|reject-plan)'`
- `pnpm --filter @fusion/core exec vitest run
src/__tests__/task-update-awaiting-approval-reason.test.ts
--silent=passed-only --reporter=dot`
- Core and dashboard typechecks
- Scoped ESLint and strict changeset validation
---
[](https://github.com/EveryInc/compound-engineering-plugin)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Manually approved plans now resume correctly in their workflow.
* Rejected plans are fully cleared, preventing stale approval
information from affecting future decisions.
* Approval records accurately reflect the latest plan, including when
plan details are unavailable.
* Failed cleanup keeps rejected plans from being released prematurely.
* **Tests**
* Added coverage for approval and rejection persistence, workflow state,
and cleanup failures.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Advance the durable creation epoch for each explicit Planning Mode action while preserving idempotency for transport retries. Recover soft-deleted task links with a fresh claim key and cover live, deleted, and retry paths.
## Summary
Restores the non-blocking full suite on `main` after a multi-package
failure cluster (latest red run:
[30777132347](https://github.com/Runfusion/Fusion/actions/runs/30777132347)).
### Product fixes
- **MissionManager**: move `handleDiscardInterviewSession` /
`handleConfirmDelete` **above** `if (!isActive) return null` so hiding
the inline Missions tab does not drop hooks (`Rendered fewer hooks than
expected`).
- **task-update dependency re-spec**: emit `task:moved` only when the
column actually changes, with resolved endpoints (not hardcoded
`todo`→`triage`); fixes `laneCache.set is not a function` harness
failures and deleted-column announcements.
### Bookkeeping / suite alignment
- **Schema applier tests**: baseline `0042`, project table count `104`
(+4 FN-8685 consumer tables), historical `0000` fixture includes
`mission_validator_runs` for 0042 upgrades, version lists include
0041/0042.
- **Ledgers**: archived-gate TS inventory (lifecycle-ops), legacy
collection ledger (`LEGACY_PLANNER_WAKE_COLUMNS`), worktree capacity
audited bounds + two admission readers (scheduler + triage).
- **Desktop**: pin `actions/setup-java@v5.6.0` after Dependabot bump.
- **Dashboard tests**: FN-8701 ToolCallDetails nested `<pre>`;
CreateRoomModal re-pins agent mock after empty-once.
## Test plan
- [x] `@fusion/core` task-update + ledger tests
- [x] `@fusion/core` schema-applier subset (table count,
0000/0001/0002/0003 upgrades, concurrent)
- [x] `@fusion/dashboard` AgentLogViewer / CreateRoomModal /
MissionManager overview tests
- [x] `@fusion/desktop` release-workflow tests
- [ ] CI full-suite green on this PR
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Dependency re-specification now moves held tasks to the resolved
workflow intake lane when available.
- Task move events are emitted only when a task’s column changes.
- Improved mission draft deletion handling for locked, missing, and
failed operations.
- Prevented mission management issues when inline Missions are shown or
hidden.
- Updated database migration handling for lifecycle-related tables.
- **Tests**
- Expanded coverage for workflow lanes, database upgrades, worktree
limits, and release workflows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
FN-8704 failed at the graph parse node because PROMPT.md was only
"DUPLICATE: FN-8676". Filesystem validation treated non-empty as planned
and admitted the card into WIP, which then looped on parse failure.
Treat a sole DUPLICATE redirect as unplanned: block dispatch and hold
release, badge as awaiting planning, and if parse still sees that shape
rebound to needs-replan with feedback instead of parking failed.
Failed parks left in the WIP column still counted as running agents and
file-scope lease holders, so they consumed maxWorktrees/maxConcurrent and
could serialize unrelated todos. Match review-lane semantics: status
failed is never a live top-level holder.
Expand task update lane coverage across every direct and failure-isolated producer.
- Inventory direct and safe task:updated producer routes.
- Exercise warm and cold lane metadata through public producer operations.
- Cover lifecycle, workflow integrity, and completion update paths.
Files changed:
.../task-updated-lanes-emit-surfaces.test.ts | 308 +++++++++++++++++----
1 file changed, 256 insertions(+), 52 deletions(-)
Fusion-Task-Id: FN-8658
Fusion-Task-Lineage: 7d93593c-6f5a-4299-96f3-f4ee8f260e41
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
The field was defined in persistence and serialization, the executor's Plan
Review replan-cap park wrote it, the triage manual gate null-cleared it, and
the dashboard special-cases it (isReviewBudgetExhaustedApproval badge + detail
explanation) — but updateTask's field-by-field merge never applied the key, so
every writer silently dropped it. FN-8647's 15-cycle non-converging Plan Review
loop therefore parked with a generic 'needs approval' and no hint it was a cap
escalation.
Merge contract, pinned by tests with a measured revert proof (3/4 fail
pre-fix): set persists, explicit null clears, a status write that leaves
awaiting-approval without addressing the reason auto-clears it so an approved
or replanned card cannot carry a stale escalation reason into its next park,
and unrelated updates leave it untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
**`check-fnxc-future-dates` exits 1 on `origin/main`.** This is the last
stamp causing it.
```
packages/core/src/task-store/lifecycle-ops.ts: 1 future-dated FNXC stamp, baseline allows 0
FNXC:Diagnostics 2026-08-01-00:50 (today is 2026-07-31)
```
Corrected to `2026-07-31-00:50`. One character.
`check-fnxc-future-dates` now exits 0; `tsc --noEmit` clean.
## Why this was left behind
Four PRs converged on this red main — #3262, #3263, #3265, and my own
#3266 (closed as superseded). Between them they covered the census rise
and the boundary-work stamps. **None touched `lifecycle-ops.ts`**, so
the gate stayed red after the others landed.
That is the predictable failure of parallel work on one symptom:
everyone fixes the part they saw first, and the residue survives because
each author checked "is main green now?" against their own branch rather
than against main.
## I claimed before working this time
```
node scripts/check-file-claimed.mjs packages/core/src/task-store/lifecycle-ops.ts
→ UNCLAIMED
```
Then pushed the branch before editing. I did the opposite on #3266 —
built it, then discovered #3265 already covered it — which was the sixth
duplication of the phase and my third. The tool answers in one command;
the discipline is running it *first*.
## Verification
- `check-fnxc-future-dates` — **exit 0** (was exit 1 on main)
- `census --strict` — exit 0 (already green; #3265's marker landed)
- `tsc --noEmit` (core) — 0 errors
- one-character diff, no behaviour change
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What
Pins the **last two uncovered lane reads** in `packages/core`.
Test-only. This closes the per-site core audit.
| site | what it decides |
|---|---|
| `async-mission-store.ts:1179` | is an ARCHIVED card valid terminal
evidence for mission repair? |
| `task-id-integrity.ts:502` | does an archived child still count as a
LIVE lineage child? |
## Measured
```
mission-store: 39 passed clean; 1 failed | 38 passed blinded
lineage: 3 passed clean; 1 failed | 2 passed blinded
lint clean; fnxc-future-dates: none added; census unchanged
```
Both blinds confirmed applied with `git diff --stat` before each run.
## The third adjacent-pair split
`:1179` is the **archived** half of a pair whose **complete** half
(`:1178`, *one line above*) was already covered by a test in the same
file, written for exactly this concern. Terminal evidence is "done OR
supported archived state," so an archived card is equally valid repair
evidence — but on a board whose archive lane is `vaulted` the archived
half could not see it, and reconciliation threw `TASK_NOT_TERMINAL` for
a card that was genuinely filed away. Same refusal the covered case
fixed, reached through the other door.
That is now the third confirmed instance in core (after `team-analytics`
in #3227 and the scheduler pair earlier). **Being adjacent to a covered
resolver is not coverage**, and it is the most reliable place to look.
## What breaks without the lineage read
An archived child is filed away, not live, so it must not hold the
delete gate shut. Renamed, it still counted as live and
`TaskHasLineageChildrenError` blocked the parent's delete **forever** —
the operator archived the child *precisely* to clear the way, and the
gate could not see that they had.
## A fixture detail I got wrong first
My first mission fixture created a live card in a `vaulted` column and
failed with `deleted or archived without a valid retained tombstone and
archive snapshot` — nothing to do with the lane read.
The `archived` verdict requires **all three** of `deletedAt !== null`,
an archive-snapshot row, and `isArchived(column)`. A live card merely
sitting in an archive-trait column is `invalid-deleted`, not `archived`.
The test now archives for real and *then* renames the recorded lane,
which isolates the third condition — the only one under test. Recorded
in the file so the next person does not re-derive it.
## Paired positives
Both files pin the complement: a WORKING child still counts as live.
Recognising the renamed archive lane must not degrade into "no child is
ever live" — that would silently **disable** the lineage gate and let a
parent be deleted out from under real descendants, which is worse than
the bug being fixed.
## Core audit complete
**14 sites blinded individually: 9 already covered, 5 uncovered, all 5
now pinned** (#3233, #3234, this PR).
Every `resolveProjectColumnsForRoles` call site in `packages/engine` and
`packages/core` has now been blinded. Remaining unaudited: `dashboard`
(2 files) and `cli` (1) — I claim nothing about those.
Found by a live browser E2E of the coding workflow in test mode: every
scripted full-task run failed at `steps#0:step-execute` with `Step 4 out
of range (task has 4 steps)`, rebounding through recovery forever.
**Root cause:** `fn_task_update.step` has been **0-based since FN-6607**
(executor.ts FNXC:StepNumbering — the old `step - 1` conversion made
Step 0 impossible to mark). `mock-provider.ts` still sent `index + 1`,
so test mode marked steps 1..N instead of 0..N-1: Step 0 (Preflight)
never completed and step N threw out-of-range. Test mode's full-task
path has been broken since June.
**Also fixes the test that pinned the bug:** `mock-provider.test.ts`
expected `{ step: 1 }` for a fixture whose first unfinished step is
index 0 — the expectation encoded the 1-based off-by-one.
Verified: 12/12 mock-provider tests; the live E2E instance completes the
task after this patch (see follow-up screenshot in the session).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What
Pins **both** create-time duplicate guards in
`branch-and-pr-entities.ts`. Test-only.
| site | method | excludes |
|---|---|---|
| `:445` | `findRecentTasksByContentFingerprint` | ARCHIVED (unless
`includeArchived`) |
| `:484` | `findRecentTasksBySourceParentTaskId` | COMPLETE and ARCHIVED
|
Blinding either back to its literals left the entire 16-file
lane-detector set green. **No test in `packages/core` reaches either
method.**
## Measured
```
converted: Tests 8 passed (8)
blinded :445 Tests 1 failed | 7 passed (8) <- only the fingerprint case
blinded :484 Tests 2 failed | 6 passed (8) <- only the sibling cases
lint clean; fnxc-future-dates: none added; census unchanged
```
**Each blind fails exactly its own cases.** That matters: it proves the
two resolvers are pinned *independently*, rather than one broad test
appearing to cover both. Blinding `:445` leaves every sibling case green
and vice versa — so neither is riding on the other's coverage.
## They fail in opposite directions
This is why both belong in one file:
- **Fingerprint guard** — a renamed board leaves archived cards in the
candidate set, so filing a new task is **refused as a duplicate** of one
the operator already archived. The create is blocked and the thing
blocking it is invisible.
- **Sibling guard** — a renamed board leaves finished siblings in the
"recent live siblings" set, so completed work keeps counting as active.
One over-includes into a *refusal*, the other over-includes into
*phantom activity*. Neither raises an error.
## Positives pinned too
A LIVE fingerprint match is still a duplicate candidate;
`includeArchived: true` opts the renamed archived lane back in; a
WORKING sibling is still live. Excluding the finished lanes must not
degrade into excluding everything, or the guards stop guarding — the
failure mode a lane-widening change invites.
## A fixture detail that would have made this vacuous
Both queries cut off at `Date.now() - windowMs`, with `windowMs` capped
at 24h. The sibling harness I copied from seeds a **fixed past
timestamp**, which falls outside that window — every case would then
pass on an empty result, including the ones that are supposed to fail
under blinding. Fixtures are seeded at current time instead, and the
reason is recorded in the file so nobody "tidies" it back to a frozen
date.
## Progress
3 of the 5 uncovered core sites are now pinned (`store.ts:1135` in
#3233, these two here). Remaining and unclaimed:
`async-mission-store.ts:1179` and `task-id-integrity.ts:502`.
## What
Pins the **open-undo query's finished-lane exclusion** in
`packages/core/src/store.ts`. Test-only.
`findOpenRevertTaskForSource` answers *"is there an OPEN undo task for
this source?"* — the question behind the dashboard's Undo affordance. It
answers by **excluding the finished lanes**, so a prior undo that
already landed does not keep rendering as open.
Blinding that exclusion back to `ne(column,"archived"),
ne(column,"done")` left the entire 16-file lane-detector set green. **No
test in `packages/core` reaches this method at all.** The dashboard-side
twin (`taskRevert.ts`, #3129) is tested; the store-side query behind it
was not.
## Measured
| | default (control) | renamed complete | renamed archived | working
lane |
|---|---|---|---|---|
| converted | pass | pass | pass | pass |
| blinded to `["done","archived"]` | pass | **FAIL** | **FAIL** | pass |
```
converted: Test Files 1 passed (1) / Tests 4 passed (4)
blinded: Test Files 1 failed (1) / Tests 2 failed | 2 passed (4)
lint clean; fnxc-future-dates: none added; census unchanged
```
Blind confirmed applied with `git diff --stat` before the run.
## What breaks without it
On a board whose complete lane is `shipped`, neither literal matches, so
a **done** undo task is never excluded and the query keeps returning it.
The card shows an undo already in flight *forever*, and the real
affordance is unreachable. Nothing errors — the button is just
permanently wrong, which is why it went unnoticed.
## Includes the paired positive
An undo still in a **working** lane IS reported as open. Excluding the
finished lanes must not degrade into excluding everything, or the
affordance breaks in the other direction and no undo is ever reported in
flight. Both new failing cases are renamed-lane cases; both survivors
are cases that should survive.
## Where this came from
Per-site blinding of all 14 remaining `resolveProjectColumnsForRoles`
call sites in `core`, run against a 16-file detector set. **9 covered, 5
uncovered:**
| site | verdict |
|---|---|
| `store.ts:1135` | **uncovered** → pinned here |
| `async-mission-store.ts:1179` (archived) | **uncovered** — its
neighbour `:1178` (complete) is covered |
| `branch-and-pr-entities.ts:445` | **uncovered** |
| `branch-and-pr-entities.ts:484` | **uncovered** |
| `task-id-integrity.ts:502` | **uncovered** |
| `reads.ts` ×3, analytics ×3, `eval-automation`, `task-artifacts-ops`,
`async-mission-store:1178` | covered |
The first run of that probe was **invalid and I nearly published it**:
it reported all 14 sites "COVERED" with *zero failing tests*. zsh does
not word-split unquoted parameter expansions, so `vitest run $DET`
passed 16 paths as one argument and vitest exited 1 with "No test files
found" — which my script read as a failing test. The re-run treats that
string as `INVALID` rather than a result. Third time this session a
wrong reading came from test *selection* rather than from blinding.
## Flagged, not guessed
The four remaining uncovered sites are named above rather than quietly
left; `async-mission-store` shows the same adjacent-pair split as
`team-analytics` in #3227, which is now the third confirmed instance of
that shape.
## What
**Fixes a red main.**
`workflow-reconciliation-production-shape.pg.test.ts` has been failing
with `expected 'todo' to be 'triage'`. Test-only.
Found while establishing a clean baseline for an unrelated coverage
audit — my tree was clean at `origin/main` (`76c73238a0`), so this is
not something I introduced. It is in the non-blocking suite, which is
why it has stayed red.
## It is not a regression — the test was the stale half
The delete path was deliberately fixed to re-home occupants using
`resolveEntryColumnId(resolveDefaultWorkflowIr())` instead of
`BUILTIN_CODING_WORKFLOW_IR`. This assertion was not updated with it.
The two IRs are **not the same board**:
| IR | entry column |
|---|---|
| `BUILTIN_CODING_WORKFLOW_IR` (`builtin:legacy-coding`) | `triage` |
| `resolveDefaultWorkflowIr()` (the catalog default) | `todo` |
Re-homing into `triage` put cards in a column the default board never
declares. It slipped past `moveTask`'s undeclared-target guard **only
because `triage` is a legacy id** and the recovery-rehome path exempts
those — so the guard that exists to stop exactly this could not see it.
So `todo` is the correct behaviour and the literal `"triage"` was what
needed fixing.
## Why it asserts a resolver rather than `"todo"`
Swapping one hardcoded id for another would be the identical trap one
rename later — the same class of defect this whole program exists to
remove. The expectation now derives from **the same two functions the
product path calls**, so it cannot drift out of sync with them again.
I also added the complement: the card must genuinely have **left** the
vanished column, not merely match whatever a resolver returns. Without
it, a resolver that started returning `custom-hold` would pass.
## Proven not appeasement
Reverting the product line to the legacy IR — the original defect —
fails this test:
```
AssertionError: expected 'triage' to be 'todo'
Test Files 1 failed (1) / Tests 1 failed | 6 passed (7)
```
That is the check that matters for a test edit that turns a red green.
It fails on the defect it describes.
## Measured
```
before: Tests 1 failed | 6 passed (7)
after: Tests 7 passed (7)
16-file detector set: 181 passed (16 files) [was 1 failed | 180 passed]
lint clean
```
## Note on the reading
I got this wrong twice before getting it right, and the record is worth
having. My first read was "the test is stale, `triage` was merged away."
My second was "the builtin IR still declares `triage`, so the
*behaviour* is the defect" — which the IR file superficially supports.
Only the third reading, of the FNXC note at the fix site, showed the
file I was reading is the **legacy** IR and not the default one. Two of
those three readings would have produced a confidently wrong PR; the
deciding evidence was the comment the fixing author left at the call
site, which is a good argument for writing them.
## What
Pins `aggregateTeamAnalytics`' **in-flight lane read** (`activeLanes`) —
the unpinned half of an adjacent resolver pair. Test-only, no product
change.
`completeLanes` and `activeLanes` are declared **two lines apart**.
Every existing case in this file asserts only `totals.tasksCompleted`,
so the in-flight query `activeLanes` feeds was never observed.
Measured on main:
| blinded resolver | result |
|---|---|
| `completeLanes` → `["done"]` | **FAILS** the file (1 failed / 3
passed) — pinned |
| `activeLanes` → `["in-progress","in-review"]` | **entirely GREEN** (4
passed) — unpinned |
One resolver held, its neighbour not, in a file named
`team-analytics-renamed-lanes`. This is the half-covered-pair shape the
program keeps finding; being *next to* a covered resolver is not
coverage.
## How I found it
Rather than blind core's 17 files one at a time, I made
`resolveProjectColumnsForRoles` itself return legacy-only — its own
documented degrade path — which blinds **all 116 call sites in one
edit**. The full core suite then reported **24 failures across 16
files** out of 4,987 tests, which maps the covered areas in a single
run: the analytics renamed-lane pg tests, the archived-lane family,
eval-automation, mission-store, and the resolver's own tests.
That global probe finds *areas* that are covered, not *resolvers* — so
the pairs still needed individual blinding, which is what surfaced this
one. `workflow-analytics.ts` has the identical two-resolver shape and
**both halves are covered**; the gap is specific to `team-analytics.ts`.
## Two things have to be right, and both are now asserted
1. **The SQL must ASK for the board's real wip lane** — `activeLanes`.
2. **`buildTeamAnalytics` must RECOGNISE the row it gets back.** It
classifies via `isWipColumnRole(query.columnFlagsByName?.get(name),
name)`, which **without flags falls back to `name === "in-progress"`**
and drops a renamed lane it already fetched.
So supplying `columnFlagsByName` is part of the caller contract, not
test scaffolding: **widening the query alone would still report zero.**
A test that only widened the first half would pass while the feature
stayed broken.
## Measured
```
converted: Test Files 1 passed (1) / Tests 7 passed (7)
blinded activeLanes: Test Files 1 failed (1) / Tests 2 failed | 5 passed (7)
lint clean; fnxc-future-dates: none added; census unchanged
```
Blind confirmed applied with `git diff --stat` before each run.
## What breaks without it
A per-agent `tasksInProgress: 0` sitting beside a nonzero completed
count and real token spend — an agent that looks idle while it is
working. Same wrong-but-plausible shape this file's own header
describes: nothing errors, and a plausible-looking number is the least
likely defect for anyone to file.
## Flagged, not guessed
- `packages/core` is not my package; this is additive tests only. I
raised the same note on #3225.
- The global probe shows core has **substantial** renamed-lane coverage
— it is not the uniformly-unpinned surface I implied when I first
reported 17 unaudited files. Correcting that here rather than leaving
the stronger claim standing.
- Still unblinded individually: the resolver pairs in
`async-mission-store.ts` (1178/1179) and the archived reads in
`task-store/reads.ts` (396/615/793). Their *files* fail under the global
blind, so something covers each area — but that is not per-resolver
evidence, and I am not claiming it is.
## What
Pins the **engine-downtime timing shift's wip read** in
`packages/core/src/store.ts`. Test-only — no product change. First
audited site in `core`.
`reconcileActiveTimingForEngineDowntime` (FN-7011/FN-7975) excludes
proven stopped-engine wall-clock from a card's active time. It finds the
cards to fix by querying the board's wip lane.
**Blinding that read back to `["in-progress"]` left every test that
touches the sweep green — 4 in this file plus 424 in the two engine
files that exercise it, 428 in total.**
## Why 428 tests were blind to it
The existing store double is 10 lines and contains **both** documented
anti-patterns, either one sufficient on its own:
1. **`listTasks: vi.fn(async () => tasks)` ignores its `column`
argument** — it returns the same rows whichever lane is requested. A
fake that ignores its own filter cannot see a filter bug, which is
exactly the bug this resolver exists to fix.
2. **No `listWorkflowDefinitions`** — `resolveProjectColumnsForRoles`
then returns the legacy ids and nothing else (an intentional degrade in
`project-lane-vocabulary.ts` so an unreadable workflow list cannot fail
a sweep). The resolved set and the literal set were *equal by
construction*.
The new double fixes both and changes nothing else. **The existing cases
keep the original double on purpose:** they are about heartbeat and
threshold arithmetic, not lanes, and rewriting them would put unrelated
churn in the same commit.
## Measured
| | default (control) | renamed | differential | non-wip card |
|---|---|---|---|---|
| converted | pass | pass | pass | pass |
| blinded to `["in-progress"]` | pass | **FAIL** | **FAIL** | pass |
```
converted: Test Files 1 passed (1) / Tests 8 passed (8)
blinded: Test Files 1 failed (1) / Tests 2 failed | 6 passed (8)
engine neighbours (project-engine-unpause-active-timing + self-healing): 424 tests, green and unchanged
lint clean; fnxc-future-dates: none added; census unchanged
```
Blind confirmed applied with `git diff --stat` before each run, not
inferred from the tool's exit code.
## What breaks without it
On a board whose wip lane is `building`, the sweep queries
`in-progress`, finds **no tasks**, and shifts no anchor. Every card
silently absorbs the stopped-engine wall-clock the sweep exists to
exclude. The reported active time is simply wrong and nothing fails to
signal it — the same silent-wrong-number shape as the evaluator defect
in #3224.
## Also covers the complement
A held card *outside* the wip lane is **not** shifted. Widening a lane
read is the kind of change that can quietly turn a targeted sweep into a
board-wide rewrite; a card in `todo` has no stopped-engine time to
exclude, and there is now a case saying so.
## Scope note
`packages/core` is not my package. This is an additive test file with no
product change, so collision risk is low, but I am flagging it rather
than assuming: **16 of core's 17 files with resolver call sites remain
unaudited** and I claim nothing about them. The audit method and its
failure modes are documented in #3223 if core's owner wants to continue
it.
## The false-green
#3160 (mine) proved `liveSearchPredicate` honours a resolved archive
set: hand it `Set(["archived","filed"])` and `filed` appears in the
bound params. That contract is real and still correct.
**Nothing proved `reads.ts` passes one.** It is a unit test of the
collaborator, so blinding the resolver at the call site cannot fail it.
A conversion, a test that looks like it covers it, and no connection
between them.
## The measurement — and the instrument matters
| site | vs. the predicate unit test | vs. a test that drives `reads.ts`
|
|---|---|---|
| `reads.ts:396` cold-storage list | 0 failed | **1 failed — covered** |
| `reads.ts:615` incremental sync | 0 failed | 0 failed — **UNCOVERED**
|
| `reads.ts:793` search | 0 failed | 0 failed — **UNCOVERED** |
Against `search-excludes-renamed-archive-lane.test.ts` all three read as
uncovered — an artefact of asking a file that never executes `reads.ts`.
Against `cold-storage-renamed-archive-lane.test.ts`, which drives
`listTasksImpl` for real, 396 is covered and the other two genuinely are
not.
That is rule 2 of #3214 one level up: *the test must reach the site*,
and a unit test of the collaborator never does. Had I stopped at the
first instrument I would have reported three uncovered resolvers, one of
them wrongly.
## What 793 costs on a renamed board
`searchTasks` backs the **CREATE-time near-duplicate check**. Without
the resolved lanes threaded, search stops excluding the board's archive
lane, and creating a task can be refused as a duplicate of one the
operator archived long ago — with no way to see why, because the
matching card is not on the board. Precisely the symptom #3160 set out
to fix; this pins the wiring that delivers it.
## An assertion I got wrong, and the correction
I expected an unreadable workflow list to leave `archivedColumns`
**undefined** via the call-site `.catch(() => undefined)`. It does not:
`resolveProjectColumnsForRoles` catches internally and returns its
**legacy-seeded** set, so `Set(["archived"])` is threaded and the
`.catch` never fires on that path. Two layers fail soft and the inner
one wins.
The case now asserts the guarantee that actually holds either way —
**never an empty set** (which would exclude nothing and return archived
rows in every search), legacy id always excluded. Recorded at the site,
because the mechanism is not obvious from the call.
## Flagged, not papered over
**`reads.ts:615` is left uncovered on purpose.** It composes Drizzle
conditions and runs them against `layer.db` with no injectable seam, so
pinning it needs a real database and belongs with the `.pg` suites. A
test asserting "the query was built" rather than "the rows were
excluded" would satisfy the ratchet and prove nothing.
Also flagged from this sweep: `workflow-analytics.ts` and
`team-analytics.ts` (4 resolvers) are **unmeasurable in my environment**
— their renamed-lane coverage lives in `.pg` suites, and this worktree
has no TCP PostgreSQL (`pg_isready` reports a Unix socket; the harness
probes TCP, so `pgDescribe` correctly skips). Not claimed either way.
## Census
**Unchanged — `CONVERSION QUEUE EMPTY`, `AVAILABLE: 0`.** Converts
nothing; closes coverage on a conversion the census already counts as
done.
## Verification
```
as written Tests 4 passed (4)
BLIND reads.ts:793 Tests 1 failed | 3 passed (4)
restored Tests 4 passed (4)
```
Anti-vacuity case included: every other assertion reads a mock's
arguments and would pass if the search were never reached, so one case
pins that the primary search path actually ran. Typecheck clean.
No changeset: test-only, behavior-preserving, no published-package
surface.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 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 -->