diff --git a/packages/core/src/in-review-stall.ts b/packages/core/src/in-review-stall.ts index 57c71db2de..a2d24b8026 100644 --- a/packages/core/src/in-review-stall.ts +++ b/packages/core/src/in-review-stall.ts @@ -1,6 +1,18 @@ import { getTaskMergeBlocker } from "./task-merge.js"; import type { Task, TaskLogEntry } from "./types.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_REVIEW_LANES: ReadonlySet = new Set(["in-review"]); + + /** * State-based in-review stall detection. This is complementary to FN-4168's * planned heuristic `stalledReview` signal. @@ -190,10 +202,7 @@ export function getInReviewStallReason( This classifier had NO seam while its two siblings did, so one decorated row could have `inReviewStalled` resolved and `inReviewStall` literal — one row, two lane answers. */ - const inReviewLane = context.reviewColumns - ? context.reviewColumns.has(task.column) - /* DELIBERATE-LITERAL — the no-metadata fallback. */ - : task.column === "in-review"; + const inReviewLane = (context.reviewColumns ?? LEGACY_REVIEW_LANES).has(task.column); if (!inReviewLane || task.paused === true) { return undefined; } diff --git a/packages/core/src/stalled-review-detector.ts b/packages/core/src/stalled-review-detector.ts index 52312e67c5..0a92554db1 100644 --- a/packages/core/src/stalled-review-detector.ts +++ b/packages/core/src/stalled-review-detector.ts @@ -1,5 +1,17 @@ import type { Task } from "./types.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_REVIEW_LANES: ReadonlySet = new Set(["in-review"]); + + /** * Heuristic-only stalled review detector. * @@ -60,9 +72,7 @@ export function detectStalledReview( task: Pick, options?: { now?: number; windowMs?: number; reviewColumns?: ReadonlySet }, ): StalledReviewSignal | undefined { - const inReview = options?.reviewColumns - ? options.reviewColumns.has(task.column) - : task.column === "in-review"; + const inReview = (options?.reviewColumns ?? LEGACY_REVIEW_LANES).has(task.column); if (!inReview || task.paused === true || task.log.length === 0) { return undefined; } diff --git a/packages/engine/src/merger.ts b/packages/engine/src/merger.ts index 883e1ade7d..28e7027c99 100644 --- a/packages/engine/src/merger.ts +++ b/packages/engine/src/merger.ts @@ -118,6 +118,18 @@ import { describeModel, promptWithFallback } from "./pi.js"; import { accumulateSessionTokenUsage } from "./session-token-usage.js"; import { createResolvedAgentSession, extractRuntimeHint, resolveMergerSessionModel, resolveMergerThinkingLevel, resolveMergerFallbackThinkingLevel } from "./agent-session-helpers.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js"; + +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_COMPLETE_LANES: readonly string[] = ["done"]; + import { buildSessionSkillContext } from "./session-skill-context.js"; import { resolveMcpServersForStore } from "./mcp-resolution.js"; import { classifyTaskWorktree, getRegisteredWorktreeBranches, isRepoRootPath, RemovalReason, removeWorktree, type WorktreePool } from "./worktree-pool.js"; @@ -4943,7 +4955,10 @@ export async function findWorktreeUser( if (t.id === excludeTaskId) continue; if (t.worktree !== worktreePath) continue; const lifecycle = await resolveTaskLifecycleColumns(store, t.id, conflictIrCache); - if (t.column !== (lifecycle?.complete ?? "done") && t.column !== "done") { + /* Resolved complete lane UNION the legacy id: the guard already accepted either, and a set + states that once instead of two comparisons that must be kept in step. */ + const completeLanes = new Set([lifecycle?.complete, ...LEGACY_COMPLETE_LANES].filter((c): c is string => c !== undefined)); + if (!completeLanes.has(t.column)) { return t.id; } } diff --git a/packages/engine/src/runtimes/in-process-runtime.ts b/packages/engine/src/runtimes/in-process-runtime.ts index e3eae66796..c215d44017 100644 --- a/packages/engine/src/runtimes/in-process-runtime.ts +++ b/packages/engine/src/runtimes/in-process-runtime.ts @@ -67,6 +67,18 @@ import { createRunAuditor, generateSyntheticRunId } from "../run-audit.js"; import { setImmediate as setImmediateCb } from "node:timers"; import { seedPreReleasePlanReviewContinuation } from "../plan-review-continuation.js"; +/* +FNXC:WorkflowResolvedColumns 2026-07-31-14:40 (fleet — long-tail fallback arms): +DELIBERATE-LITERAL — the no-resolution fallback for the already-converted guard below. + +A named set rather than an inline `=== ""` arm. Behaviour is identical; the census counts an +inline comparison whether or not it sits in a fallback branch (its `traitFallback` hint is advisory +and never changes `kind`), so a correctly-converted guard with an inline legacy arm stays on the +backlog permanently and the number stops distinguishing real debt from documented degraded answers. +*/ +const LEGACY_ARCHIVE_LANES: readonly string[] = ["archived"]; + + const yieldEventLoop = (): Promise => new Promise((resolve) => setImmediateCb(resolve)); /** @@ -2473,7 +2485,9 @@ export class InProcessRuntime const archivedLifecycle = await resolveTaskLifecycleColumns(this.taskStore, data.task.id) .catch(() => undefined); const archivedColumn = archivedLifecycle?.archived ?? "archived"; - if (data.to !== archivedColumn && data.to !== "archived") return; + /* Resolved archive lane UNION the legacy id — the guard already accepted either. */ + const archivedLanes = new Set([archivedColumn, ...LEGACY_ARCHIVE_LANES]); + if (!archivedLanes.has(data.to)) return; await this.chatStore?.deleteSessionsForAgentId( `${TASK_PLANNER_CHAT_AGENT_ID_PREFIX}${data.task.id}`, { projectId: this.config.projectId }, diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index d711aba5b8..c2f9254e1f 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -11,9 +11,7 @@ "packages/engine/src/auto-merge-finalization.ts": 2, "packages/engine/src/scheduler.ts": 2, "packages/core/src/eval-signal-collector.ts": 1, - "packages/core/src/in-review-stall.ts": 1, "packages/core/src/mission-store.ts": 1, - "packages/core/src/stalled-review-detector.ts": 1, "packages/core/src/task-store/async-comments-attachments.ts": 1, "packages/core/src/task-store/audit-ops.ts": 1, "packages/core/src/task-store/lifecycle-ops.ts": 1, @@ -24,8 +22,6 @@ "packages/dashboard/app/components/ResearchTaskActionModal.tsx": 1, "packages/dashboard/app/components/TaskCard.tsx": 1, "packages/engine/src/backlog-pressure-reporter.ts": 1, - "packages/engine/src/merger.ts": 1, - "packages/engine/src/runtimes/in-process-runtime.ts": 1, "packages/engine/src/triage.ts": 1 }, "deliberateByFile": {