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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +45 -0
- package/README.md +19 -15
- package/bin/lib/oxe-agent-install.cjs +125 -24
- package/bin/lib/oxe-dashboard.cjs +21 -5
- package/bin/lib/oxe-project-health.cjs +120 -42
- package/bin/lib/oxe-release.cjs +77 -4
- package/bin/oxe-cc.js +155 -78
- package/commands/oxe/debug.md +6 -1
- package/commands/oxe/discuss.md +7 -2
- package/commands/oxe/execute.md +7 -2
- package/commands/oxe/plan-agent.md +7 -2
- package/commands/oxe/plan.md +7 -2
- package/commands/oxe/scan.md +6 -1
- package/commands/oxe/spec.md +6 -1
- package/commands/oxe/verify.md +6 -1
- package/docs/CONTENT-MIGRATION-AUDIT.md +49 -0
- package/docs/RELEASE-READINESS.md +8 -0
- package/docs/RUNTIME-SMOKE-MATRIX.md +9 -2
- package/lib/runtime/compiler/graph-compiler.js +32 -0
- package/lib/runtime/context/context-pack-builder.d.ts +15 -0
- package/lib/runtime/context/context-pack-builder.js +78 -0
- package/lib/runtime/events/catalog.d.ts +1 -1
- package/lib/runtime/events/catalog.js +5 -0
- package/lib/runtime/executor/action-tool-map.d.ts +3 -0
- package/lib/runtime/executor/action-tool-map.js +41 -0
- package/lib/runtime/executor/built-in-tools.d.ts +8 -0
- package/lib/runtime/executor/built-in-tools.js +267 -0
- package/lib/runtime/executor/index.d.ts +6 -0
- package/lib/runtime/executor/index.js +12 -0
- package/lib/runtime/executor/llm-task-executor.d.ts +29 -0
- package/lib/runtime/executor/llm-task-executor.js +138 -0
- package/lib/runtime/executor/node-prompt-builder.d.ts +3 -0
- package/lib/runtime/executor/node-prompt-builder.js +36 -0
- package/lib/runtime/executor/stream-completion.d.ts +38 -0
- package/lib/runtime/executor/stream-completion.js +105 -0
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/index.js +2 -0
- package/lib/runtime/models/failure.d.ts +5 -0
- package/lib/runtime/models/failure.js +2 -0
- package/lib/runtime/plugins/capability-adapter.d.ts +9 -0
- package/lib/runtime/plugins/capability-adapter.js +111 -8
- package/lib/runtime/plugins/plugin-abi.d.ts +8 -0
- package/lib/runtime/plugins/plugin-registry.d.ts +2 -1
- package/lib/runtime/plugins/plugin-registry.js +6 -1
- package/lib/runtime/reducers/run-state-reducer.js +39 -2
- package/lib/runtime/scheduler/scheduler.d.ts +14 -2
- package/lib/runtime/scheduler/scheduler.js +131 -11
- package/lib/runtime/verification/verification-manifest.d.ts +5 -2
- package/lib/sdk/index.cjs +10 -5
- package/lib/sdk/index.d.ts +21 -10
- package/oxe/agents/oxe-assumptions-analyzer.md +136 -0
- package/oxe/agents/oxe-codebase-mapper.md +142 -0
- package/oxe/agents/oxe-debugger.md +145 -0
- package/oxe/agents/oxe-executor.md +139 -0
- package/oxe/agents/oxe-integration-checker.md +142 -0
- package/oxe/agents/oxe-plan-checker.md +143 -0
- package/oxe/agents/oxe-planner.md +151 -0
- package/oxe/agents/oxe-research-synthesizer.md +146 -0
- package/oxe/agents/oxe-researcher.md +163 -0
- package/oxe/agents/oxe-ui-auditor.md +151 -0
- package/oxe/agents/oxe-ui-checker.md +157 -0
- package/oxe/agents/oxe-ui-researcher.md +179 -0
- package/oxe/agents/oxe-validation-auditor.md +154 -0
- package/oxe/agents/oxe-verifier.md +132 -0
- package/oxe/personas/README.md +91 -39
- package/oxe/personas/architect.md +149 -37
- package/oxe/personas/db-specialist.md +149 -36
- package/oxe/personas/debugger.md +155 -38
- package/oxe/personas/executor.md +164 -38
- package/oxe/personas/planner.md +165 -36
- package/oxe/personas/researcher.md +148 -35
- package/oxe/personas/ui-specialist.md +164 -36
- package/oxe/personas/verifier.md +174 -39
- package/oxe/templates/CONFIG.md +3 -3
- package/oxe/templates/EXECUTION-RUNTIME.template.md +1 -1
- package/oxe/templates/FIXTURE-PACK.template.json +29 -22
- package/oxe/templates/FIXTURE-PACK.template.md +20 -11
- package/oxe/templates/IMPLEMENTATION-PACK.template.json +55 -39
- package/oxe/templates/IMPLEMENTATION-PACK.template.md +28 -16
- package/oxe/templates/INVESTIGATION.template.md +38 -38
- package/oxe/templates/PLAN.template.md +63 -32
- package/oxe/templates/REFERENCE-ANCHORS.template.md +18 -14
- package/oxe/templates/RESEARCH.template.md +11 -11
- package/oxe/templates/SPEC.template.md +6 -6
- package/oxe/templates/SUMMARY.template.md +33 -3
- package/oxe/templates/config.template.json +1 -1
- package/oxe/workflows/debug.md +9 -7
- package/oxe/workflows/execute.md +31 -28
- package/oxe/workflows/forensics.md +5 -3
- package/oxe/workflows/milestone.md +12 -12
- package/oxe/workflows/next.md +1 -1
- package/oxe/workflows/plan.md +409 -132
- package/oxe/workflows/references/adaptive-discovery.md +27 -27
- package/oxe/workflows/references/flow-robustness-contract.md +80 -80
- package/oxe/workflows/references/session-path-resolution.md +71 -71
- package/oxe/workflows/references/workflow-runtime-contracts.json +127 -127
- package/oxe/workflows/scan.md +355 -69
- package/oxe/workflows/spec.md +302 -9
- package/oxe/workflows/ui-review.md +5 -4
- package/oxe/workflows/ui-spec.md +4 -3
- package/oxe/workflows/verify.md +12 -9
- package/oxe/workflows/workstream.md +16 -16
- package/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/compiler/graph-compiler.ts +40 -0
- package/packages/runtime/src/context/context-pack-builder.ts +80 -0
- package/packages/runtime/src/events/catalog.ts +5 -0
- package/packages/runtime/src/executor/action-tool-map.ts +46 -0
- package/packages/runtime/src/executor/built-in-tools.ts +276 -0
- package/packages/runtime/src/executor/index.ts +6 -0
- package/packages/runtime/src/executor/llm-task-executor.ts +194 -0
- package/packages/runtime/src/executor/node-prompt-builder.ts +45 -0
- package/packages/runtime/src/executor/stream-completion.ts +145 -0
- package/packages/runtime/src/index.ts +3 -0
- package/packages/runtime/src/models/failure.ts +11 -0
- package/packages/runtime/src/plugins/capability-adapter.ts +117 -10
- package/packages/runtime/src/plugins/plugin-abi.ts +9 -0
- package/packages/runtime/src/plugins/plugin-registry.ts +10 -1
- package/packages/runtime/src/reducers/run-state-reducer.ts +59 -2
- package/packages/runtime/src/scheduler/scheduler.ts +152 -14
- package/packages/runtime/src/verification/verification-manifest.ts +12 -8
- package/vscode-extension/oxe-agents-1.6.0.vsix +0 -0
- package/vscode-extension/oxe-agents-1.7.0.vsix +0 -0
- 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
|
+
}
|
|
@@ -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 {
|
|
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 =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
151
|
-
output
|
|
211
|
+
success: result.exitCode === 0,
|
|
212
|
+
output,
|
|
152
213
|
evidence_paths: resolveEvidencePaths(projectRoot, manifest),
|
|
153
214
|
side_effects_applied: manifest.sideEffects,
|
|
154
|
-
error: result.
|
|
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
|
|
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)
|
|
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)
|
|
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 };
|