fix: main is RED on two gates — record the new fallback, repoint six future-dated stamps (#3261)

`9094d1640e` (globalPause gates every graph node entry) reddened **two**
lifecycle gates on main. Both are fixed here, in separate commits.

## 1. The census ratchet went 0 → 2

`isTerminalColumnTask` in `scheduler.ts`:

```ts
const flags = columnFlagsForTask(task);
if (flags) return flags.complete === true || flags.archived === true;
return task.column === "done" || task.column === "archived";   // ← counted
```

**The code is correct.** It resolves traits first and falls back only
when the workflow is unreadable. The census counts fallback literals on
purpose — *"a fallback literal is still a literal and should go when the
trait path becomes unconditional"* — and reports them beside the backlog
as already-converted. Its own remedy for a legitimate one is a
`DELIBERATE-LITERAL` marker at the site.

Recorded rather than converted because **there is nothing to convert
to**: a task whose workflow cannot be read has no resolved lane, and
treating it as non-terminal would count a finished card's retained
worktree against live capacity — the opposite of what the surrounding
fix does.

Marker sits in the declaration's **leading** comments; an inline one
attaches to the wrong node and is silently ignored, which cost a
miscount once before. Baseline re-recorded in the same commit, since the
census tracks deliberate counts and reports a marker addition as
`RECLASSIFIED`.

## 2. The stamp gate was red as well

Six files stamped `2026-08-01-00:2x` while UTC was `2026-07-31`:

```
workflow-column-boundary.ts             2      workflow-graph-task-runner.ts   1
workflow-column-boundary-hooks.ts       1      in-process-runtime.ts           5 (allows 4)
workflow-column-boundary-capacity.test  1
```

This checkout is UTC-7, so "just after midnight local" is tomorrow in
UTC — the case AGENTS.md documents, which passes `pnpm lint` locally
*because* the local clock agrees with what was written. Second
occurrence today; I fixed the same shape on #3208 for another worker.

Repointed to `2026-07-31-22:2x`, preserving relative order. **Zero
non-comment lines changed** — 8 lines across 6 files, verified by
diffing out FNXC lines.

## Measured

| check | before | after |
|---|---|---|
| `census --strict` | **1** | **0** |
| backlog | **2** | **0** (DELIBERATE-LITERAL 148 → 150) |
| `check-fnxc-future-dates` | **1** | **0** |
| `pnpm test:gate` | 0 | 0 |
| `census-reclassification-message` | 2 failed | **1 failed** |

That last row is deliberate: the remaining failure is the
expired-premise case #3260 fixes, and I have not touched it. The
capacity test from `9094d1640e` still passes 9/9.

## Why this landed at all

Both gates run in `pr-checks.yml`, so a PR carrying either would have
gone red. Worth someone checking how it merged — a stale merge base
would explain it, and if so the same hole is open for the next merge.
This commit is contained in:
gsxdsm
2026-07-31 16:29:37 -07:00
committed by GitHub
parent 9094d1640e
commit 78d411cfe2
8 changed files with 23 additions and 10 deletions

View File

@@ -57,7 +57,7 @@ function invariantError() {
describe("workflow column boundary — global pause suspends at every node entry", () => {
/*
FNXC:EnginePause 2026-08-01-00:30:
FNXC:EnginePause 2026-07-31-22:30:
Operator regression: Stop AI Engine (globalPause) did not stop the graph — a live run started a
fresh Plan Review model session two minutes after pause, because no node boundary ever re-read
settings. These fail if the `isPaused` probe is removed from onNodeEntry.

View File

@@ -2309,7 +2309,7 @@ export class InProcessRuntime
private async drainWorkflowContinuations(): Promise<void> {
if (this.workflowContinuationDrainActive || this.status !== "active") return;
/*
FNXC:EnginePause 2026-08-01-00:20:
FNXC:EnginePause 2026-07-31-22:20:
A pause-suspended run persists a runnable continuation (same mechanism as capacity). Without
this gate the drain would re-dispatch it on the next tick and the graph would bounce
suspend→dispatch→suspend forever while paused — and worse, dispatch genuinely new work under

View File

@@ -1277,7 +1277,7 @@ export class Scheduler {
hottest write path (26 emit sites against 7, measured), not a signature change here. */
if (task.column === "in-progress") this.failedTaskIds.add(task.id);
/*
FNXC:MissionReconciliation 2026-08-01-00:00:
FNXC:MissionReconciliation 2026-07-31-22:00:
In-place failure parks do not emit task:moved, but they release the
task's durable symbol lock. Reconcile any mission-linked failure update
so the roadmap records withheld provenance without fabricating completion.
@@ -2230,6 +2230,19 @@ export class Scheduler {
membership — wip cards without a worktree yet still reserve (they are about to acquire), and
terminal lanes are excluded because their retained worktrees are cleanup-owned, not capacity.
*/
/*
FNXC:WorkflowResolvedColumns 2026-07-31-20:55 (u12 — the ratchet caught this, correctly):
DELIBERATE-LITERAL — the unresolvable-workflow default. The trait path above is the real answer;
this arm is reached only when `columnFlagsForTask` returns undefined, i.e. the task's workflow
could not be read at all, and it then gives the same answer the pre-trait code gave.
Recorded rather than converted because there is nothing to convert TO: a task with no readable
workflow has no resolved lane, and treating it as non-terminal would count a finished card's
retained worktree against live capacity — the opposite of what the surrounding fix does.
Marker sits in the DECLARATION's leading comments, not inline: markers are read from a node's
leading comments, so a mid-expression one attaches to the wrong node and is silently ignored.
*/
const isTerminalColumnTask = (task: Task): boolean => {
const flags = columnFlagsForTask(task);
if (flags) return flags.complete === true || flags.archived === true;

View File

@@ -61,7 +61,7 @@ export function createExecutorColumnBoundaryHooks(
// KTD-3 drift-park loop fix (PR #2342): detectDrift clears the stale pin
// row fields so an ordinary requeue re-resolves the CURRENT IR fresh.
clearPin: pinPersistence.clearPin,
/* FNXC:EnginePause 2026-08-01-00:20: settings re-read per node entry — event-independent. */
/* FNXC:EnginePause 2026-07-31-22:20: settings re-read per node entry — event-independent. */
isPaused: async () => {
try {
const settings = await store.getSettings();

View File

@@ -103,7 +103,7 @@ export interface WorkflowColumnBoundaryDeps {
/** Persist a durable continuation before control returns to the scheduler. */
onSuspend?: (suspension: Extract<WorkflowColumnBoundaryEntryResult, { kind: "suspended" }>) => void | Promise<void>;
/*
FNXC:EnginePause 2026-08-01-00:20 (Stop AI Engine did not stop the graph):
FNXC:EnginePause 2026-07-31-22:20 (Stop AI Engine did not stop the graph):
Operator-observed regression: with `globalPause: true` the graph runner kept crossing node
boundaries — a live run started a NEW Plan Review step (fresh model session) two minutes after
Stop AI Engine, and the plan-review→replan loop kept cycling "attempt N/unbounded". The legacy
@@ -290,7 +290,7 @@ export function createWorkflowColumnBoundary(
const toColumn = node.column;
/*
FNXC:EnginePause 2026-08-01-00:20:
FNXC:EnginePause 2026-07-31-22:20:
Pause gates EVERY node entry — columnless and same-column nodes included, because each node
can start a real AI session regardless of whether the card moves. Suspend with the same
durable-continuation mechanism capacity uses, so unpause resumes at exactly this node; the

View File

@@ -177,7 +177,7 @@ export interface WorkflowColumnBoundaryHooks {
clearPin?: () => void | Promise<void>;
onWarn?: (message: string, detail: Record<string, unknown>) => void;
onSuspend?: WorkflowColumnBoundaryDeps["onSuspend"];
/** FNXC:EnginePause 2026-08-01-00:20: polled at every node entry (see boundary deps). */
/** FNXC:EnginePause 2026-07-31-22:20: polled at every node entry (see boundary deps). */
isPaused?: WorkflowColumnBoundaryDeps["isPaused"];
}

View File

@@ -83,7 +83,7 @@
"packages/engine/src/restart-recovery-coordinator.ts": 1,
"packages/engine/src/runtime-resolution.ts": 1,
"packages/engine/src/runtimes/in-process-runtime.ts": 4,
"packages/engine/src/scheduler.ts": 3,
"packages/engine/src/scheduler.ts": 2,
"packages/engine/src/triage.ts": 6,
"packages/engine/src/workflow-work-processor.ts": 1,
"scripts/lib/lifecycle-column-census-ast.mjs": 3,

View File

@@ -4,6 +4,8 @@
"deliberateByFile": {
"packages/core/src/task-store/async-comments-attachments.ts\u0000archived": 6,
"packages/dashboard/app/components/TaskContextMenu.tsx\u0000in-review": 3,
"packages/engine/src/scheduler.ts\u0000archived": 3,
"packages/engine/src/scheduler.ts\u0000done": 3,
"packages/engine/src/scheduler.ts\u0000in-progress": 3,
"packages/engine/src/scheduler.ts\u0000in-review": 3,
"packages/engine/src/self-healing.ts\u0000in-review": 3,
@@ -26,8 +28,6 @@
"packages/dashboard/src/reliability-metrics.ts\u0000in-review": 2,
"packages/engine/src/auto-merge-finalization.ts\u0000done": 2,
"packages/engine/src/cli-agent/state-machine.ts\u0000done": 2,
"packages/engine/src/scheduler.ts\u0000archived": 2,
"packages/engine/src/scheduler.ts\u0000done": 2,
"packages/engine/src/self-healing.ts\u0000done": 2,
"packages/engine/src/triage.ts\u0000triage": 2,
"packages/engine/src/usage-limit-detector.ts\u0000archived": 2,