fix(core): count live optional-gate sessions (code review, plan review) as running agents

An In Review column with one MERGING task and one live CODE REVIEW task showed
1/2 processing: gate sessions run with task.status left null, so the shared
isRunningAgentTask predicate only saw the merge-pipeline statuses. Count a
pending workflow-step-result lease (the durable live-gate signal; FN-8492
fails orphaned ones) as Running on any unpaused, non-terminal row. Covers
plan review, code review, browser verification, post-merge verification, and
custom optional steps across column headers, footer stats, admission, and CLI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-22 17:35:14 -07:00
parent c469f90b12
commit e9ff8a55ca
3 changed files with 45 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Board column and footer running counts now include live Code Review, Plan Review, and other gate sessions.
category: fix
dev: `isRunningAgentTask` treats a `pending` workflow-step-result lease as Running; shared by column headers, footer stats, admission, and CLI counts.

View File

@@ -40,6 +40,26 @@ describe("live agent count predicates", () => {
expect(isRunningAgentTask(task({ column: "working", columnCountsTowardWip: true, columnTerminalKind: "none" }))).toBe(true);
});
it("counts a live pending workflow-step gate lease as running in any non-terminal lane", () => {
const pendingCodeReview = [{ workflowStepId: "code-review", workflowStepName: "Code Review", status: "pending" as const, startedAt: "2026-07-22T05:00:00.000Z" }];
// In Review: MERGING task + live CODE REVIEW gate must both count (was 1/2).
expect(isRunningAgentTask(task({ column: "in-review", columnIsReviewOrMerge: true, workflowStepResults: pendingCodeReview }))).toBe(true);
// Planning-lane gate (plan-review) with status cleared to null also counts.
const pendingPlanReview = [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending" as const, startedAt: "2026-07-22T05:00:00.000Z" }];
expect(isRunningAgentTask(task({ column: "todo", columnIsIntakeOrHold: true, workflowStepResults: pendingPlanReview }))).toBe(true);
// A running gate is never Waiting.
expect(isWaitingAgentTask(task({ column: "todo", columnIsIntakeOrHold: true, workflowStepResults: pendingPlanReview }))).toBe(false);
// Pause and terminal columns still dominate.
expect(isRunningAgentTask(task({ column: "in-review", columnIsReviewOrMerge: true, workflowStepResults: pendingCodeReview, paused: true }))).toBe(false);
expect(isRunningAgentTask(task({ column: "in-review", columnIsReviewOrMerge: true, workflowStepResults: pendingCodeReview, userPaused: true }))).toBe(false);
expect(isRunningAgentTask(task({ column: "done", columnTerminalKind: "complete", workflowStepResults: pendingCodeReview }))).toBe(false);
// Terminal step records are not live leases.
const passed = [{ workflowStepId: "code-review", workflowStepName: "Code Review", status: "passed" as const, completedAt: "2026-07-22T05:10:00.000Z" }];
expect(isRunningAgentTask(task({ column: "in-review", columnIsReviewOrMerge: true, workflowStepResults: passed }))).toBe(false);
const failed = [{ workflowStepId: "code-review", workflowStepName: "Code Review", status: "failed" as const, completedAt: "2026-07-22T05:10:00.000Z" }];
expect(isRunningAgentTask(task({ column: "in-review", columnIsReviewOrMerge: true, workflowStepResults: failed }))).toBe(false);
});
it("enriches terminal, waiting, and WIP traits from board flags", () => {
const complete = enrichRunningAgentTaskShapeFromFlags(task({ column: "shipped", sessionFile: "/tmp/stale" }), { complete: true, countsTowardWip: true });
expect(complete.columnTerminalKind).toBe("complete");

View File

@@ -13,7 +13,7 @@ export type ColumnTerminalKind = "none" | "complete" | "archived";
* The deliberately small, pure shape used by all top-level live-agent counts.
* Store- and board-backed callers must attach trait-derived fields first.
*/
export type RunningAgentTaskShape = Pick<Task, "column" | "status" | "paused" | "userPaused" | "sessionFile" | "checkedOutBy"> & {
export type RunningAgentTaskShape = Pick<Task, "column" | "status" | "paused" | "userPaused" | "sessionFile" | "checkedOutBy"> & Partial<Pick<Task, "workflowStepResults">> & {
columnTerminalKind?: ColumnTerminalKind;
/** Trait-derived intake/hold membership, used by {@link isWaitingAgentTask}. */
columnIsIntakeOrHold?: boolean;
@@ -88,6 +88,19 @@ export function enrichRunningAgentTaskShapeFromFlags<T extends RunningAgentTaskS
};
}
/*
FNXC:ConcurrencyIndicators 2026-07-22-05:45:
Lane-owned optional gates (Code Review / Browser Verification / Plan Review) run their reviewer
session with task.status left null — the durable live signal is the step's `pending`
workflow-step-result lease (U3/KTD-4; FN-8492 fails orphaned leases, so pending ≈ live).
Without counting it, an In Review column with one MERGING task and one live CODE REVIEW task
showed 1/2 processing, and admission under-counted the live reviewer. A pending lease on an
unpaused, non-terminal row counts as Running everywhere the shared predicate is used.
*/
function hasLiveWorkflowStepLease(task: RunningAgentTaskShape): boolean {
return task.workflowStepResults?.some((result) => result.status === "pending") === true;
}
function terminalKind(task: RunningAgentTaskShape): ColumnTerminalKind {
// Legacy literals are intentionally fixture-only degradation when workflow IR is unavailable.
return task.columnTerminalKind ?? (task.column === "done" ? "complete" : task.column === "archived" ? "archived" : "none");
@@ -98,6 +111,8 @@ function terminalKind(task: RunningAgentTaskShape): ColumnTerminalKind {
* Planning may run in any non-terminal workflow column. Unpaused WIP columns
* count as execute holders (sessionFile is not on the board/DB row path).
* Active review/merge statuses count only in review/merge columns.
* A live `pending` workflow-step lease (e.g. an in-flight Code Review gate)
* counts in any non-terminal column, since gate sessions run with null status.
*/
export function isRunningAgentTask(task: RunningAgentTaskShape): boolean {
if (task.paused || task.userPaused || terminalKind(task) !== "none") return false;
@@ -106,6 +121,8 @@ export function isRunningAgentTask(task: RunningAgentTaskShape): boolean {
if (ACTIVE_IN_REVIEW_AGENT_STATUSES.has(String(task.status ?? ""))) {
return task.columnIsReviewOrMerge ?? task.column === "in-review";
}
// A live gate-session lease (pending step result) is Running even with a null status.
if (hasLiveWorkflowStepLease(task)) return true;
const isWip = task.columnCountsTowardWip ?? task.column === "in-progress";
return isWip;
}