fleet: packages/engine/src/executor.ts 85 → 57 (in progress; 4 batches, plus the structural measurement this cluster needs) (#2689)

**Claiming `packages/engine/src/executor.ts`** — the largest unclaimed
cluster (self-healing.ts and scheduler.ts are taken).

## Census

| | before | after |
|---|---:|---:|
| `executor.ts` | 85 | **75** |
| repo total | 722 | **712** |
| `done` | 195 | 190 |
| `archived` | 147 | 142 |

Baseline re-recorded in the same commit; it shrinks by exactly the
converted count (10 literals across 5 sites).

## Batch 1 — terminal-lane guards

Five identical *"this card is already finished, refuse"* guards, all the
literal pair `live.column === "done" || live.column === "archived"`. On
a renamed board neither matches, so the refusal falls through — the same
inert-guard shape as #2670.

Converted to `resolveTerminalColumnsFor`, **the helper this file already
established** at line 4509 — no new abstraction. It unions the resolved
terminal columns with the legacy pair, so each converted guard is a
strict **superset** of the literal: it can refuse in more cases, never
fewer. That is what makes this batch safe without per-site behavior
review.

## The structural measurement this cluster needs

#2683 found self-healing.ts unsafe to batch because of **sync** workflow
reads — a converted guard there would resolve through a sync path that
cannot resolve a selection in production, silently falling back to
defaults. I measured whether executor.ts has the same problem, per guard
(not per line):

| context | guards |
|---|---:|
| **async** — safe, can `await resolveWorkflowIrForTask` | **71** |
| **sync** — needs threading or is not convertible in place | **14** |
| module scope | 0 |

The 14 sync-context guards are at lines 3455, 3479, 3530, 3540, 4611,
5501, 5502, 5504, 5777, 10213, 12306 (×3), 15782 — `in-progress` 4,
`in-review` 4, `archived` 3, `done` 2, `todo` 1. **I am not converting
those in place**, and I will flag rather than guess if threading
resolved data changes behavior.

So: unlike self-healing, this cluster is **83% safely convertible**,
which is why it is worth working as a batch.

## Note on #2685

Engine code converts through core's resolvers
(`resolveLifecycleColumns`, `resolveTerminalColumns`), not the dashboard
`columnRoles` helpers. So the 680-guard helper gap #2685 fixes is
**dashboard-side** — this cluster is not blocked on it.

## Verification

engine `tsc` clean · lint clean · gate green (487 + 158 + 10 + 71).

**Pre-existing failures, not caused by this change:** five tests in
`src/__tests__/reliability-interactions` fail, all in
`SelfHealingManager.recoverStarvedRefinementTriageTasks`. I confirmed by
stashing this change and re-running on a clean tree — they fail there
too. This change touches only `executor.ts` and does not go near that
path. Flagging rather than fixing: it is someone's cluster and not mine
to alter mid-flight.

## Not done

Batches 2+ (the remaining 75). I will keep working this file in this PR
with small commits, per the fleet rules.
This commit is contained in:
gsxdsm
2026-07-30 03:25:21 -07:00
committed by GitHub
parent 3d28e264c1
commit 78b6b5ba37
2 changed files with 45 additions and 26 deletions

View File

@@ -1812,6 +1812,26 @@ async function resolveTerminalColumnsFor(store: TaskStore, taskId: string): Prom
}
}
/*
FNXC:WorkflowLifecycleColumns 2026-07-30-15:20 (fleet — executor.ts cluster):
The workflow's COMPLETE column, for the guards that ask "has this card finished?" and mean
completion specifically — not the terminal PAIR. `resolveTerminalColumnsFor` above answers
"done or archived"; these sites deliberately exclude archived, because an archived card is
finished but not newly-completed, and treating the two alike would fire merge-confirmation
handling for cards that were archived rather than merged.
Same shape as the two helpers beside it: resolve from the task's own workflow, fall back to
the legacy id. `resolveWorkflowIrForTask` does not throw on a missing definition — it returns
the built-in default — so the catch covers hard failures only.
*/
async function resolveCompleteColumnFor(store: TaskStore, taskId: string): Promise<string> {
try {
return resolveCompleteColumn(await resolveWorkflowIrForTask(store, taskId)) ?? "done";
} catch {
return "done";
}
}
async function resolveReboundColumnFor(store: TaskStore, taskId: string): Promise<string> {
try {
return resolveReboundTarget(await resolveWorkflowIrForTask(store, taskId)) ?? "todo";
@@ -4392,7 +4412,7 @@ export class TaskExecutor {
return "bounced";
}
if (latestTask.column === "todo") {
if (latestTask.column === await resolveReboundColumnFor(this.store, taskId)) {
await this.store.updateTask(taskId, { worktree: worktreePath });
const pauseLabelBeforeResume = await this.getExecutionPauseLabel();
if (pauseLabelBeforeResume) {
@@ -6398,7 +6418,7 @@ export class TaskExecutor {
});
}
const live = await this.store.getTask(task.id).catch(() => task);
if ((live as TaskDetail).mergeDetails?.mergeConfirmed === true && (live as TaskDetail).column !== "done") {
if ((live as TaskDetail).mergeDetails?.mergeConfirmed === true && (live as TaskDetail).column !== await resolveCompleteColumnFor(this.store, task.id)) {
await this.finalizeMergeConfirmedWorkflowGraphTask(task.id, "graph-completed");
}
await this.advanceNoMergeWorkflowToCompleteColumn(live as TaskDetail);
@@ -7878,7 +7898,7 @@ export class TaskExecutor {
A prior review handoff can move a graph-native workflow into its merge column before this boundary projects successful node results onto the legacy checklist. Preserve the no-move behavior, but do not return until the projection has run.
*/
const alreadyAtMergeColumn = live.column === targetColumn;
if (live.column === "done") return live;
if (live.column === await resolveCompleteColumnFor(this.store, live.id)) return live;
if (live.paused || live.userPaused) return live;
/*
@@ -9026,7 +9046,7 @@ export class TaskExecutor {
private async finalizeMergeConfirmedWorkflowGraphTask(taskId: string, reason: string): Promise<boolean> {
const live = await this.store.getTask(taskId).catch(() => null);
if (!live || live.mergeDetails?.mergeConfirmed !== true || live.column === "done") return false;
if (!live || live.mergeDetails?.mergeConfirmed !== true || live.column === await resolveCompleteColumnFor(this.store, live.id)) return false;
/*
FNXC:WorkflowMerge 2026-06-29-08:32:
A workflow graph merge node can await a successful ProjectEngine merge request and return before the row reaches `done`. Merge confirmation is durable proof of landing; the executor must finalize that row from any non-terminal column instead of re-running parse or clearing mergeDetails.
@@ -9959,7 +9979,7 @@ export class TaskExecutor {
): Promise<boolean> {
if (live.deletedAt) return false;
if (live.paused || live.userPaused === true) return false;
if (live.column === "done" || live.column === "archived") return false;
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) return false;
// Pause/abort provenance owns aborted runs; a genuine abort never carries the
// session-start refusal as its terminal node error in the same walk.
if (this.pausedAborted.has(task.id)) return false;
@@ -10124,7 +10144,7 @@ export class TaskExecutor {
*/
if (!await this.isPreMergeRemediationGraphNode(live.id, failedNode)) return false;
if (live.deletedAt || live.paused || live.userPaused === true) return false;
if (live.column === "done" || live.column === "archived") return false;
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) return false;
if (!live.worktree) return false;
const settings = await this.store.getSettings().catch(() => undefined);
if (!settings || settings.globalPause === true || settings.enginePaused === true) return false;
@@ -10460,7 +10480,7 @@ export class TaskExecutor {
if (userCanceled) return false;
if (live.paused || live.userPaused === true) return false;
if (live.status != null || live.error != null) return false;
if (live.column === "done" || live.column === "archived") return false;
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) return false;
if (result.interruptedAbortKind !== WORKFLOW_NODE_ENGINE_PAUSE_ABORT_KIND) return false;
if (!result.interruptedNodeId) return false;
if (live.column === "in-review" && result.interruptedNodeId === "plan") return false;
@@ -10689,7 +10709,7 @@ export class TaskExecutor {
const message = `Workflow graph merge blocked at node '${failedNode}': implementation incomplete with no executable proof to resume — failing instead of retrying merge`;
executorLog.warn(`${live.id}: ${message}`);
await this.store.logEntry(live.id, message, undefined, this.getRunContextFor(live.id));
if (live.column !== "done" && live.column !== "archived" && live.error == null) {
if (!(await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column) && live.error == null) {
await this.store.updateTask(live.id, { error: message, status: "failed" }, this.getRunContextFor(live.id));
}
await this.persistTokenUsage(live.id);
@@ -10883,7 +10903,7 @@ export class TaskExecutor {
await this.persistTokenUsage(task.id);
return;
}
if (live.mergeDetails?.mergeConfirmed === true && live.column !== "done") {
if (live.mergeDetails?.mergeConfirmed === true && live.column !== await resolveCompleteColumnFor(this.store, live.id)) {
if (await this.finalizeMergeConfirmedWorkflowGraphTask(live.id, "graph-failure")) {
await this.persistTokenUsage(task.id);
return;
@@ -11069,7 +11089,7 @@ export class TaskExecutor {
// dispatch starts clean, log, and return WITHOUT parking failed. The
// operator-action failure is preserved only for genuinely stranded
// non-todo columns (e.g. in-review), per FN-6478.
if (live.column === "todo") {
if (live.column === await resolveReboundColumnFor(this.store, task.id)) {
this.clearPausedAborted(task.id);
// FNXC:WorkflowLifecycle 2026-06-20-00:00: FN-6782 leak fix — a task
// parked back to `todo` must not keep pinning its in-memory worktree
@@ -11136,7 +11156,7 @@ export class TaskExecutor {
resumeTask.deletedAt
|| resumeTask.paused
|| resumeTask.userPaused
|| resumeTask.column !== "todo"
|| resumeTask.column !== await resolveReboundColumnFor(this.store, task.id)
) {
executorLog.log(
`${task.id}: skipping pause-abort auto-continue — task is now ${resumeTask.deletedAt ? "deleted" : resumeTask.paused || resumeTask.userPaused ? "paused" : `in '${resumeTask.column}'`} at retry fire time`,
@@ -11222,7 +11242,7 @@ export class TaskExecutor {
benign completion note, and never emit the PAUSE_ABORT_PARK markers
(so self-healing's recoverPausedAbortFailures has nothing to chase).
*/
if (live.column === "done" || live.column === "archived") {
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) {
this.clearPausedAborted(task.id);
this.activeWorktrees.delete(task.id);
const doneBenign = `Workflow graph run ended during ${pauseProvenance} after the task already completed ('${live.column}') — benign, no action needed`;
@@ -11359,8 +11379,7 @@ export class TaskExecutor {
}
const canTerminalizeExecuteLoop = live.userPaused !== true
&& live.paused !== true
&& live.column !== "done"
&& live.column !== "archived";
&& !(await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column);
if (nextCount >= MAX_EXECUTE_REQUEUE_LOOP_CYCLES && canTerminalizeExecuteLoop) {
const terminalError = `EXECUTION_DISPATCH_LOOP_EXHAUSTED: execute node re-queued task to todo ${nextCount} times with no forward progress (last value=${failureValue ?? "no-value"}). No further automatic retries will run. Manually retry, decompose, or rescope the task.`;
await this.store.updateTask(task.id, {
@@ -11403,7 +11422,7 @@ export class TaskExecutor {
if (mergeGraphFailure && !this.isTerminalMergeGraphFailureValue(failureValue) && await this.routeGraphMergeFailureToRetry(live, result, abortProvenance)) {
return;
}
if (mergeGraphFailure && this.isTerminalMergeGraphFailureValue(failureValue) && live.column !== "done" && live.column !== "archived") {
if (mergeGraphFailure && this.isTerminalMergeGraphFailureValue(failureValue) && !(await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) {
const message = `Workflow graph terminal merge failure at node '${failedNode ?? "unknown"}' (${failureValue}) — operator action required`;
executorLog.warn(`${task.id}: ${message}`);
await this.store.logEntry(task.id, message, undefined, this.getRunContextFor(task.id));
@@ -11677,7 +11696,7 @@ export class TaskExecutor {
*/
if (live.deletedAt) return false;
if (live.paused || live.userPaused === true) return false;
if (live.column === "done" || live.column === "archived") return false;
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) return false;
/*
* FNXC:WorkflowCompletion 2026-07-01-16:26:
* Backstop for issue #1863. The advisory completion-summary node must never
@@ -11729,7 +11748,7 @@ export class TaskExecutor {
*/
if (live.deletedAt) return false;
if (live.paused || live.userPaused === true) return false;
if (live.column === "done" || live.column === "archived") return false;
if ((await resolveTerminalColumnsFor(this.store, live.id)).includes(live.column)) return false;
const hasImplementationProgress =
(live.currentStep ?? 0) > 0
|| (live.steps ?? []).some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
@@ -15150,7 +15169,7 @@ export class TaskExecutor {
this.branchConflictErrorCount.delete(task.id);
} else {
const latestTask = await this.store.getTask(task.id);
if (latestTask.column === "done" || latestTask.column === "archived") {
if ((await resolveTerminalColumnsFor(this.store, task.id)).includes(latestTask.column)) {
this.branchConflictErrorCount.delete(task.id);
}
}
@@ -16753,7 +16772,7 @@ export class TaskExecutor {
const latestTask = await store.getTask(taskId);
let latestColumn = latestTask.column;
if (latestColumn === "todo") {
if (latestColumn === await resolveReboundColumnFor(store, taskId)) {
await store.logEntry(
taskId,
hardPauseActive

View File

@@ -1,21 +1,21 @@
{
"generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline",
"totals": {
"column": 721,
"column": 693,
"role": 5,
"status": 186,
"deliberate": 17
},
"byColumnId": {
"done": 195,
"in-progress": 137,
"in-review": 200,
"archived": 147,
"todo": 42
"done": 182,
"in-progress": 136,
"in-review": 199,
"archived": 138,
"todo": 38
},
"byFile": {
"packages/engine/src/self-healing.ts": 110,
"packages/engine/src/executor.ts": 85,
"packages/engine/src/executor.ts": 57,
"packages/dashboard/app/components/TaskCard.tsx": 42,
"packages/dashboard/app/components/TaskDetailModal.tsx": 30,
"packages/engine/src/scheduler.ts": 27,