oxe-cc 1.0.0 → 1.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.
Files changed (207) hide show
  1. package/.cursor/commands/oxe-ask.md +1 -1
  2. package/.cursor/commands/oxe-capabilities.md +1 -1
  3. package/.cursor/commands/oxe-checkpoint.md +1 -1
  4. package/.cursor/commands/oxe-compact.md +1 -1
  5. package/.cursor/commands/oxe-dashboard.md +1 -1
  6. package/.cursor/commands/oxe-debug.md +1 -1
  7. package/.cursor/commands/oxe-discuss.md +1 -1
  8. package/.cursor/commands/oxe-execute.md +2 -2
  9. package/.cursor/commands/oxe-forensics.md +1 -1
  10. package/.cursor/commands/oxe-help.md +1 -1
  11. package/.cursor/commands/oxe-loop.md +1 -1
  12. package/.cursor/commands/oxe-milestone.md +1 -1
  13. package/.cursor/commands/oxe-next.md +1 -1
  14. package/.cursor/commands/oxe-obs.md +1 -1
  15. package/.cursor/commands/oxe-plan-agent.md +1 -1
  16. package/.cursor/commands/oxe-plan.md +1 -1
  17. package/.cursor/commands/oxe-project.md +1 -1
  18. package/.cursor/commands/oxe-quick.md +1 -1
  19. package/.cursor/commands/oxe-research.md +1 -1
  20. package/.cursor/commands/oxe-retro.md +1 -1
  21. package/.cursor/commands/oxe-review-pr.md +1 -1
  22. package/.cursor/commands/oxe-route.md +1 -1
  23. package/.cursor/commands/oxe-scan.md +1 -1
  24. package/.cursor/commands/oxe-security.md +1 -1
  25. package/.cursor/commands/oxe-session.md +2 -2
  26. package/.cursor/commands/oxe-ship.md +45 -0
  27. package/.cursor/commands/oxe-skill.md +1 -1
  28. package/.cursor/commands/oxe-spec.md +1 -1
  29. package/.cursor/commands/oxe-ui-review.md +1 -1
  30. package/.cursor/commands/oxe-ui-spec.md +1 -1
  31. package/.cursor/commands/oxe-update.md +1 -1
  32. package/.cursor/commands/oxe-validate-gaps.md +1 -1
  33. package/.cursor/commands/oxe-verify.md +1 -1
  34. package/.cursor/commands/oxe-workstream.md +1 -1
  35. package/.cursor/commands/oxe.md +4 -4
  36. package/.github/copilot-instructions.md +91 -1
  37. package/.github/prompts/oxe-ask.prompt.md +1 -1
  38. package/.github/prompts/oxe-capabilities.prompt.md +1 -1
  39. package/.github/prompts/oxe-checkpoint.prompt.md +1 -1
  40. package/.github/prompts/oxe-compact.prompt.md +1 -1
  41. package/.github/prompts/oxe-dashboard.prompt.md +1 -1
  42. package/.github/prompts/oxe-debug.prompt.md +1 -1
  43. package/.github/prompts/oxe-discuss.prompt.md +1 -1
  44. package/.github/prompts/oxe-execute.prompt.md +2 -2
  45. package/.github/prompts/oxe-forensics.prompt.md +1 -1
  46. package/.github/prompts/oxe-help.prompt.md +1 -1
  47. package/.github/prompts/oxe-loop.prompt.md +1 -1
  48. package/.github/prompts/oxe-milestone.prompt.md +1 -1
  49. package/.github/prompts/oxe-next.prompt.md +1 -1
  50. package/.github/prompts/oxe-obs.prompt.md +1 -1
  51. package/.github/prompts/oxe-plan-agent.prompt.md +1 -1
  52. package/.github/prompts/oxe-plan.prompt.md +1 -1
  53. package/.github/prompts/oxe-project.prompt.md +1 -1
  54. package/.github/prompts/oxe-quick.prompt.md +1 -1
  55. package/.github/prompts/oxe-research.prompt.md +1 -1
  56. package/.github/prompts/oxe-retro.prompt.md +1 -1
  57. package/.github/prompts/oxe-review-pr.prompt.md +1 -1
  58. package/.github/prompts/oxe-route.prompt.md +1 -1
  59. package/.github/prompts/oxe-scan.prompt.md +1 -1
  60. package/.github/prompts/oxe-security.prompt.md +1 -1
  61. package/.github/prompts/oxe-session.prompt.md +2 -2
  62. package/.github/prompts/oxe-ship.prompt.md +45 -0
  63. package/.github/prompts/oxe-skill.prompt.md +1 -1
  64. package/.github/prompts/oxe-spec.prompt.md +1 -1
  65. package/.github/prompts/oxe-ui-review.prompt.md +1 -1
  66. package/.github/prompts/oxe-ui-spec.prompt.md +1 -1
  67. package/.github/prompts/oxe-update.prompt.md +1 -1
  68. package/.github/prompts/oxe-validate-gaps.prompt.md +1 -1
  69. package/.github/prompts/oxe-verify.prompt.md +1 -1
  70. package/.github/prompts/oxe-workstream.prompt.md +1 -1
  71. package/.github/prompts/oxe.prompt.md +3 -3
  72. package/AGENTS.md +43 -28
  73. package/CHANGELOG.md +158 -0
  74. package/README.md +72 -50
  75. package/bin/banner.txt +1 -1
  76. package/bin/lib/oxe-project-health.cjs +1 -1
  77. package/commands/oxe/ask.md +5 -1
  78. package/commands/oxe/checkpoint.md +1 -1
  79. package/commands/oxe/compact.md +1 -1
  80. package/commands/oxe/debug.md +1 -1
  81. package/commands/oxe/execute.md +2 -2
  82. package/commands/oxe/forensics.md +1 -1
  83. package/commands/oxe/loop.md +1 -1
  84. package/commands/oxe/milestone.md +1 -1
  85. package/commands/oxe/next.md +1 -1
  86. package/commands/oxe/obs.md +1 -1
  87. package/commands/oxe/oxe.md +3 -3
  88. package/commands/oxe/project.md +1 -1
  89. package/commands/oxe/research.md +1 -1
  90. package/commands/oxe/retro.md +1 -1
  91. package/commands/oxe/review-pr.md +1 -1
  92. package/commands/oxe/route.md +1 -1
  93. package/commands/oxe/scan.md +1 -1
  94. package/commands/oxe/security.md +1 -1
  95. package/commands/oxe/session.md +2 -2
  96. package/commands/oxe/ship.md +49 -0
  97. package/commands/oxe/spec.md +2 -2
  98. package/commands/oxe/ui-review.md +1 -1
  99. package/commands/oxe/ui-spec.md +1 -1
  100. package/commands/oxe/validate-gaps.md +1 -1
  101. package/commands/oxe/verify.md +2 -2
  102. package/commands/oxe/workstream.md +1 -1
  103. package/lib/runtime/audit/audit-trail.d.ts +71 -0
  104. package/lib/runtime/audit/audit-trail.js +154 -0
  105. package/lib/runtime/audit/index.d.ts +2 -0
  106. package/lib/runtime/audit/index.js +18 -0
  107. package/lib/runtime/audit/policy-pack.d.ts +15 -0
  108. package/lib/runtime/audit/policy-pack.js +57 -0
  109. package/lib/runtime/context/context-pack-builder.d.ts +15 -0
  110. package/lib/runtime/context/context-pack-builder.js +42 -0
  111. package/lib/runtime/context/context-pack-store.d.ts +38 -0
  112. package/lib/runtime/context/context-pack-store.js +142 -0
  113. package/lib/runtime/context/context-profiles.d.ts +11 -0
  114. package/lib/runtime/context/context-profiles.js +51 -0
  115. package/lib/runtime/context/index.d.ts +2 -0
  116. package/lib/runtime/context/index.js +2 -0
  117. package/lib/runtime/decision/decision-engine.d.ts +43 -0
  118. package/lib/runtime/decision/decision-engine.js +127 -0
  119. package/lib/runtime/decision/decision-memo.d.ts +53 -0
  120. package/lib/runtime/decision/decision-memo.js +173 -0
  121. package/lib/runtime/decision/index.d.ts +2 -0
  122. package/lib/runtime/decision/index.js +18 -0
  123. package/lib/runtime/delivery/index.d.ts +1 -0
  124. package/lib/runtime/delivery/index.js +1 -0
  125. package/lib/runtime/delivery/promotion-pipeline.d.ts +39 -0
  126. package/lib/runtime/delivery/promotion-pipeline.js +127 -0
  127. package/lib/runtime/index.d.ts +3 -0
  128. package/lib/runtime/index.js +4 -0
  129. package/lib/runtime/plugins/capability-matrix.d.ts +20 -0
  130. package/lib/runtime/plugins/capability-matrix.js +59 -0
  131. package/lib/runtime/plugins/index.d.ts +2 -0
  132. package/lib/runtime/plugins/index.js +2 -0
  133. package/lib/runtime/plugins/plugin-manifest.d.ts +22 -0
  134. package/lib/runtime/plugins/plugin-manifest.js +91 -0
  135. package/lib/runtime/plugins/plugin-registry.js +5 -0
  136. package/lib/runtime/policy/policy-engine.d.ts +28 -1
  137. package/lib/runtime/policy/policy-engine.js +96 -5
  138. package/lib/runtime/reducers/run-state-reducer.d.ts +26 -0
  139. package/lib/runtime/reducers/run-state-reducer.js +117 -1
  140. package/lib/runtime/scheduler/agent-registry.d.ts +44 -0
  141. package/lib/runtime/scheduler/agent-registry.js +96 -0
  142. package/lib/runtime/scheduler/agent-roles.d.ts +54 -0
  143. package/lib/runtime/scheduler/agent-roles.js +62 -0
  144. package/lib/runtime/scheduler/index.d.ts +3 -0
  145. package/lib/runtime/scheduler/index.js +3 -0
  146. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +2 -0
  147. package/lib/runtime/scheduler/multi-agent-coordinator.js +91 -4
  148. package/lib/runtime/scheduler/run-journal.d.ts +18 -0
  149. package/lib/runtime/scheduler/run-journal.js +54 -0
  150. package/lib/runtime/scheduler/scheduler.d.ts +11 -1
  151. package/lib/runtime/scheduler/scheduler.js +135 -7
  152. package/lib/runtime/verification/index.d.ts +1 -0
  153. package/lib/runtime/verification/index.js +1 -0
  154. package/lib/runtime/verification/verification-manifest.d.ts +58 -0
  155. package/lib/runtime/verification/verification-manifest.js +129 -0
  156. package/oxe/workflows/ask.md +4 -0
  157. package/oxe/workflows/checkpoint.md +14 -10
  158. package/oxe/workflows/debug.md +19 -15
  159. package/oxe/workflows/execute.md +30 -2
  160. package/oxe/workflows/forensics.md +13 -9
  161. package/oxe/workflows/help.md +97 -49
  162. package/oxe/workflows/loop.md +17 -13
  163. package/oxe/workflows/obs.md +4 -0
  164. package/oxe/workflows/oxe.md +64 -31
  165. package/oxe/workflows/project.md +6 -1
  166. package/oxe/workflows/references/workflow-runtime-contracts.json +23 -0
  167. package/oxe/workflows/research.md +32 -28
  168. package/oxe/workflows/retro.md +4 -0
  169. package/oxe/workflows/review-pr.md +15 -11
  170. package/oxe/workflows/scan.md +4 -0
  171. package/oxe/workflows/security.md +14 -10
  172. package/oxe/workflows/session.md +17 -1
  173. package/oxe/workflows/ship.md +142 -0
  174. package/oxe/workflows/spec.md +15 -0
  175. package/oxe/workflows/ui-review.md +20 -16
  176. package/oxe/workflows/ui-spec.md +7 -3
  177. package/oxe/workflows/validate-gaps.md +13 -9
  178. package/oxe/workflows/verify.md +42 -3
  179. package/package.json +1 -1
  180. package/packages/runtime/src/audit/audit-trail.ts +243 -0
  181. package/packages/runtime/src/audit/index.ts +2 -0
  182. package/packages/runtime/src/audit/policy-pack.ts +62 -0
  183. package/packages/runtime/src/context/context-pack-builder.ts +66 -0
  184. package/packages/runtime/src/context/context-pack-store.ts +197 -0
  185. package/packages/runtime/src/context/context-profiles.ts +60 -0
  186. package/packages/runtime/src/context/index.ts +2 -0
  187. package/packages/runtime/src/decision/decision-engine.ts +174 -0
  188. package/packages/runtime/src/decision/decision-memo.ts +211 -0
  189. package/packages/runtime/src/decision/index.ts +2 -0
  190. package/packages/runtime/src/delivery/index.ts +1 -0
  191. package/packages/runtime/src/delivery/promotion-pipeline.ts +180 -0
  192. package/packages/runtime/src/index.ts +5 -0
  193. package/packages/runtime/src/plugins/capability-matrix.ts +83 -0
  194. package/packages/runtime/src/plugins/index.ts +2 -0
  195. package/packages/runtime/src/plugins/plugin-manifest.ts +113 -0
  196. package/packages/runtime/src/plugins/plugin-registry.ts +5 -0
  197. package/packages/runtime/src/policy/policy-engine.ts +138 -7
  198. package/packages/runtime/src/reducers/run-state-reducer.ts +143 -1
  199. package/packages/runtime/src/scheduler/agent-registry.ts +132 -0
  200. package/packages/runtime/src/scheduler/agent-roles.ts +109 -0
  201. package/packages/runtime/src/scheduler/index.ts +3 -0
  202. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +106 -4
  203. package/packages/runtime/src/scheduler/run-journal.ts +62 -0
  204. package/packages/runtime/src/scheduler/scheduler.ts +168 -8
  205. package/packages/runtime/src/verification/index.ts +1 -0
  206. package/packages/runtime/src/verification/verification-manifest.ts +192 -0
  207. package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
