oxe-cc 1.5.1 → 1.7.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 (125) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +19 -15
  4. package/bin/lib/oxe-agent-install.cjs +125 -24
  5. package/bin/lib/oxe-dashboard.cjs +21 -5
  6. package/bin/lib/oxe-project-health.cjs +120 -42
  7. package/bin/lib/oxe-release.cjs +77 -4
  8. package/bin/oxe-cc.js +155 -78
  9. package/commands/oxe/debug.md +6 -1
  10. package/commands/oxe/discuss.md +7 -2
  11. package/commands/oxe/execute.md +7 -2
  12. package/commands/oxe/plan-agent.md +7 -2
  13. package/commands/oxe/plan.md +7 -2
  14. package/commands/oxe/scan.md +6 -1
  15. package/commands/oxe/spec.md +6 -1
  16. package/commands/oxe/verify.md +6 -1
  17. package/docs/CONTENT-MIGRATION-AUDIT.md +49 -0
  18. package/docs/RELEASE-READINESS.md +8 -0
  19. package/docs/RUNTIME-SMOKE-MATRIX.md +9 -2
  20. package/lib/runtime/compiler/graph-compiler.js +32 -0
  21. package/lib/runtime/context/context-pack-builder.d.ts +15 -0
  22. package/lib/runtime/context/context-pack-builder.js +78 -0
  23. package/lib/runtime/events/catalog.d.ts +1 -1
  24. package/lib/runtime/events/catalog.js +5 -0
  25. package/lib/runtime/executor/action-tool-map.d.ts +3 -0
  26. package/lib/runtime/executor/action-tool-map.js +41 -0
  27. package/lib/runtime/executor/built-in-tools.d.ts +8 -0
  28. package/lib/runtime/executor/built-in-tools.js +267 -0
  29. package/lib/runtime/executor/index.d.ts +6 -0
  30. package/lib/runtime/executor/index.js +12 -0
  31. package/lib/runtime/executor/llm-task-executor.d.ts +29 -0
  32. package/lib/runtime/executor/llm-task-executor.js +138 -0
  33. package/lib/runtime/executor/node-prompt-builder.d.ts +3 -0
  34. package/lib/runtime/executor/node-prompt-builder.js +36 -0
  35. package/lib/runtime/executor/stream-completion.d.ts +38 -0
  36. package/lib/runtime/executor/stream-completion.js +105 -0
  37. package/lib/runtime/index.d.ts +1 -0
  38. package/lib/runtime/index.js +2 -0
  39. package/lib/runtime/models/failure.d.ts +5 -0
  40. package/lib/runtime/models/failure.js +2 -0
  41. package/lib/runtime/plugins/capability-adapter.d.ts +9 -0
  42. package/lib/runtime/plugins/capability-adapter.js +111 -8
  43. package/lib/runtime/plugins/plugin-abi.d.ts +8 -0
  44. package/lib/runtime/plugins/plugin-registry.d.ts +2 -1
  45. package/lib/runtime/plugins/plugin-registry.js +6 -1
  46. package/lib/runtime/reducers/run-state-reducer.js +39 -2
  47. package/lib/runtime/scheduler/scheduler.d.ts +14 -2
  48. package/lib/runtime/scheduler/scheduler.js +131 -11
  49. package/lib/runtime/verification/verification-manifest.d.ts +5 -2
  50. package/lib/sdk/index.cjs +10 -5
  51. package/lib/sdk/index.d.ts +21 -10
  52. package/oxe/agents/oxe-assumptions-analyzer.md +136 -0
  53. package/oxe/agents/oxe-codebase-mapper.md +142 -0
  54. package/oxe/agents/oxe-debugger.md +145 -0
  55. package/oxe/agents/oxe-executor.md +139 -0
  56. package/oxe/agents/oxe-integration-checker.md +142 -0
  57. package/oxe/agents/oxe-plan-checker.md +143 -0
  58. package/oxe/agents/oxe-planner.md +151 -0
  59. package/oxe/agents/oxe-research-synthesizer.md +146 -0
  60. package/oxe/agents/oxe-researcher.md +163 -0
  61. package/oxe/agents/oxe-ui-auditor.md +151 -0
  62. package/oxe/agents/oxe-ui-checker.md +157 -0
  63. package/oxe/agents/oxe-ui-researcher.md +179 -0
  64. package/oxe/agents/oxe-validation-auditor.md +154 -0
  65. package/oxe/agents/oxe-verifier.md +132 -0
  66. package/oxe/personas/README.md +91 -39
  67. package/oxe/personas/architect.md +149 -37
  68. package/oxe/personas/db-specialist.md +149 -36
  69. package/oxe/personas/debugger.md +155 -38
  70. package/oxe/personas/executor.md +164 -38
  71. package/oxe/personas/planner.md +165 -36
  72. package/oxe/personas/researcher.md +148 -35
  73. package/oxe/personas/ui-specialist.md +164 -36
  74. package/oxe/personas/verifier.md +174 -39
  75. package/oxe/templates/CONFIG.md +3 -3
  76. package/oxe/templates/EXECUTION-RUNTIME.template.md +1 -1
  77. package/oxe/templates/FIXTURE-PACK.template.json +29 -22
  78. package/oxe/templates/FIXTURE-PACK.template.md +20 -11
  79. package/oxe/templates/IMPLEMENTATION-PACK.template.json +55 -39
  80. package/oxe/templates/IMPLEMENTATION-PACK.template.md +28 -16
  81. package/oxe/templates/INVESTIGATION.template.md +38 -38
  82. package/oxe/templates/PLAN.template.md +63 -32
  83. package/oxe/templates/REFERENCE-ANCHORS.template.md +18 -14
  84. package/oxe/templates/RESEARCH.template.md +11 -11
  85. package/oxe/templates/SPEC.template.md +6 -6
  86. package/oxe/templates/SUMMARY.template.md +33 -3
  87. package/oxe/templates/config.template.json +1 -1
  88. package/oxe/workflows/debug.md +9 -7
  89. package/oxe/workflows/execute.md +31 -28
  90. package/oxe/workflows/forensics.md +5 -3
  91. package/oxe/workflows/milestone.md +12 -12
  92. package/oxe/workflows/next.md +1 -1
  93. package/oxe/workflows/plan.md +409 -132
  94. package/oxe/workflows/references/adaptive-discovery.md +27 -27
  95. package/oxe/workflows/references/flow-robustness-contract.md +80 -80
  96. package/oxe/workflows/references/session-path-resolution.md +71 -71
  97. package/oxe/workflows/references/workflow-runtime-contracts.json +127 -127
  98. package/oxe/workflows/scan.md +355 -69
  99. package/oxe/workflows/spec.md +302 -9
  100. package/oxe/workflows/ui-review.md +5 -4
  101. package/oxe/workflows/ui-spec.md +4 -3
  102. package/oxe/workflows/verify.md +12 -9
  103. package/oxe/workflows/workstream.md +16 -16
  104. package/package.json +1 -1
  105. package/packages/runtime/package.json +1 -1
  106. package/packages/runtime/src/compiler/graph-compiler.ts +40 -0
  107. package/packages/runtime/src/context/context-pack-builder.ts +80 -0
  108. package/packages/runtime/src/events/catalog.ts +5 -0
  109. package/packages/runtime/src/executor/action-tool-map.ts +46 -0
  110. package/packages/runtime/src/executor/built-in-tools.ts +276 -0
  111. package/packages/runtime/src/executor/index.ts +6 -0
  112. package/packages/runtime/src/executor/llm-task-executor.ts +194 -0
  113. package/packages/runtime/src/executor/node-prompt-builder.ts +45 -0
  114. package/packages/runtime/src/executor/stream-completion.ts +145 -0
  115. package/packages/runtime/src/index.ts +3 -0
  116. package/packages/runtime/src/models/failure.ts +11 -0
  117. package/packages/runtime/src/plugins/capability-adapter.ts +117 -10
  118. package/packages/runtime/src/plugins/plugin-abi.ts +9 -0
  119. package/packages/runtime/src/plugins/plugin-registry.ts +10 -1
  120. package/packages/runtime/src/reducers/run-state-reducer.ts +59 -2
  121. package/packages/runtime/src/scheduler/scheduler.ts +152 -14
  122. package/packages/runtime/src/verification/verification-manifest.ts +12 -8
  123. package/vscode-extension/oxe-agents-1.6.0.vsix +0 -0
  124. package/vscode-extension/oxe-agents-1.7.0.vsix +0 -0
  125. package/vscode-extension/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ import https from 'https';
