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:
55
packages/core/src/__tests__/capacity.test.ts
Normal file
55
packages/core/src/__tests__/capacity.test.ts
Normal 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" });
|
||||
});
|
||||
});
|
||||
37
packages/core/src/capacity.ts
Normal file
37
packages/core/src/capacity.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user