feat(FN-4199): complete Step 1 — add capacity helper and settings

Fusion-Task-Id: FN-4199
Fusion-Task-Lineage: fd530f3d-ef2e-4684-ab05-5aaab6856f15
This commit is contained in:
Fusion
2026-05-13 03:56:47 -07:00
committed by gsxdsm
parent 3e2d646f2a
commit d31c17b41a
5 changed files with 103 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { computeCapacityRisk } from "../capacity.js";
describe("computeCapacityRisk", () => {
it("is at risk when todo exceeds threshold and idle non-ephemeral count is zero", () => {
expect(
computeCapacityRisk({
todoCount: 21,
inProgressCount: 3,
inReviewCount: 1,
idleNonEphemeralAgentCount: 0,
threshold: 20,
}),
).toMatchObject({
atRisk: true,
reason: "todo-exceeds-threshold-and-no-idle-agents",
});
});
it("is not at risk when idle non-ephemeral agents are available", () => {
expect(
computeCapacityRisk({
todoCount: 30,
inProgressCount: 2,
inReviewCount: 4,
idleNonEphemeralAgentCount: 1,
threshold: 20,
}),
).toMatchObject({ atRisk: false, reason: "ok" });
});
it("is not at risk when todo equals threshold", () => {
expect(
computeCapacityRisk({
todoCount: 20,
inProgressCount: 0,
inReviewCount: 0,
idleNonEphemeralAgentCount: 0,
threshold: 20,
}),
).toMatchObject({ atRisk: false, reason: "ok" });
});
it("is not at risk when todo is below threshold with zero idle agents", () => {
expect(
computeCapacityRisk({
todoCount: 19,
inProgressCount: 1,
inReviewCount: 1,
idleNonEphemeralAgentCount: 0,
threshold: 20,
}),
).toMatchObject({ atRisk: false, reason: "ok" });
});
});

View File

@@ -0,0 +1,37 @@
/**
* Capacity risk signals are based on a strict todo threshold.
* The warning only fires when todoCount is greater than (not equal to) the threshold
* and there are zero idle non-ephemeral agents available to drain the queue.
*/
export const DEFAULT_CAPACITY_RISK_TODO_THRESHOLD = 20;
export interface CapacityRiskSignal {
atRisk: boolean;
todoCount: number;
inProgressCount: number;
inReviewCount: number;
idleNonEphemeralAgentCount: number;
threshold: number;
reason: "todo-exceeds-threshold-and-no-idle-agents" | "ok";
}
export function computeCapacityRisk(input: {
todoCount: number;
inProgressCount: number;
inReviewCount: number;
idleNonEphemeralAgentCount: number;
threshold: number;
}): CapacityRiskSignal {
const atRisk =
input.todoCount > input.threshold && input.idleNonEphemeralAgentCount === 0;
return {
atRisk,
todoCount: input.todoCount,
inProgressCount: input.inProgressCount,
inReviewCount: input.inReviewCount,
idleNonEphemeralAgentCount: input.idleNonEphemeralAgentCount,
threshold: input.threshold,
reason: atRisk ? "todo-exceeds-threshold-and-no-idle-agents" : "ok",
};
}

View File

@@ -132,6 +132,11 @@ export type { PiExtensionEntry, PiExtensionSettings, PiExtensionSource } from ".
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
export { computeBlockerFanoutMap, BLOCKER_ESCALATION_COLUMNS, isStaleBlockedByBlocker } from "./blocker-fanout.js";
export type { BlockerFanoutEntry, BlockerEscalation, ComputeBlockerFanoutOptions } from "./blocker-fanout.js";
export {
computeCapacityRisk,
DEFAULT_CAPACITY_RISK_TODO_THRESHOLD,
} from "./capacity.js";
export type { CapacityRiskSignal } from "./capacity.js";
export {
getTaskMergeBlocker,
getTaskCompletionBlocker,

View File

@@ -227,6 +227,8 @@ export const DEFAULT_PROJECT_SETTINGS = {
specStalenessEnabled: false,
specStalenessMaxAgeMs: 6 * 60 * 60 * 1000,
taskStuckTimeoutMs: 600_000,
// Capacity risk warning default: only warn once todo is meaningfully backlogged.
capacityRiskTodoThreshold: 20,
staleHighFanoutBlockerAgeThresholdMs: 2 * 60 * 60 * 1000,
aiSessionTtlMs: 7 * 24 * 60 * 60 * 1000,
aiSessionCleanupIntervalMs: 60 * 60 * 1000,

View File

@@ -4969,6 +4969,10 @@ export interface AgentStats {
failedRuns: number;
/** Success rate (0-1) */
successRate: number;
/** Number of idle non-ephemeral agents available for queue drain */
idleNonEphemeralCount: number;
/** Number of tasks currently in the todo column */
todoTaskCount: number;
}
/** Trigger source for an agent self-reflection run */