@@ -0,0 +1,109 @@
1
+ import type { TaskResult } from './scheduler';
2
+
3
+ export type AgentRole = 'planner' | 'executor' | 'reviewer' | 'verifier';
4
+
5
+ export interface AgentBudget {
6
+ max_tokens: number;
7
+ max_time_ms: number;
8
+ max_retries: number;
9
+ consumed_tokens: number;
10
+ consumed_time_ms: number;
11
+ consumed_retries: number;
12
+ }
13
+
14
+ export interface CooperativeHandoff {
15
+ handoff_id: string;
16
+ from_agent_id: string;
17
+ to_agent_id: string;
18
+ from_role: AgentRole;
19
+ to_role: AgentRole;
20
+ work_item_id: string;
21
+ context_pack_ref: string | null;
22
+ transferred_at: string;
23
+ }
24
+
25
+ export interface AgentActionLog {
26
+ agent_id: string;
27
+ role: AgentRole;
28
+ work_item_id: string;
29
+ action: 'execute' | 'verify' | 'review' | 'plan';
30
+ result: 'success' | 'failure';
31
+ duration_ms: number;
32
+ timestamp: string;
33
+ }
34
+
35
+ export function createBudget(
36
+ opts: Partial<Omit<AgentBudget, 'consumed_tokens' | 'consumed_time_ms' | 'consumed_retries'>> = {}
37
+ ): AgentBudget {
38
+ return {
39
+ max_tokens: opts.max_tokens ?? Infinity,
40
+ max_time_ms: opts.max_time_ms ?? Infinity,
41
+ max_retries: opts.max_retries ?? Infinity,
42
+ consumed_tokens: 0,
43
+ consumed_time_ms: 0,
44
+ consumed_retries: 0,
45
+ };
46
+ }
47
+
48
+ export function consumeBudget(
49
+ budget: AgentBudget,
50
+ delta: { tokens?: number; time_ms?: number; retries?: number }
51
+ ): AgentBudget {
52
+ return {
53
+ ...budget,
54
+ consumed_tokens: budget.consumed_tokens + (delta.tokens ?? 0),
55
+ consumed_time_ms: budget.consumed_time_ms + (delta.time_ms ?? 0),
56
+ consumed_retries: budget.consumed_retries + (delta.retries ?? 0),
57
+ };
58
+ }
59
+
60
+ export function isBudgetExhausted(budget: AgentBudget): boolean {
61
+ return (
62
+ budget.consumed_tokens >= budget.max_tokens ||
63
+ budget.consumed_time_ms >= budget.max_time_ms ||
64
+ budget.consumed_retries >= budget.max_retries
65
+ );
66
+ }
67
+
68
+ export function buildHandoff(opts: {
69
+ from_agent_id: string;
70
+ to_agent_id: string;
71
+ from_role: AgentRole;
72
+ to_role: AgentRole;
73
+ work_item_id: string;
74
+ context_pack_ref?: string | null;
75
+ }): CooperativeHandoff {
76
+ return {
77
+ handoff_id: `hoff-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
78
+ from_agent_id: opts.from_agent_id,
79
+ to_agent_id: opts.to_agent_id,
80
+ from_role: opts.from_role,
81
+ to_role: opts.to_role,
82
+ work_item_id: opts.work_item_id,
83
+ context_pack_ref: opts.context_pack_ref ?? null,
84
+ transferred_at: new Date().toISOString(),
85
+ };
86
+ }
87
+
88
+ // ─── ArbitrationEngine ────────────────────────────────────────────────────────
89
+
90
+ export class ArbitrationEngine {
91
+ /**
92
+ * Choose the best result from multiple competing agents.
93
+ * Prefers success; among successes prefers more evidence; falls back to first.
94
+ */
95
+ arbitrate(results: Array<{ agent_id: string; result: TaskResult }>): TaskResult {
96
+ if (results.length === 0) {
97
+ return { success: false, failure_class: null, evidence: [], output: 'no results to arbitrate' };
98
+ }
99
+
100
+ const successes = results.filter((r) => r.result.success);
101
+ if (successes.length === 0) {
102
+ return results[0].result;
103
+ }
104
+
105
+ // Among successes, prefer the one with most evidence
106
+ successes.sort((a, b) => b.result.evidence.length - a.result.evidence.length);
107
+ return successes[0].result;
108
+ }
109
+ }
@@ -1 +1,4 @@
1
1
  export * from './scheduler';
2
+ export * from './run-journal';
3
+ export * from './agent-registry';
4
+ export * from './agent-roles';
@@ -4,6 +4,8 @@ import type { WorkspaceManager } from '../workspace/workspace-manager';
4
4
  import type { TaskExecutor, TaskResult, SchedulerContext } from './scheduler';
5
5
  import { Scheduler } from './scheduler';
6
6
  import type { WorkspaceLease } from '../models/workspace';
7
+ import { buildHandoff } from './agent-roles';
8
+ import type { CooperativeHandoff } from './agent-roles';
7
9
 
8
10
  export type CoordinationMode = 'parallel' | 'competitive' | 'cooperative';
9
11
 
@@ -31,6 +33,7 @@ export interface CoordinationResult {
31
33
  failed: string[];
32
34
  blocked: string[];
33
35
  agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
36
+ handoffs?: CooperativeHandoff[];
34
37
  }
35
38
 
36
39
  // ─── Parallel mode ───────────────────────────────────────────────────────────
@@ -194,6 +197,108 @@ async function competeTwoAgents(
194
197
  return winner;
195
198
  }
196
199
 
200
+ // ─── Cooperative mode ────────────────────────────────────────────────────────
201
+ // planner (agent[0]) does a dry-run to collect context, then hands off to
202
+ // executor (agent[1]) which performs the real run. Handoffs are recorded.
203
+
204
+ async function runCooperative(
205
+ graph: ExecutionGraph,
206
+ opts: CoordinationOptions
207
+ ): Promise<CoordinationResult> {
208
+ if (opts.agents.length < 2) {
209
+ throw new Error('Cooperative mode requires at least 2 agents');
210
+ }
211
+ const [planner, executor] = opts.agents;
212
+ const { projectRoot, sessionId, runId } = opts;
213
+ const handoffs: CooperativeHandoff[] = [];
214
+
215
+ appendEvent(projectRoot, sessionId, {
216
+ type: 'RunStarted',
217
+ run_id: runId,
218
+ payload: { mode: 'cooperative', planner: planner.id, executor: executor.id },
219
+ });
220
+
221
+ const completed: string[] = [];
222
+ const failed: string[] = [];
223
+
224
+ for (const wave of graph.waves) {
225
+ for (const nodeId of wave.node_ids) {
226
+ const node = graph.nodes.get(nodeId)!;
227
+
228
+ // Phase 1: planner allocates workspace + signals readiness (no output used)
229
+ const planAlloc = await planner.workspaceManager.allocate({
230
+ work_item_id: nodeId,
231
+ attempt_number: 1,
232
+ strategy: node.workspace_strategy,
233
+ mutation_scope: node.mutation_scope,
234
+ });
235
+ await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
236
+
237
+ const handoff = buildHandoff({
238
+ from_agent_id: planner.id,
239
+ to_agent_id: executor.id,
240
+ from_role: 'planner',
241
+ to_role: 'executor',
242
+ work_item_id: nodeId,
243
+ context_pack_ref: null,
244
+ });
245
+ handoffs.push(handoff);
246
+
247
+ appendEvent(projectRoot, sessionId, {
248
+ type: 'AttemptStarted',
249
+ run_id: runId,
250
+ work_item_id: nodeId,
251
+ payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
252
+ });
253
+
254
+ // Phase 2: executor performs the real task
255
+ const execAlloc = await executor.workspaceManager.allocate({
256
+ work_item_id: nodeId,
257
+ attempt_number: 1,
258
+ strategy: node.workspace_strategy,
259
+ mutation_scope: node.mutation_scope,
260
+ });
261
+
262
+ let result: TaskResult;
263
+ try {
264
+ result = await executor.executor.execute(node, execAlloc, runId, 1);
265
+ } catch (e) {
266
+ result = { success: false, failure_class: 'env', evidence: [], output: String(e) };
267
+ }
268
+ await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
269
+
270
+ if (result.success) {
271
+ completed.push(nodeId);
272
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative' } });
273
+ } else {
274
+ failed.push(nodeId);
275
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative', failure_class: result.failure_class } });
276
+ break;
277
+ }
278
+ }
279
+ if (failed.length > 0) break;
280
+ }
281
+
282
+ appendEvent(projectRoot, sessionId, {
283
+ type: 'RunCompleted',
284
+ run_id: runId,
285
+ payload: { mode: 'cooperative', completed: completed.length, failed: failed.length },
286
+ });
287
+
288
+ return {
289
+ mode: 'cooperative',
290
+ run_id: runId,
291
+ completed,
292
+ failed,
293
+ blocked: [],
294
+ agent_results: [
295
+ { agent_id: planner.id, completed: [], failed: [] },
296
+ { agent_id: executor.id, completed, failed },
297
+ ],
298
+ handoffs,
299
+ };
300
+ }
301
+
197
302
  // ─── Public API ──────────────────────────────────────────────────────────────
198
303
 
199
304
  export class MultiAgentCoordinator {
@@ -201,10 +306,7 @@ export class MultiAgentCoordinator {
201
306
  switch (opts.mode) {
202
307
  case 'parallel': return runParallel(graph, opts);
203
308
  case 'competitive': return runCompetitive(graph, opts);
204
- case 'cooperative':
205
- // Cooperative mode: planner (agent[0]) prepares context,
206
- // executor (agent[1]) implements. For R3, delegate to sequential parallel.
207
- return runParallel(graph, { ...opts, mode: 'parallel' });
309
+ case 'cooperative': return runCooperative(graph, opts);
208
310
  default:
209
311
  throw new Error(`Unknown coordination mode: ${opts.mode}`);
210
312
  }
@@ -0,0 +1,62 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import type { RunResult } from './scheduler';
4
+
5
+ export interface RunJournal {
6
+ run_id: string;
7
+ paused_at: string | null;
8
+ cancelled: boolean;
9
+ eligible_work_items: string[];
10
+ completed_work_items: string[];
11
+ failed_work_items: string[];
12
+ blocked_work_items: string[];
13
+ pending_gates: string[];
14
+ replay_cursor: string | null;
15
+ scheduler_state: 'running' | 'paused' | 'cancelled' | 'completed';
16
+ partial_result: Omit<RunResult, 'status'> | null;
17
+ }
18
+
19
+ function journalPath(projectRoot: string, runId: string): string {
20
+ return path.join(projectRoot, '.oxe', 'runs', runId, 'journal.json');
21
+ }
22
+
23
+ export function saveJournal(projectRoot: string, runId: string, journal: RunJournal): void {
24
+ const p = journalPath(projectRoot, runId);
25
+ fs.mkdirSync(path.dirname(p), { recursive: true });
26
+ fs.writeFileSync(p, JSON.stringify(journal, null, 2), 'utf8');
27
+ }
28
+
29
+ export function loadJournal(projectRoot: string, runId: string): RunJournal | null {
30
+ const p = journalPath(projectRoot, runId);
31
+ if (!fs.existsSync(p)) return null;
32
+ try {
33
+ return JSON.parse(fs.readFileSync(p, 'utf8')) as RunJournal;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function deleteJournal(projectRoot: string, runId: string): void {
40
+ const p = journalPath(projectRoot, runId);
41
+ try {
42
+ fs.unlinkSync(p);
43
+ } catch {
44
+ // ignore if not found
45
+ }
46
+ }
47
+
48
+ export function createJournal(runId: string): RunJournal {
49
+ return {
50
+ run_id: runId,
51
+ paused_at: null,
52
+ cancelled: false,
53
+ eligible_work_items: [],
54
+ completed_work_items: [],
55
+ failed_work_items: [],
56
+ blocked_work_items: [],
57
+ pending_gates: [],
58
+ replay_cursor: null,
59
+ scheduler_state: 'running',
60
+ partial_result: null,
61
+ };
62
+ }
@@ -4,6 +4,13 @@ import type { EventInput } from '../events/bus';
4
4
  import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
5
5
  import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
6
6
  import type { WorkspaceLease } from '../models/workspace';
7
+ import {
8
+ saveJournal,
9
+ loadJournal,
10
+ deleteJournal,
11
+ createJournal,
12
+ } from './run-journal';
13
+ import type { RunJournal } from './run-journal';
7
14
 
8
15
  export interface TaskResult {
9
16
  success: boolean;
@@ -32,7 +39,7 @@ export interface SchedulerContext {
32
39
 
33
40
  export interface RunResult {
34
41
  run_id: string;
35
- status: 'completed' | 'failed' | 'cancelled';
42
+ status: 'completed' | 'failed' | 'cancelled' | 'paused';
36
43
  completed: string[];
37
44
  failed: string[];
38
45
  blocked: string[];
@@ -43,10 +50,13 @@ type NodeStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'bl
43
50
  export class Scheduler {
44
51
  private cancelled = false;
45
52
  private paused = false;
53
+ private journal: RunJournal | null = null;
54
+ private ctx: SchedulerContext | null = null;
46
55
 
47
56
  async run(graph: ExecutionGraph, ctx: SchedulerContext): Promise<RunResult> {
48
57
  this.cancelled = false;
49
58
  this.paused = false;
59
+ this.ctx = ctx;
50
60
 
51
61
  const status = new Map<string, NodeStatus>();
52
62
  for (const id of graph.nodes.keys()) status.set(id, 'pending');
@@ -55,10 +65,26 @@ export class Scheduler {
55
65
  const failed: string[] = [];
56
66
  const blocked: string[] = [];
57
67
 
68
+ this.journal = createJournal(ctx.runId);
69
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
70
+
58
71
  this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId } });
59
72
 
60
73
  for (const wave of graph.waves) {
61
74
  if (this.cancelled) break;
75
+
76
+ // Respect pause: persist journal and return paused result
77
+ if (this.paused) {
78
+ this.journal.scheduler_state = 'paused';
79
+ this.journal.paused_at = new Date().toISOString();
80
+ this.journal.completed_work_items = completed.slice();
81
+ this.journal.failed_work_items = failed.slice();
82
+ this.journal.blocked_work_items = blocked.slice();
83
+ this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
84
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
85
+ return { run_id: ctx.runId, status: 'paused', completed, failed, blocked };
86
+ }
87
+
62
88
  const waveFailed = await this.runWave(
63
89
  wave.node_ids,
64
90
  graph,
@@ -68,6 +94,13 @@ export class Scheduler {
68
94
  failed,
69
95
  blocked
70
96
  );
97
+
98
+ // Sync journal after each wave
99
+ this.journal.completed_work_items = completed.slice();
100
+ this.journal.failed_work_items = failed.slice();
101
+ this.journal.blocked_work_items = blocked.slice();
102
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
103
+
71
104
  if (waveFailed) break;
72
105
  }
73
106
 
@@ -95,6 +128,116 @@ export class Scheduler {
95
128
  payload: { run_id: ctx.runId, status: finalStatus },
96
129
  });
97
130
 
131
+ this.journal.scheduler_state = this.cancelled ? 'cancelled' : 'completed';
132
+ this.journal.completed_work_items = completed.slice();
133
+ this.journal.failed_work_items = failed.slice();
134
+ this.journal.blocked_work_items = blocked.slice();
135
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
136
+
137
+ return { run_id: ctx.runId, status: finalStatus, completed, failed, blocked };
138
+ }
139
+
140
+ /**
141
+ * Recover a previously paused run by loading its journal and re-running
142
+ * only the work items that haven't completed yet.
143
+ */
144
+ async recover(runId: string, ctx: SchedulerContext, graph: ExecutionGraph): Promise<RunResult | null> {
145
+ const journal = loadJournal(ctx.projectRoot, runId);
146
+ if (!journal || journal.scheduler_state !== 'paused') return null;
147
+
148
+ // Restore state from journal
149
+ this.cancelled = false;
150
+ this.paused = false;
151
+ this.ctx = ctx;
152
+ this.journal = { ...journal, scheduler_state: 'running', paused_at: null };
153
+
154
+ const restoredCompleted = new Set(journal.completed_work_items);
155
+ const restoredFailed = new Set(journal.failed_work_items);
156
+ const restoredBlocked = new Set(journal.blocked_work_items);
157
+
158
+ const status = new Map<string, NodeStatus>();
159
+ for (const id of graph.nodes.keys()) {
160
+ if (restoredCompleted.has(id)) status.set(id, 'completed');
161
+ else if (restoredFailed.has(id)) status.set(id, 'failed');
162
+ else if (restoredBlocked.has(id)) status.set(id, 'blocked');
163
+ else status.set(id, 'pending');
164
+ }
165
+
166
+ const completed = [...restoredCompleted];
167
+ const failed = [...restoredFailed];
168
+ const blocked = [...restoredBlocked];
169
+
170
+ saveJournal(ctx.projectRoot, runId, this.journal);
171
+
172
+ this.emit(ctx, { type: 'RunStarted', payload: { run_id: ctx.runId, recovered: true } });
173
+
174
+ for (const wave of graph.waves) {
175
+ if (this.cancelled) break;
176
+ if (this.paused) {
177
+ this.journal.scheduler_state = 'paused';
178
+ this.journal.paused_at = new Date().toISOString();
179
+ this.journal.completed_work_items = completed.slice();
180
+ this.journal.failed_work_items = failed.slice();
181
+ this.journal.blocked_work_items = blocked.slice();
182
+ this.journal.partial_result = { run_id: ctx.runId, completed, failed, blocked };
183
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
184
+ return { run_id: ctx.runId, status: 'paused', completed, failed, blocked };
185
+ }
186
+
187
+ // Skip waves fully completed
188
+ const allDone = wave.node_ids.every(
189
+ (id) => restoredCompleted.has(id) || restoredFailed.has(id) || restoredBlocked.has(id)
190
+ );
191
+ if (allDone) continue;
192
+
193
+ const waveFailed = await this.runWave(
194
+ wave.node_ids,
195
+ graph,
196
+ ctx,
197
+ status,
198
+ completed,
199
+ failed,
200
+ blocked
201
+ );
202
+
203
+ this.journal.completed_work_items = completed.slice();
204
+ this.journal.failed_work_items = failed.slice();
205
+ this.journal.blocked_work_items = blocked.slice();
206
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
207
+
208
+ if (waveFailed) break;
209
+ }
210
+
211
+ for (const [id, s] of status) {
212
+ if (s === 'pending') {
213
+ status.set(id, 'blocked');
214
+ blocked.push(id);
215
+ this.emit(ctx, {
216
+ type: 'WorkItemBlocked',
217
+ work_item_id: id,
218
+ payload: { reason: 'upstream_wave_failed' },
219
+ });
220
+ }
221
+ }
222
+
223
+ const finalStatus: RunResult['status'] = this.cancelled
224
+ ? 'cancelled'
225
+ : failed.length > 0
226
+ ? 'failed'
227
+ : 'completed';
228
+
229
+ this.emit(ctx, {
230
+ type: 'RunCompleted',
231
+ payload: { run_id: ctx.runId, status: finalStatus, recovered: true },
232
+ });
233
+
234
+ this.journal.scheduler_state = this.cancelled ? 'cancelled' : 'completed';
235
+ this.journal.completed_work_items = completed.slice();
236
+ this.journal.failed_work_items = failed.slice();
237
+ this.journal.blocked_work_items = blocked.slice();
238
+ saveJournal(ctx.projectRoot, ctx.runId, this.journal);
239
+ deleteJournal(ctx.projectRoot, ctx.runId);
240
+
98
241
  return { run_id: ctx.runId, status: finalStatus, completed, failed, blocked };
99
242
  }
100
243
 
@@ -107,11 +250,11 @@ export class Scheduler {
107
250
  failed: string[],
108
251
  blocked: string[]
109
252
  ): Promise<boolean> {
110
- // Partition: eligible vs blocked-by-dep
111
253
  const eligible: string[] = [];
112
254
  const depsNotMet: string[] = [];
113
255
 
114
256
  for (const id of nodeIds) {
257
+ if (status.get(id) === 'completed') continue; // already done in recovery
115
258
  const node = graph.nodes.get(id)!;
116
259
  const depsMet = node.depends_on.every((dep) => status.get(dep) === 'completed');
117
260
  if (depsMet) {
@@ -121,7 +264,6 @@ export class Scheduler {
121
264
  }
122
265
  }
123
266
 
124
- // Nodes whose deps weren't met in this wave → blocked
125
267
  for (const id of depsNotMet) {
126
268
  status.set(id, 'blocked');
127
269
  blocked.push(id);
@@ -132,21 +274,18 @@ export class Scheduler {
132
274
  });
133
275
  }
134
276
 
135
- // Separate read-only (no mutation_scope) from mutation nodes
136
277
  const readOnly = eligible.filter((id) => {
137
278
  const node = graph.nodes.get(id)!;
138
279
  return node.mutation_scope.length === 0;
139
280
  });
140
281
  const mutations = eligible.filter((id) => !readOnly.includes(id));
141
282
 
142
- // Run read-only nodes in parallel
143
283
  if (readOnly.length > 0) {
144
284
  await Promise.all(
145
285
  readOnly.map((id) => this.runNode(id, graph, ctx, status, completed, failed))
146
286
  );
147
287
  }
148
288
 
149
- // Run mutation nodes sequentially to avoid scope conflicts
150
289
  for (const id of mutations) {
151
290
  if (this.cancelled) break;
152
291
  await this.runNode(id, graph, ctx, status, completed, failed);
@@ -214,7 +353,6 @@ export class Scheduler {
214
353
  return;
215
354
  }
216
355
 
217
- // Policy failures never retry
218
356
  if (lastResult.failure_class === 'policy') break;
219
357
 
220
358
  if (attempt < maxAttempts) {
@@ -246,7 +384,6 @@ export class Scheduler {
246
384
  }
247
385
  }
248
386
 
249
- // All attempts exhausted
250
387
  this.emit(ctx, {
251
388
  type: 'WorkItemBlocked',
252
389
  work_item_id: nodeId,
@@ -258,14 +395,37 @@ export class Scheduler {
258
395
 
259
396
  pause(): void {
260
397
  this.paused = true;
398
+ if (this.journal && this.ctx) {
399
+ this.journal.scheduler_state = 'paused';
400
+ this.journal.paused_at = new Date().toISOString();
401
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
402
+ }
261
403
  }
262
404
 
263
405
  resume(): void {
264
406
  this.paused = false;
407
+ if (this.journal && this.ctx) {
408
+ this.journal.scheduler_state = 'running';
409
+ this.journal.paused_at = null;
410
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
411
+ }
265
412
  }
266
413
 
267
414
  cancel(): void {
268
415
  this.cancelled = true;
416
+ if (this.journal && this.ctx) {
417
+ this.journal.cancelled = true;
418
+ this.journal.scheduler_state = 'cancelled';
419
+ saveJournal(this.ctx.projectRoot, this.ctx.runId, this.journal);
420
+ }
421
+ }
422
+
423
+ getJournal(): RunJournal | null {
424
+ return this.journal;
425
+ }
426
+
427
+ static loadJournal(projectRoot: string, runId: string): RunJournal | null {
428
+ return loadJournal(projectRoot, runId);
269
429
  }
270
430
 
271
431
  private emit(
@@ -1 +1,2 @@
1
1
  export * from './verification-compiler';
2
+ export * from './verification-manifest';