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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user