From 3b618f2530fc81fb8794d85cd99666082702e799 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 05:08:10 -0700 Subject: [PATCH] =?UTF-8?q?fleet:=20mission-execution-loop.ts=2010=20?= =?UTF-8?q?=E2=86=92=202=20(one=20rule,=20five=20copies;=20and=20why=20the?= =?UTF-8?q?se=20converted=20where=20store.ts's=20look-alikes=20could=20not?= =?UTF-8?q?)=20(#2711)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claiming **`packages/engine/src/mission-execution-loop.ts`** (10). Every one of its 10 census sites is the **same rule written five times**: ```ts linkedTask.column === "done" || linkedTask.column === "archived" ``` ## Census before/after | | before | after | |---|---:|---:| | `mission-execution-loop.ts` | **10** | **2** | Converted 4 of the 5 copies (8 of 10 sites) to the complete/archived roles via core's `resolveTaskLifecycleColumns`. Each site already had `this.taskStore` in scope inside an async method **and already had the linked task fetched**, so the resolution rides along with a read that was happening anyway. ## Why these converted where `store.ts`'s look-alikes could not (#2709) Both read **another task's** column. The difference is not whose column it is: - **Here** — async methods, store at hand, one task per call. A resolution is already affordable. - **`store.ts`** — synchronous `filter` callbacks over a prefetched `taskById` map, where per-dep resolution means N awaits inside a sync predicate on a path that prefetches precisely to avoid per-item I/O. Same-looking code, opposite verdicts. The distinguishing question is **"is a resolution already affordable here"**, not "whose column is it" — worth stating because a fleet worker pattern-matching on the receiver alone would get both wrong. ## Not deduped, deliberately The right shape is one predicate used five times rather than five inline copies — and the FNXC comments above each copy show their intent has already drifted apart. But introducing that predicate is a **new abstraction**, which the fleet rules exclude, and it would fold five reviewable substitutions into one design change. Flagged as the obvious follow-up instead of smuggled in. ## Remaining: 2 The fifth copy, at the `hasLiveFixTask` site, where the terminal check is one clause of a longer `Boolean(...)` expression whose other clauses I would have had to reflow. Reviewability, not difficulty. ## Verification `pnpm test:gate` **GREEN** (158 + 10 + 487 + 71) · the four mission suites **150/150** · `pnpm lint` clean · engine `tsc` clean · `--strict` exits 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- packages/engine/src/mission-execution-loop.ts | 35 ++++++++++++++++--- .../lib/lifecycle-column-census-baseline.json | 2 +- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/engine/src/mission-execution-loop.ts b/packages/engine/src/mission-execution-loop.ts index 5fc784b2bf..9b498d054a 100644 --- a/packages/engine/src/mission-execution-loop.ts +++ b/packages/engine/src/mission-execution-loop.ts @@ -25,7 +25,9 @@ import type { Mission, ValidationDiagnostics, } from "@fusion/core"; -import { MissionRemediationStoppedError, normalizeMissionAssertionType, normalizeValidationDiagnostics, renderValidationFailureDescription } from "@fusion/core"; +import { MissionRemediationStoppedError, normalizeMissionAssertionType, normalizeValidationDiagnostics, renderValidationFailureDescription, + resolveTaskLifecycleColumns, +} from "@fusion/core"; import { GitCheckoutMaterializer, type CheckoutMaterializer, type VerificationOutcome } from "./mission-verification.js"; import { createFnAgent, promptWithFallback, type AgentResult } from "./pi.js"; import { mergeEffectiveSettings } from "./effective-settings.js"; @@ -318,7 +320,13 @@ export class MissionExecutionLoop extends EventEmitter { // If the feature has a linked task that's already done, re-trigger validation if (feature.taskId) { const linkedTask = await this.taskStore.getTask(feature.taskId).catch(() => null); - if (linkedTask && (linkedTask.column === "done" || linkedTask.column === "archived")) { + const linkedLifecycle = linkedTask + ? await resolveTaskLifecycleColumns(this.taskStore, linkedTask.id) + : undefined; + if (linkedTask && ( + linkedTask.column === (linkedLifecycle?.complete ?? "done") + || linkedTask.column === (linkedLifecycle?.archived ?? "archived") + )) { await this.processTaskOutcome(feature.taskId); } } @@ -335,7 +343,13 @@ export class MissionExecutionLoop extends EventEmitter { if (feature.taskId) { try { const linkedTask = await this.taskStore.getTask(feature.taskId).catch(() => null); - if (linkedTask && (linkedTask.column === "done" || linkedTask.column === "archived")) { + const linkedLifecycle = linkedTask + ? await resolveTaskLifecycleColumns(this.taskStore, linkedTask.id) + : undefined; + if (linkedTask && ( + linkedTask.column === (linkedLifecycle?.complete ?? "done") + || linkedTask.column === (linkedLifecycle?.archived ?? "archived") + )) { await this.processTaskOutcome(feature.taskId); } recoveredCount++; @@ -361,7 +375,13 @@ export class MissionExecutionLoop extends EventEmitter { try { const linkedTask = await this.taskStore.getTask(feature.taskId).catch(() => null); - if (linkedTask && (linkedTask.column === "done" || linkedTask.column === "archived")) { + const linkedLifecycle = linkedTask + ? await resolveTaskLifecycleColumns(this.taskStore, linkedTask.id) + : undefined; + if (linkedTask && ( + linkedTask.column === (linkedLifecycle?.complete ?? "done") + || linkedTask.column === (linkedLifecycle?.archived ?? "archived") + )) { loopLog.log(`Recovery: re-triggering implementing feature ${feature.id} from completed task ${feature.taskId}`); await this.processTaskOutcome(feature.taskId); recoveredCount++; @@ -665,7 +685,12 @@ export class MissionExecutionLoop extends EventEmitter { if (!taskId) return null; const linkedTask = await this.taskStore.getTask(taskId).catch(() => null); const column = linkedTask?.column; - if (!column || column === "done" || column === "archived") return null; + const premergeLifecycle = linkedTask ? await resolveTaskLifecycleColumns(this.taskStore, linkedTask.id) : undefined; + if ( + !column + || column === (premergeLifecycle?.complete ?? "done") + || column === (premergeLifecycle?.archived ?? "archived") + ) return null; return column; } diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index 1f623b75a6..f3be903dce 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -7,7 +7,6 @@ "packages/dashboard/app/components/TaskDetailModal.tsx": 30, "packages/engine/src/scheduler.ts": 26, "packages/core/src/store.ts": 12, - "packages/engine/src/mission-execution-loop.ts": 10, "packages/core/src/task-store/async-comments-attachments.ts": 9, "packages/dashboard/app/components/TaskContextMenu.tsx": 9, "packages/dashboard/src/github-tracking-comments.ts": 9, @@ -92,6 +91,7 @@ "packages/engine/src/auto-merge-finalization.ts": 2, "packages/engine/src/cli-agent/state-machine.ts": 2, "packages/engine/src/merger-scope-auto-widen.ts": 2, + "packages/engine/src/mission-execution-loop.ts": 2, "plugins/fusion-plugin-even-cards/src/cards/board-cards.ts": 2, "plugins/fusion-plugin-reports/src/store/report-store.ts": 2, "packages/cli/src/commands/pr.ts": 1,