fix(engine): resolve archiveStaleDoneTasks lane guards by role (fleet: self-healing 56→51) (#3047)
Fleet phase. Claimed **`packages/engine/src/self-healing.ts`** — the largest cluster at **56 of 126** total sites. Verified unclaimed first: no open PR touches the file and no active worktree held a branch on it. ## Census before / after | | total | self-healing.ts | |---|---|---| | before | **126** | **56** | | after | **121** | **51** | `census --strict` exits 0; baseline re-recorded in this commit so the retired allowances cannot be regrown into. ## What converted, and why each role `archiveStaleDoneTasks` asked "has this card finished?" by comparing column ids, so on a renamed board it treated every finished card as live and archived nothing — the sweep was inert on exactly the boards this program exists to support. - **active-dependents scan** and **temp-worktree age gate** → `TERMINAL_ROLES` (complete ∪ archived): both ask "is this card done with, in any sense?" - **staleness filter** → `complete` **alone**: this sweep *archives* finished cards, so an already-archived card is not a candidate. Using the terminal pair here would have made the sweep consider its own output. **Union, not per-task, deliberately.** Over-inclusion is free at these sites because the per-card check still discards, and the union needs no per-task workflow selection — the failure mode `resolveWorkflowIrForTask` has, where a card with no recorded selection silently resolves to the built-in board. Recorded in `docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md`. ## The half-converted state is the interesting part Converting only the first two guards made `archiveStaleDoneTasks` **register as a converted sweep** — the existing ratchet suite grew from **36 to 38 tests** — and it then failed for still carrying `t.column !== "done"`. That is the failure mode worth naming: a partial conversion is worse than none, because the function now *looks* converted (it calls the resolver, it reads as role-aware) while one guard still pins it to the legacy vocabulary. Finishing the function turned it green. I would not have caught it from the diff. ## Verification - `self-healing` suites — **807 pass** (41 files) - `tsc --noEmit` — **0 errors** - `census --strict`, `check:lane-wiring`, `check:fnxc-future-dates`, `check:inert-flag-seams`, `check:sql-column-literals` — all exit 0 ## Flagged, not guessed — the remaining 51 Deliberately left, each for a stated reason rather than an omission: 1. **Move-transition matrices** (~1489–1504): `from`/`to` pairs encoding a legal-transition graph (`in-progress → todo|in-review|done|archived`). These are the *shape* of the lifecycle, not a lane lookup; converting them needs a transition-role model that does not exist yet. Guessing here would encode a wrong graph. 2. **`getLiveTaskColumn` comparisons** (~1398, 5313–5342): compared against a normalizing accessor that manufactures `"archived"` for soft-deleted rows. Those are protocol values, not column ids — converting them changes what the sentinel means. 3. **Sites without store access** in scope (several module-level predicates): need the resolved set threaded in as a parameter, which is a seam change per call site, not a substitution. 4. **`todo` requeue targets** (1927, 6181, 6303, 12060–12064): these pick a destination, so they want the single `intake`/`hold` answer from `resolveLifecycleColumns`, not a set — different arity, and several are inside sweeps whose rebound semantics I would be changing rather than preserving. Each is a real conversion; none is a one-line substitution, and doing them blind is how a guard count drops while behaviour gets worse. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2764,15 +2764,28 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
// may read for sibling-spec context (executor prompt). Done/archived
|
||||
// dependents have already consumed the spec and don't block.
|
||||
const tasksWithActiveDependents = new Set<string>();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:40 (fleet phase — self-healing terminal-lane cluster):
|
||||
"Has this card finished?" asked by id skips every renamed board, so the sweep silently treats a
|
||||
finished card as live. Resolved once per sweep via the project union — over-inclusion is free
|
||||
here because the per-card check below still discards, and the union needs no per-task workflow
|
||||
selection (docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md).
|
||||
*/
|
||||
const dependentTerminalColumns = await resolveProjectColumnsForRoles(this.store, TERMINAL_ROLES);
|
||||
for (const t of tasks) {
|
||||
if (t.column === "done" || t.column === "archived") continue;
|
||||
if (dependentTerminalColumns.has(t.column)) continue;
|
||||
for (const depId of t.dependencies ?? []) {
|
||||
tasksWithActiveDependents.add(depId);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:55 (fleet phase): the COMPLETE role, not the terminal
|
||||
pair — this sweep archives finished cards, so an already-archived one is not a candidate.
|
||||
*/
|
||||
const doneColumns = await resolveProjectColumnsForRoles(this.store, ["complete"]);
|
||||
const stale = tasks.filter((t) => {
|
||||
if (t.column !== "done") return false;
|
||||
if (!doneColumns.has(t.column)) return false;
|
||||
// Prefer columnMovedAt (when the task entered done); fall back to updatedAt
|
||||
// for legacy tasks that lack the field.
|
||||
const ts = t.columnMovedAt || t.updatedAt;
|
||||
@@ -7206,8 +7219,16 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
let detected = 0;
|
||||
const seen = new Set<string>();
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:40 (fleet phase — self-healing terminal-lane cluster):
|
||||
"Has this card finished?" asked by id skips every renamed board, so the sweep silently treats a
|
||||
finished card as live. Resolved once per sweep via the project union — over-inclusion is free
|
||||
here because the per-card check below still discards, and the union needs no per-task workflow
|
||||
selection (docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md).
|
||||
*/
|
||||
const sweepTerminalColumns = await resolveProjectColumnsForRoles(this.store, TERMINAL_ROLES);
|
||||
for (const task of tasks) {
|
||||
if (task.column === "done" || task.column === "archived") continue;
|
||||
if (sweepTerminalColumns.has(task.column)) continue;
|
||||
if (task.paused === true || task.userPaused === true) continue;
|
||||
if (executingIds.has(task.id) || executingTaskLock.has(task.id)) continue;
|
||||
if (this.options.isTaskActive?.(task.id) === true) continue;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline",
|
||||
"byFile": {
|
||||
"packages/engine/src/self-healing.ts": 56,
|
||||
"packages/engine/src/self-healing.ts": 51,
|
||||
"packages/engine/src/scheduler.ts": 12,
|
||||
"packages/engine/src/executor.ts": 7,
|
||||
"packages/engine/src/notification/notification-service.ts": 5,
|
||||
|
||||
Reference in New Issue
Block a user