diff --git a/.changeset/live-gate-running-count.md b/.changeset/live-gate-running-count.md new file mode 100644 index 0000000000..e57060c6b6 --- /dev/null +++ b/.changeset/live-gate-running-count.md @@ -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. diff --git a/packages/core/src/__tests__/live-agent-count.test.ts b/packages/core/src/__tests__/live-agent-count.test.ts index 9f9e8671d2..5b8730e6e8 100644 --- a/packages/core/src/__tests__/live-agent-count.test.ts +++ b/packages/core/src/__tests__/live-agent-count.test.ts @@ -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"); diff --git a/packages/core/src/live-agent-count.ts b/packages/core/src/live-agent-count.ts index ee2a766d63..f0b71664c5 100644 --- a/packages/core/src/live-agent-count.ts +++ b/packages/core/src/live-agent-count.ts @@ -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 & { +export type RunningAgentTaskShape = Pick & Partial> & { columnTerminalKind?: ColumnTerminalKind; /** Trait-derived intake/hold membership, used by {@link isWaitingAgentTask}. */ columnIsIntakeOrHold?: boolean; @@ -88,6 +88,19 @@ export function enrichRunningAgentTaskShapeFromFlags 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; }