2
+ import http from 'http';
3
+ import { URL } from 'url';
4
+
5
+ export interface ChatMessage {
6
+ role: 'system' | 'user' | 'assistant' | 'tool';
7
+ content: string | null;
8
+ tool_calls?: ToolCall[];
9
+ tool_call_id?: string;
10
+ name?: string;
11
+ }
12
+
13
+ export interface ToolCall {
14
+ id: string;
15
+ type: 'function';
16
+ function: {
17
+ name: string;
18
+ arguments: string;
19
+ };
20
+ }
21
+
22
+ export interface CompletionResponse {
23
+ content: string;
24
+ tool_calls: ToolCall[];
25
+ finish_reason: string | null;
26
+ }
27
+
28
+ export interface StreamCompletionOptions {
29
+ baseUrl: string;
30
+ apiKey: string;
31
+ model: string;
32
+ messages: ChatMessage[];
33
+ tools?: ToolSchema[];
34
+ maxTokens?: number;
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ export interface ToolSchema {
39
+ type: 'function';
40
+ function: {
41
+ name: string;
42
+ description: string;
43
+ parameters: Record<string, unknown>;
44
+ };
45
+ }
46
+
47
+ export async function streamCompletion(opts: StreamCompletionOptions): Promise<CompletionResponse> {
48
+ const url = new URL('/chat/completions', opts.baseUrl.replace(/\/$/, '') + '/');
49
+ const body = JSON.stringify({
50
+ model: opts.model,
51
+ messages: opts.messages,
52
+ max_tokens: opts.maxTokens ?? 4096,
53
+ stream: true,
54
+ ...(opts.tools?.length ? { tools: opts.tools, tool_choice: 'auto' } : {}),
55
+ });
56
+
57
+ const rawText = await fetchWithTimeout(url, opts.apiKey, body, opts.timeoutMs ?? 120_000);
58
+ return parseSseResponse(rawText);
59
+ }
60
+
61
+ function fetchWithTimeout(url: URL, apiKey: string, body: string, timeoutMs: number): Promise<string> {
62
+ return new Promise((resolve, reject) => {
63
+ const isHttps = url.protocol === 'https:';
64
+ const transport = isHttps ? https : http;
65
+ const chunks: Buffer[] = [];
66
+
67
+ const req = transport.request(
68
+ {
69
+ hostname: url.hostname,
70
+ port: url.port || (isHttps ? 443 : 80),
71
+ path: url.pathname + url.search,
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ 'Authorization': `Bearer ${apiKey}`,
76
+ 'Content-Length': Buffer.byteLength(body),
77
+ },
78
+ },
79
+ (res) => {
80
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
81
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
82
+ res.on('error', reject);
83
+ },
84
+ );
85
+
86
+ const timer = setTimeout(() => {
87
+ req.destroy(new Error(`LLM request timed out after ${timeoutMs}ms`));
88
+ }, timeoutMs);
89
+
90
+ req.on('close', () => clearTimeout(timer));
91
+ req.on('error', (err) => { clearTimeout(timer); reject(err); });
92
+ req.write(body);
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ function parseSseResponse(raw: string): CompletionResponse {
98
+ let content = '';
99
+ const toolCallsMap = new Map<number, { id: string; name: string; args: string }>();
100
+ let finish_reason: string | null = null;
101
+
102
+ for (const line of raw.split('\n')) {
103
+ const trimmed = line.trim();
104
+ if (!trimmed.startsWith('data:')) continue;
105
+ const payload = trimmed.slice(5).trim();
106
+ if (payload === '[DONE]') break;
107
+
108
+ let chunk: Record<string, unknown>;
109
+ try { chunk = JSON.parse(payload); } catch { continue; }
110
+
111
+ const choices = chunk.choices as Array<Record<string, unknown>> | undefined;
112
+ if (!choices?.length) continue;
113
+
114
+ const delta = choices[0].delta as Record<string, unknown> | undefined;
115
+ if (!delta) continue;
116
+
117
+ if (typeof delta.content === 'string') content += delta.content;
118
+ if (choices[0].finish_reason) finish_reason = choices[0].finish_reason as string;
119
+
120
+ const deltaToolCalls = delta.tool_calls as Array<Record<string, unknown>> | undefined;
121
+ if (deltaToolCalls) {
122
+ for (const dtc of deltaToolCalls) {
123
+ const idx = dtc.index as number;
124
+ if (!toolCallsMap.has(idx)) {
125
+ toolCallsMap.set(idx, { id: '', name: '', args: '' });
126
+ }
127
+ const entry = toolCallsMap.get(idx)!;
128
+ if (dtc.id) entry.id += dtc.id as string;
129
+ const fn = dtc.function as Record<string, unknown> | undefined;
130
+ if (fn?.name) entry.name += fn.name as string;
131
+ if (fn?.arguments) entry.args += fn.arguments as string;
132
+ }
133
+ }
134
+ }
135
+
136
+ const tool_calls: ToolCall[] = [...toolCallsMap.entries()]
137
+ .sort(([a], [b]) => a - b)
138
+ .map(([, tc]) => ({
139
+ id: tc.id || `call_${Math.random().toString(36).slice(2)}`,
140
+ type: 'function' as const,
141
+ function: { name: tc.name, arguments: tc.args },
142
+ }));
143
+
144
+ return { content, tool_calls, finish_reason };
145
+ }
@@ -39,3 +39,6 @@ export * from './scheduler/multi-agent-coordinator';
39
39
  // R4 Public ABI — Decision, Audit & Enterprise
40
40
  export * from './decision/index';
41
41
  export * from './audit/index';
42
+
43
+ // R5 Public ABI — LLM Task Executor
44
+ export * from './executor/index';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Canonical failure classification used by TaskResult and VerificationManifest.
3
+ * Both must import from this file — never redefine inline.
4
+ */
5
+ export type FailureClass =
6
+ | 'env' // infrastructure / environment problem (network, disk, permissions)
7
+ | 'policy' // blocked by policy engine before execution
8
+ | 'test' // verification / acceptance test failed
9
+ | 'timeout' // task or run exceeded time budget
10
+ | 'evidence_missing' // required evidence was not collected
11
+ | null; // success — no failure
@@ -1,11 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { spawnSync } from 'child_process';
3
+ import { spawn } from 'child_process';
4
4
  import type {
5
5
  OxePlugin,
6
6
  ToolProvider,
7
7
  ToolInvocationInput,
8
8
  ToolInvocationResult,
9
+ PreInvokeResult,
9
10
  VerifierProvider,
10
11
  VerificationInput,
11
12
  } from './plugin-abi';
@@ -19,6 +20,56 @@ interface CapabilityManifest {
19
20
  evidenceOutputs: string[];
20
21
  checkTypes: string[];
21
22
  dir: string;
23
+ timeoutMs: number;
24
+ preInvokeHook: string | null;
25
+ postInvokeHook: string | null;
26
+ }
27
+
28
+ const DEFAULT_CAPABILITY_TIMEOUT_MS = 60_000;
29
+
30
+ export async function runCapabilityAsync(
31
+ program: string,
32
+ args: string[],
33
+ env: NodeJS.ProcessEnv,
34
+ cwd: string,
35
+ timeoutMs: number,
36
+ onChunk?: (chunk: string, stream: 'stdout' | 'stderr') => void,
37
+ ): Promise<{ exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }> {
38
+ return new Promise((resolve) => {
39
+ const proc = spawn(program, args, { cwd, env, stdio: 'pipe' });
40
+ const stdoutChunks: Buffer[] = [];
41
+ const stderrChunks: Buffer[] = [];
42
+ let timedOut = false;
43
+
44
+ const timer = setTimeout(() => {
45
+ timedOut = true;
46
+ proc.kill('SIGTERM');
47
+ }, timeoutMs);
48
+
49
+ proc.stdout.on('data', (chunk: Buffer) => {
50
+ stdoutChunks.push(chunk);
51
+ onChunk?.(chunk.toString(), 'stdout');
52
+ });
53
+ proc.stderr.on('data', (chunk: Buffer) => {
54
+ stderrChunks.push(chunk);
55
+ onChunk?.(chunk.toString(), 'stderr');
56
+ });
57
+
58
+ proc.on('close', (exitCode) => {
59
+ clearTimeout(timer);
60
+ resolve({
61
+ exitCode,
62
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
63
+ stderr: Buffer.concat(stderrChunks).toString('utf8'),
64
+ timedOut,
65
+ });
66
+ });
67
+
68
+ proc.on('error', (err) => {
69
+ clearTimeout(timer);
70
+ resolve({ exitCode: null, stdout: '', stderr: String(err), timedOut: false });
71
+ });
72
+ });
22
73
  }
23
74
 
24
75
  function parseFrontmatter(text: string): Record<string, string> {
@@ -62,6 +113,7 @@ function loadCapabilityManifests(projectRoot: string): CapabilityManifest[] {
62
113
  const fm = parseFrontmatter(raw);
63
114
  const id = String(fm.id || '').trim();
64
115
  if (!id) return null;
116
+ const rawTimeout = parseInt(String(fm.timeout_ms || ''), 10);
65
117
  return {
66
118
  id,
67
119
  entrypoint: String(fm.entrypoint || '').trim() || null,
@@ -69,6 +121,9 @@ function loadCapabilityManifests(projectRoot: string): CapabilityManifest[] {
69
121
  evidenceOutputs: parseArrayField(fm.evidence_outputs),
70
122
  checkTypes: parseArrayField(fm.check_types || fm.supports_checks),
71
123
  dir,
124
+ timeoutMs: isNaN(rawTimeout) ? DEFAULT_CAPABILITY_TIMEOUT_MS : rawTimeout,
125
+ preInvokeHook: String(fm.pre_invoke_hook || '').trim() || null,
126
+ postInvokeHook: String(fm.post_invoke_hook || '').trim() || null,
72
127
  };
73
128
  })
74
129
  .filter((item): item is CapabilityManifest => Boolean(item));
@@ -124,7 +179,7 @@ function buildToolProvider(projectRoot: string, manifest: CapabilityManifest): T
124
179
  };
125
180
  }
126
181
  const ext = path.extname(entrypoint).toLowerCase();
127
- const env = {
182
+ const env: NodeJS.ProcessEnv = {
128
183
  ...process.env,
129
184
  OXE_CAPABILITY_INPUT: JSON.stringify(input.params || {}),
130
185
  OXE_CAPABILITY_RUN_ID: input.run_id,
@@ -141,19 +196,71 @@ function buildToolProvider(projectRoot: string, manifest: CapabilityManifest): T
141
196
  program = 'powershell';
142
197
  args = ['-File', entrypoint];
143
198
  }
144
- const result = spawnSync(program, args, {
145
- cwd: projectRoot,
146
- encoding: 'utf8',
147
- env,
148
- });
199
+ const result = await runCapabilityAsync(program, args, env, projectRoot, manifest.timeoutMs);
200
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
201
+ if (result.timedOut) {
202
+ return {
203
+ success: false,
204
+ output,
205
+ evidence_paths: resolveEvidencePaths(projectRoot, manifest),
206
+ side_effects_applied: manifest.sideEffects,
207
+ error: `Capability ${manifest.id} timed out after ${manifest.timeoutMs}ms`,
208
+ };
209
+ }
149
210
  return {
150
- success: result.status === 0 && !result.error,
151
- output: [result.stdout || '', result.stderr || ''].filter(Boolean).join('\n').trim(),
211
+ success: result.exitCode === 0,
212
+ output,
152
213
  evidence_paths: resolveEvidencePaths(projectRoot, manifest),
153
214
  side_effects_applied: manifest.sideEffects,
154
- error: result.error ? String(result.error) : result.status === 0 ? undefined : (result.stderr || result.stdout || `Capability exited with status ${result.status}`),
215
+ error: result.exitCode === 0 ? undefined : (result.stderr || result.stdout || `Capability exited with status ${result.exitCode}`),
155
216
  };
156
217
  },
218
+
219
+ async preInvoke(input: ToolInvocationInput): Promise<PreInvokeResult> {
220
+ if (!manifest.preInvokeHook) return { allowed: true };
221
+ const hookPath = path.isAbsolute(manifest.preInvokeHook)
222
+ ? manifest.preInvokeHook
223
+ : path.join(manifest.dir, manifest.preInvokeHook);
224
+ const env: NodeJS.ProcessEnv = {
225
+ ...process.env,
226
+ OXE_CAPABILITY_INPUT: JSON.stringify(input.params || {}),
227
+ OXE_CAPABILITY_RUN_ID: input.run_id,
228
+ OXE_CAPABILITY_WORK_ITEM_ID: input.work_item_id,
229
+ OXE_CAPABILITY_ATTEMPT_ID: input.attempt_id,
230
+ OXE_CAPABILITY_WORKSPACE_ROOT: input.workspace_root,
231
+ };
232
+ const ext = path.extname(hookPath).toLowerCase();
233
+ let program = hookPath;
234
+ let args: string[] = [];
235
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { program = process.execPath; args = [hookPath]; }
236
+ else if (ext === '.ps1') { program = 'powershell'; args = ['-File', hookPath]; }
237
+ const result = await runCapabilityAsync(program, args, env, projectRoot, 10_000);
238
+ return result.exitCode === 0
239
+ ? { allowed: true }
240
+ : { allowed: false, reason: result.stderr || result.stdout || `pre_invoke_hook exited with status ${result.exitCode}` };
241
+ },
242
+
243
+ async postInvoke(input: ToolInvocationInput, _result: ToolInvocationResult): Promise<void> {
244
+ if (!manifest.postInvokeHook) return;
245
+ const hookPath = path.isAbsolute(manifest.postInvokeHook)
246
+ ? manifest.postInvokeHook
247
+ : path.join(manifest.dir, manifest.postInvokeHook);
248
+ const env: NodeJS.ProcessEnv = {
249
+ ...process.env,
250
+ OXE_CAPABILITY_INPUT: JSON.stringify(input.params || {}),
251
+ OXE_CAPABILITY_RUN_ID: input.run_id,
252
+ OXE_CAPABILITY_WORK_ITEM_ID: input.work_item_id,
253
+ OXE_CAPABILITY_ATTEMPT_ID: input.attempt_id,
254
+ OXE_CAPABILITY_WORKSPACE_ROOT: input.workspace_root,
255
+ OXE_INVOKE_SUCCESS: _result.success ? '1' : '0',
256
+ };
257
+ const ext = path.extname(hookPath).toLowerCase();
258
+ let program = hookPath;
259
+ let args: string[] = [];
260
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { program = process.execPath; args = [hookPath]; }
261
+ else if (ext === '.ps1') { program = 'powershell'; args = ['-File', hookPath]; }
262
+ await runCapabilityAsync(program, args, env, projectRoot, 10_000).catch(() => {});
263
+ },
157
264
  };
158
265
  }
