Commit Graph

12930 Commits

Author SHA1 Message Date
gsxdsm
78d87f0a10 test(core): pin the search archive-lane WIRING — the predicate was covered, the hand-off was not (#3220)
## 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>
2026-07-31 11:50:02 -07:00
gsxdsm
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.
2026-07-31 11:42:15 -07:00
gsxdsm
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 -->
2026-07-31 11:29:25 -07:00
gsxdsm
2868eb4797 docs(learnings): blind the resolver to find uncovered conversions (#3214)
Sibling to #3203 (`a-falling-count-is-not-evidence`), which records that
a metric moving is not proof the system moved. **This is the positive
procedure**: how to find out whether a landed conversion is held by
anything, and how to write a test that holds it.

## The measurement it is written from

Of **64 resolved lane sets in `self-healing.ts`, 26 had no test that
could distinguish them from the literal they replaced** — including
three conversions I shipped that same day, and two halves of sweeps I
had already recorded as covered.

## The procedure

```
- const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
+ const reviewColumns = new Set<string>(["in-review"]);
```

Suite fails → covered. Suite passes → nothing in the tree can tell the
conversion from the literal. One resolver, one 17-second run — cheaper
than writing the conversion was.

## Why the census cannot answer this

| instrument | question |
|---|---|
| census / lane-wiring ratchet | is this site written in the resolved
vocabulary? |
| blinding | does anything break if it stops being? |

Neither substitutes for the other. A conversion merged with 204 green
tests behind it and zero able to see it.

## Four rules, each paid for by a test that proved nothing

1. **Blind each resolver separately** — coverage is per-resolver, not
per-sweep. Twice a sweep recorded as done was half-done, because control
flow short-circuited before the second guard.
2. **The fixture must reach the branch the resolver gates.** A card in a
renamed *wip* lane cannot exercise a *terminal* skip — it is caught by
the wip∪review set first.
3. **Assert a path-specific side effect, never a return value.**
`outcome === "reclaimed"` is reachable without the guarded branch.
4. **A store fake must honour `options.column`.** Flat and call-order
stubs answer identically whatever column is requested — a fake that
ignores its own filter cannot see a filter bug.

## The two shapes a ratchet cannot distinguish

- **resolved gate, literal branch** — reads as *unwired*, was a live
defect (#3208: a working agent lost its task link)
- **passed-but-unread** — reads as *wired*, is dead code (#3212)

A ratchet counting call sites scores the first as debt and the second as
done. Both wrong.

## Why a doc and not more PR comments

Everything above currently lives in ~20 PR descriptions. The next person
to touch a lane conversion will not read those. `docs/solutions/` is
where this project already keeps the things it learned the expensive
way, and the frontmatter (`applies_when: deciding whether a lane
conversion is actually protected by a test`) is what makes it findable.

## Verification

`pnpm test:gate` 13 + 161 + 499 + 71 · lint · fnxc-dates (TZ=UTC) ·
`self-healing-docs` 2 passed. Docs only; no changeset, per the AGENTS.md
rule for internal docs.
2026-07-31 11:18:23 -07:00
gsxdsm
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 -->
2026-07-31 11:15:40 -07:00
gsxdsm
f94ff68391 docs(engine): record that agentParkedColumns is inert at this call site (passed-but-unread) (#3212)
Closes the last entry I could not pin on the #3115 coverage map — by
establishing **why** it cannot be pinned, rather than leaving it open or
forcing a green test around it.

## `agentParkedColumns` is inert at this call site

It influences exactly one output — `shouldPreserveParkedLink` — and
`recoverAgentsRunningOnInactiveTasks` **never reads it**. The gate is:

```ts
if (proof.hasFreshRun || proof.hasActiveExecution) continue;
```

Neither depends on the lane. So passing a resolved set changes nothing
today, and blinding it back to the legacy ids leaves every test green
**because there is no behaviour to observe**. That is not a coverage gap
— there is nothing there to cover.

## Kept, not deleted

- Removing it makes this call site read as **unwired** to the
lane-wiring ratchet, inviting the next worker to "fix" it by re-adding
exactly this.
- If the gate ever adopts `shouldPreserveParkedLink` — the
parked-specific semantics the sibling sweep uses
(`recoverDriftedAgentTaskLinks`, wired in #3208 an hour ago) — the
resolved set is already correct here.

## The shape worth naming

**Passed-but-unread** is the mirror of the **resolved-gate,
literal-branch** defect #3208 fixed. Both read as converted while
deciding nothing — and only one of them is a bug.

A ratchet that counts call sites cannot tell them apart: #3208's site
looked *unwired* and was a live defect; this one looks *wired* and is
dead code. That is why the distinction belongs in a comment at the site
rather than in a baseline number.

## Map status

**21 of 26 pinned**, 1 established as unpinnable-by-construction, 4
remaining with obstacles recorded:
- `reclaimHoldColumns` / `reclaimReviewColumns` — both audit paths emit
the same `branch:auto-reclaim` type, differing only by a `trigger`
string the branch-level scan also produces.
- `completedHoldColumns`, `wsDoneColumns`, `doneMetaColumns` and others
have since gone green from other workers' PRs.

## Verification

`pnpm test:gate` 13 + 161 + 499 + 71 · lint · census `--strict` ·
fnxc-dates (TZ=UTC) — green. Comment-only change.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added an explanatory note clarifying parked-column handling and its
connection to future parked-link preservation behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 11:05:17 -07:00
gsxdsm
c027a72d23 fix(engine): a live agent lost its task link on a renamed hold lane (found while testing, not converting) (#3208)
**A defect, not a coverage gap** — found while trying to pin
`agentParkedColumns` from the #3115 map.

`recoverDriftedAgentTaskLinks` enters its preservation branch on a
**resolved** question (`isPreWipColumn`) and then decided it on a
**literal** one: `evaluateParkedAgentTaskLink` was called without
`parkedColumns`, so parked-ness fell back to `todo`/`triage`. The
sibling sweep passes the resolved set; this call site did not.

On a renamed board: the card is pre-wip, `isParkedTaskColumn` says no,
`shouldPreserveParkedLink` is false, and **an agent with a fresh
heartbeat run has its task link cleared — while it is working.**

`task-agent-sync.ts` predicted this in writing when the parameter was
introduced:

> *"turning a stale-link bug into a dropped-link bug, since the card
would be treated as unparked and its live agent link cleared"*

That is what an unpassed optional lane parameter costs — the same
missed-pair shape as #2956, #2963 and #3186.

## The test needed two fixture corrections, both caught by failing

- the renamed IR had **no hold column**, so no card could be pre-wip at
all;
- the **per-task selection readers** were missing, so `isPreWipColumn`
resolved the default IR and the branch was never entered.

Either alone made the case pass while exercising nothing. Third time
today a fixture passed for a reason unrelated to the resolver — a
pattern, not an anecdote.

## Measured

15 pass; removing `parkedColumns` from the call fails exactly this case.

## Note on how it was found

I had discarded a probe at this sweep earlier for failing to
discriminate. Coming back with the obstacle understood — the fixture
must reach the branch the resolver gates — turned a coverage miss into a
defect find. The five discards this session were not wasted; three of
them named the obstacle that made a later attempt work.

## Verification

`self-healing-agent-link-drift` **15 passed** · `pnpm test:gate` 161 +
13 + 499 + 71 · lint · lane-wiring — green.
2026-07-31 10:49:57 -07:00
gsxdsm
8661b739ff fix(scheduler): a board with TWO complete columns left dependents waiting forever (#3210)
## The defect

On a board declaring more than one complete-trait column — a merged lane
and a shipped lane, say — a card landing in the **second** one was never
recognised as finished, so nothing unblocked its dependents. Silent: no
error, the dependent just waits.

Two problems, the same shape:

1. **`TaskMoveLanes` carried one id per role.** That is right for
*"where should this card go"* and wrong for *"is this column one of the
finished lanes"*, which is a **membership** question. The payload could
not express such a board at all.
2. **`mergeParkedColumns` rebuilt `terminal` as `new Set([complete,
archived])`** — discarding `base.terminal`, which the sync IR path had
already resolved correctly, and narrowing a membership set back to
first-match-per-role.

Point 2 contradicted the note sitting directly above it in the same
file:

> `terminal` is a MEMBERSHIP set, and it is not the same question as
`complete`/`archived`. […] A workflow may declare more than one
complete-trait column […] and `to === parked.complete` sees only the
first and silently skips the rest.

The reasoning was already written down. The overlay added later didn't
honour it.

## Fix

`TaskMoveLanes.terminal?: readonly string[]`, filled from
`columnsWithFlag(ir, "complete"|"archived")` rather than the first-match
`resolveLifecycleColumns`, and the merge is now a **union** of base,
payload, and the single lanes.

Optional, so all 12 emitters and every listener keep compiling — a
listener that ignores it is exactly as correct as before. Union is the
direction `scheduler.ts` already argues for at line ~422: a superset
costs one extra query; a subset **silently withholds work from a
finished card**.

`complete` deliberately stays first-match — a set would be the wrong
shape for a move *target*. Both questions now coexist rather than one
replacing the other.

## How it was found, and what it corrects

Supplying `task:moved` lanes fixed every *other* renamed-board case in
`scheduler-renamed-hold-events` — measured **10 passed / 1 failed** —
and left exactly this one broken. That same measurement is why I
narrowed my earlier claim on #3082: the other behaviours were never
broken in production, because all 12 emitters already carry lanes. This
is the residue that was genuinely broken.

## Tests — both with anti-vacuity controls

| control | result |
|---|---|
| revert `toTaskMoveLanes` | **2 of 4** core tests fail (the terminal
pair) |
| revert the scheduler union | the new engine test fails, **and only
it** (1 failed / 11 passed) |
| both restored | 4 passed, 12 passed |

The 2 core tests that pass either way are shape invariants asserted on
purpose (`complete` must stay first-match; a column-less IR returns
`undefined` rather than an invented lane) — flagging that so the control
isn't read as 4-of-4.

The pre-existing scheduler case emits **without** lanes, which no
production emitter does, so it exercises the sync fallback. The new one
emits `toTaskMoveLanes(ir)` — the shape that actually ships.

## Measured

| check | result |
|---|---|
| `@fusion/core` / `@fusion/engine` tsc | exit 0 / exit 0 |
| eslint | clean |
| `census --strict`, `check:fnxc-future-dates`, `check:changesets` |
exit 0 |
| every `TaskMoveLanes` consumer | 24 passed |
| `pnpm test:gate` | **exit 0 — 744 tests, up 12** |

## Census

No guard converted; this is a payload-shape fix. Backlog unchanged at
11, all deferred.
2026-07-31 10:47:15 -07:00
gsxdsm
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>
2026-07-31 10:47:03 -07:00
gsxdsm
e9a57ca8ba docs(learnings): a falling count is not evidence that anything changed (#3203)
## The metric counterpart to #3200

#3200 (merged) records the **shapes** an inert conversion takes, and its
grammatical tell is the portable one: *if a claim can be written without
running anything, it has not been tested.* I offered this material there
and said I would write it as a sibling rather than bloat that doc; it
merged without it, so here it is.

That doc is about **claims**. This one is about **numbers**.

## The tell

**A count that falls is not evidence that anything changed.** Every gate
here reports a number, and a number goes down three ways — work
happened, the code got denser and the scan stopped matching, or someone
lowered the allowance. Only the first is progress, and from inside the
check all three look identical.

Four times in one phase:

| what moved | what actually happened |
|---|---|
| census 12 → 2 (`scheduler.ts`, #3051) | ten guards routed through
`resolveTaskWorkflowIrSync`, which answers with the DEFAULT board under
PostgreSQL. Byte-identical. Refuted in #3058 |
| census 45 → 44 (`triage.ts`, #3114) | converted the exact arm #3108
flagged hours earlier. #3126 reverted it — three PRs for one line |
| ratchet 20 → 15 | not a conversion: #3065 rewrote `a === x \|\| a ===
y` as `set.has(a)`. It printed *"total fell — re-record"*, which would
have **permanently retired live guards** |
| ratchet 22 → 9 | `scheduler.ts` reported **0** while 13 guards still
fell back to the default board |

Rows three and four are the dangerous shape: **the gate went quiet
exactly when someone improved the code**, and the remedy it suggested
was to lower the allowance.

## Also recorded

- **One defect, four spellings** (#3062, #3068, #3079, #3181) — each fix
correct about the shape in front of it and blind to a respelling. The
lesson is not "write a better regex": enumerating consuming syntax is a
losing game, and the durable form keys on the *source*.
- **An instrument that runs nowhere and one that cannot fail are the
same defect.** `check:inert-sync-lanes` was invoked by nothing for six
PRs; `check:quarantine-ledger` ran nowhere *and* omitted `--strict`, so
wiring it alone would have been theatre. Includes the mechanical audit
that finds both.
- **Base drift makes branch numbers incomparable** — three false alarms,
one of them mine, from comparing against a remembered figure. The
procedure that works is extracting both scripts and running them against
one tree; that is how #3169 and #3181 were shown additive (22 = 13 + 7 +
2), which decided merge order and collapsed one into six lines inside
the other.
- **A pick-work list at 100% false positives**, because under-reporting
deferrals is the direction that manufactures the #3108 → #3114
collision.

## Every claim is a measurement

No mechanism here is derived from reading. Nine PRs cited, each the one
that produced or refuted the finding — including the ones where I was
wrong: a stale number I mistook for a regression, and two future-dated
stamps of my own that the full ratchet set caught before they shipped
(one earlier one it did not, and that broke `main`).

## Census before / after

```
before:  COLUMN guards (the backlog):   12
after:   COLUMN guards (the backlog):   12
```

Docs only.

## Verification

`test:gate` exit 0 · `fnxc-future-dates`, `lifecycle-columns`,
`inert-sync-lanes`, `quarantine-ledger`, `inert-flag-seams`,
`lane-wiring`, `sql-column-literals` — all exit 0 · `pnpm lint` clean.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added guidance for interpreting workflow metrics and avoiding
misleading conclusions from declining counts.
* Documented detection blind spots, branch comparison issues, false
positives, and validation procedures.
* Included a practical checklist for reviewing metrics, quality gates,
comparisons, and potential conversions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:44:21 -07:00
gsxdsm
bad39e2ca3 test(core): ledger the legacy-id collections that gate a live column — the class the census cannot count (#3209)
## What

A population ratchet over **legacy-id collections consulted against a
live column value** (`SOME_SET.has(task.column)`), recorded as 23 sites.

## Why

The census scans `===`/`!==` comparisons. A Set or array literal is a
**definition**, so no census run has ever pointed at one. Three
found-by-hand defects came from that blind spot:

| collection | symptom |
|---|---|
| `GITHUB_TRACKING_EDITABLE_COLUMNS` | operator could not toggle GitHub
tracking **at all** on a renamed board — no error, affordance absent
(#3149) |
| `TIME_INDICATOR_COLUMNS` | wrong elapsed-time indicator on cards |
| `BLOCKER_ESCALATION_COLUMNS` | escalation skipped renamed lanes |

#3149 enumerated the population by hand and concluded *"this is where
the remaining renamed-board defects actually live."* A number in a PR
body rots. This is that enumeration as a ratchet.

## Census

**Unchanged — `AVAILABLE: 0` before and after, 12 documented deferrals
both sides.** This PR converts nothing. It ratchets a class the census
*structurally cannot see*, which is the point: the backlog reading zero
has never meant the lane vocabulary is fully converted, only that the
measurable part is. Recording that plainly instead of claiming a delta
this change does not produce.

## What it claims, and what it deliberately does not

It claims the **population** is the recorded set. It does **not** claim
each site is correct — 20 of the 23 are #3149's assessment ("most are
already correct, either no-flags fallbacks or seed-then-add resolved
sets"), and I did not re-verify them. Blessing sites I have not read is
how a ledger becomes a list of things someone once glanced at. A new
entry fails the test and a human reads that **one** site; that is the
entire mechanism.

## My own detector's pick-work list was 100% false positives

Measured, and the reason this ships with **no candidate list**. The
heuristic "no role-helper call in the file" flagged three sites; all
three were fine:

- `agent-role-policy.ts:32` — a documented **FLAGGED, NOT FIXED**
deferral with its reasoning recorded
- `DocumentsView.tsx:88` — already converted, flags-first; the flags
arrive as a threaded **object**, so a scan for resolver *calls* cannot
see the conversion
- `agent-assignment.ts:118` — a `DELIBERATE-LITERAL` fallback behind an
injected `countsAsAssignmentLoad` callback, reviewed `2026-07-31-05:40`

That is the same failure `--triage`'s pick-work list had before #3194
fixed it, from the same cause: **inferring "unexamined" from the absence
of a pattern rather than from evidence.** A detector that cannot
distinguish "not yet looked at" from "looked at and settled" must not be
pointed at a work queue. It can still hold a population steady, which is
all this does.

## Verification

Mutation-verified in **both** directions — a ledger fails by missing
additions *or* by keeping ghosts:

```
### baseline                                        Tests  4 passed (4)
### MUTATION 1 — new unrecorded gating collection
+   "packages/engine/src/worktree-pool.ts :: NEW_LANE_GATE",
                                                    Tests  1 failed | 3 passed (4)
### MUTATION 2 — recorded site vanishes (ghost)
+   "packages/engine/src/worktree-pool.ts :: managedRenamed",
+   "packages/engine/src/worktree-pool.ts :: managed",
                                                    Tests  2 failed | 2 passed (4)
### restored                                        Tests  4 passed (4)
```

Two anti-vacuity cases guard the detector: it still finds the
collections whose defects motivated the file, and it does **not** claim
plain comparisons (asserted against `self-healing.ts`, dense with column
comparisons and no gating collection) — pulling those in would
double-count a class that already has a gate.

## Flagged, not guessed

- **Line numbers are excluded** from ledger entries — they drift with
unrelated edits and would fail this test for reasons that are not about
lane vocabulary.
- **Comments stripped before scanning:** `TaskDetailModal.tsx` and
`TaskCard.tsx` both quote their own collection by name in FNXC notes
explaining the bug it caused. Counting prose would fire the ledger on
the files that document the hazard most carefully.
- **Stated reach limits** (in-file): only *named* collections consulted
as `.has`/`.includes`; the argument must mention column/lane;
property-reached collections are missed. A miss is a site nobody is
watching — not a false green on a listed site.

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>
2026-07-31 10:44:08 -07:00
gsxdsm
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.
2026-07-31 10:38:48 -07:00
gsxdsm
23403e1426 test(engine): pin the agent sweep's terminal skip (21st resolver, after a corrected fixture) (#3206)
`agentLinkTerminalColumns` was uncovered on the #3115 map. The existing
case uses `todo` and `in-progress`, so the terminal skip is never the
deciding branch.

## The corrected fixture is the lesson

An earlier attempt of mine put the card in a renamed **wip** lane and
stayed green when blinded — **correctly**. Such a card is caught by the
wip∪review set first, so the terminal resolver never decides anything.
The card has to rest in a renamed **complete** lane for this guard to be
the one that matters.

That is the same class as my two discards on the branch-conflict sweeps:
**the fixture has to reach the branch the resolver gates.** A test can
exercise the sweep, pass, and still never touch the line under test.

## What the literal costs

A finished task's agent is not skipped, so the sweep **unlinks an agent
from a task that completed normally** — churn on a row that needed no
repair, and a lost link if that agent was about to be reused.

## Observable

Asserts `syncExecutionTaskLink` — the action the guard prevents — rather
than a return value, per the rule from #3202.

## Measured

421 pass; blinding `agentLinkTerminalColumns` fails exactly this case.

**21 of 26 pinned** across 20 merged PRs.

## Still open, with the obstacle recorded

`reclaimHoldColumns` / `reclaimReviewColumns` resist: both audit paths
in that sweep emit the same `branch:auto-reclaim` type, differing only
by a `trigger` string the branch-level scan also produces, so no
observable I found isolates the bucket resolvers from the branch scan.
`agentParkedColumns` needs a fixture where parked-ness changes the
outcome — mine forced the proof true via a fresh run.

## Verification

`self-healing.test.ts` **421 passed** · `pnpm test:gate` full pass ·
lint — green.
2026-07-31 10:33:25 -07:00
gsxdsm
5c5f6d8155 fix(core): mark the two archived STATE sites at the site — converting them destroys live work (#3157)
The LANE/STATE triage (#3154) found that **two of the eight** Drizzle
`archived` sites are STATE markers that must never be resolved. That
classification lived only in `archived-column-gate-parity.test.ts`.

A coordinated three-encoding conversion **edits these files**. A
converter working file-by-file sees the same `eq(column, "archived")`
shape as the six LANE sites, with nothing in front of them to tell the
two apart.

So the markers go at the sites.

## `task-mutation-ops.ts` — `cleanupArchivedTasksImpl`

Enumerates rows Fusion itself archived, then **removes their
directories**.

Widening it to the resolved archived-lane set would feed cards **merely
resting in a board's archived-trait lane** into a filesystem delete.
This is the only site in this family where a wrong conversion **destroys
work** rather than hiding an affordance.

## `async-self-healing.ts` — `listSoftDeletedColumnDriftCandidates`

Finds soft-deleted rows whose column **drifted** from the marker they
are supposed to carry. Resolving it would classify a soft-deleted row in
a renamed archive lane as drift and "repair" a row that is already
correct.

## Why this is defensive rather than cosmetic

The triage exists to make the conversion safe. A classification the
converter **cannot see while editing the file** does not do that — it
only helps someone who happens to read the gate's test file first, which
is not how a file-by-file sweep proceeds.

Both are marked DELIBERATE-LITERAL with the reason and a pointer to the
parity test holding the full eight-site split.

## Measured

- Comment-only.
- Parity test **2/2**; archive + soft-delete suites — **5 files / 15
tests pass**.
- `tsc --noEmit -p packages/core` clean; census `--strict` and
`check-sql-column-literals` clean.
- **No census movement** — a DELIBERATE-LITERAL marker on a STATE site
is a classification, and these were never counted as lane debt.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:30:41 -07:00
gsxdsm
dd3bf6a764 docs(core): the archived TS remainder is EMPTY — enumerated, closing the triage (#3171)
#3156 sampled the TS inventory and said the conversion is *"the six LANE
Drizzle sites plus whatever small TS remainder is neither a fallback arm
nor a sentinel"*.

**That remainder is zero.** I left three sites unchecked when I wrote
it. All three are fallback arms:

| site | shape |
|---|---|
| `live-agent-count.ts:164` | `task.columnTerminalKind ?? (task.column
=== "done" ? … )` — the resolved value wins via `??` |
| `store.ts:1972` | `if (!lanes) return dep.column !== "done" && …` — an
explicit no-metadata branch |
| `branch-and-pr-entities.ts:525` | `lanes === undefined ? task.column
=== "archived" : task.column === lanes.archived` |

With the previously classified entries, **every** site in
`AUDITED_TS_SITES` is now accounted for as a fallback arm, a
STATE/sentinel comparison, or a converted guard's retained literal.
**None is an unconverted LANE guard.**

## So the cluster is done

"52 sites across three encodings" is fully triaged, and the convertible
work is the six Drizzle LANE sites plus the log-entry gate — all
additive, so the inventories never moved.

What the gate now protects is a population of **fallback arms and STATE
markers**, which is exactly what it should protect: each is the
documented answer for a caller that supplies no resolved set, or a
marker that must never be resolved.

A future **drop** in any of the three counts means someone removed a
fallback or converted a STATE site — both regressions. That is the check
this file was built to make, and is now the only check it needs to make.

## Enumerated, not sampled

I sampled this inventory twice and each pass changed the size estimate —
first "52 sites, real blast radius", then "six plus a small remainder".
A third estimate would have been worth less than a complete count, so
this pass covers every entry.

That is the honest close: the number stopped moving because I stopped
guessing at it.

## Measured

- Comment-only; parity test **2/2**.
- `tsc --noEmit -p packages/core` clean; census `--strict` clean. **No
census movement.**

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:30:30 -07:00
gsxdsm
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.
2026-07-31 10:30:16 -07:00
gsxdsm
25fa5e7c44 fix(core): the log-entry archive gate, converted — the parity objection is met, not bypassed (#3165)
I converted this in #3110, the parity gate failed, and I reverted it.
**The gate was right** — and the reason was subtler than "one encoding
moved". Knowing it is what makes this conversion possible.

## Why the first attempt failed

My version hoisted the comparison onto a local:

```ts
const pgRowColumn = String(pgRow.column ?? "");
const rowIsArchivedLane = archivedLanes ? archivedLanes.has(pgRowColumn) : pgRowColumn === "archived";
```

That gate's TS scan keys on the **property** being named `column` —
deliberately, because the receiver is variously `task`, `row`, `dep`,
`t`. Losing the `.column` access dropped the TS count while SQL and raw
held steady, which it reads as divergence.

**Behaviourally identical, structurally invisible.** Same failure mode I
hit from the other direction in #3163, where I collapsed a Drizzle
fallback into a string array.

## The fix

Keep `pgRow.column === "archived"` **verbatim** as the fallback; add the
resolved path in front of it. No encoding's count moves, an unwired or
degraded caller behaves exactly as before, and the gate is **satisfied
rather than worked around** — the same additive shape as the six Drizzle
LANE sites (#3160, #3162, #3163).

## What it fixes

A LANE question: *"is this row in the board's archive lane, so logging
is read-only?"*

Against the literal, a card the operator filed away on a renamed board
kept **accepting log writes** — new activity accruing on closed work.
`deletedAt` covers the soft-delete half, which is why the gap is narrow
and why it stayed invisible: the common path is soft-delete.

## The recorded omission is retired properly

`log-entry-archived-lane-gate.test.ts` carried the renamed case as a
**deliberate omission** with its reason. It is now the first case in the
file, and the note explains why the earlier judgement changed rather
than quietly disappearing — a deferral that vanishes without explanation
is how the next reader loses the thread.

## Measured

- **3/3** in that file (renamed case added); parity test **2/2**,
inventories unmoved.
- **MUTATION**: dropping the resolved branch fails the renamed case and
leaves the legacy **control** and the live-lane **negative** green.
- log-entry / archived / audit suites — **3 files / 9 tests pass**.
- `tsc --noEmit -p packages/core` clean; census `--strict`,
`check-sql-column-literals` clean.
- `check-fnxc-future-dates` is red on `main` from `task-update.ts`
(another lane's stamps), not from these files.

## Census

**Unchanged** — the literal remains the fallback arm, by design.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:27:31 -07:00
gsxdsm
762d232ad6 test(core): ratchet the sentinel-task-id argument at zero — the third inert-conversion mechanism (#3204)
## What

A test-only zero-population ratchet: no task-scoped lane resolver may be
called with a **string literal** where a row id belongs.

## Why

`resolveTaskLifecycleColumns(store, taskId)` and its siblings resolve
the workflow bound to *that task*. Hand one a literal and there is no
task to read a selection for, so the resolver falls back to the
**default board** and answers with full confidence. The call
type-checks, reads as a finished conversion, and is correct on every
board Fusion ships — because the default board is the answer it returns.

**This shipped.** `triage.ts`'s startup sweep called
`resolvePlannerLanes(this.store, "")` and built its swept-column set
from the result (#2806 measured it, #3201 fixed it). It was a
*sweep-wide* defect rather than a per-card one: it resolved once for the
whole board and could not be right for any workflow but the default, so
a card parked in a renamed hold column with a stale `planning` status
was never swept and held a planning admission slot permanently.

Note what this means for the other two inert mechanisms' fixes —
**making the resolver async would not repair it**, because the defect is
the argument, not the resolver.

## Why a guard and not just the existing E2E

`workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts` covers the **one**
triage site and lives in the `.pg` lane, so it is skipped whenever no
PostgreSQL is reachable — including the merge gate. The defect is the
*argument*, which makes it visible in source text with no database, no
running engine, and no knowledge of what the resolver does.

## Census

**Unchanged — 0 guards before, 0 after.** This PR converts nothing; it
is a ratchet over a class the census structurally cannot see (the census
scans column literals, not resolver arguments). Recording that plainly
rather than claiming a delta this change does not produce.

Population of the guarded class is **zero today** — the only textual
match in the tree is prose in `triage.ts` documenting its own fixed bug.
A zero-population ratchet is the instrument here, not a weakness: it
cannot fail until someone reintroduces the defect, and it costs one
source scan.

## Verification

**Mutation-verified, not asserted.** Re-adding the exact shipped shape
to a real production file:

```
+   "packages/engine/src/replan-target.ts:188 — resolvePlannerLanes",
 Tests  1 failed | 3 passed (4)
```

Restoring the file returns it to `4 passed`. Working tree left clean.

Three anti-vacuity cases carry the file, because a scan that reports
success by finding nothing is otherwise indistinguishable from a broken
scanner:
- the matcher **does** fire on the historical text
(`resolvePlannerLanes(this.store, "")`);
- it does **not** fire on the ordinary shapes that fill the codebase
(`task.id`, `taskId`, `row.id`) — a matcher flagging everything would
pass the case above while being unusable;
- the walker still reaches production source (>20 real
`resolveTaskLifecycleColumns` call sites), which is what makes the zero
a measurement rather than an empty scan.

## Flagged, not guessed

- **Comments are stripped before scanning**, and here that is required
rather than tidy: `triage.ts` quotes the offending call verbatim to
explain the hazard. Counting it would make the guard fire on the file
that correctly documents the defect, training readers to silence the
guard instead of heeding it.
- **`resolveReboundTarget(ir)` / `resolveLifecycleColumns(ir)` are
deliberately excluded** — they are IR-scoped and take no task id;
including them would flag correct code.
- **Known limit, stated in the file:** a sentinel arriving through a
*variable* (`const id = ""; resolve(store, id)`) is invisible to a text
scan. The literal form is what shipped and what the next person is most
likely to write; the variable form still needs the `.pg` E2E. Two
instruments, different reach — not full coverage of the class.

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>
2026-07-31 10:27:18 -07:00
gsxdsm
40e64468d2 fix(dashboard): GitHub tracking was unreachable on a renamed board — a defect class the census cannot see (#3149)
The census backlog is verified-exhausted (12 guards, every blocker
re-checked in #3082). This is from the class **the census structurally
cannot count**, and it is a real capability loss.

## The defect

```ts
const GITHUB_TRACKING_EDITABLE_COLUMNS: Set<ColumnId> =
  new Set<ColumnId>(["triage", "todo", "in-progress", "in-review", "ideas"]);

function canTaskEditGithubTracking(column, workflowId) {
  return GITHUB_TRACKING_EDITABLE_COLUMNS.has(column) || workflowId === CODING_IDEAS_WORKFLOW_ID;
}
```

No resolved branch, no flags fallback. On a board whose lanes are
renamed this matched **nothing**, so the helper returned `false` for
every task and `showGithubTrackingSection` hid the section outright.
**The operator could not turn GitHub tracking on or off** — no error, no
explanation, the affordance simply absent. The only thing keeping it
reachable was the unrelated `builtin:coding-ideas` escape hatch on the
right-hand side.

## Why no gate saw it, and why this class matters now

The census counts **comparisons** against legacy ids. This is a **Set
literal — a definition** — consulted with `.has()`. Nothing in the
backlog ever pointed here. It is the same blind spot that hid
`TIME_INDICATOR_COLUMNS` and `BLOCKER_ESCALATION_COLUMNS`, both of which
were also found by hand rather than by any gate.

I found it by scanning for legacy-id **collections that gate a live
column value**, rather than for comparisons: **19 such sites** across
the tree. Most are already correct — either `if (!flags) return
LEGACY_…has(column)` fallbacks, or seed-then-add resolved sets
(`agent-reflection.ts`, `ephemeral-worker-manager.ts`). This one had
neither.

With the comparison backlog at 12 and every remaining entry blocked or
documented, **this is where the remaining renamed-board defects actually
live.**

## The fix

The set's meaning is "not finished" — every lane except complete and
archived — which is what the roles now express:

```ts
if (workflowId === CODING_IDEAS_WORKFLOW_ID) return true;
if (!columnFlags) return GITHUB_TRACKING_EDITABLE_COLUMNS.has(column);   // unchanged pre-fetch
return !isCompleteColumnRole(columnFlags, column) && !isArchivedColumnRole(columnFlags, column);
```

The caller passes `detailColumnFlags` — the **task-identity-guarded**
value. `workflowMoveMetadata` outlives a task switch, and this file's
own `2026-07-30-17:30` note records **six** review findings from
consumers that read around that guard. Passing the unguarded value would
answer about the previous card's workflow: worse than the legacy
fallback, because it is confidently wrong rather than merely stale.

## Verification

| | result |
|---|---|
| suite | **3 passed** |
| mutation (restore the literal) | **1 failed \| 2 passed** — the
renamed-WIP case only |
| dashboard `tsc -p tsconfig.app.json` | **0 errors** |
| census `--strict` | exit 0, **unchanged** — this class is invisible to
it |

The test drives the **production path** (`fetchBoardWorkflows` →
`resolveTaskWorkflowMetadata` → `currentColumnFlags`) rather than
injecting flags as props, so it covers the producer as well as the
consumer. `building` and `shipped` collide with no legacy id, so a
surviving `.has(column)` cannot pass by luck; the `todo` control pins
that the default vocabulary is unaffected, and the renamed-COMPLETE
negative pins that the fix does not hand editability to a finished card.

Note: `tsconfig.test-check.json` fails on `main` as well — pre-existing,
and **zero** of its errors come from this branch's files.

## Suggested follow-up

The remaining 17 collection sites deserve the same pass, and the scan
that found this should probably become a gate — a census that counts
comparisons will keep reporting zero while this class quietly grows. I
have not built that here because the existing gates already need
`#3136`'s attention first, and adding a sixth advisory check that nobody
blocks on would repeat the pattern this session keeps running into.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:24:34 -07:00
gsxdsm
d6079970e8 fix(self-healing): 18 recovery rebounds hardcoded todo and THREW on a renamed board (#3150, first slice) (#3152)
First slice of #3150. `self-healing.ts` held **26** `moveTask` calls
with a legacy literal target; this converts the **18 `todo` rebounds**.

## Why this is worse than a guard, and documented already

`task-store/moves.ts` records it from a previous incident:

> `moveTaskInternal` **REJECTS** a target the workflow does not declare
(`TransitionRejectionError: unknown-column`) … completion handoff did
not silently no-op — it **THREW**.

Every one of these 18 is a **recovery**. On a renamed board they threw
instead of rebounding, so the strand each sweep exists to clear survived
*and* the sweep reported failure. The reliability layer meant to be the
backstop was the layer that broke.

## Why the census never saw it

It counts **comparisons** against legacy ids. A move target is an
**argument**. That is the third blind spot of the same instrument, and
all three have now produced real defects found by hand:

| blind spot | found this session |
|---|---|
| definitions | `GITHUB_TRACKING_EDITABLE_COLUMNS` — tracking
unreachable on renamed boards (#3149) |
| collections | swept: 30 sites, 29 already correct, 1 defect (the
above) |
| **targets** | **this** — 26 in one file, 31 tree-wide |

## Why 18 sites at once is safe

`resolveReboundTargetForTask` **degrades to `"todo"`** when no workflow
resolves, and `self-healing.ts` already used it at line 745. On every
board we ship, the resolved answer *is* `todo` — so default behaviour is
unchanged **by construction**, not by inspection. The control case pins
exactly that, and it is the reason this can land as one change rather
than eighteen.

## Scope, and what I deliberately did not touch

Converted: the 18 `todo` rebounds.

**Not** converted: the `done`, `archived` and `in-review` targets. They
need different helpers and genuine reasoning about which lane a
completion or an archive belongs in — converting them by analogy is
exactly the half-conversion this program keeps paying for. Sites with no
resolver in scope are unchanged.

The audit behind the split is in the commit: of 26 sites, 5 had resolved
lanes in scope, 4 had an IR, 17 had nothing — and `lanesOfReclaim`
returns **Sets**, which is the wrong arity for a target (a move takes
exactly one column, per the `moves.ts` note).

## Verification

| | result |
|---|---|
| engine `tsc` | **0 errors** |
| **all 43 self-healing suites** | **843 passed** |
| census `--strict` | exit 0, **unchanged** — invisible to it |
| `check-inert-sync-lanes` | exit 0 |
| differential | restoring the literal → **1 failed \| 1 passed**,
renamed case only |

The new test drives a **public entry point**
(`reconcileInReviewUnmetDependencies`, the FN-6793 contract) rather than
calling the helper directly, so it covers the producer path too.

One harness note worth keeping: the first version of the test failed
**upstream** of the target, because the sweep selects rows via
`resolveProjectColumnsForRoles` — a *project-level* resolver reading
`listWorkflowDefinitions`, not the task's own selection. Without that
mocked, the renamed card was never considered and the failure looked
like the fix not working. That distinction (project-level vocabulary vs
per-task IR) will bite the next slices too.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Tasks now move to workflow-specific rebound, completion, and archive
columns instead of fixed default destinations.
* Retrying and recovering tasks works correctly on boards with renamed
lifecycle columns.
* Added safe fallback behavior for workflows without custom lifecycle
settings.
* **Tests**
* Added coverage to prevent legacy hardcoded task destinations and
verify renamed-column recovery scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:24:22 -07:00
gsxdsm
4c62124589 fix(core): the last four archived LANE sites — the ones that already held a store (#3163)
Completes the six **LANE** sites from the triage (#3154). #3160 and
#3162 did the two predicate builders that needed threading; these four
already had `store` in scope, so each is a resolve-and-spread at the
site.

## What each fixes on a renamed board

| site | defect |
|---|---|
| `store.ts` revert lookup | a done/archived prior undo attempt kept
surfacing as an **open** undo task — the store-side twin of the
dashboard defect fixed in #3129 |
| `branch-group-ops.ts` | the near-duplicate marker cleanup found **no
live rows at all**, so stale markers survived. Its own header says stale
markers alter operator decisions |
| `branch-and-pr-entities:438` | the CREATE-time fingerprint duplicate
guard kept archived cards in the candidate set — a new task could be
refused as a duplicate of one already filed away |
| `branch-and-pr-entities:470` | recent-sibling lookup counted finished
siblings as candidates |

## The parity gate caught my first version — and it was right

I collapsed the fingerprint fallback into a **string array**
(`["archived"]`) and pushed `ne(col, lane)` in a loop. Behaviourally
identical, and it **dropped the Drizzle encoding's literal count**,
because the gate scans for the `ne(..., "archived")` *expression shape*.
TS and raw held steady, so the encodings diverged — precisely what that
gate exists to catch, catching it.

The fix: keep every fallback as a literal `ne(..., "archived")`
**expression** rather than data.

That is what makes these conversions **additive** — the resolved path is
added, the literal stays, no encoding's count moves, and an unconverted
board builds byte-identical SQL. Same property as #3160/#3162, now with
a demonstrated failure mode for getting it wrong. Worth knowing for
whoever does the remaining TS remainder: *behaviourally identical* is
not sufficient; the shape has to survive too.

## Measured

- Parity test **2/2**, inventories unmoved — the point.
- archived / branch / near-duplicate / merge-blocker suites — **6 files
/ 46 tests pass**.
- `tsc --noEmit -p packages/core` clean; census `--strict`,
`check-sql-column-literals`, `check-fnxc-future-dates` clean.

## Census

**Unchanged** — literals remain as fallback arms, by design.

## Where the cluster stands

All **six LANE** Drizzle sites are now converted (#3160, #3162, this).
The **two STATE** sites are marked in place and must never be converted
(#3157). What remains is the small TS remainder that is neither a
fallback arm nor a sentinel, identified in #3156.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:24:07 -07:00
gsxdsm
e1a01cb252 docs(dashboard): the research-modal archive filter is prop threading, not a data-fetch change (#3134)
Correcting the **shape** of the last census entry in this file. The
existing note sizes it as needing new data. It does not, and the
difference changes who can pick it up and for how much.

## What the note gets right

A per-**task** map (`columnFlagsByTaskId`) cannot help here. It is built
from board-resident rows, and the rows this filter cares about are
**archived** ones — exactly what a board map omits. Threading that map
would look converted and leave the case it exists for unresolved. That
reasoning is correct and I kept it.

## What it misses

This guard does not ask a per-task question.

*"Is `task.column` an archive lane"* is a question about a **column**,
and the answer lives in the workflow definition — a lane exists there
whether or not any row currently sits in it. **Archived rows being
absent from the board is irrelevant to a column-keyed answer.**

That map already exists on the board:

- `ListView.tsx:756` derives `columnFlagsById` (`ColumnId -> flags`)
from its workflow columns
- `useExecutorStats` takes the same shape

## So the real cost

`MainContent → ResearchView → this modal`, plus sourcing the column map
where MainContent renders ResearchView (it holds none today).

Three layers for one guard is a real cost and a fair thing to decline —
but it is a **different decision** from *"needs new data"*, and the two
have very different prices. The note as written would have the next
reader believe an API change is required.

## Left counted and unconverted, deliberately

A three-component prop chain wants to be someone's considered change,
not a drive-by on the last entry in a file — especially from me, at the
end of a long session where two rushed changes already went wrong.
Recording the corrected shape is the part that was cheap and wrong to
leave.

## Measured

- Comment-only.
- `ResearchView.test.tsx` — **27 tests pass**.
- `tsc --noEmit -p tsconfig.app.json` clean; census `--strict`,
`check-fnxc-future-dates` clean.

## Census

**No movement.** The entry stays, with an accurate price on it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:18:41 -07:00
gsxdsm
97b945f980 docs(engine): the scheduler flag's reason went stale, and two deferrals read as unexamined (#3142)
My own flag on these two literals went stale, in exactly the way I have
spent this session cataloguing in other people's notes.

## What the note said, and why it is now wrong

It said these two stay because converting them would be **inert** — the
sync resolver answers with the default board. True when written.

#3128 then converted the rest of this listener by deferring each resolve
into a `void (async () => ...)` block, which reaches the **async**
resolver and is genuinely correct. So async resolution *is* available
here now, and my stated reason no longer explains why these two are
different.

## The real reason, which #3128 itself states

Three branches down, in its own note:

> The `planningTaskIds.delete` stays SYNCHRONOUS — it is the
edge-trigger bookkeeping, and deferring it would let a second update
re-enter this branch.

Both remaining literals are that case:

| literal | why it cannot move behind an await |
|---|---|
| `failedTaskIds.add` | edge-trigger bookkeeping raced against
`moveTask` clearing the failure metadata — its own comment says so.
Deferring the add can miss that window. |
| PR-monitoring guard | it gates `getTrackedPrs()` /
`startMonitoring()`, where `tracked.has(task.id)` **is** the re-entrance
guard. Move the lane answer behind an await and two updates for the same
task can both pass that check before either starts — **double-starting a
monitor**. |

## Why the distinction is worth a PR

"Blocked on a resolver" invites the next person to wait for the sync
reader. What these actually need is somewhere to put the answer that is
**not behind an await** — the emitter-carried `lanes` #3109 added to
`task:moved`, whose extension to `task:updated` is measured as expensive
rather than impossible (#3123: 26 emit sites against 7, on the hottest
write path).

Those are different tickets with different owners. Leaving the wrong one
written down is how a blocker outlives its cause — the failure I have
now found in five separate notes this session, including two of my own.

## Measured

- Comment-only.
- `src/__tests__/scheduler*` — **14 files / 144 tests pass**.
- `tsc --noEmit -p packages/engine` clean; `check-inert-sync-lanes` and
census `--strict` clean.
- `check-fnxc-future-dates` is red from `main`'s own #3128 stamps —
**#3139** fixes that; this branch inherits it and does not add to it.

## Census

No movement. Both literals stay counted, now with the correct reason
attached.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:18:30 -07:00
gsxdsm
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>
2026-07-31 10:18:18 -07:00
gsxdsm
aef88a2976 test(engine): pin the PR-conflict sweep's worktree-owner index (20th resolver, after two discarded attempts) (#3202)
`prConflictWipColumns` builds the worktree-owner index behind
`ownedByOtherInProgressTask` — the guard that stops this sweep
**deleting a worktree another live task is executing in**. Keyed on the
id, that index is empty on a renamed board, so every worktree reads as
unowned.

## Two discarded attempts, and why they matter more than the fix

**1. Asserted `result.outcome !== "reclaimed"`.** It failed *with the
fix in place* — `reclaimed` is reachable through a second path this
guard does not gate. **An outcome assertion cannot isolate a guard in a
sweep with several routes to the same outcome.** That also explains my
earlier discard on `reclaimSelfOwnedBranchConflicts`, which has the same
shape.

**2. Asserted `removeWorktree` was not called — but overrode the task's
branch while leaving its id.** The reclaim path also requires
`branchOwnerTaskId === taskIdUpper`, so the branch was never reachable
and the case passed **blinded**: vacuous for a reason that had nothing
to do with lanes.

The shipped version asserts `removeWorktree`, which runs **only** on the
guarded branch and is the irreversible part, and keeps the default
id/branch pair so that branch is genuinely reachable.

## Measured

16 pass; blinding `prConflictWipColumns` fails exactly this case.

**20 of 26 pinned** across 19 merged PRs.

## Generalisation

For sweeps with multiple paths to one outcome, the discriminating
observable is a **path-specific side effect** — `removeWorktree`, a
`task:reconcile-*` audit type, a specific `reason` string — not the
return value. Every case I landed today that stuck used one; both
discards asserted a return value.

## Verification

`self-healing-pr-conflict` **16 passed** · `pnpm test:gate` 13 + 161 +
487 + 71 · lint — green.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved protection for active worktrees during pull request conflict
recovery, including tasks in renamed workflow lanes.

* **Tests**
* Added regression coverage to verify that worktrees owned by other
tasks are not removed incorrectly.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 10:15:21 -07:00
gsxdsm
5f97fbcb06 docs(learnings): a blocker described four times, wrong twice — instrument before you file (#3200)
Records the method that moved a `triage.ts` site flagged unconvertible
for four cycles. The method transfers; the three conversions do not.

## Four mechanisms, split by derivation rather than care

| # | claimed mechanism | derived from | held? |
|---|---|---|---|
| 1 | merged intake/hold vocabularies | reading | no |
| 2 | orphan arm scoped to `source === "selection"` | reading + one test
run | partly |
| 3 | provenance verifies by `ir.id`, which builtins lack | reading a
**comment** | **no — filed as #3187, closed as wrong** |
| 4 | two test harnesses cannot answer a selection query | instrumented
isolation | **yes** |

(3) is the expensive one. The text I quoted was **historical prose
describing code that had been removed**, sitting directly above a
paragraph saying exactly that. I read a rationale as an implementation,
and it reached an issue other lanes could have acted on.

## The isolation took three runs

```
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
```

Change one variable, let the suite answer. Available from cycle one.

## Why this is not just "test more"

Every wrong mechanism was plausible, specific, and consistent with the
code as read. **Plausibility is what made them dangerous** — each was
good enough to write down, publish and act on. The failure mode is not
sloppiness; it is that a careful reading of a large file *feels* like
evidence.

The tell is grammatical: **a claim that can be written without running
anything is a hypothesis, not a measurement.** "This cannot be converted
because X" versus "reverting X fails these 3 of 8 cases."

## The corollary, including its negative result

Once the harness was the suspect, a class fell out: a test that stubs a
reader **broken in production** proves the call site's logic while
unable to see that production resolves nothing. Eight files stubbed
`resolveTaskWorkflowIrSync` — one masking a live defect, four redundant
(#3198), one legitimate.

The doc also records that the obvious generalisation **fails**:
`getTaskWorkflowSelection` is equally degraded under PostgreSQL but
stubbing it masks nothing, because the resolver prefers the async twin
and both answer the same. The distinguishing property is that the reader
returns something *incorrect*, not merely *unused*. Written down so
nobody repeats the 120-file sweep.

Docs only; `check-fnxc-future-dates` exit 0.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
  * Added a case study for diagnosing an unconvertible workflow site.
* Documented controlled-run findings identifying the realistic mock
store as the cause.
* Clarified the difference between reading-based hypotheses and
instrumented evidence.
* Added guidance for distinguishing conversion, orphan-arm, and
mock-store issues.
* Recorded an audit of related test stubs, including redundant, masking,
legitimate, and unresolved cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:07:07 -07:00
gsxdsm
9234ca2402 fix(triage): the startup sweep resolved its columns from a SENTINEL task id — a characterization test already pinned it (#3201)
Fourth and last convertible site in `triage.ts`. This one needed a
different fix from the other three, and the codebase already said so.

## The defect

```ts
const sweepLanes = resolvePlannerLanes(this.store, "");
const sweepColumns = [...new Set(["triage", "todo", sweepLanes.intake, sweepLanes.hold])];
```

There is no task `""`. No selection can be read for it, no board
resolved — the lanes come back as the **default** board's and the union
collapses to the legacy pair `{triage, todo}`. On a renamed board the
sweep queries columns the card is not in, so its stale `planning` status
survives and it **holds a planning admission slot indefinitely**.

## A characterization test already pinned this, and called the fix
correctly

`workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts` documents it as a
third inert-conversion mechanism — *"inert by construction rather than
by environment"* — and its header says:

> making `resolvePlannerLanes` async would **NOT** repair this site,
because the defect is the argument, not the resolver

That is right, and it is why this fix differs from #3191 / #3193 /
#3195, which all used the async twin. Here the sweep has **no task to
resolve against** and wants every column playing these roles **anywhere
in the project** — so the correct resolver is
`resolveProjectColumnsForRoles(store, ["intake", "hold"])`, the same
helper `self-healing.ts` already uses for the same purpose.

The legacy pair stays in the union deliberately: the note at the site
explains that `triage` and `todo` must both be swept for pre-U11 and
Coding (Ideas) rows, and extra columns are free because the sweep only
**reads** and filters on `status === "planning"` first.

## The test is inverted, not deleted

It asserted `"planning"` survives — the bug. It now asserts the status
is cleared. Keeping the case with its original reasoning intact
preserves the file's record of what the defect *was*.

## Measured

| | result |
|---|---|
| broad suite (triage / planning / self-healing) | **76 files, 1302
tests passed** |
| differential | restoring the sentinel call → **1 failed \| 2 passed**
|
| `census --strict`, `check-fnxc-future-dates` | exit 0 |

**Inert count unchanged at 4 for `triage.ts`** — these lanes fed an
*array literal*, not a comparison, so the ratchet never counted them.
Third fix this session in that blind-spot class, stated so the number is
not read as the whole picture.

## What remains in this file

Two sites: the `task:moved` wake handler and the evacuation handler —
both **synchronous arrow callbacks** whose answers are consumed in-tick.
Genuinely blocked on the emitter-side work in #3082, with corroborating
evidence attached there.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:04:15 -07:00
gsxdsm
09edce2366 test(engine): cover #3112's executor lane conversion — three renamed-board cases main lacks (#3118)
**I flagged these four as unconvertible in #3104. #3109 landed and
dissolved both of my reasons, so the flag comes off.** Leaving a
"blocked" note standing behind a blocker that no longer exists is the
exact decay this program keeps paying for — I have now found three other
people's deferrals in that state this session, and I am not adding a
fourth of my own.

## Both blockers, and why they are gone

| my stated blocker | why it is gone |
|---|---|
| **A.** `trackTaskDisposal` writes `pendingTaskDisposals` in *this*
tick, and the wip branch reads that map to serialise a fast bounce
(FN-5256). Deferring branch selection to a microtask reopens that race.
| Reading `lanes` off the payload needs **no await**. The prologue stays
synchronous and the race stays closed. |
| **B.** It is an if / else-if **chain**, so the guards are entangled
and convert together or not at all. | They convert together here. |

#3109 made the **emitter** carry the resolved lanes, which is the one
route that removes the dilemma instead of trading one horn for the
other.

`lanes` is optional and fail-soft to `undefined` — *"unknown, never
legacy"* — so each guard keeps its literal as the fallback, following
the `mergeParkedColumns` convention #3109 established in `scheduler.ts`.
An emit path that cannot resolve is no worse than before.

## What it fixes

On a renamed board: execution never started on a move into the board's
own wip lane, terminal session release never ran on a move into its
archive lane, and neither `from` guard fired — so in-flight work was not
aborted when a card left implementation. Nothing errored; the engine
simply stopped reacting.

## Census

| | before | after |
|---|---|---|
| `executor.ts` | 4 | **0** |
| repo backlog | 45 | **41** |

## Measured

- 3 new cases added to the FN-7717 suite; file **13/13 pass**.
- **MUTATION**: restoring the `archived` literal fails the renamed case.
- **The paired negative is the load-bearing one.** `done`/`in-review`
deliberately keep their merge leases across the transition (FN-6736 /
Phase C–D). The renamed **complete** lane must therefore *not* release —
a conversion that released on every terminal-ish lane would satisfy the
positive case and quietly break the guarantee that file already exists
to protect.
- A **fail-soft** case pins that an emit carrying no `lanes` behaves
exactly as before.
- `src/__tests__/executor*` — **84 files / 853 tests pass**.
- `tsc --noEmit -p packages/engine` clean; census `--strict`,
`check-lane-wiring`, `check-inert-sync-lane-conversions`,
`check-fnxc-future-dates` clean.

## Note on #3104

That PR (merged) added the flag and the sharpened reasoning. This one
removes it. The reasoning there was correct at the time and is what made
it possible to check quickly whether #3109 actually addressed it — a
flag that states its blocker precisely is cheap to retire, which is the
argument for writing them that way.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:56:10 -07:00
gsxdsm
339d4451af test(engine): drop four redundant sync-reader stubs — they fed the broken reader the right answer (#3198)
Completes the audit filed as #3197.

## Why a stub here is not neutral

`resolveTaskWorkflowIrSync` answers with the **default board for every
task** in production — its selection reader returns `undefined`
unconditionally under PostgreSQL. A test that stubs it with a working IR
proves its call site's *logic* while being structurally unable to notice
that the real path resolves nothing. The suite stays green even if the
site goes inert, which is the failure this whole phase has been chasing.

## The audit, complete

Deleted each stub and checked whether the suite still discriminates:

| file | without the stub | verdict |
|---|---|---|
| `planner-lane-resolution` | 7 passed | redundant → **removed** |
| `triage-undeclared-column-rescue` | 7 passed | redundant → **removed**
|
| `recover-approved-intake-post-u11` | 6 passed | redundant →
**removed** |
| `workflow-scheduler-parked-columns-live-e2e.pg` | 2 passed | redundant
→ **removed** |
| `planner-lanes-async-resolution` | 1 failed | **legitimate** — the
stub is its subject |
| `scheduler-renamed-hold-events` | **3 failed** | **masking** — see
#3082 |
| `triage.test.ts`, `triage-release-renamed-hold` | — | resolved in
#3191 / #3193 / #3195 |

**Only the redundant four are touched.**

`planner-lanes-async-resolution` stubs the reader *deliberately*, to
contrast the two resolvers given the same store and task — removing it
would delete the point of the file. That is the case that makes this a
hand audit rather than a ratchet: a hit is not presumptively a defect.

`scheduler-renamed-hold-events` is left alone because its three failures
**are the finding, not the fix**. They correspond to the 13 inert guards
`check-inert-sync-lanes` counts in `scheduler.ts` — two independent
instruments agreeing that those handlers are green in tests and dead in
production on renamed boards. They live in synchronous `task:*`
listeners, so they need the emitter-side work in #3082, not a stub edit.

## Verification

- 4 files / **22 tests pass** without the stubs
- engine `tsc` 0 errors
- `census --strict`, `check-inert-sync-lanes`,
`check-fnxc-future-dates`: exit 0

The PG e2e was the one file I had marked unaudited when filing #3197; it
ran here and is included rather than left as an open question.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:50:01 -07:00
gsxdsm
1f5f296c11 test(engine): pin the workspace land-lease owner check (19th resolver) (#3199)
Nineteenth resolver from the coverage map on #3115. The terminal-owner
reclaim directly above it proves the behaviour with `column: "done"` —
**the id** — so blinding `leaseOwnerCompleteColumns` left all 20 tests
green.

## What the literal costs

That set is what `isWorkspaceOwnerLive` consults. Keyed on the id, an
owner resting in a renamed completion lane reads as **live**, so its
land lease is never reclaimed. The workspace repo stays leased by a task
that has finished, and **every later land against that repo waits behind
a phantom**.

## A note on how this resolver came to exist

`isWorkspaceOwnerLive` is one of the sites I flagged earlier today as
**unconvertible** — synchronous, no store handle, converting it would
mean a signature change I had excluded from that PR's scope.

Someone threaded the resolved set through its callers instead. That is
the better answer than either converting in place or leaving it, and
this test pins it — so the threading cannot be undone silently.

## Measured

21 pass; blinding `leaseOwnerCompleteColumns` fails exactly this case.

**19 of 26 pinned** across 18 merged PRs.

## Verification

`self-healing-workspace` **21 passed** · `pnpm test:gate` full pass ·
lint — green.
2026-07-31 09:47:08 -07:00
gsxdsm
73bff5f88c test(engine): pin the orphan-only sweep's project query — the harness could not see a filter bug (#3196)
Eighteenth resolver from the coverage map on #3115, and **why** it was
uncovered is the interesting part.

## A fake that ignores its own filter cannot see a filter bug

Every case in this file stubs `listTasks` to return the same task
**whatever column is asked for**:

```ts
(store.listTasks as ...).mockResolvedValue([failedReviewTask()]);
```

So the project query is never exercised. Blinding `orphanReviewColumns`
changes which column is *requested*, the fake answers identically, and
nothing fails. Eight passing tests, and the selection logic among them
was untested.

That is the same blindness the production sweep had — querying a column
that does not exist and finding nothing — reproduced in the harness that
was supposed to catch it.

## The case

`listTasks` honours the column, so a card resting in a renamed review
lane is found **only if the query asked for that lane**. Keyed on the
id, the sweep asked for `in-review`, got nothing, and a failed
orphan-only card **stayed failed forever**.

## Measured

9 pass; blinding `orphanReviewColumns` fails exactly this case.

**18 of 26 pinned** across 17 merged PRs.

## Generalisation worth checking elsewhere

Any sweep whose test stubs `listTasks` with a flat `mockResolvedValue`
has this hole. The fix is a store fake that filters on `options.column`
— the shape `self-healing-query-filter-blindness.test.ts` already uses.
I would look there first for the remaining map entries.

## Verification

`self-healing-orphan-only-scope` **9 passed** · `pnpm test:gate` full
pass · lint — green.
2026-07-31 09:38:54 -07:00
gsxdsm
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>
2026-07-31 09:36:03 -07:00
gsxdsm
f703b499d3 census: --triage's pick-work list was 100% false positives (#3194)
## Every entry on the pick-work list was already decided

`--triage` prints a list headed *"unexamined, by file — this is the list
to pick work from"*. On `main` it held 2 sites. **Both carry a full
deferral note:**

| site | what its note says |
|---|---|
| `triage.ts:793` | *"the arm goes back to the literal, which is
**honest about being one**"* — restored by #3126 after #3114 converted
it inertly |
| `scheduler.ts:1323` | *"the second of the two **honest literals** …
converting it here **would be inert**"* |

Neither phrasing was in the marker set. So the pick-list was **100%
false positives**.

## Why this direction of error is the expensive one

Under-reporting a deferral sends a worker at a site whose owner already
wrote down why it must not move. That is not a hypothetical failure — it
is the sequence that cost three PRs: **#3108** flagged a site with both
blockers named and a test behind it, **#3114** converted it anyway hours
later, **#3126** reverted it. A pick-list that nominates decided sites
reproduces exactly that.

Over-reporting has the opposite failure — it hides real work — so the
added phrases are specific to *declining a conversion* (`honest
literal`, `would be inert`), not generic words that appear in ordinary
notes.

## Verified in both directions

Not asserted. I resolved **each of the 12** remaining guards to its
individual marker:

```
 1. scheduler.ts:1238            FLAGGED
 2. scheduler.ts:1323            honest literal
 3. audit-ops.ts:231             Not converted
 4. lifecycle-ops.ts:667         do not convert
 5. merge-queue-ops-2.ts:53      FLAGGED
 6. moves.ts:346                 STAYS INLINE
 7. task-id-integrity.ts:445     Left counted
 8. ResearchTaskActionModal:66   SIZED, NOT
 9. TaskCard.tsx:406             FLAGGED
10. notification-service:1245    FLAGGED
11. self-healing.ts:6054         FLAGGED
12. triage.ts:793                honest about being one
```

**Unexamined is 0.** That is a meaningful state, not just a small
number: the conversion backlog is fully *triaged*, every remaining
literal has a recorded reason, and the next person to touch one is
reading an argument rather than guessing.

## Census before / after

```
before:  COLUMN guards (the backlog):   12
after:   COLUMN guards (the backlog):   12
```

Unchanged, as required — `--triage` is opt-in and moves no count and no
exit code. `--json` and `--strict` verified unaffected.

## Verification

`test:gate` exit 0 · `--strict` exit 0 · `--json` exit 0 · plus
`fnxc-future-dates`, `lifecycle-columns`, `inert-sync-lanes`,
`quarantine-ledger`, `inert-flag-seams`, `lane-wiring`,
`sql-column-literals` — all exit 0. One script; no production file
touched.

*Process note: my first draft of this PR carried a future-dated FNXC
stamp — the third time I have done that. I have switched to taking the
stamp from `date -u` per #3174 rather than typing it, and my pre-push
run of the full `pr-checks` ratchet set (not `test:gate` alone) caught
it before it left the branch, which is what that habit is for.*
2026-07-31 09:35:51 -07:00
gsxdsm
c8268a6454 test(engine): pin the temp-merge sweep's terminal grace (17th resolver) (#3192)
Seventeenth resolver from the coverage map on #3115. The two cases
around this one use `done` and `archived` — **the ids** — so blinding
`mergeTempTerminalColumns` left all 21 tests green.

## What the literal costs

The terminal check selects the **shorter grace**: a finished task's temp
merge worktree is reaped after `DONE_TASK_TEMP_WORKTREE_GRACE_MS`
instead of the full stale window. Keyed on the ids, a card in a renamed
completion lane never qualified, so its worktree lingered for the long
window — **disk held by work that already finished**.

## The second cost, which is why this asserts on the audit reason

Without the resolver the sweep eventually acts, but records `reason:
"stale"` instead of `"done-task-stale"`. So its own trail
**misattributes why it acted**.

A sweep that does roughly the right thing under the wrong label is the
kind of defect nobody notices until they are reading audit events during
an incident — and then the record actively misleads. Asserting only on
the file being gone would have passed either way.

## Measured

22 pass; blinding `mergeTempTerminalColumns` fails exactly this case.

**17 of 26 pinned** across 16 merged PRs. Also re-measured this turn:
`wsDoneColumns` and `doneMetaColumns` have gone green independently, so
the map keeps drifting as the fleet adds coverage — re-run before
picking the next entry.

## Verification

`self-healing-tempdir-sweep` **22 passed** · `pnpm test:gate` full pass
· lint — green.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed stale-task handling for tasks in terminal columns of custom
workflows.
* These tasks now correctly follow the done-task grace period and record
the appropriate audit reason.

* **Tests**
  * Added regression coverage to verify the corrected behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 09:24:35 -07:00
gsxdsm
f755f44734 test(engine): pin the completion fan-out's review dependent bucket (16th resolver) (#3190)
Sixteenth resolver from the coverage map on #3115.

`completedReviewColumns` reads the **dependents** resting in review when
a blocker completes. No case in this file put a dependent in a renamed
review lane, so blinding it left all 13 tests green.

## What the literal costs

A dependent sitting in review is never read, so its `blockedBy` is never
cleared when the blocker finishes. **It stays blocked by work that is
already done** — the most visible form of this class, because the board
simply stops moving.

## Measured

14 pass; blinding `completedReviewColumns` fails exactly this case.

## Note for anyone continuing the map

`completedHoldColumns` in this same sweep measured as **already
covered**, so only the review bucket was owed. Three buckets, three
resolvers, covered independently — the same per-resolver granularity
that found the missing halves in #3138 and #3186, where my own earlier
tests pinned one resolver of a pair and I had recorded the sweep as
done.

**16 of 26 pinned** across 15 merged PRs.

## Verification

`self-healing-completion-fanout` **14 passed** · `pnpm test:gate` full
pass · lint — green.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed task completion reconciliation for workflows with renamed lanes.
* Dependent tasks in review lanes are now correctly unblocked when their
blocker moves to a custom completion lane.

* **Tests**
  * Added regression coverage for custom workflow lane configurations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 09:13:21 -07:00
gsxdsm
da10131d3f test(triage): the mock store could not be ASKED for a selection — 231 cases exercised a shape production cannot produce (#3189)
`createMockStore` in `triage.test.ts` defined **neither**
`getTaskWorkflowSelection` nor its async twin. So
`resolveWorkflowIrForTaskWithProvenance` **threw** calling them and took
its catch branch, reporting `source: "default"` in the sense of *"the
lookup failed"*. Production stores always expose both readers — every
case in this file was exercising a store shape that cannot exist.

Returning `undefined` models the real answer: the store **can** be asked
and says there is no selection row, which is what a pre-U11 card
actually presents.

## Why it mattered

`triage.ts`'s post-U11 intake recovery gates on that provenance. A
*failed* lookup correctly refuses to claim a workflow lacks `triage`, so
the orphan arm stayed off and the recovery depended on
`resolvePlannerLanes` **failing** and falling back to legacy ids —
correctness resting on a resolver's failure mode.

In #3141 I measured the async conversion of that site as failing 13
cases and **twice reported it as a production constraint**. It was this
harness. That is the concrete cost of a mock that cannot answer a
question production always can.

## Behaviour-preserving on its own

**380 passed across 26 triage/recovery suites.**

## What this deliberately does NOT do

It does not convert the site. I prototyped the full unblock — a
`selectionAbsent` flag on the determinate `!workflowId` branch, its
single consumer, and the async conversion — and it works: the
previously-failing suite goes **237 passed**.

But with a realistic store the orphan arm starts firing for no-selection
rows, which changes recovery flow in **5
`triage-stuck-requeue-preserve-draft` cases** that currently assert the
refusing behaviour. Whether accepting a legacy `triage` row there is
correct is a lifecycle-semantics decision about migration, not a harness
fix. So it is reverted and reported rather than bundled.

Findings and the measured branch table are on #3141.

## One correction carried from this work

I filed #3187 claiming provenance verifies resolution via `ir.id ===
workflowId`, which cannot pass for builtins. **That was wrong** — the
live code uses a symbol marker, and the text I quoted was historical
prose describing what was removed. Closed with the measurement:

```
store with NO selection readers    -> source: default    (catch: could not ask)
store answering builtin selection  -> source: selection  ✓
```

That is the same class of error this PR fixes — reasoning from what
something says rather than what it does.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Improved workflow-resolution test coverage by supporting stores with
no selected workflow.
* Added synchronous and asynchronous test readers for workflow
selection.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:07:45 -07:00
gsxdsm
893b6421be test(engine): pin the contamination sweep's WIP bucket (15th resolver) (#3188)
Fifteenth resolver from the coverage map on #3115. Every case in this
file seeds the candidate in `in-review`, so only the review bucket was
exercised — blinding `contaminationWipColumns` left the file green.

## Why the WIP bucket matters

A card sent back for a fix **re-enters execution while its branch still
carries the foreign commits**, so contamination is discovered there as
often as in review. Keyed on the id, that bucket read nothing on a
renamed board and the card kept a branch built on someone else's work —
which is what this sweep exists to re-anchor.

## Two facts the fixture had to learn, both from failing first

- **This is an ACTION site and deliberately skips a card whose own board
cannot be read**, rather than guessing from the project union. A fake
with only `listWorkflowDefinitions` resolves the default IR, the card is
reported unclassifiable, and the case fails for a reason unrelated to
the resolver under test. The per-task selection readers are required.
- **The WIP bucket's predicate is not the review bucket's.** It
additionally requires `paused === true` with `pausedReason` of
`branch-cross-contamination` or `branch-conflict-unrecoverable`. A card
merely resting in the wip lane is not a candidate — the FN-5704
manual-review contract this sweep mirrors.

Neither is guessable from the resolver. Both came from the test failing
twice, and I would have shipped something that exercised nothing had the
first version passed.

## Measured

3 pass; blinding `contaminationWipColumns` fails exactly this case.

**15 of 26 pinned** across 14 merged PRs.

## Verification

`self-healing-foreign-only-contamination` **3 passed** · `pnpm
test:gate` full pass · lint — green.
2026-07-31 08:59:26 -07:00
gsxdsm
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 -->
2026-07-31 08:56:32 -07:00
gsxdsm
6bc90ccbe2 fix(core): allow-list the legacy workflow IR — it found a fourth bug my grep missed (#3185)
## The name is the defect

`BUILTIN_CODING_WORKFLOW_IR` reads like the default and **is** the
legacy workflow (`builtin:legacy-coding`). Post-U11 they differ by
exactly one column — `triage` — the one a caller most often wants
absent.

**Four bugs have come from reaching for it by name:**

1. two move-path resolvers disagreed on the no-selection default →
*"workflow move policy preflight is stale"* on every flag-on move
(recorded in `resolveDefaultWorkflowIr`'s own header)
2. the TUI board rendered a `triage` lane the default board lacks —
#3178
3. `deleteWorkflow` re-homed occupants into `triage` — #3183
4. **`board-workflows.ts`** described a *custom* workflow whose
definition failed to load using legacy columns — the #3178 symptom
through the dashboard route. **Fixed here.**

It type-checks, it is the obvious identifier, and on the five shared
columns it behaves correctly. The mistake only shows on the column that
differs.

## I said the sweep was complete last round. It wasn't.

My grep excluded paths and truncated at `head -10`; it missed two sites.
**The allow-list found both on its first run.**

That is the lesson the sibling sync-resolver ratchet already records —
*"FOUND BY THIS RATCHET, not by the grep that seeded the list"* — and I
had just quoted that file while repeating the mistake.

## One site is allow-listed rather than fixed, and I tried the fix first

`workflow-graph-executor.run()`'s default `ir` is unreachable in
production (both callers pass it explicitly). But
`workflow-graph-executor-parity.test.ts`, in the **engine-core gate
suite**, drives the method *without* the argument to assert the
historical seam sequence.

Switching it to the catalog default rewrites what "parity" means:
**measured, 6 gate tests fail** with `expected 'failure' to be
'success'`. Reverted, and recorded at the call site *and* in the
allow-list entry so nobody repeats the experiment.

That is what an allow-list is for: a legitimate narrow use next to a
plausible-looking wrong one.

## Guard construction

Follows the repo's existing call-site allow-lists (sync resolver, engine
blocking-shellout, detached-spawn script guard).

- **Comments stripped before scanning** — `activity-analytics.ts` and
`TaskContextMenu.tsx` name this constant in notes *about past bugs*
while correctly avoiding it. Counting prose would train readers to
allow-list mentions.
- **Anti-vacuity**: the scan still sees the catalog's own uses, so a
renamed constant or broken walker cannot make the guard pass by finding
nothing.
- **Stale-entry**: the list cannot rot into files that no longer touch
it — the decay every ledger in this repo has hit.

## Measured

- Guard **3/3**; `tsc --noEmit` clean in core, engine, dashboard.
- census `--strict`, `check-fnxc-future-dates` clean.

## Census

**No movement — that is the point.** This class has no column literal to
count, which is why the census never saw any of the four bugs.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:45:31 -07:00
gsxdsm
39a2e0481a test(engine): pin the merged-review sweep's HOLD bucket (14th resolver — the half my own test missed) (#3186)
Fourteenth resolver from the coverage map on #3115, and it is the other
half of a sweep **I converted and tested myself**.

The file already pinned `mergedReviewColumns`. Blinding
`mergedHoldColumns` back to `["todo"]` left all 71 tests green — no case
put a merge-confirmed card in a renamed hold lane.

## The lane is not hypothetical

A merge-confirmed card gets **rebounded to hold** by other recovery
paths — a failed post-merge step, a requeue. So *merged but sitting in
hold* is exactly the state this sweep's second bucket exists to
finalize. Keyed on the id, that bucket read nothing on a renamed board
and the card stayed unfinished **while its commit was already on the
base branch**.

## The lesson, repeated

This is #3138's finding again: a test that pins one resolver of a pair
reads as covering the sweep. I wrote the earlier case, recorded the
sweep as done, and it was half-done.

**Only blinding each resolver separately finds this.** A single passing
revert proves one guard — which is why the map is keyed by resolver, not
by sweep.

## Measured

72 pass; blinding `mergedHoldColumns` fails exactly this case.

**14 of 26 pinned** across 13 merged PRs.

## Verification

`self-healing-query-filter-blindness` **72 passed** · `pnpm test:gate`
full pass · lint — green.
2026-07-31 08:45:19 -07:00
gsxdsm
9c00699e61 test(engine): pin the stalled-card watchdog's terminal skip on a renamed board (13th resolver) (#3182)
Thirteenth resolver from the verified coverage map on #3115.

`sweepTerminalColumns` was uncovered: the case directly above it asserts
the terminal skip using `done` and `archived` — **the ids** — so
blinding the resolver left the file green.

## What the literal costs

The skip matched nothing on a renamed board, so **finished cards were
scanned as live**, and a card parked in a renamed completion lane could
be reported stalled.

A watchdog that cries about completed work is worse than a quiet one: it
trains operators to ignore the alert. That is the exact failure this
sweep's own dedup logic was built to avoid, reintroduced through the
lane vocabulary.

## The case

The renamed twin of the existing terminal-skip test — same assertion,
same shape, different vocabulary. That is the whole point: the original
passes either way, so it cannot see the conversion.

**Measured:** 10 pass; blinding `sweepTerminalColumns` fails exactly
this case.

## Map status

**13 of 26 pinned** across 12 merged PRs, plus `starvedWaitingColumns`
now covered by another worker independently. Re-measure before picking
the next one — the map drifts green as the fleet adds coverage, and I
have already caught it stale once today.

## Verification

`self-healing-stalled-card-watchdog` **10 passed** · `pnpm test:gate`
full pass · lint — green.
2026-07-31 08:34:16 -07:00
gsxdsm
757ce71731 fix(core): deleting a workflow re-homed its cards into triage, a lane the default board lacks (#3183)
A **third door** into the drift #3178 just fixed in the TUI. Found by
looking for siblings of that bug — **not** by the census, which
structurally cannot see this class: there is no column literal here to
count. The wrong answer comes from reading the wrong IR.

## The bug

`deleteWorkflow` clears each occupant's selection so they fall back to
the built-in default, then re-homes them to *"the default workflow's
entry column"* — its own comment's words.

It read that entry column from `BUILTIN_CODING_WORKFLOW_IR`, which is
`builtin:legacy-coding`, **not** the catalog default. Post-U11 the two
differ by exactly the column this reads:

```
default  todo, in-progress, in-review, done, archived
legacy   triage, todo, in-progress, in-review, done, archived
```

**Measured, not inferred:** `resolveEntryColumnId` answers `triage` for
the legacy IR and `todo` for the default.

## Why it got past the guard built for exactly this

`moveTask` rejects a target the workflow does not declare — **except**
under `recoveryRehome` with a **legacy id**, the #1411 escape hatch that
keeps a custom-workflow card rescuable.

`triage` *is* a legacy id. So the rehome slipped through the check that
exists to stop this, and left the card in a lane its new workflow has no
node for — the undeclared-column state other reconcilers exist to
repair.

## Measured

- 3 new cases; **MUTATION**: restoring the legacy constant fails the
anti-vacuity case.
- The first two cases pin the two IRs' entry columns as **facts in the
suite** rather than claims in a comment — that difference is the entire
reason the bug existed. They go quiet, correctly, if the IRs ever
converge again.
- The third pins the **call site**, because the first two would keep
passing against the unfixed code: they describe the IRs, not the caller.
That gap is how an anti-vacuity case earns its place.
- `src/__tests__/workflow*` — **30 files / 402 tests pass**.
- `tsc --noEmit -p packages/core` clean; census `--strict`,
`check-fnxc-future-dates` clean.

## Census

**Unchanged — and that is the finding.** This defect has no literal to
count. `builtin-workflows.ts` already records the move-path resolvers as
fixed for the same drift, #3178 fixed the TUI, and this is the third
instance. The census measures *comparisons*; a surface that resolves the
**wrong workflow** produces identical-looking code and a wrong answer.

If there is appetite for a next sweep, that is where I would point it:
sites that resolve a workflow at all, rather than guards that compare a
column.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:34:04 -07:00
gsxdsm
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>
2026-07-31 08:20:06 -07:00
gsxdsm
5eec7dc73b test(engine): pin the completed-blocked park release on a renamed board (12th resolver) (#3180)
Twelfth resolver from the verified coverage map on #3115.

`completedBlockedHoldColumns` was uncovered: every case in this file
seeds the park in `todo`, where the literal is correct, so blinding the
resolver left all 21 tests green.

## What the literal costs

A completed-blocked park rests in the board's **hold** lane, which is
only called `todo` on the built-in workflow. Keyed on the id, the sweep
selects nothing on a renamed board — so **finished work stays parked
behind a blocker that has already cleared**, stranded exactly as FN-7926
describes. Silently: a sweep that selects no rows reports success.

## Two fixture facts, found by the test failing first

- **The completion-blocker gate resolves the *blocker's* own workflow**,
so the per-task selection readers are required too.
`listWorkflowDefinitions` alone leaves the renamed complete lane
unrecognised and the park is rejected for the wrong reason — a
green-for-the-wrong-reason test, which is the exact thing this effort
removes.
- **The blocker must rest in the renamed complete lane**, not the legacy
one, or the case proves nothing about the board it claims to test.

I only learned both because the first version failed. Had it passed, I
would have shipped a test that exercised none of this.

## Measured

21 pass; blinding `completedBlockedHoldColumns` fails exactly this case.

## Map status

**12 of 26 pinned.** Also re-measured six entries this turn:
`starvedWaitingColumns` is **now covered by another worker's test**
(#3128-era, peer-progress vocabulary), so the map is drifting green
underneath me as the fleet adds coverage too — worth re-running before
anyone picks the next entry.

## Verification

`execute-requeue-loop-guard` **21 passed** · `pnpm test:gate` full pass
· lint — green.
2026-07-31 08:19:52 -07:00
gsxdsm
bc37185026 gate: enforce the quarantine deletion ratchet — nothing ran it, and it could not fail (#3167)
## A policy with nothing enforcing it

AGENTS.md states the deletion ratchet plainly:

> A quarantined test is **DELETED after 14 days** (`quarantinedAt` + 2
weeks) unless rescued.

Nothing enforced it, and it failed in two independent ways:

1. **`check:quarantine-ledger` omitted `--strict`.** The script only
exits non-zero with that flag (`check-quarantine-ledger.mjs:197`:
`return args.strict && (summary.expired > 0 || summary.near > 0) ? 1 :
0`). Without it, it is a report that always exits 0.
2. **No workflow ran it.** I audited all 12 `check:*` scripts against
`pr-checks.yml` and `full-suite.yml`: this is the only one appearing in
neither.

Either alone would have made it toothless. Together, a quarantined test
could sit past its deletion date indefinitely with every gate green.

## This exact shape is already documented in the file I edited

The comment above the lifecycle-column ratchet in `pr-checks.yml` says:

> `pnpm census:lifecycle-columns` (no `--strict`) and nothing ran it, so
three PRs lowered counts without re-recording and left allowances the
deleted guards could return through while this gate stayed green.

Script supports enforcement → package script omits the flag → no
workflow runs it. Same three steps, different ratchet. That precedent is
why I went looking.

## What changed

- `check:quarantine-ledger` now passes `--strict`
- wired into `pr-checks.yml` beside its siblings

It fires **5 days before** the deadline, not after, so the response is
still delete-or-rescue rather than an overdue entry. That window is the
script author's design; I did not invent it.

## Mutation evidence

Against a temp ledger, real one restored after:

| Ledger state | Result |
|---|---|
| today (1 entry, 13 days remaining) | exit 0 |
| entry inside the 5-day near window | **exit 1** |
| entry 6 days past deadline | **exit 1**, reports `EXPIRED (6 days
overdue)` |
| real ledger restored | exit 0 |

Without `--strict` all four exit 0 — which is the state on `main`.

## Honest note on what this will do

This is a **deadline ratchet**: it fires on a timer by design. The
current entry (`useTasks-hydration-freshness.test.ts`, deadline
2026-08-13) will trip it on **2026-08-08** unless someone deletes or
rescues it first. That is the intended behaviour and the whole point —
AGENTS.md is explicit that rescue "requires evidence the test catches
real regressions plus a root-cause fix — not stabilization passes." A
gate that never fires enforces nothing.

## Census before / after

```
before:  COLUMN guards (the backlog):   13
after:   COLUMN guards (the backlog):   13
```

Unchanged — this touches no lifecycle guard. It is gate wiring.

## Verification

`test:gate` exit 0 · `check:quarantine-ledger --strict` exit 0 on the
real ledger · both failure arms mutation-verified · ledger file restored
byte-for-byte.
2026-07-31 08:17:00 -07:00
gsxdsm
7d9d097acf fix(cli): the TUI board fell back to the LEGACY workflow — it rendered a triage lane the default no longer has (#3178)
Found by following an unexplained number rather than by a sweep: while
re-verifying #3141 the resolver reported `intake: "todo"` where
`BUILTIN_CODING_WORKFLOW_IR` resolves `intake: "triage"`. That
divergence is correct and intentional inside core — and wrong here.

## The defect

`dashboard.ts` resolved a task's columns as `def?.ir ??
BUILTIN_CODING_WORKFLOW_IR`, and its card-chip fields the same way. That
constant is the **legacy** monolithic IR (`builtin:legacy-coding`); the
catalog's actual default is `resolveDefaultWorkflowIr()`. Post-U11 they
differ **by a whole column**:

```
default  todo, in-progress, in-review, done, archived          (planning merged into todo)
legacy   triage, todo, in-progress, in-review, done, archived
```

So a task with **no workflow selection row** was rendered against a
six-column board including `triage` — a lane the real default no longer
declares.

## The same drift is already documented as fixed elsewhere

`builtin-workflows.ts` records it:

> `prepareWorkflowMovePolicyPreflightImpl` resolved the default through
the catalog while `resolveTaskWorkflowIrForMove` used the raw constant,
so a task with NO selection row produced two different workflow
signatures and every flag-ON move threw *"workflow move policy preflight
is stale"*. Both sides (and the sync resolver) now call this helper so
the default cannot drift again.

This surface was missed, and it is the **last non-test consumer of the
legacy constant outside core**.

## Test scope, stated because it is narrow

Driving the TUI end-to-end needs a rendered terminal and a live store.
That harness does not exist here, and building one to assert a fallback
would be testing the harness. So the test pins the two facts that make
the bug possible and the fix meaningful:

1. **the two IRs genuinely disagree, about `triage` specifically** — if
a future change re-merges them, this reports it rather than leaving the
fix silently pointless;
2. **the source no longer reaches for the legacy constant.**

(2) is a source assertion, weaker than driving the code. It is used for
the same reason as the `FloatingWindow` aria-label scan: the defect is a
**value at a call site**, there is no single render that reaches both
sites, and a per-site render test would pin the one someone bothered to
write. Both assertions are anti-vacuity guarded — the IR comparison
fails if either side stops resolving to a v2 column set.

## Verification

| | result |
|---|---|
| cli `tsc` | **0 errors** |
| new test | **2 passed** |
| mutation — restore `?? BUILTIN_CODING_WORKFLOW_IR` | **1 failed / 2**
|
| census `--strict`, `check-fnxc-future-dates` | exit 0 (this class is
invisible to the census — an argument, not a comparison) |

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:13:57 -07:00
gsxdsm
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>
2026-07-31 08:13:45 -07:00
gsxdsm
79a292b57c docs(agents): take FNXC timestamps from date -u, not the local clock (#3174)
I have patched this same breakage **three times today**, and it is not a
per-file defect — the convention is under-specified.

`check-fnxc-future-dates` validates against **UTC**. A stamp written
from a clock **behind** UTC is a future stamp the moment UTC rolls over,
and `pnpm lint` passes locally because the local date agrees with what
was written. Nothing in the authoring loop can catch it. It surfaces
only as a **red main for everybody else**.

## The evidence

Four separate breakages in one day, four files, at least two authors:

| file | stamps |
|---|---|
| `packages/engine/src/scheduler.ts` | 7 dated 2026-08-01 → 08-06 |
| the scheduler PG test | 1 |
| `packages/core/src/task-store/task-update.ts` | 2, fixed by two
different people |

Every one was a **real time on the wrong day** — nobody was careless,
they read their own clock.

## What changed

`AGENTS.md` already specifies the *format* (`yyyy-MM-dd-hh:mm`) and says
nothing about the *clock*, so every worker reasonably used their own.
This adds the one missing sentence, plus the impossible-hour rule the
gate also enforces — which produced its own main-red earlier today
(#3006 normalized four hour-26 stamps).

Docs only; no changeset, per the AGENTS.md rule for internal docs.

## Note

This PR will show red on Gate until **#3173** merges — main's
inert-sync-lane allowance is stale (11 → 7, never re-recorded),
unrelated to this change and inherited by every open PR.
2026-07-31 08:10:41 -07:00
gsxdsm
b8bfcd031b tooling: answer "is this file claimed?" in one command (#3175)
Addresses the root cause of a pattern I have now measured four times.

## The finding

**Every fleet worker pushes as the same GitHub account.** `gh pr list
--author "@me"` returns **all 17 open PRs** — mine and teammates' are
indistinguishable. So "is this file already being converted?" can only
be answered by fetching every open PR's file list by hand: 25+ API calls
that no worker makes before starting. I didn't either.

## The measured cost

| PR | Outcome | Landed instead as |
|---|---|---|
| #3096 | shrank to a test | teammate's serialisation + union |
| #3116 | shrank to a test | `preExecLiveColumns`,
`starvedWaitingColumns`, … |
| #3140 | shrank to a test | #3137 |
| #3125 | **shrank to nothing — closed** | #3135 |

Plus #3118, a teammate independently writing the same coverage I wrote
for #3112.

**In every case both implementations were correct and independently
reached the same design** — #3137 chose payload-first-with-sync-fallback
for the same reason I did. This is not carelessness; the fleet is doing
correct work twice and discovering coverage gaps by accident, when
rebases collide.

## What this adds

```
$ node scripts/check-file-claimed.mjs packages/engine/src/self-healing.ts
CLAIMED    packages/engine/src/self-healing.ts
             #3152  fix(self-healing): 18 recovery rebounds hardcoded `todo` …
```

**On its first run it reported `self-healing.ts` claimed by #3152 —
which I had no way to know a moment earlier.** Exits non-zero when
claimed, so it can gate work: `node scripts/check-file-claimed.mjs
<path> && start-work`.

## Deliberate limits

- **It cannot see unpushed work**, so it narrows the collision window
rather than closing it. Two workers starting the same file minutes apart
still collide. Closing that needs **distinguishable authorship** — a
per-worker `Co-Authored-By` or a title prefix — which is a coordination
decision, not a script.
- **A `gh` failure exits 2 and says UNKNOWN, not unclaimed.** A claim
check that fails open is worse than none, which is the same false-green
shape this program keeps finding elsewhere.

Also adds a short AGENTS.md pointer next to the other standing rules.

## Verification

Run against a claimed and an unclaimed path; both answers correct, exit
codes as documented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a command-line check for open pull requests that may already be
modifying specified files.
* Reports matching pull request details and clearly indicates whether
each file is claimed.
* Returns distinct statuses for claimed files, unclaimed files, and
unavailable GitHub checks.

* **Documentation**
* Added guidance for checking file ownership before beginning conversion
work.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:10:29 -07:00
gsxdsm
f8fb9b1473 test(engine): pin the unmet-dependency rebound on a renamed board (11th resolver from the coverage map) (#3176)
Eleventh resolver from the verified coverage map on #3115.

`unmetDepReviewColumns` was uncovered: the existing FN-6778/FN-6779 case
uses `in-review`, where the literal is correct, so blinding the resolver
left the file green.

## What the literal costs

The sweep selects **no card**. A review card whose dependency is still
unmet is never rebounded — it sits in review, **eligible for merge,
ahead of the work it depends on**. That is precisely the ordering
violation this sweep exists to prevent, and it fails silently: no error,
no audit event, nothing to notice.

## Measured

3 pass; blinding `unmetDepReviewColumns` fails exactly the new case.

## Map status

**11 of 26 resolvers pinned** across 10 merged PRs. The remaining 15
need real harness work — I threw away two probes earlier today that
passed while proving nothing (`reconcileInReviewBranchRebind` never
entered its loop; `recoverAgentsRunningOnInactiveTasks` stayed green
under both blindings), and recorded them on #3164 rather than shipping
green decoration.

## Verification

`in-review-unmet-dependency-reconcile` **3 passed** · `pnpm test:gate`
full pass · lint — green.
2026-07-31 07:59:02 -07:00