diff --git a/packages/engine/src/__tests__/workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts b/packages/engine/src/__tests__/workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts index de1f91e444..f9ed404101 100644 --- a/packages/engine/src/__tests__/workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts +++ b/packages/engine/src/__tests__/workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts @@ -116,15 +116,25 @@ pgDescribe("startup sweep lane vocabulary, resolved from a sentinel task id", () expect(await sweepThenRead(store, taskId)).toBeNull(); }); - it("CHARACTERIZATION — the same card on a RENAMED board is NEVER swept", async () => { - /* - The card is in none of the four queried columns (`triage`, `todo`, and the two sentinel-resolved - lanes, which are both `todo`), so the sweep does not see it. Its stale `planning` status survives - and it holds a planning admission slot indefinitely. - */ + /* + FNXC:WorkflowLifecycleColumns 2026-07-31-23:59: + WAS A CHARACTERIZATION, IS NOW A REGRESSION TEST — the defect it pinned is fixed. + + It asserted `"planning"` survives: the card sat in none of the four queried columns, because the + sentinel `""` task id could resolve no board and the union collapsed to the legacy pair. The header + above notes that making `resolvePlannerLanes` async would NOT repair this, "because the defect is + the argument, not the resolver" — which is right, and is why the fix is a PROJECT-level resolver. + + `resolveProjectColumnsForRoles(store, ["intake", "hold"])` asks the question this sweep actually has: + there is no task to resolve against, and it wants every column playing those roles anywhere in the + project. The renamed planning column is now queried, so the stale status is cleared. + + The assertion is inverted rather than deleted, so the file keeps its record of what the bug WAS. + */ + it("clears a stale planning status on a RENAMED board", async () => { const store = h.store(); const taskId = await stalePlanningCard(store, RENAMED_VOCAB, "wf-renamed-sweep"); - expect(await sweepThenRead(store, taskId)).toBe("planning"); + expect(await sweepThenRead(store, taskId)).toBeNull(); }); }); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index eff94864c1..24ce724eb1 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -36,6 +36,7 @@ import { resolveWorkflowIrForTask, resolveLifecycleColumns, resolveWorkflowIrForTaskWithProvenance, + resolveProjectColumnsForRoles, workflowHasColumn, getStepParser, computePlanApprovalFingerprint, @@ -970,8 +971,25 @@ export class TriageProcessor { Querying extra columns is free here: the sweep only READS and every row is filtered on `status === "planning"` before anything is written. */ - const sweepLanes = resolvePlannerLanes(this.store, ""); - const sweepColumns = [...new Set(["triage", "todo", sweepLanes.intake, sweepLanes.hold])]; + /* + FNXC:WorkflowResolvedColumns 2026-07-31-23:59 (SYNC -> PROJECT-LEVEL, the last convertible site + in this file): + `resolvePlannerLanes(this.store, "")` was called with an EMPTY task id — there is no task here, so + it could never resolve one and returned the DEFAULT board's lanes. The sweep therefore queried + `{triage, todo}` on every board, and a stale `planning` card resting in a RENAMED planning column + was never swept: it holds a planning admission slot permanently, which is the exact failure the + note above describes. + + The right shape is PROJECT-level, not per-task: this sweep has no task to resolve against and + wants every column that plays these roles anywhere in the project. `resolveProjectColumnsForRoles` + is that resolver, already used for the same purpose in `self-healing.ts`. + + The legacy pair stays in the union deliberately — the note above explains why (`triage` and `todo` + must both be swept for pre-U11 and Coding (Ideas) rows), and querying extra columns is free here + because the sweep only READS and every row is filtered on `status === "planning"` first. + */ + const projectPlannerColumns = await resolveProjectColumnsForRoles(this.store, ["intake", "hold"]); + const sweepColumns = [...new Set(["triage", "todo", ...projectPlannerColumns])]; const swept = await Promise.all( sweepColumns.map((column) => this.store.listTasks({ column, slim: true })), );