refactor(core): resolve the hand-off archive guard by role (fleet) (#3054)

## Census

| | column guards |
|---|---|
| before (this branch) | **122** |
| after | **118** |

Baseline re-recorded in the same commit, as the ratchet requires. (Four
of the delta land with #3052; this PR carries `moves.ts`.)

## What changed

`handoffToReviewImpl` refuses a hand-off from an archived card. Against
the literal `archived`, a board whose archive lane is renamed **never
matched** — so an archived card could be handed to review, and the
invariant `HandoffInvariantViolationError` exists to protect was
silently unenforced.

The IR is resolved at the guard rather than 28 lines below where
`handoffTarget` already reads it; the later read now **reuses** it
instead of resolving twice. The hoist is safe because this function has
already awaited `readTaskRowAsync` above — no new tick boundary. That's
the specific hazard blocking the scheduler cluster, so I checked it here
rather than assuming.

Absent or trait-free IR keeps the legacy id: unconverted boards are
byte-identical.

## Fleet intelligence: the backlog is now essentially fully triaged

I worked down the census top-files list and verified each before
writing. **Every remaining cluster is claimed, fallback-by-design, or
documented-blocked:**

| cluster | guards | status |
|---|---|---|
| `self-healing.ts` | 51 | **claimed** — checked out in another worktree
(`convert/self-healing-lane-cluster-u7`) |
| `scheduler.ts` | 12 | **blocked**, documented at line 907 —
`task:moved` prologue is synchronous; hoisting reorders this listener
against every other subscriber |
| `notification-service.ts` | 5 | **blocked**, documented — needs the
wedge-episode contract serialised first; the second site needs
gate-placement judgement in `handleTaskUpdated` |
| `executor.ts` | 4 | **claimed** (`fleet/executor-lifecycle-roles`) |
| `restart-recovery-coordinator.ts` | 4 | **trait-fallback arms** — the
census counts these as already converted |
| `taskRevert.ts` | 2 | **blocked**, documented — would classify a
*neighbour* task with the modal's own flags (the wrong-row shape, worse
than the literal) |
| `project-store-ops.ts` | 2 | **blocked**, documented — the dead SQLite
twin; its first statement throws under PostgreSQL |
| `github-tracking-state.ts`, `planner-overseer.ts`,
`async-mission-store-queries.ts`, `register-task-workflow-routes.ts` | 2
each | **trait-fallback arms** |
| `auto-merge-finalization.ts` | 2 | one is the `catch`-block degraded
fallback; the other is a reason string |

So the mechanical conversions are done. What's left needs either a
design change (scheduler's event payload, notification's episode
contract) or per-row lane data that doesn't exist at the call site yet
(`taskRevert`).

**That's the useful signal for the fleet**: further census reduction
isn't a matter of more conversion passes. Forcing these would produce
exactly the "conversions that break the code and improve the number" the
learnings doc is named for.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 03:07:19 -07:00
committed by GitHub
parent b16318d3e3
commit 998d75da3b

View File

@@ -307,7 +307,26 @@ export async function handoffToReviewImpl(store: TaskStore, taskId: string, opts
throw error;
}
if (task.column === "archived" || task.deletedAt != null) {
/*
FNXC:WorkflowResolvedColumns 2026-07-31-12:30 (fleet — moves.ts cluster):
THE ARCHIVE GUARD IS A ROLE QUESTION, resolved from the task's own workflow.
This refuses a hand-off from an archived card. Against the literal `archived`, a board whose
archive lane is renamed never matched, so an archived card could be handed to review — the
invariant this error exists to protect, silently unenforced.
The IR is resolved here rather than 28 lines down where `handoffTarget` already reads it, and
that hoist is safe: this function has already awaited `readTaskRowAsync` above, so no new tick
boundary is introduced. The later read reuses this one.
Absent or trait-free IR keeps the legacy id, so an unconverted board is byte-identical.
*/
const handoffIr = await resolveWorkflowIrForTask(store, taskId).catch(() => undefined);
const handoffArchivedLanes = handoffIr ? new Set(columnsWithFlag(handoffIr, "archived")) : undefined;
const taskIsArchived = handoffArchivedLanes && handoffArchivedLanes.size > 0
? handoffArchivedLanes.has(task.column)
: task.column === "archived";
if (taskIsArchived || task.deletedAt != null) {
throw new HandoffInvariantViolationError(
taskId,
task.column,
@@ -335,7 +354,7 @@ export async function handoffToReviewImpl(store: TaskStore, taskId: string, opts
is the lane a completion handoff belongs in; a `humanReview`-only lane is somewhere a card can BE
in review, not somewhere the engine should PUT it.
*/
const handoffIr = await resolveWorkflowIrForTask(store, taskId).catch(() => undefined);
/* Reuses the IR hoisted for the archive guard above rather than resolving twice. */
const handoffTarget = (handoffIr ? resolveLifecycleColumns(handoffIr)?.review : undefined) ?? "in-review";
return store.moveTaskInternal(