docs(engine): flag triage's evacuation guard — it looks two-thirds converted and is fully literal (#3108)

Completes the sync-listener audit across the three files holding the
remaining blocked guards — `executor.ts` (#3104), `scheduler.ts`
(#3100), and this one. Triage is the most misleading of the three.

## The shape lies

The guard reads as **two resolved arms and one literal**:

> `task.column === disposeLanes.hold || task.column ===
disposeLanes.intake || task.column === "in-progress"`

So the obvious next move is to convert the third arm with the same
helper. That is wrong twice:

**1. The two "resolved" arms are not resolved.** `resolvePlannerLanes`
goes through `resolveTaskWorkflowIrSync`, which cannot answer for a
**custom** workflow — the sync selection reader returns `undefined`
unconditionally, *and* the custom-workflow IR read goes through
`store.db`, whose implementation is an unconditional throw (#3103). So
`disposeLanes.hold` / `.intake` are `todo` / `triage` on every board.
**All three arms are literal in effect.** Converting the third the same
way adds a third inert comparison and retires a census entry that is
currently telling the truth.

**2. The guard's answer is consumed synchronously** — the criterion I
had to correct in #3104. Below it, `pauseAborted.add`,
`session.dispose()` and `activeSessions.delete` mutate in-memory state
in this tick, and other paths read those maps. Contrast
`self-healing.ts`'s fan-out, where three of four guards only gated work
the listener already `void`s and so *were* convertible via the async
resolver (#3094).

## What it costs, and the obvious reading is backwards

An evacuation **into** a renamed destination still falls through and
disposes correctly — no bug there.

The failure is the other direction: on a board whose **hold or intake**
lane is renamed, arms 1 and 2 stop matching, so a card **sitting still
in its own planning lane** is treated as evacuated and its live triage
session is aborted mid-run.

I state it that way because "renamed board → guard misses → nothing
happens" is the pattern everywhere else in this program, and here it
inverts.

## Census

**Unchanged at 1**, deliberately. Blocked, and now documented as *fully
literal* rather than part-converted — which is the fact a future pass
needs in order not to make it worse.

## Measured

- Comment-only.
- `src/__tests__/triage*` — **25 files / 374 tests pass**.
- `tsc --noEmit -p packages/engine` clean; `check-fnxc-future-dates`
clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 04:40:21 -07:00
committed by GitHub
parent e43650416d
commit f5926d3b54

View File

@@ -720,6 +720,33 @@ export class TriageProcessor {
an unrelated update.
*/
if (typeof task.column !== "string") return;
/*
FNXC:WorkflowResolvedColumns 2026-07-31-23:55 (FLAGGED AND LEFT COUNTED — and the shape here is
misleading, which is why it needs saying):
This guard LOOKS two-thirds converted — two resolved arms and one literal — so the obvious next
move is to convert the third with the same helper. That would be wrong twice over.
1. `resolvePlannerLanes` goes through `resolveTaskWorkflowIrSync`, which cannot answer for a
CUSTOM workflow: the sync selection reader returns `undefined` unconditionally, AND the
custom-workflow IR read goes through `store.db`, whose implementation is an unconditional
throw (`sync-workflow-ir-second-blocker.test.ts`). So `disposeLanes.hold`/`.intake` are
`todo`/`triage` on every board. All THREE arms are literal in effect; converting the third
via the same helper adds a third inert comparison and removes a census entry that is telling
the truth.
2. The guard's answer is consumed SYNCHRONOUSLY — that is the criterion, not "the listener is
sync". Below it, `pauseAborted.add`, `session.dispose()` and `activeSessions.delete` all
mutate in-memory state in this tick, and other paths read those maps. (Contrast
`self-healing.ts`'s fan-out, where three of four guards only gated work the listener already
`void`s and were therefore convertible via the async resolver.)
WHAT IT COSTS TODAY: a card evacuated from planning into a lane the board renamed still matches
none of these, so the guard falls through and the session IS disposed — correct. The failure is
the other direction: a board whose HOLD or INTAKE lane is renamed no longer matches arms 1 and
2, so a card sitting still in its own planning lane is treated as evacuated and its live triage
session is aborted mid-run.
*/
const disposeLanes = resolvePlannerLanes(this.store, task.id);
if (task.column === disposeLanes.hold || task.column === disposeLanes.intake || task.column === "in-progress") return;
if (this.activeSubagentSessions.has(task.id)) {