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:
7
.changeset/live-gate-running-count.md
Normal file
7
.changeset/live-gate-running-count.md
Normal 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.
|
||||||
@@ -40,6 +40,26 @@ describe("live agent count predicates", () => {
|
|||||||
expect(isRunningAgentTask(task({ column: "working", columnCountsTowardWip: true, columnTerminalKind: "none" }))).toBe(true);
|
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", () => {
|
it("enriches terminal, waiting, and WIP traits from board flags", () => {
|
||||||
const complete = enrichRunningAgentTaskShapeFromFlags(task({ column: "shipped", sessionFile: "/tmp/stale" }), { complete: true, countsTowardWip: true });
|
const complete = enrichRunningAgentTaskShapeFromFlags(task({ column: "shipped", sessionFile: "/tmp/stale" }), { complete: true, countsTowardWip: true });
|
||||||
expect(complete.columnTerminalKind).toBe("complete");
|
expect(complete.columnTerminalKind).toBe("complete");
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export type ColumnTerminalKind = "none" | "complete" | "archived";
|
|||||||
* The deliberately small, pure shape used by all top-level live-agent counts.
|
* The deliberately small, pure shape used by all top-level live-agent counts.
|
||||||
* Store- and board-backed callers must attach trait-derived fields first.
|
* 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;
|
columnTerminalKind?: ColumnTerminalKind;
|
||||||
/** Trait-derived intake/hold membership, used by {@link isWaitingAgentTask}. */
|
/** Trait-derived intake/hold membership, used by {@link isWaitingAgentTask}. */
|
||||||
columnIsIntakeOrHold?: boolean;
|
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 {
|
function terminalKind(task: RunningAgentTaskShape): ColumnTerminalKind {
|
||||||
// Legacy literals are intentionally fixture-only degradation when workflow IR is unavailable.
|
// Legacy literals are intentionally fixture-only degradation when workflow IR is unavailable.
|
||||||
return task.columnTerminalKind ?? (task.column === "done" ? "complete" : task.column === "archived" ? "archived" : "none");
|
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
|
* 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).
|
* count as execute holders (sessionFile is not on the board/DB row path).
|
||||||
* Active review/merge statuses count only in review/merge columns.
|
* 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 {
|
export function isRunningAgentTask(task: RunningAgentTaskShape): boolean {
|
||||||
if (task.paused || task.userPaused || terminalKind(task) !== "none") return false;
|
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 ?? ""))) {
|
if (ACTIVE_IN_REVIEW_AGENT_STATUSES.has(String(task.status ?? ""))) {
|
||||||
return task.columnIsReviewOrMerge ?? task.column === "in-review";
|
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";
|
const isWip = task.columnCountsTowardWip ?? task.column === "in-progress";
|
||||||
return isWip;
|
return isWip;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user