iosm-cli 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +88 -46
- package/dist/core/agent-teams.d.ts.map +1 -1
- package/dist/core/agent-teams.js +38 -11
- package/dist/core/agent-teams.js.map +1 -1
- package/dist/core/blast.d.ts +62 -0
- package/dist/core/blast.d.ts.map +1 -0
- package/dist/core/blast.js +448 -0
- package/dist/core/blast.js.map +1 -0
- package/dist/core/contract.d.ts +54 -0
- package/dist/core/contract.d.ts.map +1 -0
- package/dist/core/contract.js +300 -0
- package/dist/core/contract.js.map +1 -0
- package/dist/core/failure-retrospective.d.ts +12 -0
- package/dist/core/failure-retrospective.d.ts.map +1 -0
- package/dist/core/failure-retrospective.js +115 -0
- package/dist/core/failure-retrospective.js.map +1 -0
- package/dist/core/project-index/index.d.ts +17 -0
- package/dist/core/project-index/index.d.ts.map +1 -0
- package/dist/core/project-index/index.js +323 -0
- package/dist/core/project-index/index.js.map +1 -0
- package/dist/core/project-index/types.d.ts +34 -0
- package/dist/core/project-index/types.d.ts.map +1 -0
- package/dist/core/project-index/types.js +2 -0
- package/dist/core/project-index/types.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +8 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/semantic/config.d.ts.map +1 -1
- package/dist/core/semantic/config.js +5 -0
- package/dist/core/semantic/config.js.map +1 -1
- package/dist/core/semantic/index.d.ts +1 -1
- package/dist/core/semantic/index.d.ts.map +1 -1
- package/dist/core/semantic/index.js +1 -1
- package/dist/core/semantic/index.js.map +1 -1
- package/dist/core/semantic/runtime.d.ts.map +1 -1
- package/dist/core/semantic/runtime.js +12 -1
- package/dist/core/semantic/runtime.js.map +1 -1
- package/dist/core/semantic/types.d.ts +6 -0
- package/dist/core/semantic/types.d.ts.map +1 -1
- package/dist/core/semantic/types.js +6 -0
- package/dist/core/semantic/types.js.map +1 -1
- package/dist/core/shadow-guard.d.ts +30 -0
- package/dist/core/shadow-guard.d.ts.map +1 -0
- package/dist/core/shadow-guard.js +81 -0
- package/dist/core/shadow-guard.js.map +1 -0
- package/dist/core/shared-memory.d.ts +46 -0
- package/dist/core/shared-memory.d.ts.map +1 -0
- package/dist/core/shared-memory.js +253 -0
- package/dist/core/shared-memory.js.map +1 -0
- package/dist/core/singular.d.ts +73 -0
- package/dist/core/singular.d.ts.map +1 -0
- package/dist/core/singular.js +413 -0
- package/dist/core/singular.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +14 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagents.js +1 -1
- package/dist/core/subagents.js.map +1 -1
- package/dist/core/swarm/gates.d.ts +9 -0
- package/dist/core/swarm/gates.d.ts.map +1 -0
- package/dist/core/swarm/gates.js +65 -0
- package/dist/core/swarm/gates.js.map +1 -0
- package/dist/core/swarm/index.d.ts +9 -0
- package/dist/core/swarm/index.d.ts.map +1 -0
- package/dist/core/swarm/index.js +9 -0
- package/dist/core/swarm/index.js.map +1 -0
- package/dist/core/swarm/locks.d.ts +21 -0
- package/dist/core/swarm/locks.d.ts.map +1 -0
- package/dist/core/swarm/locks.js +93 -0
- package/dist/core/swarm/locks.js.map +1 -0
- package/dist/core/swarm/planner.d.ts +16 -0
- package/dist/core/swarm/planner.d.ts.map +1 -0
- package/dist/core/swarm/planner.js +137 -0
- package/dist/core/swarm/planner.js.map +1 -0
- package/dist/core/swarm/retry.d.ts +16 -0
- package/dist/core/swarm/retry.d.ts.map +1 -0
- package/dist/core/swarm/retry.js +32 -0
- package/dist/core/swarm/retry.js.map +1 -0
- package/dist/core/swarm/scheduler.d.ts +48 -0
- package/dist/core/swarm/scheduler.d.ts.map +1 -0
- package/dist/core/swarm/scheduler.js +554 -0
- package/dist/core/swarm/scheduler.js.map +1 -0
- package/dist/core/swarm/spawn.d.ts +16 -0
- package/dist/core/swarm/spawn.d.ts.map +1 -0
- package/dist/core/swarm/spawn.js +42 -0
- package/dist/core/swarm/spawn.js.map +1 -0
- package/dist/core/swarm/state-store.d.ts +35 -0
- package/dist/core/swarm/state-store.d.ts.map +1 -0
- package/dist/core/swarm/state-store.js +106 -0
- package/dist/core/swarm/state-store.js.map +1 -0
- package/dist/core/swarm/types.d.ts +116 -0
- package/dist/core/swarm/types.d.ts.map +1 -0
- package/dist/core/swarm/types.js +2 -0
- package/dist/core/swarm/types.js.map +1 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts.map +1 -1
- package/dist/core/tools/semantic-search.js +1 -0
- package/dist/core/tools/semantic-search.js.map +1 -1
- package/dist/core/tools/shared-memory.d.ts +23 -0
- package/dist/core/tools/shared-memory.d.ts.map +1 -0
- package/dist/core/tools/shared-memory.js +134 -0
- package/dist/core/tools/shared-memory.js.map +1 -0
- package/dist/core/tools/task.d.ts +8 -1
- package/dist/core/tools/task.d.ts.map +1 -1
- package/dist/core/tools/task.js +664 -123
- package/dist/core/tools/task.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +70 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +1 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +27 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/subagent-message.js +14 -0
- package/dist/modes/interactive/components/subagent-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +81 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3481 -870
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +29 -1
- package/docs/configuration.md +5 -0
- package/docs/interactive-mode.md +171 -2
- package/docs/orchestration-and-subagents.md +96 -169
- package/package.json +4 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SwarmRetryBucket = "permission" | "dependency_import" | "test" | "timeout" | "unknown";
|
|
2
|
+
export interface SwarmRetryPolicy {
|
|
3
|
+
maxByBucket: Record<SwarmRetryBucket, number>;
|
|
4
|
+
}
|
|
5
|
+
export declare const DEFAULT_RETRY_POLICY: SwarmRetryPolicy;
|
|
6
|
+
export declare function classifyRetryBucket(errorMessage: string): SwarmRetryBucket;
|
|
7
|
+
export declare function shouldRetry(input: {
|
|
8
|
+
errorMessage: string;
|
|
9
|
+
currentRetries: number;
|
|
10
|
+
policy?: SwarmRetryPolicy;
|
|
11
|
+
}): {
|
|
12
|
+
retry: boolean;
|
|
13
|
+
bucket: SwarmRetryBucket;
|
|
14
|
+
max: number;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/core/swarm/retry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,mBAAmB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAEnG,MAAM,WAAW,gBAAgB;IAChC,WAAW,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;CAC9C;AAED,eAAO,MAAM,oBAAoB,EAAE,gBAQlC,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,gBAAgB,CAO1E;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC1B,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAS5D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const DEFAULT_RETRY_POLICY = {
|
|
2
|
+
maxByBucket: {
|
|
3
|
+
permission: 1,
|
|
4
|
+
dependency_import: 2,
|
|
5
|
+
test: 2,
|
|
6
|
+
timeout: 2,
|
|
7
|
+
unknown: 1,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
export function classifyRetryBucket(errorMessage) {
|
|
11
|
+
const msg = errorMessage.toLowerCase();
|
|
12
|
+
if (/(permission|denied|not allowed|forbidden|eacces|eprem)/.test(msg))
|
|
13
|
+
return "permission";
|
|
14
|
+
if (/(module not found|cannot find module|importerror|dependency|package)/.test(msg))
|
|
15
|
+
return "dependency_import";
|
|
16
|
+
if (/(test failed|assert|expect\(|failing test|spec failed)/.test(msg))
|
|
17
|
+
return "test";
|
|
18
|
+
if (/(timeout|timed out|deadline exceeded)/.test(msg))
|
|
19
|
+
return "timeout";
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
export function shouldRetry(input) {
|
|
23
|
+
const bucket = classifyRetryBucket(input.errorMessage);
|
|
24
|
+
const policy = input.policy ?? DEFAULT_RETRY_POLICY;
|
|
25
|
+
const max = policy.maxByBucket[bucket] ?? 0;
|
|
26
|
+
return {
|
|
27
|
+
retry: input.currentRetries < max,
|
|
28
|
+
bucket,
|
|
29
|
+
max,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/core/swarm/retry.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,oBAAoB,GAAqB;IACrD,WAAW,EAAE;QACZ,UAAU,EAAE,CAAC;QACb,iBAAiB,EAAE,CAAC;QACpB,IAAI,EAAE,CAAC;QACP,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;KACV;CACD,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,YAAoB;IACvD,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,wDAAwD,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IAC5F,IAAI,sEAAsE,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,mBAAmB,CAAC;IACjH,IAAI,wDAAwD,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IACtF,IAAI,uCAAuC,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACxE,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAI3B;IACA,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,oBAAoB,CAAC;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5C,OAAO;QACN,KAAK,EAAE,KAAK,CAAC,cAAc,GAAG,GAAG;QACjC,MAAM;QACN,GAAG;KACH,CAAC;AACH,CAAC","sourcesContent":["export type SwarmRetryBucket = \"permission\" | \"dependency_import\" | \"test\" | \"timeout\" | \"unknown\";\n\nexport interface SwarmRetryPolicy {\n\tmaxByBucket: Record<SwarmRetryBucket, number>;\n}\n\nexport const DEFAULT_RETRY_POLICY: SwarmRetryPolicy = {\n\tmaxByBucket: {\n\t\tpermission: 1,\n\t\tdependency_import: 2,\n\t\ttest: 2,\n\t\ttimeout: 2,\n\t\tunknown: 1,\n\t},\n};\n\nexport function classifyRetryBucket(errorMessage: string): SwarmRetryBucket {\n\tconst msg = errorMessage.toLowerCase();\n\tif (/(permission|denied|not allowed|forbidden|eacces|eprem)/.test(msg)) return \"permission\";\n\tif (/(module not found|cannot find module|importerror|dependency|package)/.test(msg)) return \"dependency_import\";\n\tif (/(test failed|assert|expect\\(|failing test|spec failed)/.test(msg)) return \"test\";\n\tif (/(timeout|timed out|deadline exceeded)/.test(msg)) return \"timeout\";\n\treturn \"unknown\";\n}\n\nexport function shouldRetry(input: {\n\terrorMessage: string;\n\tcurrentRetries: number;\n\tpolicy?: SwarmRetryPolicy;\n}): { retry: boolean; bucket: SwarmRetryBucket; max: number } {\n\tconst bucket = classifyRetryBucket(input.errorMessage);\n\tconst policy = input.policy ?? DEFAULT_RETRY_POLICY;\n\tconst max = policy.maxByBucket[bucket] ?? 0;\n\treturn {\n\t\tretry: input.currentRetries < max,\n\t\tbucket,\n\t\tmax,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EngineeringContract } from "../contract.js";
|
|
2
|
+
import { type SwarmRetryPolicy } from "./retry.js";
|
|
3
|
+
import type { SwarmDispatchResult, SwarmEvent, SwarmPlan, SwarmRuntimeState, SwarmSchedulerResult, SwarmTaskPlan, SwarmTaskRuntimeState } from "./types.js";
|
|
4
|
+
export interface RunSwarmSchedulerOptions {
|
|
5
|
+
runId: string;
|
|
6
|
+
plan: SwarmPlan;
|
|
7
|
+
contract: EngineeringContract;
|
|
8
|
+
maxParallel: number;
|
|
9
|
+
budgetUsd?: number;
|
|
10
|
+
existingState?: SwarmRuntimeState;
|
|
11
|
+
retryPolicy?: SwarmRetryPolicy;
|
|
12
|
+
noProgressTickLimit?: number;
|
|
13
|
+
spawnCap?: number;
|
|
14
|
+
progressHeuristic?: {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
activateAfterNoProgressTicks?: number;
|
|
17
|
+
minScore?: number;
|
|
18
|
+
};
|
|
19
|
+
conflictDensityGuard?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
threshold?: number;
|
|
22
|
+
minParallel?: number;
|
|
23
|
+
};
|
|
24
|
+
confirmSpawn?: (input: {
|
|
25
|
+
candidate: NonNullable<SwarmDispatchResult["spawnCandidates"]>[number];
|
|
26
|
+
parentTask: SwarmTaskPlan;
|
|
27
|
+
parentTaskRuntime: SwarmTaskRuntimeState;
|
|
28
|
+
state: SwarmRuntimeState;
|
|
29
|
+
}) => Promise<boolean>;
|
|
30
|
+
dispatchTask: (input: {
|
|
31
|
+
task: SwarmTaskPlan;
|
|
32
|
+
runtime: SwarmTaskRuntimeState;
|
|
33
|
+
tick: number;
|
|
34
|
+
}) => Promise<SwarmDispatchResult>;
|
|
35
|
+
onEvent?: (event: SwarmEvent, state: SwarmRuntimeState) => void;
|
|
36
|
+
onStateChanged?: (state: SwarmRuntimeState) => void;
|
|
37
|
+
shouldStop?: () => boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface RunSwarmSchedulerExtendedResult extends SwarmSchedulerResult {
|
|
40
|
+
spawnBacklog: Array<{
|
|
41
|
+
fingerprint: string;
|
|
42
|
+
description: string;
|
|
43
|
+
path: string;
|
|
44
|
+
changeType: string;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
export declare function runSwarmScheduler(options: RunSwarmSchedulerOptions): Promise<RunSwarmSchedulerExtendedResult>;
|
|
48
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../src/core/swarm/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAG1D,OAAO,EAAqC,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtF,OAAO,KAAK,EACX,mBAAmB,EACnB,UAAU,EAEV,SAAS,EAET,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACb,qBAAqB,EACrB,MAAM,YAAY,CAAC;AAyOpB,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,4BAA4B,CAAC,EAAE,MAAM,CAAC;QACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,oBAAoB,CAAC,EAAE;QACtB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QACtB,SAAS,EAAE,WAAW,CAAC,mBAAmB,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACvE,UAAU,EAAE,aAAa,CAAC;QAC1B,iBAAiB,EAAE,qBAAqB,CAAC;QACzC,KAAK,EAAE,iBAAiB,CAAC;KACzB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,YAAY,EAAE,CAAC,KAAK,EAAE;QACrB,IAAI,EAAE,aAAa,CAAC;QACpB,OAAO,EAAE,qBAAqB,CAAC;QAC/B,IAAI,EAAE,MAAM,CAAC;KACb,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAChE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpD,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,+BAAgC,SAAQ,oBAAoB;IAC5E,YAAY,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpG;AAED,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,+BAA+B,CAAC,CAsY1C"}
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { touchesConflict, HierarchicalLockManager } from "./locks.js";
|
|
2
|
+
import { evaluateRunGates, evaluateTaskGates } from "./gates.js";
|
|
3
|
+
import { DEFAULT_RETRY_POLICY, shouldRetry } from "./retry.js";
|
|
4
|
+
import { SwarmSpawnQueue } from "./spawn.js";
|
|
5
|
+
const TERMINAL_STATUSES = new Set(["done", "error", "cancelled", "blocked"]);
|
|
6
|
+
function nowIso() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
function compact(values) {
|
|
10
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))];
|
|
11
|
+
}
|
|
12
|
+
function isTerminal(status) {
|
|
13
|
+
return TERMINAL_STATUSES.has(status);
|
|
14
|
+
}
|
|
15
|
+
function severityWeight(task) {
|
|
16
|
+
if (task.severity === "high")
|
|
17
|
+
return 3;
|
|
18
|
+
if (task.severity === "medium")
|
|
19
|
+
return 2;
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
function collectDependents(plan) {
|
|
23
|
+
const result = new Map();
|
|
24
|
+
for (const task of plan.tasks) {
|
|
25
|
+
for (const dep of task.depends_on) {
|
|
26
|
+
result.set(dep, (result.get(dep) ?? 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function createTaskRuntimeState(task) {
|
|
32
|
+
return {
|
|
33
|
+
id: task.id,
|
|
34
|
+
status: task.depends_on.length === 0 ? "ready" : "pending",
|
|
35
|
+
attempts: 0,
|
|
36
|
+
depends_on: [...task.depends_on],
|
|
37
|
+
touches: [...task.touches],
|
|
38
|
+
scopes: [...task.scopes],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildInitialState(input) {
|
|
42
|
+
if (input.existingState) {
|
|
43
|
+
return {
|
|
44
|
+
...input.existingState,
|
|
45
|
+
updatedAt: nowIso(),
|
|
46
|
+
status: input.existingState.status === "completed" ? "running" : input.existingState.status,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const tasks = {};
|
|
50
|
+
for (const task of input.plan.tasks) {
|
|
51
|
+
tasks[task.id] = createTaskRuntimeState(task);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
runId: input.runId,
|
|
55
|
+
status: "running",
|
|
56
|
+
createdAt: nowIso(),
|
|
57
|
+
updatedAt: nowIso(),
|
|
58
|
+
tick: 0,
|
|
59
|
+
noProgressTicks: 0,
|
|
60
|
+
readyQueue: Object.values(tasks)
|
|
61
|
+
.filter((task) => task.status === "ready")
|
|
62
|
+
.map((task) => task.id),
|
|
63
|
+
blockedTasks: [],
|
|
64
|
+
tasks,
|
|
65
|
+
locks: {},
|
|
66
|
+
retries: {},
|
|
67
|
+
budget: {
|
|
68
|
+
limitUsd: input.budgetUsd,
|
|
69
|
+
spentUsd: 0,
|
|
70
|
+
warned80: false,
|
|
71
|
+
hardStopped: false,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function runHasOnlyBlockedTasks(state) {
|
|
76
|
+
const tasks = Object.values(state.tasks);
|
|
77
|
+
const unfinished = tasks.filter((task) => !isTerminal(task.status));
|
|
78
|
+
if (unfinished.length > 0)
|
|
79
|
+
return false;
|
|
80
|
+
return tasks.length > 0 && tasks.some((task) => task.status === "blocked");
|
|
81
|
+
}
|
|
82
|
+
function collectReadyTasks(state, planById) {
|
|
83
|
+
const ready = [];
|
|
84
|
+
for (const [taskId, runtime] of Object.entries(state.tasks)) {
|
|
85
|
+
if (runtime.status !== "pending" && runtime.status !== "ready")
|
|
86
|
+
continue;
|
|
87
|
+
const plan = planById.get(taskId);
|
|
88
|
+
if (!plan)
|
|
89
|
+
continue;
|
|
90
|
+
const depsDone = plan.depends_on.every((dep) => state.tasks[dep]?.status === "done");
|
|
91
|
+
if (!depsDone) {
|
|
92
|
+
runtime.status = "pending";
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (runtime.status !== "ready") {
|
|
96
|
+
runtime.status = "ready";
|
|
97
|
+
}
|
|
98
|
+
ready.push(taskId);
|
|
99
|
+
}
|
|
100
|
+
return ready;
|
|
101
|
+
}
|
|
102
|
+
function selectBatch(input) {
|
|
103
|
+
const sorted = [...input.readyTaskIds].sort((a, b) => {
|
|
104
|
+
const taskA = input.planById.get(a);
|
|
105
|
+
const taskB = input.planById.get(b);
|
|
106
|
+
if (!taskA || !taskB)
|
|
107
|
+
return a.localeCompare(b);
|
|
108
|
+
const severityDelta = severityWeight(taskB) - severityWeight(taskA);
|
|
109
|
+
if (severityDelta !== 0)
|
|
110
|
+
return severityDelta;
|
|
111
|
+
const dependentDelta = (input.dependents.get(b) ?? 0) - (input.dependents.get(a) ?? 0);
|
|
112
|
+
if (dependentDelta !== 0)
|
|
113
|
+
return dependentDelta;
|
|
114
|
+
return a.localeCompare(b);
|
|
115
|
+
});
|
|
116
|
+
const selected = [];
|
|
117
|
+
for (const taskId of sorted) {
|
|
118
|
+
const plan = input.planById.get(taskId);
|
|
119
|
+
if (!plan)
|
|
120
|
+
continue;
|
|
121
|
+
const hasConflictWithSelected = selected.some((existingId) => {
|
|
122
|
+
const existing = input.planById.get(existingId);
|
|
123
|
+
if (!existing)
|
|
124
|
+
return false;
|
|
125
|
+
return touchesConflict(existing.touches, plan.touches);
|
|
126
|
+
});
|
|
127
|
+
if (hasConflictWithSelected)
|
|
128
|
+
continue;
|
|
129
|
+
selected.push(taskId);
|
|
130
|
+
if (selected.length >= Math.max(1, input.maxParallel))
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
return selected;
|
|
134
|
+
}
|
|
135
|
+
function progressScore(task, dependents) {
|
|
136
|
+
const dependentWeight = dependents.get(task.id) ?? 0;
|
|
137
|
+
const touchWeight = Math.min(3, Math.max(1, task.touches.length));
|
|
138
|
+
return severityWeight(task) * 3 + dependentWeight * 2 + touchWeight;
|
|
139
|
+
}
|
|
140
|
+
function applyProgressHeuristic(input) {
|
|
141
|
+
if (input.readyTaskIds.length <= 1)
|
|
142
|
+
return input.readyTaskIds;
|
|
143
|
+
if (input.state.noProgressTicks < input.activateAfterNoProgressTicks)
|
|
144
|
+
return input.readyTaskIds;
|
|
145
|
+
const scored = input.readyTaskIds
|
|
146
|
+
.map((taskId) => {
|
|
147
|
+
const plan = input.planById.get(taskId);
|
|
148
|
+
if (!plan)
|
|
149
|
+
return undefined;
|
|
150
|
+
return { taskId, score: progressScore(plan, input.dependents), severity: plan.severity };
|
|
151
|
+
})
|
|
152
|
+
.filter((item) => item !== undefined)
|
|
153
|
+
.sort((a, b) => b.score - a.score || a.taskId.localeCompare(b.taskId));
|
|
154
|
+
if (scored.length === 0)
|
|
155
|
+
return input.readyTaskIds;
|
|
156
|
+
const topScore = scored[0].score;
|
|
157
|
+
const threshold = Math.max(input.minScore, topScore - 2);
|
|
158
|
+
const filtered = scored
|
|
159
|
+
.filter((item) => item.score >= threshold || item.severity === "high")
|
|
160
|
+
.map((item) => item.taskId);
|
|
161
|
+
return filtered.length > 0 ? filtered : [scored[0].taskId];
|
|
162
|
+
}
|
|
163
|
+
function conflictDensity(input) {
|
|
164
|
+
const count = input.taskIds.length;
|
|
165
|
+
if (count < 2)
|
|
166
|
+
return 0;
|
|
167
|
+
let conflictingPairs = 0;
|
|
168
|
+
let totalPairs = 0;
|
|
169
|
+
for (let i = 0; i < count; i += 1) {
|
|
170
|
+
const a = input.planById.get(input.taskIds[i]);
|
|
171
|
+
if (!a)
|
|
172
|
+
continue;
|
|
173
|
+
for (let j = i + 1; j < count; j += 1) {
|
|
174
|
+
const b = input.planById.get(input.taskIds[j]);
|
|
175
|
+
if (!b)
|
|
176
|
+
continue;
|
|
177
|
+
totalPairs += 1;
|
|
178
|
+
if (touchesConflict(a.touches, b.touches)) {
|
|
179
|
+
conflictingPairs += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return totalPairs > 0 ? conflictingPairs / totalPairs : 0;
|
|
184
|
+
}
|
|
185
|
+
function applyConflictDensityGuard(input) {
|
|
186
|
+
const density = conflictDensity({ taskIds: input.readyTaskIds, planById: input.planById });
|
|
187
|
+
if (density < input.threshold) {
|
|
188
|
+
return { effectiveMaxParallel: input.maxParallel, density };
|
|
189
|
+
}
|
|
190
|
+
const scaled = Math.max(input.minParallel, Math.floor(input.maxParallel * Math.max(0.2, 1 - density)));
|
|
191
|
+
return {
|
|
192
|
+
effectiveMaxParallel: Math.max(1, Math.min(input.maxParallel, scaled)),
|
|
193
|
+
density,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function shouldStopForBudget(state) {
|
|
197
|
+
const limit = state.budget.limitUsd;
|
|
198
|
+
if (limit === undefined || limit <= 0)
|
|
199
|
+
return false;
|
|
200
|
+
if (!state.budget.warned80 && state.budget.spentUsd >= limit * 0.8) {
|
|
201
|
+
state.budget.warned80 = true;
|
|
202
|
+
}
|
|
203
|
+
if (state.budget.spentUsd >= limit) {
|
|
204
|
+
state.budget.hardStopped = true;
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
export async function runSwarmScheduler(options) {
|
|
210
|
+
const planById = new Map(options.plan.tasks.map((task) => [task.id, task]));
|
|
211
|
+
const dependents = collectDependents(options.plan);
|
|
212
|
+
const retryPolicy = options.retryPolicy ?? DEFAULT_RETRY_POLICY;
|
|
213
|
+
const noProgressLimit = Math.max(3, options.noProgressTickLimit ?? 8);
|
|
214
|
+
const spawnCap = Math.max(1, options.spawnCap ?? 30);
|
|
215
|
+
const progressHeuristicEnabled = options.progressHeuristic?.enabled !== false;
|
|
216
|
+
const progressHeuristicActivateAfter = Math.max(1, options.progressHeuristic?.activateAfterNoProgressTicks ?? 2);
|
|
217
|
+
const progressHeuristicMinScore = Math.max(1, options.progressHeuristic?.minScore ?? 4);
|
|
218
|
+
const conflictGuardEnabled = options.conflictDensityGuard?.enabled !== false;
|
|
219
|
+
const conflictGuardThreshold = Math.min(1, Math.max(0, options.conflictDensityGuard?.threshold ?? 0.45));
|
|
220
|
+
const conflictGuardMinParallel = Math.max(1, options.conflictDensityGuard?.minParallel ?? 1);
|
|
221
|
+
const state = buildInitialState({
|
|
222
|
+
runId: options.runId,
|
|
223
|
+
plan: options.plan,
|
|
224
|
+
budgetUsd: options.budgetUsd,
|
|
225
|
+
existingState: options.existingState,
|
|
226
|
+
});
|
|
227
|
+
const lockManager = new HierarchicalLockManager();
|
|
228
|
+
const taskGateByTaskId = new Map();
|
|
229
|
+
const spawned = new SwarmSpawnQueue();
|
|
230
|
+
const events = [];
|
|
231
|
+
for (const [taskId, taskState] of Object.entries(state.tasks)) {
|
|
232
|
+
if (taskState.status === "running") {
|
|
233
|
+
taskState.status = "pending";
|
|
234
|
+
taskState.startedAt = undefined;
|
|
235
|
+
}
|
|
236
|
+
if (taskState.status === "done") {
|
|
237
|
+
const plan = planById.get(taskId);
|
|
238
|
+
if (plan) {
|
|
239
|
+
taskGateByTaskId.set(taskId, evaluateTaskGates({ ...plan, touches: taskState.touches }, options.contract));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const emit = (type, message, payload, taskId) => {
|
|
244
|
+
const event = {
|
|
245
|
+
type,
|
|
246
|
+
timestamp: nowIso(),
|
|
247
|
+
runId: options.runId,
|
|
248
|
+
tick: state.tick,
|
|
249
|
+
message,
|
|
250
|
+
payload,
|
|
251
|
+
...(taskId ? { taskId } : {}),
|
|
252
|
+
};
|
|
253
|
+
events.push(event);
|
|
254
|
+
options.onEvent?.(event, state);
|
|
255
|
+
};
|
|
256
|
+
emit("run_started", `Swarm run ${options.runId} started`, {
|
|
257
|
+
tasks: options.plan.tasks.length,
|
|
258
|
+
maxParallel: options.maxParallel,
|
|
259
|
+
budgetUsd: options.budgetUsd,
|
|
260
|
+
});
|
|
261
|
+
options.onStateChanged?.(state);
|
|
262
|
+
while (true) {
|
|
263
|
+
if (options.shouldStop?.()) {
|
|
264
|
+
state.status = "stopped";
|
|
265
|
+
state.lastError = "Run interrupted by user.";
|
|
266
|
+
emit("run_stopped", state.lastError);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
state.tick += 1;
|
|
270
|
+
state.updatedAt = nowIso();
|
|
271
|
+
emit("tick", `scheduler_tick=${state.tick}`);
|
|
272
|
+
if (shouldStopForBudget(state)) {
|
|
273
|
+
state.status = "stopped";
|
|
274
|
+
state.lastError = "Budget hard-stop reached.";
|
|
275
|
+
emit("run_stopped", state.lastError, {
|
|
276
|
+
budgetLimitUsd: state.budget.limitUsd,
|
|
277
|
+
spentUsd: state.budget.spentUsd,
|
|
278
|
+
});
|
|
279
|
+
options.onStateChanged?.(state);
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
const readyTaskIds = collectReadyTasks(state, planById);
|
|
283
|
+
state.readyQueue = [...readyTaskIds];
|
|
284
|
+
state.blockedTasks = Object.values(state.tasks)
|
|
285
|
+
.filter((task) => task.status === "blocked")
|
|
286
|
+
.map((task) => task.id)
|
|
287
|
+
.sort((a, b) => a.localeCompare(b));
|
|
288
|
+
if (readyTaskIds.length === 0) {
|
|
289
|
+
const allTerminal = Object.values(state.tasks).every((task) => isTerminal(task.status));
|
|
290
|
+
if (allTerminal) {
|
|
291
|
+
const runGate = evaluateRunGates({
|
|
292
|
+
taskStates: state.tasks,
|
|
293
|
+
taskGateResults: [...taskGateByTaskId.values()],
|
|
294
|
+
contract: options.contract,
|
|
295
|
+
});
|
|
296
|
+
emit("gate_run", runGate.pass ? "run_gates_passed" : "run_gates_failed", {
|
|
297
|
+
warnings: runGate.warnings,
|
|
298
|
+
failures: runGate.failures,
|
|
299
|
+
});
|
|
300
|
+
if (runGate.pass && Object.values(state.tasks).every((task) => task.status === "done" || task.status === "blocked")) {
|
|
301
|
+
state.status = runHasOnlyBlockedTasks(state) ? "blocked" : "completed";
|
|
302
|
+
emit(state.status === "completed" ? "run_completed" : "run_blocked", `Swarm run ${state.status}`);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
state.status = "failed";
|
|
306
|
+
state.lastError = runGate.failures.join(" | ") || "Run gates failed.";
|
|
307
|
+
emit("run_failed", state.lastError);
|
|
308
|
+
}
|
|
309
|
+
options.onStateChanged?.(state);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
state.noProgressTicks += 1;
|
|
313
|
+
if (state.noProgressTicks >= noProgressLimit) {
|
|
314
|
+
state.status = "blocked";
|
|
315
|
+
state.lastError = "No progress threshold reached.";
|
|
316
|
+
emit("run_blocked", state.lastError, { noProgressTicks: state.noProgressTicks });
|
|
317
|
+
options.onStateChanged?.(state);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
options.onStateChanged?.(state);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const progressReady = progressHeuristicEnabled
|
|
324
|
+
? applyProgressHeuristic({
|
|
325
|
+
readyTaskIds,
|
|
326
|
+
planById,
|
|
327
|
+
dependents,
|
|
328
|
+
state,
|
|
329
|
+
activateAfterNoProgressTicks: progressHeuristicActivateAfter,
|
|
330
|
+
minScore: progressHeuristicMinScore,
|
|
331
|
+
})
|
|
332
|
+
: readyTaskIds;
|
|
333
|
+
const guard = conflictGuardEnabled
|
|
334
|
+
? applyConflictDensityGuard({
|
|
335
|
+
readyTaskIds: progressReady,
|
|
336
|
+
planById,
|
|
337
|
+
maxParallel: options.maxParallel,
|
|
338
|
+
threshold: conflictGuardThreshold,
|
|
339
|
+
minParallel: conflictGuardMinParallel,
|
|
340
|
+
})
|
|
341
|
+
: { effectiveMaxParallel: options.maxParallel, density: conflictDensity({ taskIds: progressReady, planById }) };
|
|
342
|
+
emit("tick", "scheduler_guards", {
|
|
343
|
+
ready: readyTaskIds.length,
|
|
344
|
+
progress_candidates: progressReady.length,
|
|
345
|
+
conflict_density: Number(guard.density.toFixed(3)),
|
|
346
|
+
effective_max_parallel: guard.effectiveMaxParallel,
|
|
347
|
+
no_progress_ticks: state.noProgressTicks,
|
|
348
|
+
});
|
|
349
|
+
const preselected = selectBatch({
|
|
350
|
+
readyTaskIds: progressReady,
|
|
351
|
+
planById,
|
|
352
|
+
maxParallel: guard.effectiveMaxParallel,
|
|
353
|
+
dependents,
|
|
354
|
+
});
|
|
355
|
+
const selected = [];
|
|
356
|
+
for (const taskId of preselected) {
|
|
357
|
+
const plan = planById.get(taskId);
|
|
358
|
+
if (!plan)
|
|
359
|
+
continue;
|
|
360
|
+
const lockCheck = lockManager.canAcquire(taskId, plan.touches);
|
|
361
|
+
if (!lockCheck.ok) {
|
|
362
|
+
const runtime = state.tasks[taskId];
|
|
363
|
+
if (runtime) {
|
|
364
|
+
runtime.status = "blocked";
|
|
365
|
+
runtime.lastError = `Lock conflict: ${lockCheck.conflicts
|
|
366
|
+
.map((conflict) => `${conflict.touch}<->${conflict.conflictingTouch}`)
|
|
367
|
+
.join(", ")}`;
|
|
368
|
+
emit("task_blocked", runtime.lastError, { conflicts: lockCheck.conflicts }, taskId);
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
selected.push(taskId);
|
|
373
|
+
}
|
|
374
|
+
if (selected.length === 0) {
|
|
375
|
+
state.noProgressTicks += 1;
|
|
376
|
+
if (state.noProgressTicks >= noProgressLimit) {
|
|
377
|
+
state.status = "blocked";
|
|
378
|
+
state.lastError = "No dispatch candidates after lock/budget filters.";
|
|
379
|
+
emit("run_blocked", state.lastError, { ready: readyTaskIds });
|
|
380
|
+
options.onStateChanged?.(state);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
options.onStateChanged?.(state);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
let progressThisTick = false;
|
|
387
|
+
const dispatchContexts = [];
|
|
388
|
+
for (const taskId of selected) {
|
|
389
|
+
const plan = planById.get(taskId);
|
|
390
|
+
const runtime = state.tasks[taskId];
|
|
391
|
+
if (!plan || !runtime)
|
|
392
|
+
continue;
|
|
393
|
+
lockManager.acquire(taskId, runtime.touches.length > 0 ? runtime.touches : plan.touches);
|
|
394
|
+
state.locks = lockManager.snapshot();
|
|
395
|
+
emit("lock_acquired", `lock acquired for ${taskId}`, { touches: runtime.touches }, taskId);
|
|
396
|
+
runtime.status = "running";
|
|
397
|
+
runtime.attempts += 1;
|
|
398
|
+
runtime.startedAt = nowIso();
|
|
399
|
+
emit("task_running", `task ${taskId} running`, { attempt: runtime.attempts }, taskId);
|
|
400
|
+
dispatchContexts.push({ taskId, plan, runtime });
|
|
401
|
+
}
|
|
402
|
+
options.onStateChanged?.(state);
|
|
403
|
+
const dispatchResults = await Promise.all(dispatchContexts.map(async ({ taskId, plan, runtime }) => {
|
|
404
|
+
let result;
|
|
405
|
+
try {
|
|
406
|
+
result = await options.dispatchTask({
|
|
407
|
+
task: plan,
|
|
408
|
+
runtime,
|
|
409
|
+
tick: state.tick,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
result = {
|
|
414
|
+
taskId,
|
|
415
|
+
status: "error",
|
|
416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return { taskId, plan, runtime, result };
|
|
420
|
+
}));
|
|
421
|
+
for (const { taskId, plan, runtime, result } of dispatchResults) {
|
|
422
|
+
if (result.touchesRefined && result.touchesRefined.length > 0) {
|
|
423
|
+
runtime.touches = compact(result.touchesRefined);
|
|
424
|
+
lockManager.downgrade(taskId, runtime.touches);
|
|
425
|
+
state.locks = lockManager.snapshot();
|
|
426
|
+
}
|
|
427
|
+
if (typeof result.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0) {
|
|
428
|
+
state.budget.spentUsd += result.costUsd;
|
|
429
|
+
}
|
|
430
|
+
if (result.status === "done") {
|
|
431
|
+
runtime.status = "done";
|
|
432
|
+
runtime.completedAt = nowIso();
|
|
433
|
+
runtime.lastError = undefined;
|
|
434
|
+
progressThisTick = true;
|
|
435
|
+
emit("task_done", `task ${taskId} done`, undefined, taskId);
|
|
436
|
+
const gateResult = evaluateTaskGates({ ...plan, touches: runtime.touches }, options.contract);
|
|
437
|
+
taskGateByTaskId.set(taskId, gateResult);
|
|
438
|
+
emit("gate_task", gateResult.pass ? "task_gates_passed" : "task_gates_failed", {
|
|
439
|
+
warnings: gateResult.warnings,
|
|
440
|
+
failures: gateResult.failures,
|
|
441
|
+
}, taskId);
|
|
442
|
+
}
|
|
443
|
+
else if (result.status === "blocked") {
|
|
444
|
+
runtime.status = "blocked";
|
|
445
|
+
runtime.lastError = result.error ?? "Task blocked by user input or policy.";
|
|
446
|
+
emit("task_blocked", runtime.lastError, undefined, taskId);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
const errorMessage = result.error ?? "Unknown task failure.";
|
|
450
|
+
const currentRetries = state.retries[taskId] ?? 0;
|
|
451
|
+
const retryDecision = shouldRetry({
|
|
452
|
+
errorMessage,
|
|
453
|
+
currentRetries,
|
|
454
|
+
policy: retryPolicy,
|
|
455
|
+
});
|
|
456
|
+
if (retryDecision.retry) {
|
|
457
|
+
state.retries[taskId] = currentRetries + 1;
|
|
458
|
+
runtime.status = "ready";
|
|
459
|
+
runtime.lastError = errorMessage;
|
|
460
|
+
emit("task_retry", `retry ${state.retries[taskId]}/${retryDecision.max} for ${taskId} (${retryDecision.bucket})`, {
|
|
461
|
+
error: errorMessage,
|
|
462
|
+
bucket: retryDecision.bucket,
|
|
463
|
+
failureCause: result.failureCause ?? retryDecision.bucket,
|
|
464
|
+
}, taskId);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
runtime.status = "error";
|
|
468
|
+
runtime.completedAt = nowIso();
|
|
469
|
+
runtime.lastError = errorMessage;
|
|
470
|
+
emit("task_error", errorMessage, { bucket: retryDecision.bucket, failureCause: result.failureCause ?? retryDecision.bucket }, taskId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const candidate of result.spawnCandidates ?? []) {
|
|
474
|
+
if (spawned.size() >= spawnCap)
|
|
475
|
+
break;
|
|
476
|
+
const requiresConfirmation = candidate.severity === "high" || plan.spawn_policy === "manual_high_risk";
|
|
477
|
+
if (requiresConfirmation && options.confirmSpawn) {
|
|
478
|
+
const approved = await options.confirmSpawn({
|
|
479
|
+
candidate,
|
|
480
|
+
parentTask: plan,
|
|
481
|
+
parentTaskRuntime: runtime,
|
|
482
|
+
state,
|
|
483
|
+
});
|
|
484
|
+
if (!approved) {
|
|
485
|
+
emit("spawn_rejected", `spawn rejected from ${taskId}`, {
|
|
486
|
+
description: candidate.description,
|
|
487
|
+
path: candidate.path,
|
|
488
|
+
changeType: candidate.changeType,
|
|
489
|
+
severity: candidate.severity,
|
|
490
|
+
}, taskId);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const queued = spawned.enqueue(candidate);
|
|
495
|
+
if (!queued.accepted)
|
|
496
|
+
continue;
|
|
497
|
+
emit("spawn_enqueued", `spawn queued from ${taskId}`, {
|
|
498
|
+
fingerprint: queued.fingerprint,
|
|
499
|
+
description: candidate.description,
|
|
500
|
+
path: candidate.path,
|
|
501
|
+
changeType: candidate.changeType,
|
|
502
|
+
severity: candidate.severity,
|
|
503
|
+
}, taskId);
|
|
504
|
+
}
|
|
505
|
+
lockManager.release(taskId);
|
|
506
|
+
state.locks = lockManager.snapshot();
|
|
507
|
+
emit("lock_released", `lock released for ${taskId}`, undefined, taskId);
|
|
508
|
+
options.onStateChanged?.(state);
|
|
509
|
+
}
|
|
510
|
+
if (shouldStopForBudget(state)) {
|
|
511
|
+
state.status = "stopped";
|
|
512
|
+
state.lastError = "Budget hard-stop reached.";
|
|
513
|
+
emit("run_stopped", state.lastError, {
|
|
514
|
+
budgetLimitUsd: state.budget.limitUsd,
|
|
515
|
+
spentUsd: state.budget.spentUsd,
|
|
516
|
+
});
|
|
517
|
+
options.onStateChanged?.(state);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
state.noProgressTicks = progressThisTick ? 0 : state.noProgressTicks + 1;
|
|
521
|
+
options.onStateChanged?.(state);
|
|
522
|
+
if (!progressThisTick && state.noProgressTicks >= noProgressLimit) {
|
|
523
|
+
state.status = "blocked";
|
|
524
|
+
state.lastError = "No measurable progress within scheduler threshold.";
|
|
525
|
+
emit("run_blocked", state.lastError, { noProgressTicks: state.noProgressTicks });
|
|
526
|
+
options.onStateChanged?.(state);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const runGate = evaluateRunGates({
|
|
531
|
+
taskStates: state.tasks,
|
|
532
|
+
taskGateResults: [...taskGateByTaskId.values()],
|
|
533
|
+
contract: options.contract,
|
|
534
|
+
});
|
|
535
|
+
if (state.status === "completed" && !runGate.pass) {
|
|
536
|
+
state.status = "failed";
|
|
537
|
+
state.lastError = runGate.failures.join(" | ") || "Run gates failed.";
|
|
538
|
+
emit("run_failed", state.lastError);
|
|
539
|
+
}
|
|
540
|
+
const drainedSpawn = spawned.drain(spawnCap).map(({ fingerprint, candidate }) => ({
|
|
541
|
+
fingerprint,
|
|
542
|
+
description: candidate.description,
|
|
543
|
+
path: candidate.path,
|
|
544
|
+
changeType: candidate.changeType,
|
|
545
|
+
}));
|
|
546
|
+
return {
|
|
547
|
+
state,
|
|
548
|
+
taskGates: [...taskGateByTaskId.values()],
|
|
549
|
+
runGate,
|
|
550
|
+
events,
|
|
551
|
+
spawnBacklog: drainedSpawn,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
//# sourceMappingURL=scheduler.js.map
|