oxe-cc 1.0.0 → 1.3.0

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.
Files changed (322) hide show
  1. package/.cursor/commands/oxe-ask.md +3 -3
  2. package/.cursor/commands/oxe-capabilities.md +3 -3
  3. package/.cursor/commands/oxe-checkpoint.md +3 -3
  4. package/.cursor/commands/oxe-compact.md +3 -3
  5. package/.cursor/commands/oxe-dashboard.md +3 -3
  6. package/.cursor/commands/oxe-debug.md +3 -3
  7. package/.cursor/commands/oxe-discuss.md +3 -3
  8. package/.cursor/commands/oxe-execute.md +7 -4
  9. package/.cursor/commands/oxe-forensics.md +3 -3
  10. package/.cursor/commands/oxe-help.md +3 -3
  11. package/.cursor/commands/oxe-loop.md +3 -3
  12. package/.cursor/commands/oxe-milestone.md +3 -3
  13. package/.cursor/commands/oxe-next.md +3 -3
  14. package/.cursor/commands/oxe-obs.md +3 -3
  15. package/.cursor/commands/oxe-plan-agent.md +3 -3
  16. package/.cursor/commands/oxe-plan.md +3 -3
  17. package/.cursor/commands/oxe-project.md +3 -3
  18. package/.cursor/commands/oxe-quick.md +3 -3
  19. package/.cursor/commands/oxe-research.md +3 -3
  20. package/.cursor/commands/oxe-retro.md +3 -3
  21. package/.cursor/commands/oxe-review-pr.md +3 -3
  22. package/.cursor/commands/oxe-route.md +3 -3
  23. package/.cursor/commands/oxe-scan.md +3 -3
  24. package/.cursor/commands/oxe-security.md +3 -3
  25. package/.cursor/commands/oxe-session.md +4 -4
  26. package/.cursor/commands/oxe-ship.md +45 -0
  27. package/.cursor/commands/oxe-skill.md +3 -3
  28. package/.cursor/commands/oxe-spec.md +3 -3
  29. package/.cursor/commands/oxe-ui-review.md +3 -3
  30. package/.cursor/commands/oxe-ui-spec.md +3 -3
  31. package/.cursor/commands/oxe-update.md +3 -3
  32. package/.cursor/commands/oxe-validate-gaps.md +3 -3
  33. package/.cursor/commands/oxe-verify.md +6 -3
  34. package/.cursor/commands/oxe-workstream.md +3 -3
  35. package/.cursor/commands/oxe.md +6 -6
  36. package/.github/copilot-instructions.md +94 -4
  37. package/.github/prompts/oxe-ask.prompt.md +3 -3
  38. package/.github/prompts/oxe-capabilities.prompt.md +3 -3
  39. package/.github/prompts/oxe-checkpoint.prompt.md +3 -3
  40. package/.github/prompts/oxe-compact.prompt.md +3 -3
  41. package/.github/prompts/oxe-dashboard.prompt.md +3 -3
  42. package/.github/prompts/oxe-debug.prompt.md +3 -3
  43. package/.github/prompts/oxe-discuss.prompt.md +3 -3
  44. package/.github/prompts/oxe-execute.prompt.md +7 -4
  45. package/.github/prompts/oxe-forensics.prompt.md +3 -3
  46. package/.github/prompts/oxe-help.prompt.md +3 -3
  47. package/.github/prompts/oxe-loop.prompt.md +3 -3
  48. package/.github/prompts/oxe-milestone.prompt.md +3 -3
  49. package/.github/prompts/oxe-next.prompt.md +3 -3
  50. package/.github/prompts/oxe-obs.prompt.md +3 -3
  51. package/.github/prompts/oxe-plan-agent.prompt.md +3 -3
  52. package/.github/prompts/oxe-plan.prompt.md +3 -3
  53. package/.github/prompts/oxe-project.prompt.md +3 -3
  54. package/.github/prompts/oxe-quick.prompt.md +3 -3
  55. package/.github/prompts/oxe-research.prompt.md +3 -3
  56. package/.github/prompts/oxe-retro.prompt.md +3 -3
  57. package/.github/prompts/oxe-review-pr.prompt.md +3 -3
  58. package/.github/prompts/oxe-route.prompt.md +3 -3
  59. package/.github/prompts/oxe-scan.prompt.md +3 -3
  60. package/.github/prompts/oxe-security.prompt.md +3 -3
  61. package/.github/prompts/oxe-session.prompt.md +4 -4
  62. package/.github/prompts/oxe-ship.prompt.md +45 -0
  63. package/.github/prompts/oxe-skill.prompt.md +3 -3
  64. package/.github/prompts/oxe-spec.prompt.md +3 -3
  65. package/.github/prompts/oxe-ui-review.prompt.md +3 -3
  66. package/.github/prompts/oxe-ui-spec.prompt.md +3 -3
  67. package/.github/prompts/oxe-update.prompt.md +3 -3
  68. package/.github/prompts/oxe-validate-gaps.prompt.md +3 -3
  69. package/.github/prompts/oxe-verify.prompt.md +6 -3
  70. package/.github/prompts/oxe-workstream.prompt.md +3 -3
  71. package/.github/prompts/oxe.prompt.md +5 -5
  72. package/AGENTS.md +43 -28
  73. package/CHANGELOG.md +193 -0
  74. package/README.md +610 -529
  75. package/bin/banner.txt +1 -1
  76. package/bin/lib/oxe-agent-install.cjs +69 -69
  77. package/bin/lib/oxe-azure.cjs +1445 -1445
  78. package/bin/lib/oxe-context-engine.cjs +867 -867
  79. package/bin/lib/oxe-dashboard.cjs +76 -28
  80. package/bin/lib/oxe-operational.cjs +2144 -1340
  81. package/bin/lib/oxe-project-health.cjs +483 -1
  82. package/bin/lib/oxe-runtime-semantics.cjs +12 -0
  83. package/bin/oxe-cc.js +554 -152
  84. package/commands/oxe/ask.md +7 -3
  85. package/commands/oxe/capabilities.md +2 -2
  86. package/commands/oxe/checkpoint.md +3 -3
  87. package/commands/oxe/compact.md +3 -3
  88. package/commands/oxe/dashboard.md +2 -2
  89. package/commands/oxe/debug.md +3 -3
  90. package/commands/oxe/discuss.md +2 -2
  91. package/commands/oxe/execute.md +7 -4
  92. package/commands/oxe/forensics.md +3 -3
  93. package/commands/oxe/help.md +2 -2
  94. package/commands/oxe/loop.md +3 -3
  95. package/commands/oxe/milestone.md +3 -3
  96. package/commands/oxe/next.md +3 -3
  97. package/commands/oxe/obs.md +3 -3
  98. package/commands/oxe/oxe.md +5 -5
  99. package/commands/oxe/plan-agent.md +2 -2
  100. package/commands/oxe/plan.md +2 -2
  101. package/commands/oxe/project.md +3 -3
  102. package/commands/oxe/quick.md +2 -2
  103. package/commands/oxe/research.md +3 -3
  104. package/commands/oxe/retro.md +3 -3
  105. package/commands/oxe/review-pr.md +3 -3
  106. package/commands/oxe/route.md +3 -3
  107. package/commands/oxe/scan.md +3 -3
  108. package/commands/oxe/security.md +3 -3
  109. package/commands/oxe/session.md +4 -4
  110. package/commands/oxe/ship.md +49 -0
  111. package/commands/oxe/skill.md +2 -2
  112. package/commands/oxe/spec.md +4 -4
  113. package/commands/oxe/ui-review.md +3 -3
  114. package/commands/oxe/ui-spec.md +3 -3
  115. package/commands/oxe/update.md +2 -2
  116. package/commands/oxe/validate-gaps.md +3 -3
  117. package/commands/oxe/verify.md +7 -4
  118. package/commands/oxe/workstream.md +3 -3
  119. package/lib/runtime/audit/audit-trail.d.ts +71 -0
  120. package/lib/runtime/audit/audit-trail.js +154 -0
  121. package/lib/runtime/audit/index.d.ts +2 -0
  122. package/lib/runtime/audit/index.js +18 -0
  123. package/lib/runtime/audit/policy-pack.d.ts +15 -0
  124. package/lib/runtime/audit/policy-pack.js +57 -0
  125. package/lib/runtime/context/context-pack-builder.d.ts +15 -0
  126. package/lib/runtime/context/context-pack-builder.js +42 -0
  127. package/lib/runtime/context/context-pack-store.d.ts +38 -0
  128. package/lib/runtime/context/context-pack-store.js +142 -0
  129. package/lib/runtime/context/context-profiles.d.ts +11 -0
  130. package/lib/runtime/context/context-profiles.js +51 -0
  131. package/lib/runtime/context/index.d.ts +2 -0
  132. package/lib/runtime/context/index.js +2 -0
  133. package/lib/runtime/decision/decision-engine.d.ts +43 -0
  134. package/lib/runtime/decision/decision-engine.js +127 -0
  135. package/lib/runtime/decision/decision-memo.d.ts +53 -0
  136. package/lib/runtime/decision/decision-memo.js +173 -0
  137. package/lib/runtime/decision/index.d.ts +2 -0
  138. package/lib/runtime/decision/index.js +18 -0
  139. package/lib/runtime/delivery/branch-manager.d.ts +1 -0
  140. package/lib/runtime/delivery/branch-manager.js +7 -0
  141. package/lib/runtime/delivery/ci-checks.js +34 -1
  142. package/lib/runtime/delivery/delivery-records.d.ts +34 -0
  143. package/lib/runtime/delivery/delivery-records.js +48 -0
  144. package/lib/runtime/delivery/index.d.ts +2 -0
  145. package/lib/runtime/delivery/index.js +2 -0
  146. package/lib/runtime/delivery/promotion-pipeline.d.ts +63 -0
  147. package/lib/runtime/delivery/promotion-pipeline.js +224 -0
  148. package/lib/runtime/gate/gate-manager.d.ts +41 -0
  149. package/lib/runtime/gate/gate-manager.js +108 -1
  150. package/lib/runtime/index.d.ts +5 -2
  151. package/lib/runtime/index.js +7 -1
  152. package/lib/runtime/models/gate-decision.d.ts +4 -1
  153. package/lib/runtime/models/workspace.d.ts +3 -0
  154. package/lib/runtime/plugins/capability-adapter.d.ts +12 -0
  155. package/lib/runtime/plugins/capability-adapter.js +204 -0
  156. package/lib/runtime/plugins/capability-matrix.d.ts +25 -0
  157. package/lib/runtime/plugins/capability-matrix.js +90 -0
  158. package/lib/runtime/plugins/index.d.ts +3 -0
  159. package/lib/runtime/plugins/index.js +3 -0
  160. package/lib/runtime/plugins/plugin-abi.d.ts +2 -0
  161. package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
  162. package/lib/runtime/plugins/plugin-manifest.js +95 -0
  163. package/lib/runtime/plugins/plugin-registry.d.ts +46 -0
  164. package/lib/runtime/plugins/plugin-registry.js +84 -2
  165. package/lib/runtime/policy/policy-engine.d.ts +47 -1
  166. package/lib/runtime/policy/policy-engine.js +172 -9
  167. package/lib/runtime/projection/projection-engine.d.ts +9 -1
  168. package/lib/runtime/projection/projection-engine.js +73 -3
  169. package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
  170. package/lib/runtime/reducers/run-state-reducer.js +117 -1
  171. package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
  172. package/lib/runtime/scheduler/agent-registry.js +96 -0
  173. package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
  174. package/lib/runtime/scheduler/agent-roles.js +62 -0
  175. package/lib/runtime/scheduler/index.d.ts +3 -0
  176. package/lib/runtime/scheduler/index.js +3 -0
  177. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +45 -1
  178. package/lib/runtime/scheduler/multi-agent-coordinator.js +234 -35
  179. package/lib/runtime/scheduler/run-journal.d.ts +18 -0
  180. package/lib/runtime/scheduler/run-journal.js +54 -0
  181. package/lib/runtime/scheduler/scheduler.d.ts +29 -1
  182. package/lib/runtime/scheduler/scheduler.js +387 -14
  183. package/lib/runtime/verification/index.d.ts +1 -0
  184. package/lib/runtime/verification/index.js +1 -0
  185. package/lib/runtime/verification/verification-compiler.d.ts +43 -0
  186. package/lib/runtime/verification/verification-compiler.js +137 -0
  187. package/lib/runtime/verification/verification-manifest.d.ts +67 -0
  188. package/lib/runtime/verification/verification-manifest.js +179 -0
  189. package/lib/runtime/workspace/strategies/ephemeral-container.d.ts +1 -0
  190. package/lib/runtime/workspace/strategies/ephemeral-container.js +4 -0
  191. package/lib/runtime/workspace/strategies/git-worktree.d.ts +1 -0
  192. package/lib/runtime/workspace/strategies/git-worktree.js +2 -0
  193. package/lib/runtime/workspace/strategies/inplace.d.ts +1 -0
  194. package/lib/runtime/workspace/strategies/inplace.js +2 -0
  195. package/lib/runtime/workspace/workspace-manager.d.ts +2 -1
  196. package/lib/sdk/README.md +9 -9
  197. package/lib/sdk/index.cjs +33 -24
  198. package/lib/sdk/index.d.ts +149 -14
  199. package/oxe/templates/ACTIVE-RUN.template.json +32 -32
  200. package/oxe/templates/CAPABILITIES.template.md +7 -7
  201. package/oxe/templates/CAPABILITY.template.md +45 -45
  202. package/oxe/templates/CHECKPOINTS.template.md +7 -7
  203. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -68
  204. package/oxe/templates/HYPOTHESES.template.md +33 -33
  205. package/oxe/templates/LESSONS-METRICS.template.json +13 -13
  206. package/oxe/templates/NOTES.template.md +16 -16
  207. package/oxe/templates/PLAN-REVIEW.template.md +31 -31
  208. package/oxe/templates/SESSION.template.md +34 -34
  209. package/oxe/templates/SKILL.template.md +26 -26
  210. package/oxe/templates/STATE.md +55 -55
  211. package/oxe/templates/WORKFLOW_AUTHORING.md +18 -18
  212. package/oxe/workflows/ask.md +96 -92
  213. package/oxe/workflows/capabilities.md +25 -25
  214. package/oxe/workflows/checkpoint.md +14 -10
  215. package/oxe/workflows/dashboard.md +33 -33
  216. package/oxe/workflows/debug.md +19 -15
  217. package/oxe/workflows/discuss.md +12 -12
  218. package/oxe/workflows/execute.md +44 -2
  219. package/oxe/workflows/forensics.md +13 -9
  220. package/oxe/workflows/help.md +352 -304
  221. package/oxe/workflows/loop.md +17 -13
  222. package/oxe/workflows/next.md +22 -22
  223. package/oxe/workflows/obs.md +4 -0
  224. package/oxe/workflows/oxe.md +64 -31
  225. package/oxe/workflows/plan-agent.md +9 -9
  226. package/oxe/workflows/project.md +6 -1
  227. package/oxe/workflows/quick.md +10 -10
  228. package/oxe/workflows/references/reasoning-discovery.md +28 -28
  229. package/oxe/workflows/references/reasoning-execution.md +29 -29
  230. package/oxe/workflows/references/reasoning-planning.md +32 -32
  231. package/oxe/workflows/references/reasoning-review.md +29 -29
  232. package/oxe/workflows/references/reasoning-status.md +24 -24
  233. package/oxe/workflows/references/robustness-elevation.md +295 -295
  234. package/oxe/workflows/references/workflow-runtime-contracts.json +952 -907
  235. package/oxe/workflows/research.md +32 -28
  236. package/oxe/workflows/retro.md +4 -0
  237. package/oxe/workflows/review-pr.md +15 -11
  238. package/oxe/workflows/route.md +16 -16
  239. package/oxe/workflows/scan.md +4 -0
  240. package/oxe/workflows/security.md +14 -10
  241. package/oxe/workflows/session.md +213 -197
  242. package/oxe/workflows/ship.md +142 -0
  243. package/oxe/workflows/skill.md +44 -44
  244. package/oxe/workflows/spec.md +15 -0
  245. package/oxe/workflows/ui-review.md +20 -16
  246. package/oxe/workflows/ui-spec.md +7 -3
  247. package/oxe/workflows/validate-gaps.md +13 -9
  248. package/oxe/workflows/verify-audit.md +73 -73
  249. package/oxe/workflows/verify.md +52 -3
  250. package/package.json +92 -92
  251. package/packages/runtime/package.json +17 -17
  252. package/packages/runtime/src/audit/audit-trail.ts +243 -0
  253. package/packages/runtime/src/audit/index.ts +2 -0
  254. package/packages/runtime/src/audit/policy-pack.ts +62 -0
  255. package/packages/runtime/src/compiler/graph-compiler.ts +245 -245
  256. package/packages/runtime/src/compiler/index.ts +1 -1
  257. package/packages/runtime/src/context/context-pack-builder.ts +259 -193
  258. package/packages/runtime/src/context/context-pack-store.ts +197 -0
  259. package/packages/runtime/src/context/context-profiles.ts +60 -0
  260. package/packages/runtime/src/context/index.ts +3 -1
  261. package/packages/runtime/src/decision/decision-engine.ts +174 -0
  262. package/packages/runtime/src/decision/decision-memo.ts +211 -0
  263. package/packages/runtime/src/decision/index.ts +2 -0
  264. package/packages/runtime/src/delivery/branch-manager.ts +91 -84
  265. package/packages/runtime/src/delivery/ci-checks.ts +285 -252
  266. package/packages/runtime/src/delivery/delivery-records.ts +75 -0
  267. package/packages/runtime/src/delivery/index.ts +5 -3
  268. package/packages/runtime/src/delivery/pr-manager.ts +112 -112
  269. package/packages/runtime/src/delivery/promotion-pipeline.ts +334 -0
  270. package/packages/runtime/src/events/bus.ts +92 -92
  271. package/packages/runtime/src/events/catalog.ts +29 -29
  272. package/packages/runtime/src/events/envelope.ts +14 -14
  273. package/packages/runtime/src/events/index.ts +3 -3
  274. package/packages/runtime/src/evidence/evidence-store.ts +130 -130
  275. package/packages/runtime/src/evidence/index.ts +1 -1
  276. package/packages/runtime/src/gate/gate-manager.ts +289 -137
  277. package/packages/runtime/src/gate/index.ts +1 -1
  278. package/packages/runtime/src/index.ts +41 -32
  279. package/packages/runtime/src/models/attempt.ts +19 -19
  280. package/packages/runtime/src/models/evidence.ts +21 -21
  281. package/packages/runtime/src/models/gate-decision.ts +25 -21
  282. package/packages/runtime/src/models/index.ts +8 -8
  283. package/packages/runtime/src/models/run.ts +24 -24
  284. package/packages/runtime/src/models/session.ts +11 -11
  285. package/packages/runtime/src/models/verification-result.ts +10 -10
  286. package/packages/runtime/src/models/work-item.ts +25 -25
  287. package/packages/runtime/src/models/workspace.ts +31 -28
  288. package/packages/runtime/src/plugins/capability-adapter.ts +206 -0
  289. package/packages/runtime/src/plugins/capability-matrix.ts +126 -0
  290. package/packages/runtime/src/plugins/index.ts +5 -2
  291. package/packages/runtime/src/plugins/plugin-abi.ts +97 -95
  292. package/packages/runtime/src/plugins/plugin-manifest.ts +118 -0
  293. package/packages/runtime/src/plugins/plugin-registry.ts +232 -119
  294. package/packages/runtime/src/policy/index.ts +1 -1
  295. package/packages/runtime/src/policy/policy-engine.ts +330 -113
  296. package/packages/runtime/src/projection/index.ts +1 -1
  297. package/packages/runtime/src/projection/projection-engine.ts +328 -249
  298. package/packages/runtime/src/reducers/debug-reducer.ts +36 -36
  299. package/packages/runtime/src/reducers/index.ts +2 -2
  300. package/packages/runtime/src/reducers/run-state-reducer.ts +269 -127
  301. package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
  302. package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
  303. package/packages/runtime/src/scheduler/index.ts +4 -1
  304. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +521 -231
  305. package/packages/runtime/src/scheduler/run-journal.ts +62 -0
  306. package/packages/runtime/src/scheduler/scheduler.ts +722 -281
  307. package/packages/runtime/src/verification/index.ts +2 -1
  308. package/packages/runtime/src/verification/verification-compiler.ts +436 -225
  309. package/packages/runtime/src/verification/verification-manifest.ts +252 -0
  310. package/packages/runtime/src/workspace/index.ts +5 -5
  311. package/packages/runtime/src/workspace/strategies/ephemeral-container.ts +126 -121
  312. package/packages/runtime/src/workspace/strategies/git-worktree.ts +79 -77
  313. package/packages/runtime/src/workspace/strategies/inplace.ts +38 -35
  314. package/packages/runtime/src/workspace/workspace-manager.ts +16 -15
  315. package/packages/runtime/tsconfig.json +17 -17
  316. package/vscode-extension/.vscodeignore +7 -7
  317. package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
  318. package/vscode-extension/package.json +185 -185
  319. package/vscode-extension/src/extension.js +310 -310
  320. package/vscode-extension/src/shared/contextLoader.js +137 -137
  321. package/vscode-extension/src/shared/contractBuilder.js +159 -159
  322. package/vscode-extension/src/shared/stateReader.js +101 -101