159
266
 
@@ -21,12 +21,21 @@ export interface ToolInvocationResult {
21
21
  error?: string;
22
22
  }
23
23
 
24
+ export interface PreInvokeResult {
25
+ allowed: boolean;
26
+ reason?: string;
27
+ }
28
+
24
29
  export interface ToolProvider {
25
30
  readonly name: string;
26
31
  readonly kind: 'read' | 'mutation' | 'verification' | 'analysis' | 'external_operation';
27
32
  readonly idempotent: boolean;
28
33
  supports(actionType: string): boolean;
29
34
  invoke(input: ToolInvocationInput): Promise<ToolInvocationResult>;
35
+ /** Optional: called before invoke. Return allowed:false to block execution. */
36
+ preInvoke?(input: ToolInvocationInput): Promise<PreInvokeResult>;
37
+ /** Optional: called after invoke. Errors are swallowed — does not affect outcome. */
38
+ postInvoke?(input: ToolInvocationInput, result: ToolInvocationResult): Promise<void>;
30
39
  }
31
40
 
32
41
  // ─── WorkspaceProvider ───────────────────────────────────────────────────────
@@ -53,11 +53,20 @@ export class PluginRegistry {
53
53
  return loaded;
54
54
  }
55
55
 
56
- toolProviderFor(actionType: string): ToolProvider | null {
56
+ toolProviderFor(actionType: string, required: true): ToolProvider;
57
+ toolProviderFor(actionType: string, required?: false): ToolProvider | null;
58
+ toolProviderFor(actionType: string, required = false): ToolProvider | null {
57
59
  for (const plugin of this.plugins) {
58
60
  const provider = plugin.toolProviders?.find((p) => p.supports(actionType));
59
61
  if (provider) return provider;
60
62
  }
63
+ if (required) {
64
+ const loaded = this.plugins.map(p => p.name).join(', ') || '(none)';
65
+ throw new Error(
66
+ `[plugin-registry] No provider supports action type "${actionType}". ` +
67
+ `Loaded plugins: [${loaded}]. Load errors: ${this.loadErrors.length}.`
68
+ );
69
+ }
61
70
  return null;
62
71
  }
63
72
 
@@ -61,6 +61,56 @@ export function reduce(events: OxeEvent[]): RunState {
61
61
  // Exported alias so debug-reducer can import applyEvent without circular issues
62
62
  export { applyEvent as applyEventExported };
63
63
 
64
+ // ─── State machine: valid transitions ─────────────────────────────────────────
65
+
66
+ import type { WorkItemStatus } from '../models/work-item';
67
+ import type { RunStatus } from '../models/run';
68
+
69
+ const VALID_WORK_ITEM_TRANSITIONS: Record<WorkItemStatus, readonly WorkItemStatus[]> = {
70
+ pending: ['ready'],
71
+ ready: ['running', 'completed', 'failed', 'blocked'],
72
+ running: ['completed', 'failed', 'blocked'],
73
+ failed: ['ready'], // retry path
74
+ completed: [], // terminal
75
+ blocked: [], // terminal
76
+ skipped: [], // terminal
77
+ };
78
+
79
+ const VALID_RUN_TRANSITIONS: Record<RunStatus, readonly RunStatus[]> = {
80
+ planned: ['running'],
81
+ running: ['paused', 'failed', 'completed', 'aborted', 'cancelled', 'waiting_approval'],
82
+ paused: ['running', 'cancelled'],
83
+ waiting_approval: ['running', 'cancelled'],
84
+ failed: ['replaying'],
85
+ replaying: ['running', 'failed', 'completed'],
86
+ completed: [],
87
+ aborted: [],
88
+ cancelled: [],
89
+ };
90
+
91
+ function assertWorkItemTransition(
92
+ itemId: string,
93
+ from: WorkItemStatus,
94
+ to: WorkItemStatus,
95
+ eventType: string
96
+ ): void {
97
+ const allowed = VALID_WORK_ITEM_TRANSITIONS[from] ?? [];
98
+ if (!(allowed as readonly string[]).includes(to)) {
99
+ throw new Error(
100
+ `[state-machine] Invalid work item transition for "${itemId}": ${from} → ${to} (event: ${eventType})`
101
+ );
102
+ }
103
+ }
104
+
105
+ function assertRunTransition(from: RunStatus, to: RunStatus, eventType: string): void {
106
+ const allowed = VALID_RUN_TRANSITIONS[from] ?? [];
107
+ if (!(allowed as readonly string[]).includes(to)) {
108
+ throw new Error(
109
+ `[state-machine] Invalid run transition: ${from} → ${to} (event: ${eventType})`
110
+ );
111
+ }
112
+ }
113
+
64
114
  function applyEvent(state: RunState, event: OxeEvent): RunState {
65
115
  switch (event.type) {
66
116
  case 'RunStarted': {
@@ -71,6 +121,7 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
71
121
  case 'RunCompleted': {
72
122
  if (!state.run) return state;
73
123
  const status = (event.payload as { status?: Run['status'] }).status ?? 'completed';
124
+ assertRunTransition(state.run.status, status, event.type);
74
125
  return {
75
126
  ...state,
76
127
  run: { ...state.run, status, ended_at: event.timestamp },
@@ -121,7 +172,10 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
121
172
  if (!event.work_item_id) return state;
122
173
  const workItems = new Map(state.workItems);
123
174
  const item = workItems.get(event.work_item_id);
124
- if (item) workItems.set(event.work_item_id, { ...item, status: 'completed' });
175
+ if (item) {
176
+ assertWorkItemTransition(event.work_item_id, item.status, 'completed', event.type);
177
+ workItems.set(event.work_item_id, { ...item, status: 'completed' });
178
+ }
125
179
  const completedWorkItems = new Set(state.completedWorkItems);
126
180
  completedWorkItems.add(event.work_item_id);
127
181
  // Collect evidence refs from payload
@@ -139,7 +193,10 @@ function applyEvent(state: RunState, event: OxeEvent): RunState {
139
193
  if (!event.work_item_id) return state;
140
194
  const workItems = new Map(state.workItems);
141
195
  const item = workItems.get(event.work_item_id);
142
- if (item) workItems.set(event.work_item_id, { ...item, status: 'blocked' });
196
+ if (item) {
197
+ assertWorkItemTransition(event.work_item_id, item.status, 'blocked', event.type);
198
+ workItems.set(event.work_item_id, { ...item, status: 'blocked' });
199
+ }
143
200
  const blockedWorkItems = new Set(state.blockedWorkItems);
144
201
  blockedWorkItems.add(event.work_item_id);
145
202
  return { ...state, workItems, blockedWorkItems };