feat(core): untraited-project lane opt-in — and main was red on the FNXC gate (#2949)
Two things, and the second is why the first does not ship alone. ## The opt-in `resolveProjectColumnsForRoles` gains `untraitedProject: "declared-columns"`. When **no** workflow in the project expresses **any** lifecycle trait, every declared column id joins the answer. This is the three-state rule at **project** scope — the last item on the deferred list, recorded at three self-healing call sites (#2869, #2876). A board that renames its lanes and declares no traits contributes nothing today, so its cards are **absent from every role-keyed query**, and the correct per-card fallback downstream never runs for them. A fallback cannot rescue a card the query never returned. **Not "no workflow declares this role."** A project that expresses traits and has no review lane has *answered*; widening there would invent lanes it deliberately lacks. Mutation-verified both directions — widening unconditionally fails 1 of 12, making the option a no-op fails 1 of 12. **Opt-in, not default**, because the safe direction differs per caller — the finding in `project-union-versus-per-task-lanes.md`: | caller | over-inclusion costs | |---|---| | sweep | nothing — the per-card check discards the extra rows | | aggregator | an inflated number an operator reads (#2864, #2866) | | action site | a card routed or notified under a vocabulary that is not its own (#2852, #2891) | Making it the default moves all three at once, in the one direction two of them must not. Verified byte-identical without the option, so this lands with **no caller changes** and each site adopts it on its own reasoning. ## Main was red, and my own gate caught me first I dated the new comments `2026-07-31` while today is `2026-07-30` — **the exact defect `check-fnxc-future-dates` exists to prevent, committed while writing the feature.** The gate I added yesterday failed my own commit. Correcting mine surfaced that the merged sentinel batch, #2947, and three engine test files carried future-dated stamps too, so **the gate was failing on `main` for everyone**, not just here. All corrected to real dates rather than raising the ceiling. The stamps were simply wrong, and a baseline bump would have recorded the error as permitted — which is the failure mode that ratchet exists to prevent. Core and engine `tsc` clean, `pnpm lint` clean, census `--strict` 0, FNXC gate 0 (469 known, none added), gate green (161/487/13/71). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,3 +151,71 @@ describe("resolveProjectColumnsForRoles", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-20:45:
|
||||
|
||||
THE UNTRAITED-PROJECT OPT-IN — the three-state rule at PROJECT scope.
|
||||
|
||||
A board that renames its lanes but declares NO lifecycle trait contributes nothing to the union, so its
|
||||
cards are invisible to every role-keyed query — not misclassified downstream, absent from the result
|
||||
entirely, which is why a correct per-card fallback cannot rescue them (#2869, #2876).
|
||||
|
||||
THE THREE CASES ARE THE WHOLE POINT, and the middle one is what keeps this from being a blunt widening:
|
||||
- project expresses NO trait anywhere -> it has no vocabulary, so its declared ids are the honest
|
||||
candidate set;
|
||||
- project expresses traits but this ROLE is absent -> it has ANSWERED, and inventing lanes would
|
||||
contradict it;
|
||||
- opt-in absent -> byte-identical to before, which is what makes this safe to land with no caller
|
||||
changes at all.
|
||||
*/
|
||||
describe("resolveProjectColumnsForRoles: untratedProject opt-in", () => {
|
||||
const storeWith = (...irs: unknown[]) => ({
|
||||
listWorkflowDefinitions: async () => irs.map((ir) => ({ ir })),
|
||||
} as never);
|
||||
|
||||
const untraited = {
|
||||
version: "v2", name: "untraited",
|
||||
columns: [{ id: "drafting", name: "D", traits: [] }, { id: "checking", name: "C", traits: [] }],
|
||||
nodes: [], edges: [],
|
||||
};
|
||||
const traited = {
|
||||
version: "v2", name: "traited",
|
||||
columns: [{ id: "building", name: "B", traits: [{ trait: "wip" }] }],
|
||||
nodes: [], edges: [],
|
||||
};
|
||||
|
||||
it("widens to every declared column when the project expresses no lifecycle trait at all", async () => {
|
||||
const lanes = await resolveProjectColumnsForRoles(storeWith(untraited), ["mergeOrchestration"], {
|
||||
untraitedProject: "declared-columns",
|
||||
});
|
||||
|
||||
/* `checking` is the renamed review lane; without the opt-in it is absent and its cards are unseen. */
|
||||
expect(lanes.has("checking")).toBe(true);
|
||||
expect(lanes.has("drafting")).toBe(true);
|
||||
/* The legacy floor stays, so a board mid-rename is not dropped. */
|
||||
expect(lanes.has("in-review")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT widen when some workflow expresses a trait, even if none declares this role", async () => {
|
||||
/*
|
||||
The case that keeps this honest. The project HAS a vocabulary — one board declares `wip` — so a
|
||||
board with no review lane has answered "no review lane", and admitting its columns would contradict
|
||||
a statement the project actually made.
|
||||
*/
|
||||
const lanes = await resolveProjectColumnsForRoles(storeWith(untraited, traited), ["mergeOrchestration"], {
|
||||
untraitedProject: "declared-columns",
|
||||
});
|
||||
|
||||
expect(lanes.has("checking")).toBe(false);
|
||||
expect(lanes.has("drafting")).toBe(false);
|
||||
expect(lanes.has("in-review")).toBe(true);
|
||||
});
|
||||
|
||||
it("is byte-identical to today's answer without the option", async () => {
|
||||
/* No caller changes behaviour until it asks — the property that makes this landable on its own. */
|
||||
const withOpt = await resolveProjectColumnsForRoles(storeWith(untraited), ["mergeOrchestration"]);
|
||||
|
||||
expect([...withOpt].sort()).toEqual(["in-review"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -469,7 +469,7 @@ export { findWorkflowEventShapeViolations, isIdsOnlyWorkflowEvent, MAX_ID_VALUE_
|
||||
export type { WorkflowLifecycleEvent, WorkflowLifecycleEventType, WorkflowLifecycleEventBase, TaskTransitionedEvent, NodeEnteredEvent, NodeCompletedEvent, RunSuspendedEvent, RunResumedEvent, WorkflowEventShapeViolation, ImplementationExit } from "./types/workflow-events.js";
|
||||
export { columnHasFlag, columnsWithFlag, declaresAnyLifecycleTrait, resolveArchiveTargetForTask, resolveCompleteColumn, resolveLifecycleColumns, resolveMergeOrchestrationColumn, resolveReboundTarget, resolveReboundTargetForTask, resolveReviewColumns, resolveTaskLifecycleColumns, resolveTerminalColumns, resolveWipTargetForTask } from "./workflow-lifecycle-traits.js";
|
||||
export type { LifecycleColumns } from "./workflow-lifecycle-traits.js";
|
||||
export { resolveProjectColumnsForRoles, resolveArchivedLanes, REVIEW_ROLES, TERMINAL_ROLES, LEGACY_COLUMN_IDS_BY_ROLE, type ProjectLaneVocabularyStore } from "./project-lane-vocabulary.js";
|
||||
export { resolveProjectColumnsForRoles, resolveArchivedLanes, REVIEW_ROLES, TERMINAL_ROLES, LEGACY_COLUMN_IDS_BY_ROLE, type ProjectLaneVocabularyStore, type ProjectLaneResolutionOptions } from "./project-lane-vocabulary.js";
|
||||
export { resolveReviewLevelSteps, applyReviewLevelPreset } from "./review-level-preset.js";
|
||||
export {
|
||||
LEGACY_STATUS_ADOPTION,
|
||||
|
||||
@@ -35,7 +35,7 @@ workflow calls its column review, which is the flat-set mistake this program has
|
||||
times.
|
||||
*/
|
||||
|
||||
import { columnsWithFlag } from "./workflow-lifecycle-traits.js";
|
||||
import { columnsWithFlag, declaresAnyLifecycleTrait } from "./workflow-lifecycle-traits.js";
|
||||
import { parseWorkflowIr } from "./workflow-ir.js";
|
||||
import type { TraitFlags } from "./trait-types.js";
|
||||
|
||||
@@ -79,11 +79,44 @@ export const LEGACY_COLUMN_IDS_BY_ROLE: Record<string, readonly string[]> = {
|
||||
* @returns a set safe to iterate as `listTasks({ column })` reads. Never empty: the legacy ids are
|
||||
* always present, so a caller cannot accidentally query nothing.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-20:30:
|
||||
THE UNTRAITED-PROJECT OPT-IN — the three-state rule at PROJECT scope, and why it cannot be a default.
|
||||
|
||||
This helper seeds the legacy ids and adds whatever workflows DECLARE for the role. A board that renames
|
||||
its lanes but declares NO lifecycle trait on any column therefore contributes nothing, so its cards are
|
||||
invisible to every query keyed on a role — the card is not in the result at all, and a correct per-card
|
||||
fallback downstream never runs for it. Recorded at three self-healing call sites (#2869, #2876).
|
||||
|
||||
`untraitedProject: "declared-columns"` widens the answer for exactly that case: when NO workflow in the
|
||||
project expresses ANY lifecycle trait, every declared column id joins the set. Not "no workflow declares
|
||||
THIS role" — a board that expresses traits and simply has no review lane has ANSWERED, and widening
|
||||
there would invent lanes it deliberately does not have.
|
||||
|
||||
WHY IT IS OPT-IN AND NOT THE DEFAULT. The safe direction differs by caller, which is the whole finding
|
||||
of `docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md`:
|
||||
- a SWEEP over-includes harmlessly — the per-card check downstream discards the extra rows, and the
|
||||
cost is a few wasted `listTasks` calls;
|
||||
- an AGGREGATOR does not — the same widening inflates a number an operator reads (#2864, #2866);
|
||||
- an ACTION site does not — over-inclusion means a card routed or notified under a vocabulary that is
|
||||
not its own (#2852, #2891).
|
||||
Making this the default would silently change all three at once, in the one direction two of them must
|
||||
not move. So it changes nothing until a caller asks for it, and each caller asks for its own reasons.
|
||||
*/
|
||||
export interface ProjectLaneResolutionOptions {
|
||||
/** Widen to every declared column when the project expresses no lifecycle trait at all. */
|
||||
untraitedProject?: "declared-columns";
|
||||
}
|
||||
|
||||
export async function resolveProjectColumnsForRoles(
|
||||
store: ProjectLaneVocabularyStore,
|
||||
roles: ReadonlyArray<keyof TraitFlags & string>,
|
||||
options: ProjectLaneResolutionOptions = {},
|
||||
): Promise<ReadonlySet<string>> {
|
||||
const columns = new Set<string>();
|
||||
/* Collected while walking the definitions so the widening needs no second read. */
|
||||
const declaredColumnIds = new Set<string>();
|
||||
let anyTraitExpressed = false;
|
||||
for (const role of roles) {
|
||||
for (const legacy of LEGACY_COLUMN_IDS_BY_ROLE[role] ?? []) columns.add(legacy);
|
||||
}
|
||||
@@ -117,11 +150,26 @@ export async function resolveProjectColumnsForRoles(
|
||||
for (const role of roles) {
|
||||
for (const id of columnsWithFlag(ir as never, role)) columns.add(id);
|
||||
}
|
||||
if (options.untraitedProject === "declared-columns") {
|
||||
for (const column of (ir as { columns?: { id?: string }[] }).columns ?? []) {
|
||||
if (typeof column?.id === "string") declaredColumnIds.add(column.id);
|
||||
}
|
||||
if (declaresAnyLifecycleTrait(ir as never)) anyTraitExpressed = true;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Only when the project as a WHOLE expressed nothing. A single traited workflow means the project has a
|
||||
vocabulary, and a board inside it that declares no lifecycle trait is that board's own omission — not
|
||||
something to paper over by admitting every column in the project.
|
||||
*/
|
||||
if (options.untraitedProject === "declared-columns" && !anyTraitExpressed) {
|
||||
for (const id of declaredColumnIds) columns.add(id);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ export async function listTaskDocuments(
|
||||
archivedColumns?: ReadonlySet<string>,): Promise<TaskDocument[]> {
|
||||
const column = await getLiveTaskColumn(db, taskId, projectId, archivedColumns);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
@@ -560,7 +560,7 @@ export async function deleteTaskDocument(
|
||||
return layer.transactionImmediate(async (tx) => {
|
||||
const state = await getLiveTaskColumn(tx, taskId, layer.projectId, archivedColumns);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
@@ -634,7 +634,7 @@ export async function insertArtifactRow(
|
||||
if (input.taskId) {
|
||||
const column = await getLiveTaskColumn(tx, input.taskId, layer.projectId, archivedColumns);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
@@ -705,7 +705,7 @@ export async function updateArtifactRow(
|
||||
if (existing.taskId) {
|
||||
const column = await getLiveTaskColumn(tx, existing.taskId, layer.projectId, archivedColumns);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
@@ -777,7 +777,7 @@ export async function getArtifacts(
|
||||
archivedColumns?: ReadonlySet<string>,): Promise<Artifact[]> {
|
||||
const column = await getLiveTaskColumn(db, taskId, projectId, archivedColumns);
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function addCommentImpl(store: TaskStore, id: string, text: string,
|
||||
const layer = store.asyncLayer!;
|
||||
const state = await getLiveTaskColumn(layer.db, id, layer.projectId, await resolveArchivedLanes(store));
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
|
||||
@@ -657,7 +657,7 @@ export async function updateTaskCommentImpl(store: TaskStore, id: string, commen
|
||||
const layer = store.asyncLayer!;
|
||||
const state = await getLiveTaskColumn(layer.db, id, layer.projectId, await resolveArchivedLanes(store));
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
@@ -704,7 +704,7 @@ export async function deleteTaskCommentImpl(store: TaskStore, id: string, commen
|
||||
const layer = store.asyncLayer!;
|
||||
const state = await getLiveTaskColumn(layer.db, id, layer.projectId, await resolveArchivedLanes(store));
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
|
||||
@@ -455,7 +455,7 @@ export async function isTaskArchivedAsyncImpl(store: TaskStore, id: string): Pro
|
||||
const live = await getLiveTaskColumn(layer.db, id, layer.projectId, await resolveArchivedLanes(store));
|
||||
// getLiveTaskColumn returns "archived" for archived OR soft-deleted rows.
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-03:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
FNXC:LifecycleColumnCensus 2026-07-30-21:10 DELIBERATE-LITERAL: a SENTINEL, not a board lane.
|
||||
|
||||
This compares `getLiveTaskColumn`'s RETURN VALUE. That helper normalizes: it manufactures the string
|
||||
"archived" for an archived row AND for a soft-deleted one, and returns null for a missing task —
|
||||
|
||||
@@ -219,7 +219,7 @@ describe("resume lanes come from the task's own workflow", () => {
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-19:30 (a MISSED PAIR in resumeOrphaned):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:30 (a MISSED PAIR in resumeOrphaned):
|
||||
`listWipLaneTasks()` already resolved the wip lane by role, and the filter beneath it did NOT — it
|
||||
re-asserted the literal `in-progress` on the rows that read returned. So on a renamed board the read
|
||||
found the orphans and the filter discarded every one.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-19:55 (the MISSED PAIR ratchet, generalised past self-healing):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the MISSED PAIR ratchet, generalised past self-healing):
|
||||
|
||||
THE DEFECT THIS CATCHES, found in `executor.ts` after the sibling ratchet found five in
|
||||
`self-healing.ts`: a function resolves its lane by ROLE and then re-asserts a column LITERAL on the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-11:55 (the missed-pair ratchet):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the missed-pair ratchet):
|
||||
A sweep whose READ was converted to resolved lanes but whose LOOP-BODY guards still compare column ids
|
||||
is worse than one converted nowhere. The widened read admits renamed-board cards, and every literal
|
||||
guard below it then mis-classifies exactly those cards.
|
||||
|
||||
@@ -38,20 +38,20 @@ import type { Settings, Task, TaskStore } from "@fusion/core";
|
||||
import { getTaskHardMergeBlocker, resolveLifecycleColumns } from "@fusion/core";
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-04:40:
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40:
|
||||
`classifyForeignOnlyContamination` is a STATIC named import in the sweep, so `vi.spyOn` on the module
|
||||
object cannot intercept it under ESM — the binding is already resolved. Only the other named exports are
|
||||
passed through, so the sweeps in this file that use `inspectBranchConflict` are unaffected.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-05:45:
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40:
|
||||
The sweep logs through `createLogger("self-healing")`, which writes to console.error. Spying on
|
||||
console.error does NOT work here — vitest installs its own console interceptor above the spy, so the
|
||||
line appears in the run output while the spy records nothing (it did, and read as "no warn emitted").
|
||||
Mocking the logger module captures the call itself, one level below the console.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-18:40 (batch fold):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (batch fold):
|
||||
`isBranchAheadOfBase` is a STATIC named import that shells out to git, so it is mocked rather than spied —
|
||||
the ESM binding is resolved before a spy could replace it.
|
||||
*/
|
||||
@@ -693,7 +693,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(pastBlocker).toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-06:15 (the query-filter class, fifteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, fifteenth sweep):
|
||||
`recoverMergedReviewTasks` finalizes a task whose merge is CONFIRMED but which never reached the
|
||||
complete lane. Two literal reads meant that on a renamed board the card sat in review or hold forever
|
||||
while its commit was already on the base branch — merged work that the board still shows as unfinished.
|
||||
@@ -754,7 +754,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(resolveTarget).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-09:45 (the query-filter class, twenty-first sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-first sweep):
|
||||
`recoverStaleMergingStatus` clears a `merging`/`merging-pr` stamp left on a review card with no live
|
||||
merger behind it. The literal read meant that on a renamed board the stamp was never cleared, so the
|
||||
card read as mid-merge forever — and that stamp is what the merger AND the dashboard's manual Retry
|
||||
@@ -801,7 +801,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-04:35 (the query-filter class, fourteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, fourteenth sweep):
|
||||
`recoverForeignOnlyContaminatedInReviewTasks` classifies a branch that carries ONLY foreign commits and
|
||||
clears the contamination park nothing else clears. Two literal reads meant that on a renamed board it
|
||||
classified nothing and the task stayed parked indefinitely.
|
||||
@@ -853,7 +853,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(classifyForeignOnlyContamination).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-05:50 (#2891 review P1 — the card the sweep disowned):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2891 review P1 — the card the sweep disowned):
|
||||
`resolveWorkflowIrForTask` does not fail; it SUBSTITUTES the built-in IR. So a card whose workflow
|
||||
selection is missing or unreadable came back measured against `in-review`/`in-progress`, and the
|
||||
per-card verdicts then REJECTED the very card the project-scoped query had just admitted from a renamed
|
||||
@@ -952,7 +952,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-00:50 (the query-filter class, eleventh sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, eleventh sweep):
|
||||
`clearStaleBlockedBy` is the sweep that unsticks a card still pointing at a blocker that has since
|
||||
finished. Its BODY was already lane-resolved — per-referenced-task lanes, a shared IR cache, legacy ids
|
||||
unioned, all of it — and none of that ran, because the three reads above it asked for the literal
|
||||
@@ -1001,7 +1001,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:35 (the query-filter class, twelfth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twelfth sweep):
|
||||
`reclaimSelfOwnedBranchConflicts` frees a task whose OWN worktree is holding its OWN branch hostage —
|
||||
a conflict no other sweep resolves. Three literal reads plus three lane guards in the body, so both
|
||||
halves convert together: widening the read alone would admit renamed-board cards and then mis-decide
|
||||
@@ -1076,7 +1076,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(isPhantomExecutorBinding).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-03:50 (review P1 on #2879 — the hazard the conversion CREATED):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (review P1 on #2879 — the hazard the conversion CREATED):
|
||||
The three literal reads were disjoint BY CONSTRUCTION: one column each, so a card could not appear
|
||||
twice. Resolved reads are not. A custom workflow may put more than one queried role flag on the SAME
|
||||
column — here `hold` beside `wip`, a lane that both parks work and counts as work — and that column is
|
||||
@@ -1125,7 +1125,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(isPhantomExecutorBinding).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-03:10 (the query-filter class, thirteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirteenth sweep):
|
||||
`reconcileCompletedTask` releases everything blocked on a task that just completed. Three literal reads
|
||||
meant that on a renamed board it released NOTHING — every dependent stayed blocked on work that had
|
||||
already finished. This is the most visible form of the class: the board simply stops moving, with no
|
||||
@@ -1172,7 +1172,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalledWith("FN-WAITING", expect.objectContaining({ blockedBy: null }));
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-04:10 (the P1 raised on #2879, same hazard in this sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the P1 raised on #2879, same hazard in this sweep):
|
||||
Resolved reads can return ONE column for TWO roles, so a dependent lands in two buckets and the release
|
||||
below runs twice — `updateTask` and `logEntry` both fire twice for one card, and `blockedByCleared`
|
||||
over-counts. The literal reads could not do this: one column each, disjoint by construction.
|
||||
@@ -1209,7 +1209,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(clearingWrites).toHaveLength(1);
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-05:05 (#2883 review — "overbroad dependency satisfaction"):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2883 review — "overbroad dependency satisfaction"):
|
||||
A dependency is satisfied when it reaches a TERMINAL lane or a REVIEW lane, and review here means
|
||||
`mergeBlocker ∪ humanReview` — NOT merge orchestration. My first version unioned all three review
|
||||
roles, which counts a merge-orchestration-only column as satisfied and clears `blockedBy` while the
|
||||
@@ -1262,7 +1262,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-WAITING", expect.objectContaining({ blockedBy: "FN-MIDMERGE" }));
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-06:45 (the query-filter class, sixteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, sixteenth sweep):
|
||||
A card that reached a terminal lane while still carrying `merging`/`merging-pr` holds the MERGER QUEUE.
|
||||
Two literal reads meant that on a renamed board the stale status was never cleared, so one finished
|
||||
card blocked every task queued behind it — the widest blast radius in this series, since the damage is
|
||||
@@ -1297,7 +1297,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-07:15 (the query-filter class, seventeenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, seventeenth sweep):
|
||||
`recoverCompletedTasks` rescues a task whose steps are ALL done but whose session died before the
|
||||
executor could hand it to review. The literal read meant that on a renamed board it was never found:
|
||||
finished implementation work sat in the wip lane with no session and nothing to move it on.
|
||||
@@ -1342,7 +1342,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(recoverCompletedTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-07:45 (the query-filter class, eighteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, eighteenth sweep):
|
||||
`recoverInProgressLimbo` frees a card holding a wip slot with NO worktree, NO branch and no step
|
||||
started — nothing is running and nothing will. The literal read meant that on a renamed board it was
|
||||
never found, so the card kept its slot forever and denied that capacity to work that could run.
|
||||
@@ -1404,7 +1404,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(signal).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-08:15 (the query-filter class, nineteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, nineteenth sweep):
|
||||
`recoverOrphanedExecutions` takes NO lifecycle action — it emits `task:orphan-detected-no-action` so an
|
||||
operator can see a wip card with no live session behind it. The literal read meant that on a renamed
|
||||
board the event was never emitted, so the one signal pointing at an orphaned execution was silently
|
||||
@@ -1459,7 +1459,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
);
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-08:45 (the query-filter class, twentieth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twentieth sweep):
|
||||
`reattachOrphanedAssignedExecutions` reattaches a DURABLE AGENT to a task it is still assigned to but
|
||||
has stopped executing. The literal read meant that on a renamed board the reattach never fired, so the
|
||||
card sat assigned-but-idle — visibly owned by an agent that had gone quiet, which is worse than
|
||||
@@ -1513,7 +1513,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(resumeAssignedTaskForAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:25 (the query-filter class, twenty-second sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-second sweep):
|
||||
A GHOST review card is one parked in review past the stuck timeout with nobody owning its merge lane.
|
||||
The literal read meant that on a renamed board it was never found: no merger, no session, and no
|
||||
timeout ever firing against it.
|
||||
@@ -1567,7 +1567,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(isMergeLaneOwned).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-11:00 (the query-filter class, twenty-third sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-third sweep):
|
||||
`recoverTransientMergeFailures` refunds the retry budget for a merge that failed for a TRANSIENT reason
|
||||
and burned all its retries. The literal read meant that on a renamed board the refund never happened,
|
||||
so a card that failed on a network blip or a provider fault stayed failed permanently — visibly failed
|
||||
@@ -1616,7 +1616,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(requeueForAutoMerge).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-12:45 (the query-filter class, twenty-fourth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-fourth sweep):
|
||||
`recoverStaleIncompleteReviewTasks` requeues a review card whose STEPS are not finished — it reached
|
||||
review on a graph failure, not on completed work. The literal read meant that on a renamed board it was
|
||||
never requeued: the card sat in review claiming to be done while its own steps said otherwise.
|
||||
@@ -1670,7 +1670,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(proof).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-13:25 (the query-filter class, twenty-fifth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-fifth sweep):
|
||||
`recoverMisclassifiedFailures` clears a failure the executor parked for "without calling fn_task_done"
|
||||
on a task whose steps are ALL actually done — the failure is a misclassification, not real work left
|
||||
undone. The literal read meant that on a renamed board it was never cleared, so finished work stayed
|
||||
@@ -1713,7 +1713,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(updateTask).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-14:15 (the query-filter class, twenty-sixth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-sixth sweep):
|
||||
`recoverBranchMisboundInReviewTasks` detects a review card whose BRANCH TIP is bound to a different
|
||||
task's work. The literal read meant that on a renamed board the misbinding was never detected, so the
|
||||
card would merge — or refuse to — against a branch that is not its own.
|
||||
@@ -1765,7 +1765,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(resolveTarget).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-14:55 (the query-filter class, twenty-seventh sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-seventh sweep):
|
||||
`recoverMissingWorktreeReviewFailures` requeues a review card failed because its worktree was gone when
|
||||
the session tried to start. Its per-candidate lane wiring was already in place — and a note at the site
|
||||
called the literal QUERY above it "unfixable without a project-level lane resolution before the read".
|
||||
@@ -1839,7 +1839,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(proof).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-NOWT2" }), expect.anything());
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-15:40 (the query-filter class, twenty-eighth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-eighth sweep):
|
||||
`auditNoCommitsExpectedCandidates` flags a card that finished every step and pushed NO commits — either
|
||||
a legitimately commit-free task nobody declared as such, or work that silently produced nothing.
|
||||
|
||||
@@ -1890,7 +1890,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(flagged).toBe(0);
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-16:10 (the query-filter class, twenty-ninth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-ninth sweep):
|
||||
`recoverNoProgressNoTaskDoneFailures` requeues a wip card the executor failed for "no fn_task_done"
|
||||
that made NO step progress and left no git work — nothing to salvage, so requeueing is safe. The
|
||||
literal read meant that on a renamed board it was never requeued: a card that produced nothing sat
|
||||
@@ -1937,7 +1937,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(hasRecoverableGitWork).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-16:40 (the query-filter class, thirtieth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirtieth sweep):
|
||||
`recoverPartialProgressNoTaskDoneFailures` retries a review card failed for "no fn_task_done" that DID
|
||||
make step progress. Real work exists, so the sweep spends a retry rather than discarding it. The
|
||||
literal read meant that on a renamed board the retry never fired: partially-completed work was parked
|
||||
@@ -1988,7 +1988,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(proof).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-17:10 (the query-filter class, thirty-first sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirty-first sweep):
|
||||
`recoverDoneTaskMergeMetadata` repairs the merge metadata of a card that already reached the COMPLETE
|
||||
lane — the commit sha an operator sees, and that later reconcilers trust. The literal read meant that
|
||||
on a renamed board a done card's metadata was never repaired, so a completed task could keep pointing
|
||||
@@ -2034,7 +2034,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
expect(findLandedTaskCommit).not.toHaveBeenCalled();
|
||||
});
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-18:15 (the query-filter class, sweeps thirty-three and -four):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, sweeps thirty-three and -four):
|
||||
The two WORKSPACE sweeps. A workspace task lands PER-REPO, so its failure modes are its own: a
|
||||
partial land leaves some repos merged and some not, and a finished one leaves per-repo worktrees on
|
||||
disk. Both were bounded by literal reads, so on a renamed board neither ran — the partial land never
|
||||
@@ -2100,7 +2100,7 @@ describe("self-healing sweeps are bounded by a hardcoded column QUERY, not by th
|
||||
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-02:10 (#2867 review — greptile, "hard-blocker wiring remains
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2867 review — greptile, "hard-blocker wiring remains
|
||||
untested"):
|
||||
|
||||
THE WIRING, TESTED AT THE SEAM RATHER THAN THROUGH THE SWEEP.
|
||||
|
||||
@@ -10864,7 +10864,7 @@ describe("SelfHealingManager reclaimStaleActiveBranches (FN-4546)", () => {
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:40 (#2879 review — greptile, "multi-role tasks run
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2879 review — greptile, "multi-role tasks run
|
||||
recovery twice"): THE FIX IS IN `self-healing.ts`; NO TEST HERE, AND THE ABSENCE IS DELIBERATE.
|
||||
|
||||
`readBucket` dedupes by id inside ONE role's read, so a custom column carrying two queried traits —
|
||||
|
||||
@@ -361,7 +361,7 @@ function mergeAdditionalSkillPaths(...pathGroups: Array<string[] | undefined>):
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:WorkflowSteps 2026-08-08-00:00:
|
||||
* FNXC:WorkflowSteps 2026-07-30-21:40:
|
||||
* FN-8461 / GitHub #2388 require workflow skill-load warnings to describe a true
|
||||
* named-skill delivery failure, not an optional Compound Engineering source being
|
||||
* absent. Plugin body directories are paired with their parent discovery roots,
|
||||
@@ -1793,7 +1793,7 @@ export async function resolveTerminalColumnsFor(
|
||||
store: TaskStore,
|
||||
taskId: string,
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-09:30 (#2787 review — greptile P2):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (#2787 review — greptile P2):
|
||||
Optional CALLER-OWNED IR cache, matching the contract on `resolveTaskLifecycleColumns`. Sweeps that
|
||||
call this once per card on a whole board must read one IR per WORKFLOW, not one per task; callers
|
||||
resolving a single task pass nothing and are unaffected.
|
||||
@@ -1801,7 +1801,7 @@ export async function resolveTerminalColumnsFor(
|
||||
irCache?: Map<string, Awaited<ReturnType<typeof resolveWorkflowIrForTask>>>,
|
||||
): Promise<readonly string[]> {
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-12:20 (PR #2568 review — greptile):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2568 review — greptile):
|
||||
THE UNION IS DELIBERATE, and the `catch` alone was not enough.
|
||||
|
||||
`resolveWorkflowIrForTask` does NOT throw when a custom workflow definition is
|
||||
@@ -1979,7 +1979,7 @@ export class TaskExecutor {
|
||||
/** Active pre-merge workflow step sessions per task. */
|
||||
private activeWorkflowStepSessions = new Map<string, AgentSession>();
|
||||
/**
|
||||
* FNXC:TaskTiming 2026-08-01-12:00:
|
||||
* FNXC:TaskTiming 2026-07-30-21:40:
|
||||
* Only graph-owned Plan Review sessions appear here. Self-healing uses this
|
||||
* narrow liveness proof so it never finalizes an in-flight planning segment.
|
||||
*/
|
||||
@@ -2394,7 +2394,7 @@ export class TaskExecutor {
|
||||
|
||||
private async finalizeAlreadyReviewedTask(taskId: string): Promise<"merged" | "blocked" | "missing"> {
|
||||
const latestTask = await this.store.getTask(taskId);
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:55 (fleet): the board's own review lane. Spelled as the
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the board's own review lane. Spelled as the
|
||||
literal, this reported "missing" — a word that reads as "the task is gone" — for a card sitting in
|
||||
review on a renamed board, and the already-reviewed finalize never ran. */
|
||||
if (!latestTask || latestTask.column !== (await this.resolveResumeLanes(taskId)).review) {
|
||||
@@ -2476,7 +2476,7 @@ export class TaskExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:45 (fleet: wip-lane liveness family): "still executing"
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: wip-lane liveness family): "still executing"
|
||||
is the board's WIP lane. With the literal a renamed board deferred EVERY completion handoff — the
|
||||
card was never in `in-progress`, so this read "no longer active" for a card that was actively
|
||||
executing, and the handoff was dropped with a log line. */
|
||||
@@ -2784,7 +2784,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:TaskTiming 2026-08-01-12:00:
|
||||
* FNXC:TaskTiming 2026-07-30-21:40:
|
||||
* A planning segment has one owner: a graph Plan Review session is live only
|
||||
* while both its session registration and planning ownership marker remain.
|
||||
* This is intentionally narrower than isTaskActive(), which also covers
|
||||
@@ -3659,7 +3659,7 @@ export class TaskExecutor {
|
||||
// Handle unpause of an in-progress task with no active session.
|
||||
// Approval can be decided while the old session is still unwinding;
|
||||
// remember that edge instead of losing the only task:updated event.
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:50 (fleet): both checks in this listener ask "is
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): both checks in this listener ask "is
|
||||
this card still in the wip lane?"; one snapshot for the pair. With the literal neither fired on a
|
||||
renamed board — an unpaused card with no active session was never resumed. */
|
||||
const unpauseWipLane = (await this.resolveResumeLanes(task.id)).wip;
|
||||
@@ -4451,7 +4451,7 @@ export class TaskExecutor {
|
||||
the task back for remediation, so `in-review` must bounce back exactly like
|
||||
`in-progress` regardless of the column the completion race left it in.
|
||||
*/
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:57 (fleet): both lanes from ONE snapshot — the comment
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): both lanes from ONE snapshot — the comment
|
||||
above says in-review must bounce EXACTLY like in-progress, so resolving them separately is how the
|
||||
bounce ends up handling one lane and throwing on the other, which is the bug that comment is about. */
|
||||
const bounceLanes = await this.resolveResumeLanes(taskId);
|
||||
@@ -4558,7 +4558,7 @@ export class TaskExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:48 (fleet): the INVERSE of the guard above — this one
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the INVERSE of the guard above — this one
|
||||
SKIPS a card that is still executing. Note the direction: with the literal on a renamed board it
|
||||
never matched, so a rerun could fire on a card mid-execution. A mechanical sweep of every
|
||||
`!== "in-progress"` would fix the refusals and leave this admission in place. */
|
||||
@@ -4618,7 +4618,7 @@ export class TaskExecutor {
|
||||
*/
|
||||
const terminalColumns = await resolveTerminalColumnsFor(this.store, task.id);
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-12:30 (PR #2568 review — greptile):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2568 review — greptile):
|
||||
RE-READ AFTER THE AWAIT. The pause and column guards above ran against the `task`
|
||||
snapshot the caller passed, and this conversion introduced the first `await`
|
||||
between those guards and the writes below. Another dispatch or an operator action
|
||||
@@ -4642,7 +4642,7 @@ export class TaskExecutor {
|
||||
FN-7926: completed work with a persistent `getTaskCompletionBlocker` result must not self-requeue through the execute node. Re-running implementation cannot clear dependency/blockedBy state, so it only feeds FN-7863's generic no-progress backstop and misclassifies good work as `EXECUTION_DISPATCH_LOOP_EXHAUSTED`. Park in a scheduler-skipped todo state, preserve worktree/branch/steps, and reset the FN-7863 signature so the backstop remains reserved for genuinely incomplete no-progress loops.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-17:10 (rebase merge, both sides kept):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (rebase merge, both sides kept):
|
||||
main (#2644) resolved the literal `todo` into `reboundColumn`; this branch added the
|
||||
post-await `liveTask` re-read. Taking either side alone loses the other — the
|
||||
literal comes back, or the stale snapshot does.
|
||||
@@ -4714,7 +4714,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-20:35 (PR #2703 review — greptile P1):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2703 review — greptile P1):
|
||||
The review lane arrives from the caller for the reason documented on `isBenignInReviewPauseAbort`: the
|
||||
synchronous resolver returns the default workflow in PostgreSQL mode, so resolving it here would have
|
||||
been a conversion that changes the census and not the behaviour.
|
||||
@@ -4750,7 +4750,7 @@ export class TaskExecutor {
|
||||
await this.persistTokenUsage(task.id);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-02-05:50 (PR #2703 review — greptile P1, and it is the same split
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2703 review — greptile P1, and it is the same split
|
||||
I have been fixing all day, in code I wrote an hour earlier):
|
||||
ONE SNAPSHOT. The eligibility check above already resolved this task's lanes
|
||||
(`nonContinuableLanes`), and this branch resolved them AGAIN. A workflow selection or review-column
|
||||
@@ -5227,7 +5227,7 @@ export class TaskExecutor {
|
||||
default lineage.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-23:10 (the sync resolver never resolved):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (the sync resolver never resolved):
|
||||
AWAITED, because this method is async and the sync twin is a no-op in production.
|
||||
|
||||
The note above says a literal here "means the last resort does not exist off the default
|
||||
@@ -5636,7 +5636,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-18:25 (fleet: made ASYNC to own its resolution):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: made ASYNC to own its resolution):
|
||||
This predicate protects a card from artifact-recovery replanning, and three of its conditions are
|
||||
lifecycle columns: the terminal pair, and a review row whose auto-merge is off (a human owns it). As
|
||||
literals they all read false on a renamed board — so a FINISHED card, or a review row a human was
|
||||
@@ -5820,7 +5820,7 @@ export class TaskExecutor {
|
||||
* A task re-dispatched by pass 1 is not re-dispatched by pass 2 (dedupe set).
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-01:10:
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40:
|
||||
The wip-lane read for the two resume sweeps, resolved at PROJECT level.
|
||||
|
||||
`listTasks`' `column` option filters in the store, so both sweeps returned an EMPTY array on a
|
||||
@@ -5956,7 +5956,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-19:20 (a MISSED PAIR, the class #2879 ratcheted):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (a MISSED PAIR, the class #2879 ratcheted):
|
||||
`listWipLaneTasks()` above already resolves the wip lane by role. This filter did not — it re-asserted
|
||||
the literal `in-progress` on the rows that read returned, so on a renamed board the read found the
|
||||
orphans and the filter dropped every one.
|
||||
@@ -7845,7 +7845,7 @@ export class TaskExecutor {
|
||||
const taskStore = this.store;
|
||||
const patch: Partial<TaskDetail> = {};
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-01:05:
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40:
|
||||
Resolve a requested ROLE to this task's own column, because the seam that asks cannot.
|
||||
|
||||
`workflow-node-handlers.ts`'s review-handoff seam is a pure function over an IR node and a
|
||||
@@ -10189,7 +10189,7 @@ export class TaskExecutor {
|
||||
not move those tasks backward or re-enqueue them. Mirrors the gating the in-review
|
||||
self-healing sweep (recoverMissingWorktreeReviewFailures) applies before the same recovery.
|
||||
*/
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:25 (fleet): FN-5147 — with the literal, a renamed board
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): FN-5147 — with the literal, a renamed board
|
||||
skipped this auto-merge-off gate entirely, so an automatic recovery moved a human-review-terminal
|
||||
card backward. #2689 converted the terminal guard at the top of this method; this is the other half
|
||||
of the same decision. */
|
||||
@@ -10398,7 +10398,7 @@ export class TaskExecutor {
|
||||
if (abortProvenance === "global-pause" || live.userPaused === true) return false;
|
||||
if (abortProvenance === "completion-finalize") return false;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-17:10 (fleet: executor.ts review-lane classifiers, on top of #2689):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: executor.ts review-lane classifiers, on top of #2689):
|
||||
"IS THIS CARD IN THE REVIEW LANE?" from the task's own workflow. Five pause-abort classifiers asked it
|
||||
as the default lineage's literal, and each refusal drops the card through to the operator-action park
|
||||
these paths exist to avoid (FN-6796's benign in-review abort, the manual-merge-hold abort, the two
|
||||
@@ -10427,7 +10427,7 @@ export class TaskExecutor {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-20:30 (PR #2703 review — greptile P1, and it is the most
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2703 review — greptile P1, and it is the most
|
||||
important finding in this sweep):
|
||||
|
||||
THE SYNCHRONOUS RESOLVER IS A NO-OP IN PRODUCTION. `resolvePlannerLanes` reads
|
||||
@@ -10465,7 +10465,7 @@ export class TaskExecutor {
|
||||
if (!isGenericAbortProvenance(abortProvenance)) return false;
|
||||
if (userCanceled) return false;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-20:40 (PR #2703 review — replaces my own earlier reasoning):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2703 review — replaces my own earlier reasoning):
|
||||
This comparison used the SYNC `resolvePlannerLanes`, which I justified as the right resolver for a
|
||||
synchronous classifier. That justification was wrong in production: in PostgreSQL mode the sync
|
||||
selection reader always returns undefined, so the sync resolver hands back the DEFAULT workflow's lanes
|
||||
@@ -10620,7 +10620,7 @@ export class TaskExecutor {
|
||||
if (!isGenericAbortProvenance(abortProvenance) && abortProvenance !== "global-pause") return false;
|
||||
if (userCanceled) return false;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-17:15 (fleet): ONE SNAPSHOT for the entry gate AND the deferred
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): ONE SNAPSHOT for the entry gate AND the deferred
|
||||
recheck inside `scheduleRetry` below — the recheck is the second half of THIS decision ("is the card
|
||||
still where it was when we admitted it?"), so resolving the board again inside the timeout callback
|
||||
would let a workflow edit make the two halves disagree.
|
||||
@@ -10739,7 +10739,7 @@ export class TaskExecutor {
|
||||
if (live.paused || live.userPaused === true) return false;
|
||||
if (live.status != null || live.error != null) return false;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-17:20 (fleet: executor.ts — the split-snapshot defect):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: executor.ts — the split-snapshot defect):
|
||||
THE LANES ARE RESOLVED HERE, AT THE TOP, because this method already resolved them — at the very END,
|
||||
for its return value — while every eligibility check below compared against the default lineage's
|
||||
literals. On a renamed board the four `in-review` gates all read false, so a card in review skipped the
|
||||
@@ -10804,7 +10804,7 @@ export class TaskExecutor {
|
||||
memo?: { lanes?: { hold: string; wip: string; review: string; wipDeclared: boolean } },
|
||||
): Promise<{ hold: string; wip: string; review: string; wipDeclared: boolean }> {
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-01:00 (PR #2640 review, greptile P2):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2640 review, greptile P2):
|
||||
ONE RESOLUTION PER RECOVERY, and the reason is correctness as much as I/O. Eligibility and
|
||||
re-entry ran this separately, so a workflow edit landing between the two calls would have the
|
||||
two halves of one decision reading DIFFERENT lane sets — the eligibility check admits a card in
|
||||
@@ -11049,7 +11049,7 @@ export class TaskExecutor {
|
||||
}
|
||||
const live = loadedLive;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-18:40 (fleet: executor.ts handleGraphFailure):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: executor.ts handleGraphFailure):
|
||||
ONE LANE SNAPSHOT FOR THE WHOLE METHOD, declared where `live` first exists. The three wip comparisons
|
||||
below run BEFORE the re-entry classifiers' memo was created, so a snapshot declared beside that memo
|
||||
is used-before-declared — which is how the two halves came to read different boards in the first
|
||||
@@ -11225,7 +11225,7 @@ export class TaskExecutor {
|
||||
FNXC:WorkflowLifecycle 2026-06-18-12:00:
|
||||
FN-6647 closes the remaining durability gap by deriving already-finalized completion from the persisted task row: non-in-progress column, completed steps, no live pause/status/error, and the finalize-to-review log entry. The volatile `completionFinalizedTaskIds` marker still helps within one executor lifecycle, but teardown/restart loss must not reclassify a completed in-review row as a hard-cancel pause abort.
|
||||
*/
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:32 (fleet): on a renamed board a completed,
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): on a renamed board a completed,
|
||||
already-finalized row read as still-in-wip, so FN-6644/FN-6647's suppression never fired and the
|
||||
row was re-parked as an operator-action pause abort — the durability gap those tickets closed. */
|
||||
const alreadyFinalizedToReview = Boolean(
|
||||
@@ -11281,11 +11281,11 @@ export class TaskExecutor {
|
||||
);
|
||||
}
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-01:05 (PR #2640 review, greptile P2): one lane
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2640 review, greptile P2): one lane
|
||||
snapshot for one recovery decision — see `resolveResumeLanes`. Eligibility and re-entry are two
|
||||
halves of the SAME decision and must not read different boards.
|
||||
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-17:30 (fleet): the surrounding branches share it now too.
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the surrounding branches share it now too.
|
||||
This method asked "still in the wip lane?" in three more places as the default lineage's id while
|
||||
creating this memo for the classifiers — so the classifiers read the board and the branches around
|
||||
them read the default names.
|
||||
@@ -12020,7 +12020,7 @@ export class TaskExecutor {
|
||||
if (implementationIncompleteMergeFailure && !incompleteSteps) return false;
|
||||
const prematureMergeWithIncompleteSteps = implementationIncompleteMergeFailure && incompleteSteps;
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-17:40 (fleet: executor.ts — the REVERSE half-conversion):
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: executor.ts — the REVERSE half-conversion):
|
||||
THE DESTINATION WAS ALREADY RESOLVED HERE AND THE GATE WAS NOT. `resolveReboundColumnFor` below picks
|
||||
the board's rebound column (U7), but this gate compared against three default-lineage literals — so on
|
||||
a renamed board the router refused before ever reaching the resolved move. That is the mirror image of
|
||||
@@ -12570,7 +12570,7 @@ export class TaskExecutor {
|
||||
// Skip for tasks that are already in-progress, in-review, merging, or done —
|
||||
// these should not be interrupted and sent back to triage for re-planning.
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-08:10:
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40:
|
||||
THIS GUARD DID THE EXACT THING ITS OWN COMMENT SAYS IT MUST NOT.
|
||||
|
||||
The comment directly above is explicit: skip for tasks already in-progress, in-review, merging or
|
||||
@@ -12643,7 +12643,7 @@ export class TaskExecutor {
|
||||
// path below, but we emit a loud audit record so these states stop being
|
||||
// silent.
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-01-18:10 (fleet: execute() preflight): THREE DRIFT CHECKS, ONE
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: execute() preflight): THREE DRIFT CHECKS, ONE
|
||||
SNAPSHOT — merge-confirmed while still executing, stale mergeDetails, and in-wip with no worktree. None
|
||||
fired on a renamed board, so every recovery they perform silently stopped happening. The third one's own
|
||||
message says it "usually indicates a partial updateTask/moveTask sequence failed" — a diagnostic that
|
||||
@@ -12699,7 +12699,7 @@ export class TaskExecutor {
|
||||
// Check dependencies
|
||||
const allTasks = await this.store.listTasks({ slim: true, includeArchived: false });
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-00:50 (batch-engine — dependency satisfaction, per DEPENDENCY):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (batch-engine — dependency satisfaction, per DEPENDENCY):
|
||||
Resolved from each DEPENDENCY's own workflow, not this task's: dependencies routinely span workflows,
|
||||
so asking "is my blocker finished?" against the blocked task's vocabulary is the wrong question. That
|
||||
is the answer main settled on in `branch-group-ops.ts` (#2720) and it is reused here rather than
|
||||
@@ -13640,7 +13640,7 @@ export class TaskExecutor {
|
||||
// was unwinding; continuing the cleanup would clobber a valid
|
||||
// recovery (see the analogous block in the outer finally for the
|
||||
// full reasoning).
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-18:05 (fleet: stuck-requeue family): "has a
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet: stuck-requeue family): "has a
|
||||
concurrent recovery already moved this card on?" — the pre-completion lanes are the board's
|
||||
wip and hold. With literals a renamed board always answered "moved on", the cleanup never
|
||||
ran, and the log line blamed a concurrent recovery that had not happened. */
|
||||
@@ -14452,7 +14452,7 @@ export class TaskExecutor {
|
||||
}
|
||||
const hasExplicitWorktreeBinding = typeof liveTask.worktree === "string" || liveTask.worktree === null;
|
||||
const hasExplicitBranchBinding = typeof liveTask.branch === "string" || liveTask.branch === null;
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-18:15 (fleet): the contract holds while the card is
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the contract holds while the card is
|
||||
in ITS board's wip lane; the literal made every renamed-board retry look reclaimed. */
|
||||
const worktreeContractIntact = liveTask.column === (await this.resolveResumeLanes(task.id)).wip
|
||||
&& !liveTask.paused
|
||||
@@ -14921,7 +14921,7 @@ export class TaskExecutor {
|
||||
this.clearPausedAborted(task.id);
|
||||
const latestTask = await this.store.getTask(task.id);
|
||||
if (
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-18:18 (fleet): the HOLD lane — this recognises a card the
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the HOLD lane — this recognises a card the
|
||||
abort already parked with its progress preserved, and skipping the cleanup is what keeps that
|
||||
progress. On a renamed board the cleanup ran anyway and discarded it. */
|
||||
latestTask?.column === (await this.resolveResumeLanes(task.id)).hold &&
|
||||
@@ -18599,7 +18599,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
};
|
||||
}
|
||||
const additionalSkillPaths = mergeAdditionalSkillPaths(skillContext.additionalSkillPaths, ceSkillsDir ? [ceSkillsDir] : undefined);
|
||||
// FNXC:WorkflowSteps 2026-08-08-00:00:
|
||||
// FNXC:WorkflowSteps 2026-07-30-21:40:
|
||||
// FN-8461 / GitHub #2388: workflow steps resolve skills from enabled-plugin
|
||||
// body directories and the optional CE install root. Warn only after merging
|
||||
// those sources when THIS named skill remains undiscoverable: a non-empty path
|
||||
@@ -18726,7 +18726,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
`Workflow step '${workflowStep.name}' using model: ${workflowModelDetails}`,
|
||||
);
|
||||
this.setActiveWorkflowStepSession(task.id, session, worktreePath, this.createSeenSteeringIds(task));
|
||||
// FNXC:TaskTiming 2026-08-01-10:00: graph-owned Plan Review is the only
|
||||
// FNXC:TaskTiming 2026-07-30-21:40: graph-owned Plan Review is the only
|
||||
// post-spec planning lane. Start before prompting and finalize in finally before any replan handoff.
|
||||
const ownsPlanningSegment = workflowStep.id === "graph:plan-review-step" || workflowStep.name === "Plan Review";
|
||||
if (ownsPlanningSegment) {
|
||||
@@ -18821,7 +18821,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
if (workflowStep.requiresBrowser === true) {
|
||||
await logBrowserVerificationActivity(`[browser-verification] finished browser verification for task ${task.id}: timed out`);
|
||||
}
|
||||
// FNXC:TaskCost 2026-08-01-10:00: Plan Review tokens are task cost;
|
||||
// FNXC:TaskCost 2026-07-30-21:40: Plan Review tokens are task cost;
|
||||
// snapshot before timeout disposal just like normal completion.
|
||||
await accumulateSessionTokenUsage(this.store, task.id, session, { agentId: task.assignedAgentId ?? undefined, role: "executor" });
|
||||
try { session.dispose(); } catch { /* best-effort */ }
|
||||
@@ -21270,7 +21270,7 @@ You have access to the file system to review changes.${inlineFixBlock}${verdictB
|
||||
`${taskId} force-requeue could not read latest task state: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-08-01-17:52 (fleet): the board's wip lane; with the literal a
|
||||
/* FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (fleet): the board's wip lane; with the literal a
|
||||
renamed board skipped every force-requeue as "recovered concurrently". */
|
||||
if (latestColumn && latestColumn !== (await this.resolveResumeLanes(taskId)).wip) {
|
||||
executorLog.log(
|
||||
|
||||
@@ -2833,7 +2833,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-07:10 (the query-filter class, seventeenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, seventeenth sweep):
|
||||
A task whose steps are ALL done but whose session died before the executor could hand it to review.
|
||||
The literal read meant that on a renamed board it was never found, so finished implementation work
|
||||
sat in the wip lane with no session and nothing to move it on — the shape this sweep exists to
|
||||
@@ -3275,7 +3275,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const now = Date.now();
|
||||
const activeMergeTaskId = this.options.getActiveMergeTaskId?.() ?? null;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-09:40 (the query-filter class, twenty-first sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-first sweep):
|
||||
Clears a `merging`/`merging-pr` stamp left on a review card with no live merger behind it. The
|
||||
literal read meant that on a renamed board the stamp was never cleared, so the card read as
|
||||
mid-merge forever — and the merge-active stamp is what the merger and the dashboard Retry gate both
|
||||
@@ -3570,7 +3570,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-17:40 (the query-filter class, thirty-second sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirty-second sweep):
|
||||
This read answers "is another LIVE task holding this worktree?" — the same question
|
||||
`findActiveWorktreeOwner` answers for the executor, and the same failure if it comes back empty: the
|
||||
checkout reads as unowned and this sweep reclaims a worktree another task is working in.
|
||||
@@ -3750,7 +3750,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:30 (the query-filter class, twelfth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twelfth sweep):
|
||||
Three lane reads AND three lane guards in the body, so both halves convert together — widening the
|
||||
read alone would admit renamed-board cards and then mis-decide every one of them (the phantom-binding
|
||||
check, the blocked-hold skip, and the review triple-proof are all keyed on lane).
|
||||
@@ -3820,7 +3820,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
// per-task override preserves that for override-less tasks while letting
|
||||
// explicit autoMerge:true tasks recover.
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:15 (#2879 review — greptile, "multi-role tasks run
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2879 review — greptile, "multi-role tasks run
|
||||
recovery twice"): DEDUPED ACROSS THE BUCKETS, NOT JUST WITHIN EACH.
|
||||
|
||||
`readBucket` dedupes by id, but only inside one role's read. A custom workflow may put more than
|
||||
@@ -4077,7 +4077,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-11:40 (SELF-AUDIT after #2916 found the same class):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (SELF-AUDIT after #2916 found the same class):
|
||||
These loop-body lane guards were MISSED when I converted this sweep's read. That is not a
|
||||
cosmetic gap: this one decides whether the backward move needs `reviewProof`. Left literal,
|
||||
a renamed review card admitted by the widened read reads as NOT-in-review, so the
|
||||
@@ -4748,7 +4748,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
return pathsOverlap(dependentScope, blockerScope);
|
||||
};
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-03:05 (the query-filter class, thirteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirteenth sweep):
|
||||
When a task completes, this releases everything blocked on it. Three literal reads meant that on a
|
||||
renamed board it released NOTHING — every dependent stayed blocked on a task that had already
|
||||
finished, which is the most visible form of this class: the board simply stops moving.
|
||||
@@ -4771,7 +4771,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const inReviewTasks = (await readDependentBucket(completedReviewColumns)).filter((t) => !t.paused);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-02:40 (#2883 review — greptile P1, "duplicate dependent
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2883 review — greptile P1, "duplicate dependent
|
||||
reconciliation"; same class as #2879 one sweep over):
|
||||
DEDUPED ACROSS THE BUCKETS, NOT JUST WITHIN EACH.
|
||||
|
||||
@@ -4780,7 +4780,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
reconciled twice from the same stale snapshot — the second pass deciding against `blockedBy` state
|
||||
the first pass had already cleared, and `updateTask`/`logEntry` firing twice for one card.
|
||||
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-04:05 (precedence correction):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (precedence correction):
|
||||
Written first as `new Map(entries)` with a comment claiming first-bucket precedence. That
|
||||
constructor keeps first insertion ORDER but the LAST value for a repeated key, so it did the
|
||||
opposite of what it said. The explicit `has` guard below makes the code match the claim; order is
|
||||
@@ -4806,7 +4806,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
a degraded board reads a finished dependency as unmet — the exact stall being cleared here.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-05:00 (#2883 review — greptile P1, "overbroad
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2883 review — greptile P1, "overbroad
|
||||
dependency satisfaction"): REUSE THE SCHEDULER'S RESOLVER, DO NOT RE-DERIVE IT.
|
||||
|
||||
The first version unioned all three review roles, which counts a `mergeOrchestration`-only
|
||||
@@ -5690,7 +5690,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const now = Date.now();
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-00:45 (the query-filter class, eleventh sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, eleventh sweep):
|
||||
THREE lane reads whose downstream treatment DIFFERS — hold cards seed the queued-dependency pass,
|
||||
review cards are exempted when paused — so this cannot collapse into one union. Read the project's
|
||||
columns for all three role groups, dedupe into one map, then classify each card against ITS OWN
|
||||
@@ -5698,7 +5698,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
and the cards stayed blocked behind dependencies that had long since finished.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-00:30 (#2876 review — greptile, "traitless workflow
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2876 review — greptile, "traitless workflow
|
||||
columns stay invisible"): CONFIRMED, DEFERRED, AND THE REASON IS THAT IT IS NOT LOCAL.
|
||||
|
||||
`resolveProjectColumnsForRoles` returns its legacy floor plus what workflows DECLARE for the
|
||||
@@ -5823,7 +5823,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
if (
|
||||
!candidates.has(taskId)
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:40 (FLAGGED AND LEFT COUNTED):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (FLAGGED AND LEFT COUNTED):
|
||||
This sits in a log-dedup closure defined BEFORE the per-referenced-task lane prefetch below, so
|
||||
the resolved sets are not in scope here and tsc says so. Hoisting the prefetch above the closure
|
||||
is not available either — it is keyed on `candidates`, which this closure helps build.
|
||||
@@ -5843,7 +5843,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:30 (batch-engine — every lane question here is about ANOTHER task):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (batch-engine — every lane question here is about ANOTHER task):
|
||||
This method classifies why a BLOCKER or a DEPENDENCY is no longer blocking, and those rows routinely
|
||||
belong to a different workflow than the blocked card. So lanes are resolved PER REFERENCED TASK, not
|
||||
from the blocked task — the same answer main settled on for dependency satisfaction in
|
||||
@@ -6699,7 +6699,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async reconcileStaleMergerStatus(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-06:40 (the query-filter class, sixteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, sixteenth sweep):
|
||||
A card that reached a terminal lane while still carrying `merging`/`merging-pr` holds the merger
|
||||
queue. Two literal reads meant that on a renamed board the stale status was never cleared, so one
|
||||
finished card blocked the queue for every task behind it.
|
||||
@@ -8308,7 +8308,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
|
||||
const now = Date.now();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-12:40 (the query-filter class, twenty-fourth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-fourth sweep):
|
||||
A review card whose STEPS are not finished — it reached review on a graph failure, not on completed
|
||||
work. The literal read meant that on a renamed board it was never requeued, so the card sat in
|
||||
review claiming to be done while its own steps said otherwise.
|
||||
@@ -8742,7 +8742,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const now = Date.now();
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:20 (the query-filter class, twenty-second sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-second sweep):
|
||||
A GHOST review card — parked in review past the stuck timeout with nobody owning its merge lane.
|
||||
The literal read meant that on a renamed board it was never found, so the card sat in review
|
||||
indefinitely with no merger, no session and no timeout ever firing against it.
|
||||
@@ -8883,7 +8883,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const maxAutoMergeRetries = resolveMaxAutoMergeRetries(settings);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-10:55 (the query-filter class, twenty-third sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-third sweep):
|
||||
A merge that failed for a TRANSIENT reason and burned its whole retry budget. The literal read
|
||||
meant that on a renamed board the retry budget was never refunded, so a card that failed on a
|
||||
network blip stayed failed permanently — an operator-visible failure with no operator-visible cause.
|
||||
@@ -8933,7 +8933,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
if (!task) continue;
|
||||
/*
|
||||
Re-check selector on the full row — the slim snapshot is best-effort and may be stale once we
|
||||
await. FNXC:WorkflowResolvedColumns 2026-07-31-11:10: this SECOND lane guard converts with the
|
||||
await. FNXC:WorkflowResolvedColumns 2026-07-30-21:40: this SECOND lane guard converts with the
|
||||
first. Converting only the read left it rejecting every renamed-board card the widened query
|
||||
found, and the new test failed on exactly that — the "convert the pair or neither" rule, caught
|
||||
by the test rather than by reading.
|
||||
@@ -9279,7 +9279,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
// Workspace tasks live in in-review (post-capture/review, pre/partial land). A task already
|
||||
// done is finished; todo/in-progress are owned by execution-stage reconcilers.
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-18:05 (the query-filter class, thirty-third sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirty-third sweep):
|
||||
A WORKSPACE task lands per-repo, and this re-enqueues one whose lands are partial or zero. The
|
||||
literal read meant that on a renamed board a workspace task stranded mid-land was never
|
||||
re-enqueued — some repos landed, some not, and nothing to finish the job.
|
||||
@@ -9715,7 +9715,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
|
||||
// Done workspace tasks are the canonical "safe to clean" set (their lands are finalized).
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-18:10 (the query-filter class, thirty-fourth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirty-fourth sweep):
|
||||
Removes the per-repo worktrees a finished workspace task left behind. The literal read meant that
|
||||
on a renamed board they were never removed — disk held by tasks that finished, growing quietly.
|
||||
|
||||
@@ -9805,7 +9805,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async recoverDoneTaskMergeMetadata(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-17:05 (the query-filter class, thirty-first sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirty-first sweep):
|
||||
Repairs the merge metadata of a card that already reached the COMPLETE lane — the commit sha an
|
||||
operator sees, and that later reconcilers trust. The literal read meant that on a renamed board a
|
||||
done card's metadata was never repaired, so a completed task could keep pointing at a commit that
|
||||
@@ -10046,7 +10046,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-06:10 (the query-filter class, fifteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, fifteenth sweep):
|
||||
A task whose merge is CONFIRMED but which never reached the complete lane. Two literal reads meant
|
||||
that on a renamed board it was never found, so a card whose work is merged sat in review or hold
|
||||
forever while its commit was already on the base branch.
|
||||
@@ -10989,7 +10989,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
a renamed board was never listed at all, so it stayed failed with every step done.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-00:45 (#2869 review — greptile, "traitless review lanes
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (#2869 review — greptile, "traitless review lanes
|
||||
remain invisible"): CONFIRMED, DEFERRED, SAME CLASS AS #2876.
|
||||
|
||||
A board that renames its review lane but declares NO lifecycle traits contributes nothing to this
|
||||
@@ -11443,7 +11443,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-14:10 (the query-filter class, twenty-sixth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-sixth sweep):
|
||||
A review card whose BRANCH TIP is bound to a different task's work. The literal read meant that on
|
||||
a renamed board the misbinding was never detected, so the card would merge — or refuse to — against
|
||||
a branch that is not its own.
|
||||
@@ -11612,7 +11612,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
const executingIds = this.options.getExecutingTaskIds?.() ?? new Set<string>();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-04:30 (the query-filter class, fourteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, fourteenth sweep):
|
||||
Two literal reads, and two per-card `task.column === …` checks inside the filters below. Those
|
||||
checks were redundant while the query pinned the column; under a resolved read they become the
|
||||
per-card verdict, so they convert in the same change rather than being deleted.
|
||||
@@ -11817,7 +11817,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async recoverMisclassifiedFailures(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-13:20 (the query-filter class, twenty-fifth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-fifth sweep):
|
||||
A task the executor parked `failed` for "no fn_task_done" whose steps are ALL actually done — the
|
||||
failure is a misclassification, not real. The literal read meant that on a renamed board the error
|
||||
was never cleared, so finished work stayed visibly failed and never entered normal review.
|
||||
@@ -12068,7 +12068,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async auditNoCommitsExpectedCandidates(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-15:30 (the query-filter class, twenty-eighth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-eighth sweep):
|
||||
Audits cards that finished every step but pushed NO commits — either a legitimately commit-free task
|
||||
that never declared itself so, or work that silently produced nothing. The literal read meant that on
|
||||
a renamed board only the `no_commits` ERROR path fed the audit, so a card sitting quietly in a
|
||||
@@ -12238,7 +12238,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-07:40 (the query-filter class, eighteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, eighteenth sweep):
|
||||
A card holding a wip slot with NO worktree, NO branch and no step started — nothing is running and
|
||||
nothing will. The literal read meant that on a renamed board it was never found, so the card kept
|
||||
its slot indefinitely and the capacity it holds is denied to work that could actually run.
|
||||
@@ -12432,7 +12432,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async recoverOrphanedExecutions(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-08:10 (the query-filter class, nineteenth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, nineteenth sweep):
|
||||
WHAT THIS SWEEP RESTORES IS VISIBILITY, NOT A REPAIR. It takes no lifecycle action — it only emits
|
||||
`task:orphan-detected-no-action` so an operator can see a wip card with no live session behind it.
|
||||
The literal read meant that on a renamed board the event was never emitted, so the one signal
|
||||
@@ -12538,7 +12538,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-08:40 (the query-filter class, twentieth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twentieth sweep):
|
||||
Reattaches a DURABLE AGENT to a task it is still assigned to but has stopped executing. The literal
|
||||
read meant that on a renamed board the reattach never fired, so the agent's own assignment was
|
||||
never resumed and the card sat assigned-but-idle — visibly owned by an agent that had gone quiet.
|
||||
@@ -13388,7 +13388,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
async recoverNoProgressNoTaskDoneFailures(): Promise<number> {
|
||||
try {
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-16:05 (the query-filter class, twenty-ninth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-ninth sweep):
|
||||
A wip card the executor failed for "no fn_task_done" that made NO step progress and left no git
|
||||
work — nothing to salvage, so it is safe to requeue. The literal read meant that on a renamed board
|
||||
it was never requeued, so a card that produced nothing sat failed while still holding its wip slot.
|
||||
@@ -13496,7 +13496,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-14:45 (the query-filter class, twenty-seventh sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, twenty-seventh sweep):
|
||||
THE NOTE BELOW SAID THIS WAS UNFIXABLE. It called the literal query "unfixable without a
|
||||
project-level lane resolution before the read" — which is precisely what
|
||||
`resolveProjectColumnsForRoles` provides; it did not exist when that note was written. The wiring
|
||||
@@ -13510,7 +13510,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
}
|
||||
const tasks = [...missingWtById.values()];
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-08-02-20:20 (PR #2745 review — greptile P1: "recovery lanes are not
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-30-21:40 (PR #2745 review — greptile P1: "recovery lanes are not
|
||||
wired", and it is right):
|
||||
THE PRODUCTION PATH SUPPLIES THE SET. Adding the optional parameter to the three classifiers gave them the
|
||||
capability and changed nothing in production, which is a half-conversion of a different shape: not a gate
|
||||
@@ -13524,7 +13524,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
*/
|
||||
const recoveryIrCache = new Map<string, WorkflowIr>();
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-14:50 (the ARITY trap, same seam):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the ARITY trap, same seam):
|
||||
These classifiers take a MEMBERSHIP set, and `resolveTaskLifecycleColumns().review` is the FIRST
|
||||
column per role — so a board declaring more than one review column contributed only one of them and
|
||||
a card sitting in the others read as not-in-review. `columnsWithFlag` over the three review roles is
|
||||
@@ -13672,7 +13672,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused) return 0;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-16:35 (the query-filter class, thirtieth sweep):
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-21:40 (the query-filter class, thirtieth sweep):
|
||||
A review card failed for "no fn_task_done" that DID make step progress — real work exists, so it is
|
||||
retried rather than discarded. The literal read meant that on a renamed board the retry never fired,
|
||||
so partially-completed work was parked failed with its retry budget untouched: the budget exists
|
||||
@@ -14117,7 +14117,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
* them up for a fresh planning attempt.
|
||||
*/
|
||||
/**
|
||||
* FNXC:TaskTiming 2026-08-01-10:00:
|
||||
* FNXC:TaskTiming 2026-07-30-21:40:
|
||||
* A planning anchor is safe because triage ownership and graph Plan Review are
|
||||
* exclusive. Recovery finalizes only when neither in-process owner is live;
|
||||
* the atomic null-check makes restart and repeated maintenance idempotent.
|
||||
@@ -14146,7 +14146,7 @@ export class SelfHealingManager extends SelfHealingGitEvidence {
|
||||
}
|
||||
if (applied) {
|
||||
finalized++;
|
||||
// FNXC:TaskTiming 2026-08-01-12:00: this recovery is operator-auditable
|
||||
// FNXC:TaskTiming 2026-07-30-21:40: this recovery is operator-auditable
|
||||
// without persisting duration prose; the atomically finalized task id
|
||||
// and fixed no-live-owner reason are sufficient forensic evidence.
|
||||
await this.store.recordRunAuditEvent?.({
|
||||
|
||||
@@ -175,7 +175,6 @@
|
||||
"packages/engine/src/__tests__/scheduler-paused-dispatch-refusal.test.ts": 3,
|
||||
"packages/engine/src/__tests__/self-blocked-dependency-deadlock.pg.test.ts": 1,
|
||||
"packages/engine/src/__tests__/self-healing-db-corruption.test.ts": 1,
|
||||
"packages/engine/src/__tests__/self-healing-query-filter-blindness.test.ts": 6,
|
||||
"packages/engine/src/__tests__/stale-task-reporter.test.ts": 1,
|
||||
"packages/engine/src/__tests__/task-completion-dependency-lanes.test.ts": 1,
|
||||
"packages/engine/src/__tests__/transition-pending-recovery-deadlock.pg.test.ts": 2,
|
||||
@@ -201,7 +200,6 @@
|
||||
"packages/engine/src/concurrency.ts": 5,
|
||||
"packages/engine/src/ephemeral-worker-manager.ts": 2,
|
||||
"packages/engine/src/eval-followups.ts": 2,
|
||||
"packages/engine/src/executor.ts": 40,
|
||||
"packages/engine/src/ipc/__tests__/ipc-host.test.ts": 1,
|
||||
"packages/engine/src/ipc/__tests__/ipc-worker.test.ts": 1,
|
||||
"packages/engine/src/merger.ts": 2,
|
||||
@@ -219,7 +217,6 @@
|
||||
"packages/engine/src/runtimes/__tests__/child-process-worker.test.ts": 3,
|
||||
"packages/engine/src/runtimes/in-process-runtime.ts": 4,
|
||||
"packages/engine/src/scheduler.ts": 9,
|
||||
"packages/engine/src/self-healing.ts": 8,
|
||||
"packages/engine/src/stale-task-reporter.ts": 2,
|
||||
"packages/engine/src/task-completion.ts": 1,
|
||||
"packages/engine/src/triage.ts": 9,
|
||||
|
||||
Reference in New Issue
Block a user