@@ -1,281 +1,722 @@
1
- import { appendEvent } from '../events/bus';
2
- import type { OxeEvent } from '../events/envelope';
3
- import type { EventInput } from '../events/bus';
4
- import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
5
- import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
6
- import type { WorkspaceLease } from '../models/workspace';
7
-
8
- export interface TaskResult {
9
- success: boolean;
10
- failure_class: 'env' | 'policy' | 'test' | 'timeout' | null;
11
- evidence: string[];
12
- output: string;
13
- }
14
-
15
- export interface TaskExecutor {
16
- execute(
17
- node: GraphNode,
18
- lease: WorkspaceLease,
19
- runId: string,
20
- attemptNumber: number
21
- ): Promise<TaskResult>;
22
- }
23
-
24
- export interface SchedulerContext {
25
- projectRoot: string;
26
- sessionId: string | null;
27
- runId: string;
28
- executor: TaskExecutor;
29
- workspaceManager: WorkspaceManager;
30
- onEvent?: (event: OxeEvent) => void;
31
- }
32
-
33
- export interface RunResult {
34
- run_id: string;
35
- status: 'completed' | 'failed' | 'cancelled';
36
- completed: string[];
37
- failed: string[];
38
- blocked: string[];
39
- }
40
-
41
- type NodeStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'blocked';
42
-
43
- export class Scheduler {
44
- private cancelled = false;
45
- private paused = false;
46
-
47
- async run(graph: ExecutionGraph, ctx: SchedulerContext): Promise<RunResult> {
48
- this.cancelled = false;
49
- this.paused = false;
50
-
51
- const status = new Map<string, NodeStatus>();
52
- for (const id of graph.nodes.keys()) status.set(id, 'pending');
53
-
54
- const completed: string[] = [];
55
- const failed: string[] = [];
56
- const blocked: string[] = [];
57
-
58
- this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId } });
59
-
60
- for (const wave of graph.waves) {
61
- if (this.cancelled) break;
62
- const waveFailed = await this.runWave(
63
- wave.node_ids,
64
- graph,
65
- ctx,
66
- status,
67
- completed,
68
- failed,
69
- blocked
70
- );
71
- if (waveFailed) break;
72
- }
73
-
74
- // Any remaining pending nodes become blocked
75
- for (const [id, s] of status) {
76
- if (s === 'pending') {
77
- status.set(id, 'blocked');
78
- blocked.push(id);
79
- this.emit(ctx, {
80
- type: 'WorkItemBlocked',
81
- work_item_id: id,
82
- payload: { reason: 'upstream_wave_failed' },
83
- });
84
- }
85
- }
86
-
87
- const finalStatus: RunResult['status'] = this.cancelled
88
- ? 'cancelled'
89
- : failed.length > 0
90
- ? 'failed'
91
- : 'completed';
92
-
93
- this.emit(ctx, {
94
- type: 'RunCompleted',
95
- payload: { run_id: ctx.runId, status: finalStatus },
96
- });
97
-
98
- return { run_id: ctx.runId, status: finalStatus, completed, failed, blocked };
99
- }
100
-
101
- private async runWave(
102
- nodeIds: string[],
103
- graph: ExecutionGraph,
104
- ctx: SchedulerContext,
105
- status: Map<string, NodeStatus>,
106
- completed: string[],
107
- failed: string[],
108
- blocked: string[]
109
- ): Promise<boolean> {
110
- // Partition: eligible vs blocked-by-dep
111
- const eligible: string[] = [];
112
- const depsNotMet: string[] = [];
113
-
114
- for (const id of nodeIds) {
115
- const node = graph.nodes.get(id)!;
116
- const depsMet = node.depends_on.every((dep) => status.get(dep) === 'completed');
117
- if (depsMet) {
118
- eligible.push(id);
119
- } else {
120
- depsNotMet.push(id);
121
- }
122
- }
123
-
124
- // Nodes whose deps weren't met in this wave → blocked
125
- for (const id of depsNotMet) {
126
- status.set(id, 'blocked');
127
- blocked.push(id);
128
- this.emit(ctx, {
129
- type: 'WorkItemBlocked',
130
- work_item_id: id,
131
- payload: { reason: 'dependency_not_met' },
132
- });
133
- }
134
-
135
- // Separate read-only (no mutation_scope) from mutation nodes
136
- const readOnly = eligible.filter((id) => {
137
- const node = graph.nodes.get(id)!;
138
- return node.mutation_scope.length === 0;
139
- });
140
- const mutations = eligible.filter((id) => !readOnly.includes(id));
141
-
142
- // Run read-only nodes in parallel
143
- if (readOnly.length > 0) {
144
- await Promise.all(
145
- readOnly.map((id) => this.runNode(id, graph, ctx, status, completed, failed))
146
- );
147
- }
148
-
149
- // Run mutation nodes sequentially to avoid scope conflicts
150
- for (const id of mutations) {
151
- if (this.cancelled) break;
152
- await this.runNode(id, graph, ctx, status, completed, failed);
153
- }
154
-
155
- return failed.length > 0;
156
- }
157
-
158
- private async runNode(
159
- nodeId: string,
160
- graph: ExecutionGraph,
161
- ctx: SchedulerContext,
162
- status: Map<string, NodeStatus>,
163
- completed: string[],
164
- failed: string[]
165
- ): Promise<void> {
166
- const node = graph.nodes.get(nodeId)!;
167
- status.set(nodeId, 'running');
168
- this.emit(ctx, {
169
- type: 'WorkItemReady',
170
- work_item_id: nodeId,
171
- payload: { title: node.title, wave: node.wave },
172
- });
173
-
174
- let lease: WorkspaceLease | null = null;
175
- let lastResult: TaskResult | null = null;
176
- const maxAttempts = node.policy.max_retries + 1;
177
-
178
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
179
- const attemptId = `${nodeId}-a${attempt}`;
180
-
181
- this.emit(ctx, {
182
- type: 'AttemptStarted',
183
- work_item_id: nodeId,
184
- attempt_id: attemptId,
185
- payload: { attempt_number: attempt },
186
- });
187
-
188
- try {
189
- const wsReq: WorkspaceRequest = {
190
- work_item_id: nodeId,
191
- attempt_number: attempt,
192
- strategy: node.workspace_strategy,
193
- mutation_scope: node.mutation_scope,
194
- };
195
- lease = await ctx.workspaceManager.allocate(wsReq);
196
- this.emit(ctx, {
197
- type: 'WorkspaceAllocated',
198
- work_item_id: nodeId,
199
- attempt_id: attemptId,
200
- payload: { workspace_id: lease.workspace_id, strategy: lease.strategy },
201
- });
202
-
203
- lastResult = await ctx.executor.execute(node, lease, ctx.runId, attempt);
204
-
205
- if (lastResult.success) {
206
- this.emit(ctx, {
207
- type: 'WorkItemCompleted',
208
- work_item_id: nodeId,
209
- attempt_id: attemptId,
210
- payload: { attempt_number: attempt, evidence: lastResult.evidence },
211
- });
212
- status.set(nodeId, 'completed');
213
- completed.push(nodeId);
214
- return;
215
- }
216
-
217
- // Policy failures never retry
218
- if (lastResult.failure_class === 'policy') break;
219
-
220
- if (attempt < maxAttempts) {
221
- this.emit(ctx, {
222
- type: 'RetryScheduled',
223
- work_item_id: nodeId,
224
- payload: { next_attempt: attempt + 1, reason: lastResult.failure_class },
225
- });
226
- }
227
- } catch (err) {
228
- lastResult = {
229
- success: false,
230
- failure_class: 'env',
231
- evidence: [],
232
- output: String(err),
233
- };
234
- if (attempt < maxAttempts) {
235
- this.emit(ctx, {
236
- type: 'RetryScheduled',
237
- work_item_id: nodeId,
238
- payload: { next_attempt: attempt + 1, reason: 'env' },
239
- });
240
- }
241
- } finally {
242
- if (lease) {
243
- await ctx.workspaceManager.dispose(lease.workspace_id).catch(() => {});
244
- lease = null;
245
- }
246
- }
247
- }
248
-
249
- // All attempts exhausted
250
- this.emit(ctx, {
251
- type: 'WorkItemBlocked',
252
- work_item_id: nodeId,
253
- payload: { failure_class: lastResult?.failure_class ?? 'env', max_attempts: maxAttempts },
254
- });
255
- status.set(nodeId, 'failed');
256
- failed.push(nodeId);
257
- }
258
-
259
- pause(): void {
260
- this.paused = true;
261
- }
262
-
263
- resume(): void {
264
- this.paused = false;
265
- }
266
-
267
- cancel(): void {
268
- this.cancelled = true;
269
- }
270
-
271
- private emit(
272
- ctx: SchedulerContext,
273
- input: EventInput
274
- ): void {
275
- const event = appendEvent(ctx.projectRoot, ctx.sessionId, {
276
- run_id: ctx.runId,
277
- ...input,
278
- });
279
- ctx.onEvent?.(event);
280
- }
281
- }
1
+ import { appendEvent } from '../events/bus';
2
+ import type { OxeEvent } from '../events/envelope';
3
+ import type { EventInput } from '../events/bus';
4
+ import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
5
+ import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
6
+ import type { WorkspaceLease } from '../models/workspace';
7
+ import type { GateManager } from '../gate/gate-manager';
8
+ import type { PolicyEngine, PersistedPolicyDecision, PolicyContext } from '../policy/policy-engine';
9
+ import { savePolicyDecision } from '../policy/policy-engine';
10
+ import type { PluginRegistry } from '../plugins/plugin-registry';
11
+ import type { AuditTrail } from '../audit/audit-trail';
12
+ import type { RunQuota, QuotaViolation } from '../audit/audit-trail';
13
+ import { checkQuota, consumeQuota } from '../audit/audit-trail';
14
+ import {
15
+ saveJournal,
16
+ loadJournal,
17
+ deleteJournal,
18
+ createJournal,
19
+ } from './run-journal';
20
+ import type { RunJournal } from './run-journal';
21
+
22
+ export interface TaskResult {
23
+ success: boolean;
24
+ failure_class: 'env' | 'policy' | 'test' | 'timeout' | null;
25
+ evidence: string[];
26
+ output: string;
27
+ }
28
+
29
+ export interface TaskExecutor {
30
+ execute(
31
+ node: GraphNode,
32
+ lease: WorkspaceLease,
33
+ runId: string,
34
+ attemptNumber: number
35
+ ): Promise<TaskResult>;
36
+ }
37
+
38
+ export interface SchedulerContext {
39
+ projectRoot: string;
40
+ sessionId: string | null;
41
+ runId: string;
42
+ executor: TaskExecutor;
43
+ workspaceManager: WorkspaceManager;
44
+ gateManager?: GateManager;
45
+ policyEngine?: PolicyEngine;
46
+ pluginRegistry?: PluginRegistry;
47
+ auditTrail?: AuditTrail;
48
+ quota?: RunQuota;
49
+ policyActor?: string;
50
+ onEvent?: (event: OxeEvent) => void;
51
+ }
52
+
53
+ export interface RunResult {
54
+ run_id: string;
55
+ status: 'completed' | 'failed' | 'blocked' | 'cancelled' | 'paused';
56
+ completed: string[];
57
+ failed: string[];
58
+ blocked: string[];
59
+ pending_gates?: string[];
60
+ }
61
+
62
+ type NodeStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'blocked';
63
+
64
+ export class Scheduler {
65
+ private cancelled = false;
66
+ private paused = false;
67
+ private journal: RunJournal | null = null;
68
+ private ctx: SchedulerContext | null = null;
69
+
70
+ async run(graph: ExecutionGraph, ctx: SchedulerContext): Promise<RunResult> {
71
+ this.cancelled = false;
72
+ this.paused = false;
73
+ this.ctx = ctx;
74
+
75
+ const status = new Map<string, NodeStatus>();
76
+ for (const id of graph.nodes.keys()) status.set(id, 'pending');
77
+
78
+ const completed: string[] = [];
79
+ const failed: string[] = [];
80
+ const blocked: string[] = [];
81
+
82
+ this.journal = createJournal(ctx.runId);
83
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
84
+
85
+ this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId } });
86
+ ctx.auditTrail?.record('run_started', ctx.policyActor ?? 'runtime', {
87
+ runId: ctx.runId,
88
+ detail: { session_id: ctx.sessionId ?? null },
89
+ });
90
+
91
+ for (const wave of graph.waves) {
92
+ if (this.cancelled) break;
93
+
94
+ // Respect pause: persist journal and return paused result
95
+ if (this.paused) {
96
+ this.journal.scheduler_state = 'paused';
97
+ this.journal.paused_at = new Date().toISOString();
98
+ this.journal.completed_work_items = completed.slice();
99
+ this.journal.failed_work_items = failed.slice();
100
+ this.journal.blocked_work_items = blocked.slice();
101
+ this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
102
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
103
+ ctx.auditTrail?.record('run_paused', ctx.policyActor ?? 'runtime', {
104
+ runId: ctx.runId,
105
+ detail: { completed, failed, blocked },
106
+ });
107
+ return { run_id: ctx.runId, status: 'paused', completed, failed, blocked, pending_gates: this.journal.pending_gates.slice() };
108
+ }
109
+
110
+ const waveFailed = await this.runWave(
111
+ wave.node_ids,
112
+ graph,
113
+ ctx,
114
+ status,
115
+ completed,
116
+ failed,
117
+ blocked
118
+ );
119
+
120
+ // Sync journal after each wave
121
+ this.journal.completed_work_items = completed.slice();
122
+ this.journal.failed_work_items = failed.slice();
123
+ this.journal.blocked_work_items = blocked.slice();
124
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
125
+
126
+ if (waveFailed) break;
127
+ }
128
+
129
+ // Any remaining pending nodes become blocked
130
+ for (const [id, s] of status) {
131
+ if (s === 'pending') {
132
+ status.set(id, 'blocked');
133
+ blocked.push(id);
134
+ this.emit(ctx, {
135
+ type: 'WorkItemBlocked',
136
+ work_item_id: id,
137
+ payload: { reason: 'upstream_wave_failed' },
138
+ });
139
+ }
140
+ }
141
+
142
+ const finalStatus: RunResult['status'] = this.cancelled
143
+ ? 'cancelled'
144
+ : failed.length > 0
145
+ ? 'failed'
146
+ : blocked.length > 0
147
+ ? 'blocked'
148
+ : 'completed';
149
+
150
+ this.emit(ctx, {
151
+ type: 'RunCompleted',
152
+ payload: { run_id: ctx.runId, status: finalStatus },
153
+ });
154
+ ctx.auditTrail?.record('run_completed', ctx.policyActor ?? 'runtime', {
155
+ runId: ctx.runId,
156
+ detail: {
157
+ status: finalStatus,
158
+ completed: completed.length,
159
+ failed: failed.length,
160
+ blocked: blocked.length,
161
+ pending_gates: this.journal.pending_gates.slice(),
162
+ },
163
+ });
164
+
165
+ this.journal.scheduler_state = this.cancelled ? 'cancelled' : finalStatus === 'blocked' ? 'blocked' : 'completed';
166
+ this.journal.completed_work_items = completed.slice();
167
+ this.journal.failed_work_items = failed.slice();
168
+ this.journal.blocked_work_items = blocked.slice();
169
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
170
+
171
+ return {
172
+ run_id: ctx.runId,
173
+ status: finalStatus,
174
+ completed,
175
+ failed,
176
+ blocked,
177
+ pending_gates: this.journal.pending_gates.slice(),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Recover a previously paused run by loading its journal and re-running
183
+ * only the work items that haven't completed yet.
184
+ */
185
+ async recover(runId: string, ctx: SchedulerContext, graph: ExecutionGraph): Promise<RunResult | null> {
186
+ const journal = loadJournal(ctx.projectRoot, runId);
187
+ if (!journal || journal.scheduler_state !== 'paused') return null;
188
+
189
+ // Restore state from journal
190
+ this.cancelled = false;
191
+ this.paused = false;
192
+ this.ctx = ctx;
193
+ this.journal = { ...journal, scheduler_state: 'running', paused_at: null };
194
+
195
+ const restoredCompleted = new Set(journal.completed_work_items);
196
+ const restoredFailed = new Set(journal.failed_work_items);
197
+ const restoredBlocked = new Set(journal.blocked_work_items);
198
+
199
+ const status = new Map<string, NodeStatus>();
200
+ for (const id of graph.nodes.keys()) {
201
+ if (restoredCompleted.has(id)) status.set(id, 'completed');
202
+ else if (restoredFailed.has(id)) status.set(id, 'failed');
203
+ else if (restoredBlocked.has(id)) status.set(id, 'blocked');
204
+ else status.set(id, 'pending');
205
+ }
206
+
207
+ const completed = [...restoredCompleted];
208
+ const failed = [...restoredFailed];
209
+ const blocked = [...restoredBlocked];
210
+
211
+ saveJournal(ctx.projectRoot, runId, this.journal);
212
+
213
+ this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId, recovered: true } });
214
+
215
+ for (const wave of graph.waves) {
216
+ if (this.cancelled) break;
217
+ if (this.paused) {
218
+ this.journal.scheduler_state = 'paused';
219
+ this.journal.paused_at = new Date().toISOString();
220
+ this.journal.completed_work_items = completed.slice();
221
+ this.journal.failed_work_items = failed.slice();
222
+ this.journal.blocked_work_items = blocked.slice();
223
+ this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
224
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
225
+ ctx.auditTrail?.record('run_paused', ctx.policyActor ?? 'runtime', {
226
+ runId: ctx.runId,
227
+ detail: { recovered: true, completed, failed, blocked },
228
+ });
229
+ return { run_id: ctx.runId, status: 'paused', completed, failed, blocked, pending_gates: this.journal.pending_gates.slice() };
230
+ }
231
+
232
+ // Skip waves fully completed
233
+ const allDone = wave.node_ids.every(
234
+ (id) => restoredCompleted.has(id) || restoredFailed.has(id) || restoredBlocked.has(id)
235
+ );
236
+ if (allDone) continue;
237
+
238
+ const waveFailed = await this.runWave(
239
+ wave.node_ids,
240
+ graph,
241
+ ctx,
242
+ status,
243
+ completed,
244
+ failed,
245
+ blocked
246
+ );
247
+
248
+ this.journal.completed_work_items = completed.slice();
249
+ this.journal.failed_work_items = failed.slice();
250
+ this.journal.blocked_work_items = blocked.slice();
251
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
252
+
253
+ if (waveFailed) break;
254
+ }
255
+
256
+ for (const [id, s] of status) {
257
+ if (s === 'pending') {
258
+ status.set(id, 'blocked');
259
+ blocked.push(id);
260
+ this.emit(ctx, {
261
+ type: 'WorkItemBlocked',
262
+ work_item_id: id,
263
+ payload: { reason: 'upstream_wave_failed' },
264
+ });
265
+ }
266
+ }
267
+
268
+ const finalStatus: RunResult['status'] = this.cancelled
269
+ ? 'cancelled'
270
+ : failed.length > 0
271
+ ? 'failed'
272
+ : blocked.length > 0
273
+ ? 'blocked'
274
+ : 'completed';
275
+
276
+ this.emit(ctx, {
277
+ type: 'RunCompleted',
278
+ payload: { run_id: ctx.runId, status: finalStatus, recovered: true },
279
+ });
280
+ ctx.auditTrail?.record('run_recovered', ctx.policyActor ?? 'runtime', {
281
+ runId: ctx.runId,
282
+ detail: {
283
+ status: finalStatus,
284
+ completed: completed.length,
285
+ failed: failed.length,
286
+ blocked: blocked.length,
287
+ pending_gates: this.journal.pending_gates.slice(),
288
+ },
289
+ });
290
+
291
+ this.journal.scheduler_state = this.cancelled ? 'cancelled' : finalStatus === 'blocked' ? 'blocked' : 'completed';
292
+ this.journal.completed_work_items = completed.slice();
293
+ this.journal.failed_work_items = failed.slice();
294
+ this.journal.blocked_work_items = blocked.slice();
295
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
296
+ deleteJournal(ctx.projectRoot, ctx.runId);
297
+
298
+ return {
299
+ run_id: ctx.runId,
300
+ status: finalStatus,
301
+ completed,
302
+ failed,
303
+ blocked,
304
+ pending_gates: this.journal.pending_gates.slice(),
305
+ };
306
+ }
307
+
308
+ private async runWave(
309
+ nodeIds: string[],
310
+ graph: ExecutionGraph,
311
+ ctx: SchedulerContext,
312
+ status: Map<string, NodeStatus>,
313
+ completed: string[],
314
+ failed: string[],
315
+ blocked: string[]
316
+ ): Promise<boolean> {
317
+ const eligible: string[] = [];
318
+ const depsNotMet: string[] = [];
319
+
320
+ for (const id of nodeIds) {
321
+ if (status.get(id) === 'completed') continue; // already done in recovery
322
+ const node = graph.nodes.get(id)!;
323
+ const depsMet = node.depends_on.every((dep) => status.get(dep) === 'completed');
324
+ if (depsMet) {
325
+ eligible.push(id);
326
+ } else {
327
+ depsNotMet.push(id);
328
+ }
329
+ }
330
+
331
+ for (const id of depsNotMet) {
332
+ status.set(id, 'blocked');
333
+ blocked.push(id);
334
+ this.emit(ctx, {
335
+ type: 'WorkItemBlocked',
336
+ work_item_id: id,
337
+ payload: { reason: 'dependency_not_met' },
338
+ });
339
+ }
340
+
341
+ const readOnly = eligible.filter((id) => {
342
+ const node = graph.nodes.get(id)!;
343
+ return node.mutation_scope.length === 0;
344
+ });
345
+ const mutations = eligible.filter((id) => !readOnly.includes(id));
346
+
347
+ if (readOnly.length > 0) {
348
+ await Promise.all(
349
+ readOnly.map((id) => this.runNode(id, graph, ctx, status, completed, failed, blocked))
350
+ );
351
+ }
352
+
353
+ for (const id of mutations) {
354
+ if (this.cancelled) break;
355
+ await this.runNode(id, graph, ctx, status, completed, failed, blocked);
356
+ }
357
+
358
+ return failed.length > 0;
359
+ }
360
+
361
+ private async runNode(
362
+ nodeId: string,
363
+ graph: ExecutionGraph,
364
+ ctx: SchedulerContext,
365
+ status: Map<string, NodeStatus>,
366
+ completed: string[],
367
+ failed: string[],
368
+ blocked: string[]
369
+ ): Promise<void> {
370
+ const node = graph.nodes.get(nodeId)!;
371
+ status.set(nodeId, 'running');
372
+ this.emit(ctx, {
373
+ type: 'WorkItemReady',
374
+ work_item_id: nodeId,
375
+ payload: { title: node.title, wave: node.wave },
376
+ });
377
+
378
+ let lease: WorkspaceLease | null = null;
379
+ let lastResult: TaskResult | null = null;
380
+ const maxAttempts = node.policy.max_retries + 1;
381
+ const quotaBlocked = this.consumeQuotaForNode(ctx, node);
382
+ if (quotaBlocked) {
383
+ this.blockNode(nodeId, ctx, status, blocked, 'quota_exceeded', quotaBlocked);
384
+ return;
385
+ }
386
+
387
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
388
+ const attemptId = `${nodeId}-a${attempt}`;
389
+
390
+ this.emit(ctx, {
391
+ type: 'AttemptStarted',
392
+ work_item_id: nodeId,
393
+ attempt_id: attemptId,
394
+ payload: { attempt_number: attempt },
395
+ });
396
+
397
+ try {
398
+ const policyDecision = this.evaluatePolicyForNode(node, ctx);
399
+ if (policyDecision && !policyDecision.allowed) {
400
+ this.emit(ctx, {
401
+ type: 'PolicyEvaluated',
402
+ work_item_id: nodeId,
403
+ attempt_id: attemptId,
404
+ payload: { ...policyDecision },
405
+ });
406
+ ctx.auditTrail?.record('policy_denied', ctx.policyActor ?? 'runtime', {
407
+ runId: ctx.runId,
408
+ workItemId: nodeId,
409
+ detail: { reason: policyDecision.reason, rule_id: policyDecision.rule_id },
410
+ });
411
+ this.blockNode(nodeId, ctx, status, blocked, 'policy_denied', policyDecision.reason);
412
+ return;
413
+ }
414
+
415
+ if (policyDecision) {
416
+ this.emit(ctx, {
417
+ type: 'PolicyEvaluated',
418
+ work_item_id: nodeId,
419
+ attempt_id: attemptId,
420
+ payload: { ...policyDecision },
421
+ });
422
+ }
423
+
424
+ if (policyDecision?.gate_required || node.policy.requires_human_approval) {
425
+ const gateId = await this.requestGateForNode(node, ctx, policyDecision);
426
+ this.blockNode(nodeId, ctx, status, blocked, 'pending_gate', gateId);
427
+ return;
428
+ }
429
+
430
+ const wsReq: WorkspaceRequest = {
431
+ work_item_id: nodeId,
432
+ attempt_number: attempt,
433
+ strategy: node.workspace_strategy,
434
+ mutation_scope: node.mutation_scope,
435
+ };
436
+ const workspaceManager = ctx.pluginRegistry?.workspaceProviderFor(node.workspace_strategy) ?? ctx.workspaceManager;
437
+ lease = await workspaceManager.allocate(wsReq);
438
+ this.emit(ctx, {
439
+ type: 'WorkspaceAllocated',
440
+ work_item_id: nodeId,
441
+ attempt_id: attemptId,
442
+ payload: { workspace_id: lease.workspace_id, strategy: lease.strategy },
443
+ });
444
+
445
+ lastResult = await this.executeNode(node, lease, ctx, attempt, attemptId);
446
+
447
+ if (lastResult.success) {
448
+ this.emit(ctx, {
449
+ type: 'WorkItemCompleted',
450
+ work_item_id: nodeId,
451
+ attempt_id: attemptId,
452
+ payload: { attempt_number: attempt, evidence: lastResult.evidence },
453
+ });
454
+ status.set(nodeId, 'completed');
455
+ completed.push(nodeId);
456
+ return;
457
+ }
458
+
459
+ if (lastResult.failure_class === 'policy') break;
460
+
461
+ if (attempt < maxAttempts) {
462
+ const retryBlocked = this.consumeRetryQuota(ctx);
463
+ if (retryBlocked) {
464
+ this.blockNode(nodeId, ctx, status, blocked, 'quota_exceeded', retryBlocked);
465
+ return;
466
+ }
467
+ this.emit(ctx, {
468
+ type: 'RetryScheduled',
469
+ work_item_id: nodeId,
470
+ payload: { next_attempt: attempt + 1, reason: lastResult.failure_class },
471
+ });
472
+ }
473
+ } catch (err) {
474
+ lastResult = {
475
+ success: false,
476
+ failure_class: 'env',
477
+ evidence: [],
478
+ output: String(err),
479
+ };
480
+ if (attempt < maxAttempts) {
481
+ this.emit(ctx, {
482
+ type: 'RetryScheduled',
483
+ work_item_id: nodeId,
484
+ payload: { next_attempt: attempt + 1, reason: 'env' },
485
+ });
486
+ }
487
+ } finally {
488
+ if (lease) {
489
+ await ctx.workspaceManager.dispose(lease.workspace_id).catch(() => {});
490
+ lease = null;
491
+ }
492
+ }
493
+ }
494
+
495
+ this.emit(ctx, {
496
+ type: 'WorkItemBlocked',
497
+ work_item_id: nodeId,
498
+ payload: { failure_class: lastResult?.failure_class ?? 'env', max_attempts: maxAttempts },
499
+ });
500
+ status.set(nodeId, 'failed');
501
+ failed.push(nodeId);
502
+ }
503
+
504
+ pause(): void {
505
+ this.paused = true;
506
+ if (this.journal && this.ctx) {
507
+ this.journal.scheduler_state = 'paused';
508
+ this.journal.paused_at = new Date().toISOString();
509
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
510
+ }
511
+ }
512
+
513
+ resume(): void {
514
+ this.paused = false;
515
+ if (this.journal && this.ctx) {
516
+ this.journal.scheduler_state = 'running';
517
+ this.journal.paused_at = null;
518
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
519
+ }
520
+ }
521
+
522
+ cancel(): void {
523
+ this.cancelled = true;
524
+ if (this.journal && this.ctx) {
525
+ this.journal.cancelled = true;
526
+ this.journal.scheduler_state = 'cancelled';
527
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
528
+ }
529
+ }
530
+
531
+ getJournal(): RunJournal | null {
532
+ return this.journal;
533
+ }
534
+
535
+ static loadJournal(projectRoot: string, runId: string): RunJournal | null {
536
+ return loadJournal(projectRoot, runId);
537
+ }
538
+
539
+ private async executeNode(
540
+ node: GraphNode,
541
+ lease: WorkspaceLease,
542
+ ctx: SchedulerContext,
543
+ attempt: number,
544
+ attemptId: string
545
+ ): Promise<TaskResult> {
546
+ const primaryAction = pickPrimaryAction(node, ctx.pluginRegistry);
547
+ const provider = primaryAction ? ctx.pluginRegistry?.toolProviderFor(primaryAction.type) : null;
548
+ if (!provider || !primaryAction) {
549
+ return ctx.executor.execute(node, lease, ctx.runId, attempt);
550
+ }
551
+
552
+ ctx.auditTrail?.record('plugin_invoked', ctx.policyActor ?? 'runtime', {
553
+ runId: ctx.runId,
554
+ workItemId: node.id,
555
+ resource: provider.name,
556
+ detail: { action_type: primaryAction.type, attempt_id: attemptId },
557
+ });
558
+ this.emit(ctx, {
559
+ type: 'ToolInvoked',
560
+ work_item_id: node.id,
561
+ attempt_id: attemptId,
562
+ payload: { provider: provider.name, action_type: primaryAction.type },
563
+ });
564
+
565
+ const result = await provider.invoke({
566
+ action_type: primaryAction.type,
567
+ work_item_id: node.id,
568
+ run_id: ctx.runId,
569
+ attempt_id: attemptId,
570
+ params: {
571
+ command: primaryAction.command ?? null,
572
+ targets: primaryAction.targets ?? [],
573
+ },
574
+ workspace_root: lease.root_path,
575
+ });
576
+
577
+ this.emit(ctx, {
578
+ type: result.success ? 'ToolCompleted' : 'ToolFailed',
579
+ work_item_id: node.id,
580
+ attempt_id: attemptId,
581
+ payload: {
582
+ provider: provider.name,
583
+ action_type: primaryAction.type,
584
+ evidence_paths: result.evidence_paths,
585
+ side_effects_applied: result.side_effects_applied,
586
+ error: result.error ?? null,
587
+ },
588
+ });
589
+
590
+ return {
591
+ success: result.success,
592
+ failure_class: result.success ? null : provider.kind === 'external_operation' ? 'policy' : 'env',
593
+ evidence: result.evidence_paths,
594
+ output: result.output,
595
+ };
596
+ }
597
+
598
+ private evaluatePolicyForNode(node: GraphNode, ctx: SchedulerContext): PersistedPolicyDecision | null {
599
+ if (!ctx.policyEngine) return null;
600
+ const primaryAction = pickPrimaryAction(node, ctx.pluginRegistry);
601
+ const decisionContext: PolicyContext = {
602
+ tool: primaryAction?.type ?? 'custom',
603
+ kind: node.workspace_strategy,
604
+ mutation_scope: node.mutation_scope,
605
+ affected_paths: node.mutation_scope,
606
+ side_effect_class: inferSideEffectClass(node),
607
+ mutation_count: node.mutation_scope.length,
608
+ node_policy: {
609
+ max_retries: node.policy.max_retries,
610
+ },
611
+ };
612
+ const evaluated = ctx.policyEngine.evaluate(decisionContext);
613
+ const persisted: PersistedPolicyDecision = {
614
+ ...evaluated,
615
+ run_id: ctx.runId,
616
+ work_item_id: node.id,
617
+ action: primaryAction?.type ?? 'custom',
618
+ actor: ctx.policyActor ?? 'runtime',
619
+ override: false,
620
+ rationale: null,
621
+ context: decisionContext,
622
+ };
623
+ savePolicyDecision(ctx.projectRoot, persisted);
624
+ return persisted;
625
+ }
626
+
627
+ private async requestGateForNode(
628
+ node: GraphNode,
629
+ ctx: SchedulerContext,
630
+ decision: PersistedPolicyDecision | null
631
+ ): Promise<string> {
632
+ if (!ctx.gateManager) return 'gate-missing-manager';
633
+ const scope = inferGateScope(node);
634
+ const primaryAction = pickPrimaryAction(node, ctx.pluginRegistry);
635
+ const gate = await ctx.gateManager.request(scope, {
636
+ run_id: ctx.runId,
637
+ work_item_id: node.id,
638
+ action: primaryAction?.type ?? 'custom',
639
+ description: `Gate required before executing ${node.id}`,
640
+ evidence_refs: [],
641
+ risks: [decision?.reason ?? 'human approval required'],
642
+ rationale: decision?.reason ?? 'node policy requires approval',
643
+ policy_decision_id: decision?.decision_id ?? null,
644
+ });
645
+ if (this.journal && !this.journal.pending_gates.includes(gate.gate_id)) {
646
+ this.journal.pending_gates.push(gate.gate_id);
647
+ }
648
+ ctx.auditTrail?.record('gate_requested', ctx.policyActor ?? 'runtime', {
649
+ runId: ctx.runId,
650
+ workItemId: node.id,
651
+ detail: { gate_id: gate.gate_id, scope: gate.scope, action: gate.action },
652
+ });
653
+ return gate.gate_id;
654
+ }
655
+
656
+ private blockNode(
657
+ nodeId: string,
658
+ ctx: SchedulerContext,
659
+ status: Map<string, NodeStatus>,
660
+ blocked: string[],
661
+ reason: string,
662
+ detail: string | QuotaViolation | null = null
663
+ ): void {
664
+ this.emit(ctx, {
665
+ type: 'WorkItemBlocked',
666
+ work_item_id: nodeId,
667
+ payload: { reason, detail },
668
+ });
669
+ status.set(nodeId, 'blocked');
670
+ if (!blocked.includes(nodeId)) blocked.push(nodeId);
671
+ }
672
+
673
+ private consumeQuotaForNode(ctx: SchedulerContext, node: GraphNode): string | null {
674
+ if (!ctx.quota) return null;
675
+ let quota = consumeQuota(ctx.quota, 'work_items', 1);
676
+ if (node.mutation_scope.length > 0) {
677
+ quota = consumeQuota(quota, 'mutations', 1);
678
+ }
679
+ const violation = checkQuota(quota);
680
+ ctx.quota = quota;
681
+ return violation ? `${violation.quota_type}:${violation.consumed}/${violation.limit}` : null;
682
+ }
683
+
684
+ private consumeRetryQuota(ctx: SchedulerContext): string | null {
685
+ if (!ctx.quota) return null;
686
+ ctx.quota = consumeQuota(ctx.quota, 'retries', 1);
687
+ const violation = checkQuota(ctx.quota);
688
+ return violation ? `${violation.quota_type}:${violation.consumed}/${violation.limit}` : null;
689
+ }
690
+
691
+ private emit(
692
+ ctx: SchedulerContext,
693
+ input: EventInput
694
+ ): void {
695
+ const event = appendEvent(ctx.projectRoot, ctx.sessionId, {
696
+ run_id: ctx.runId,
697
+ ...input,
698
+ });
699
+ ctx.onEvent?.(event);
700
+ }
701
+ }
702
+
703
+ function inferSideEffectClass(node: GraphNode): PolicyContext['side_effect_class'] {
704
+ if (node.mutation_scope.length > 0) return 'write_fs';
705
+ if (node.actions.some((action) => action.type === 'run_tests' || action.type === 'run_lint')) return 'spawn_process';
706
+ return 'read_fs';
707
+ }
708
+
709
+ function pickPrimaryAction(node: GraphNode, pluginRegistry?: PluginRegistry): GraphNode['actions'][number] | undefined {
710
+ const candidateActions = node.actions.filter((action) => action.type !== 'collect_evidence');
711
+ const preferredMutation = candidateActions.find((action) => action.type === 'generate_patch')
712
+ || candidateActions.find((action) => action.type === 'run_tests')
713
+ || candidateActions[0];
714
+ if (!pluginRegistry) return preferredMutation;
715
+ return candidateActions.find((action) => pluginRegistry.toolProviderFor(action.type)) || preferredMutation;
716
+ }
717
+
718
+ function inferGateScope(node: GraphNode): 'critical_mutation' | 'security' | 'plan_approval' {
719
+ if (node.mutation_scope.length > 0) return 'critical_mutation';
720
+ if (node.actions.some((action) => action.type === 'collect_evidence')) return 'security';
721
+ return 'plan_approval';
722
+ }