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,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LlmTaskExecutor = void 0;
4
+ const stream_completion_1 = require("./stream-completion");
5
+ const built_in_tools_1 = require("./built-in-tools");
6
+ const action_tool_map_1 = require("./action-tool-map");
7
+ const node_prompt_builder_1 = require("./node-prompt-builder");
8
+ const DEFAULT_SYSTEM_PROMPT = 'You are a precise software engineering agent. Use the tools provided to complete the task. ' +
9
+ 'When the task is done, summarize what was accomplished in your final message without calling any tools.';
10
+ class LlmTaskExecutor {
11
+ constructor(provider, registry, onProgress) {
12
+ this.provider = provider;
13
+ this.registry = registry;
14
+ this.onProgress = onProgress;
15
+ }
16
+ async execute(node, lease, runId, attempt) {
17
+ const prompt = (0, node_prompt_builder_1.buildNodePrompt)(node, lease, runId, attempt);
18
+ const tools = (0, action_tool_map_1.selectToolsForActions)(node.actions);
19
+ const cwd = lease.root_path;
20
+ const maxTurns = this.provider.maxTurns ?? 10;
21
+ const systemPrompt = this.provider.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
22
+ const messages = [
23
+ { role: 'system', content: systemPrompt },
24
+ { role: 'user', content: prompt },
25
+ ];
26
+ let finalOutput = '';
27
+ const evidencePaths = [];
28
+ for (let turn = 0; turn < maxTurns; turn++) {
29
+ this.emit({ type: 'turn_start', nodeId: node.id, attempt, detail: { turn } });
30
+ let response;
31
+ try {
32
+ response = await (0, stream_completion_1.streamCompletion)({
33
+ baseUrl: this.provider.baseUrl,
34
+ apiKey: this.provider.apiKey,
35
+ model: this.provider.model,
36
+ messages,
37
+ tools: tools.length > 0 ? tools : undefined,
38
+ maxTokens: this.provider.maxTokens,
39
+ timeoutMs: this.provider.timeoutMs,
40
+ });
41
+ }
42
+ catch (err) {
43
+ return { success: false, failure_class: 'env', evidence: evidencePaths, output: String(err) };
44
+ }
45
+ this.emit({ type: 'turn_complete', nodeId: node.id, attempt, detail: { turn, finish_reason: response.finish_reason } });
46
+ if (response.content)
47
+ finalOutput = response.content;
48
+ if (!response.tool_calls.length)
49
+ break;
50
+ messages.push({
51
+ role: 'assistant',
52
+ content: response.content || null,
53
+ tool_calls: response.tool_calls,
54
+ });
55
+ // Partition: idempotent tools run concurrently, mutations run serially
56
+ const [concurrent, serial] = partitionToolCalls(response.tool_calls, tools);
57
+ const concurrentResults = await Promise.all(concurrent.map((tc) => this.invokeToolCall(tc, cwd, node, evidencePaths)));
58
+ const serialResults = [];
59
+ for (const tc of serial) {
60
+ serialResults.push(await this.invokeToolCall(tc, cwd, node, evidencePaths));
61
+ }
62
+ messages.push(...concurrentResults, ...serialResults);
63
+ }
64
+ return {
65
+ success: true,
66
+ failure_class: null,
67
+ evidence: evidencePaths,
68
+ output: finalOutput,
69
+ };
70
+ }
71
+ async invokeToolCall(tc, cwd, node, evidencePaths) {
72
+ this.emit({ type: 'tool_call', nodeId: node.id, attempt: 0, detail: { tool: tc.function.name } });
73
+ let args = {};
74
+ try {
75
+ args = JSON.parse(tc.function.arguments || '{}');
76
+ }
77
+ catch {
78
+ // malformed args — pass empty
79
+ }
80
+ let result;
81
+ const builtIn = built_in_tools_1.BUILT_IN_TOOLS[tc.function.name];
82
+ if (builtIn) {
83
+ try {
84
+ result = await builtIn.execute(args, cwd);
85
+ if (tc.function.name === 'write_file' || tc.function.name === 'patch_file') {
86
+ if (typeof args.path === 'string')
87
+ evidencePaths.push(args.path);
88
+ }
89
+ }
90
+ catch (err) {
91
+ result = `[tool error] ${err}`;
92
+ }
93
+ }
94
+ else {
95
+ // Delegate to plugin registry
96
+ const provider = this.registry?.toolProviderFor(tc.function.name);
97
+ if (provider) {
98
+ try {
99
+ const res = await provider.invoke({
100
+ action_type: tc.function.name,
101
+ work_item_id: node.id,
102
+ run_id: '',
103
+ attempt_id: '',
104
+ params: args,
105
+ workspace_root: cwd,
106
+ });
107
+ result = res.output || (res.success ? 'done' : res.error ?? 'failed');
108
+ evidencePaths.push(...res.evidence_paths);
109
+ }
110
+ catch (err) {
111
+ result = `[plugin error] ${err}`;
112
+ }
113
+ }
114
+ else {
115
+ result = `[unknown tool: ${tc.function.name}]`;
116
+ }
117
+ }
118
+ this.emit({ type: 'tool_result', nodeId: node.id, attempt: 0, detail: { tool: tc.function.name, length: result.length } });
119
+ return {
120
+ role: 'tool',
121
+ tool_call_id: tc.id,
122
+ name: tc.function.name,
123
+ content: result,
124
+ };
125
+ }
126
+ emit(event) {
127
+ this.onProgress?.(event);
128
+ }
129
+ }
130
+ exports.LlmTaskExecutor = LlmTaskExecutor;
131
+ function partitionToolCalls(toolCalls, schemas) {
132
+ const idempotentNames = new Set(schemas
133
+ .map((s) => s.function.name)
134
+ .filter((name) => built_in_tools_1.BUILT_IN_TOOLS[name]?.idempotent ?? true));
135
+ const concurrent = toolCalls.filter((tc) => idempotentNames.has(tc.function.name));
136
+ const serial = toolCalls.filter((tc) => !idempotentNames.has(tc.function.name));
137
+ return [concurrent, serial];
138
+ }
@@ -0,0 +1,3 @@
1
+ import type { GraphNode } from '../compiler/graph-compiler';
2
+ import type { WorkspaceLease } from '../models/workspace';
3
+ export declare function buildNodePrompt(node: GraphNode, lease: WorkspaceLease, runId: string, attempt: number): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildNodePrompt = buildNodePrompt;
4
+ function buildNodePrompt(node, lease, runId, attempt) {
5
+ const lines = [
6
+ `# Tarefa: ${node.title}`,
7
+ '',
8
+ `**Run:** ${runId} | **Attempt:** ${attempt}`,
9
+ `**Workspace:** ${lease.root_path}`,
10
+ ];
11
+ if (node.mutation_scope.length > 0) {
12
+ lines.push(`**Escopo de mutação:** ${node.mutation_scope.join(', ')}`);
13
+ }
14
+ if (node.actions.length > 0) {
15
+ lines.push('', '## Ações requeridas');
16
+ for (const action of node.actions) {
17
+ let line = `- ${action.type}`;
18
+ if (action.command)
19
+ line += `: \`${action.command}\``;
20
+ if (action.targets?.length)
21
+ line += ` em ${action.targets.join(', ')}`;
22
+ lines.push(line);
23
+ }
24
+ }
25
+ if (node.verify.must_pass.length > 0) {
26
+ lines.push('', '## Critérios de aceite');
27
+ for (const criterion of node.verify.must_pass) {
28
+ lines.push(`- ${criterion}`);
29
+ }
30
+ }
31
+ if (node.verify.command) {
32
+ lines.push('', `**Verificação:** \`${node.verify.command}\``);
33
+ }
34
+ lines.push('', 'Execute as ações acima usando as ferramentas disponíveis e confirme o resultado.');
35
+ return lines.join('\n');
36
+ }
@@ -0,0 +1,38 @@
1
+ export interface ChatMessage {
2
+ role: 'system' | 'user' | 'assistant' | 'tool';
3
+ content: string | null;
4
+ tool_calls?: ToolCall[];
5
+ tool_call_id?: string;
6
+ name?: string;
7
+ }
8
+ export interface ToolCall {
9
+ id: string;
10
+ type: 'function';
11
+ function: {
12
+ name: string;
13
+ arguments: string;
14
+ };
15
+ }
16
+ export interface CompletionResponse {
17
+ content: string;
18
+ tool_calls: ToolCall[];
19
+ finish_reason: string | null;
20
+ }
21
+ export interface StreamCompletionOptions {
22
+ baseUrl: string;
23
+ apiKey: string;
24
+ model: string;
25
+ messages: ChatMessage[];
26
+ tools?: ToolSchema[];
27
+ maxTokens?: number;
28
+ timeoutMs?: number;
29
+ }
30
+ export interface ToolSchema {
31
+ type: 'function';
32
+ function: {
33
+ name: string;
34
+ description: string;
35
+ parameters: Record<string, unknown>;
36
+ };
37
+ }
38
+ export declare function streamCompletion(opts: StreamCompletionOptions): Promise<CompletionResponse>;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.streamCompletion = streamCompletion;
7
+ const https_1 = __importDefault(require("https"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const url_1 = require("url");
10
+ async function streamCompletion(opts) {
11
+ const url = new url_1.URL('/chat/completions', opts.baseUrl.replace(/\/$/, '') + '/');
12
+ const body = JSON.stringify({
13
+ model: opts.model,
14
+ messages: opts.messages,
15
+ max_tokens: opts.maxTokens ?? 4096,
16
+ stream: true,
17
+ ...(opts.tools?.length ? { tools: opts.tools, tool_choice: 'auto' } : {}),
18
+ });
19
+ const rawText = await fetchWithTimeout(url, opts.apiKey, body, opts.timeoutMs ?? 120000);
20
+ return parseSseResponse(rawText);
21
+ }
22
+ function fetchWithTimeout(url, apiKey, body, timeoutMs) {
23
+ return new Promise((resolve, reject) => {
24
+ const isHttps = url.protocol === 'https:';
25
+ const transport = isHttps ? https_1.default : http_1.default;
26
+ const chunks = [];
27
+ const req = transport.request({
28
+ hostname: url.hostname,
29
+ port: url.port || (isHttps ? 443 : 80),
30
+ path: url.pathname + url.search,
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${apiKey}`,
35
+ 'Content-Length': Buffer.byteLength(body),
36
+ },
37
+ }, (res) => {
38
+ res.on('data', (chunk) => chunks.push(chunk));
39
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
40
+ res.on('error', reject);
41
+ });
42
+ const timer = setTimeout(() => {
43
+ req.destroy(new Error(`LLM request timed out after ${timeoutMs}ms`));
44
+ }, timeoutMs);
45
+ req.on('close', () => clearTimeout(timer));
46
+ req.on('error', (err) => { clearTimeout(timer); reject(err); });
47
+ req.write(body);
48
+ req.end();
49
+ });
50
+ }
51
+ function parseSseResponse(raw) {
52
+ let content = '';
53
+ const toolCallsMap = new Map();
54
+ let finish_reason = null;
55
+ for (const line of raw.split('\n')) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed.startsWith('data:'))
58
+ continue;
59
+ const payload = trimmed.slice(5).trim();
60
+ if (payload === '[DONE]')
61
+ break;
62
+ let chunk;
63
+ try {
64
+ chunk = JSON.parse(payload);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ const choices = chunk.choices;
70
+ if (!choices?.length)
71
+ continue;
72
+ const delta = choices[0].delta;
73
+ if (!delta)
74
+ continue;
75
+ if (typeof delta.content === 'string')
76
+ content += delta.content;
77
+ if (choices[0].finish_reason)
78
+ finish_reason = choices[0].finish_reason;
79
+ const deltaToolCalls = delta.tool_calls;
80
+ if (deltaToolCalls) {
81
+ for (const dtc of deltaToolCalls) {
82
+ const idx = dtc.index;
83
+ if (!toolCallsMap.has(idx)) {
84
+ toolCallsMap.set(idx, { id: '', name: '', args: '' });
85
+ }
86
+ const entry = toolCallsMap.get(idx);
87
+ if (dtc.id)
88
+ entry.id += dtc.id;
89
+ const fn = dtc.function;
90
+ if (fn?.name)
91
+ entry.name += fn.name;
92
+ if (fn?.arguments)
93
+ entry.args += fn.arguments;
94
+ }
95
+ }
96
+ }
97
+ const tool_calls = [...toolCallsMap.entries()]
98
+ .sort(([a], [b]) => a - b)
99
+ .map(([, tc]) => ({
100
+ id: tc.id || `call_${Math.random().toString(36).slice(2)}`,
101
+ type: 'function',
102
+ function: { name: tc.name, arguments: tc.args },
103
+ }));
104
+ return { content, tool_calls, finish_reason };
105
+ }
@@ -17,3 +17,4 @@ export * from './context/index';
17
17
  export * from './scheduler/multi-agent-coordinator';
18
18
  export * from './decision/index';
19
19
  export * from './audit/index';
20
+ export * from './executor/index';
@@ -44,3 +44,5 @@ __exportStar(require("./scheduler/multi-agent-coordinator"), exports);
44
44
  // R4 Public ABI — Decision, Audit & Enterprise
45
45
  __exportStar(require("./decision/index"), exports);
46
46
  __exportStar(require("./audit/index"), exports);
47
+ // R5 Public ABI — LLM Task Executor
48
+ __exportStar(require("./executor/index"), exports);
@@ -0,0 +1,5 @@
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 = 'env' | 'policy' | 'test' | 'timeout' | 'evidence_missing' | null;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -6,7 +6,16 @@ interface CapabilityManifest {
6
6
  evidenceOutputs: string[];
7
7
  checkTypes: string[];
8
8
  dir: string;
9
+ timeoutMs: number;
10
+ preInvokeHook: string | null;
11
+ postInvokeHook: string | null;
9
12
  }
13
+ export declare function runCapabilityAsync(program: string, args: string[], env: NodeJS.ProcessEnv, cwd: string, timeoutMs: number, onChunk?: (chunk: string, stream: 'stdout' | 'stderr') => void): Promise<{
14
+ exitCode: number | null;
15
+ stdout: string;
16
+ stderr: string;
17
+ timedOut: boolean;
18
+ }>;
10
19
  export declare function createCapabilityPlugin(projectRoot: string, manifest: CapabilityManifest): OxePlugin;
11
20
  export declare function loadCapabilityPlugins(projectRoot: string): OxePlugin[];
12
21
  export {};
@@ -3,12 +3,47 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runCapabilityAsync = runCapabilityAsync;
6
7
  exports.createCapabilityPlugin = createCapabilityPlugin;
7
8
  exports.loadCapabilityPlugins = loadCapabilityPlugins;
8
9
  const fs_1 = __importDefault(require("fs"));
9
10
  const path_1 = __importDefault(require("path"));
10
11
  const child_process_1 = require("child_process");
11
12
  const plugin_manifest_1 = require("./plugin-manifest");
13
+ const DEFAULT_CAPABILITY_TIMEOUT_MS = 60000;
14
+ async function runCapabilityAsync(program, args, env, cwd, timeoutMs, onChunk) {
15
+ return new Promise((resolve) => {
16
+ const proc = (0, child_process_1.spawn)(program, args, { cwd, env, stdio: 'pipe' });
17
+ const stdoutChunks = [];
18
+ const stderrChunks = [];
19
+ let timedOut = false;
20
+ const timer = setTimeout(() => {
21
+ timedOut = true;
22
+ proc.kill('SIGTERM');
23
+ }, timeoutMs);
24
+ proc.stdout.on('data', (chunk) => {
25
+ stdoutChunks.push(chunk);
26
+ onChunk?.(chunk.toString(), 'stdout');
27
+ });
28
+ proc.stderr.on('data', (chunk) => {
29
+ stderrChunks.push(chunk);
30
+ onChunk?.(chunk.toString(), 'stderr');
31
+ });
32
+ proc.on('close', (exitCode) => {
33
+ clearTimeout(timer);
34
+ resolve({
35
+ exitCode,
36
+ stdout: Buffer.concat(stdoutChunks).toString('utf8'),
37
+ stderr: Buffer.concat(stderrChunks).toString('utf8'),
38
+ timedOut,
39
+ });
40
+ });
41
+ proc.on('error', (err) => {
42
+ clearTimeout(timer);
43
+ resolve({ exitCode: null, stdout: '', stderr: String(err), timedOut: false });
44
+ });
45
+ });
46
+ }
12
47
  function parseFrontmatter(text) {
13
48
  const match = String(text || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
14
49
  if (!match)
@@ -55,6 +90,7 @@ function loadCapabilityManifests(projectRoot) {
55
90
  const id = String(fm.id || '').trim();
56
91
  if (!id)
57
92
  return null;
93
+ const rawTimeout = parseInt(String(fm.timeout_ms || ''), 10);
58
94
  return {
59
95
  id,
60
96
  entrypoint: String(fm.entrypoint || '').trim() || null,
@@ -62,6 +98,9 @@ function loadCapabilityManifests(projectRoot) {
62
98
  evidenceOutputs: parseArrayField(fm.evidence_outputs),
63
99
  checkTypes: parseArrayField(fm.check_types || fm.supports_checks),
64
100
  dir,
101
+ timeoutMs: isNaN(rawTimeout) ? DEFAULT_CAPABILITY_TIMEOUT_MS : rawTimeout,
102
+ preInvokeHook: String(fm.pre_invoke_hook || '').trim() || null,
103
+ postInvokeHook: String(fm.post_invoke_hook || '').trim() || null,
65
104
  };
66
105
  })
67
106
  .filter((item) => Boolean(item));
@@ -141,18 +180,82 @@ function buildToolProvider(projectRoot, manifest) {
141
180
  program = 'powershell';
142
181
  args = ['-File', entrypoint];
143
182
  }
144
- const result = (0, child_process_1.spawnSync)(program, args, {
145
- cwd: projectRoot,
146
- encoding: 'utf8',
147
- env,
148
- });
183
+ const result = await runCapabilityAsync(program, args, env, projectRoot, manifest.timeoutMs);
184
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
185
+ if (result.timedOut) {
186
+ return {
187
+ success: false,
188
+ output,
189
+ evidence_paths: resolveEvidencePaths(projectRoot, manifest),
190
+ side_effects_applied: manifest.sideEffects,
191
+ error: `Capability ${manifest.id} timed out after ${manifest.timeoutMs}ms`,
192
+ };
193
+ }
149
194
  return {
150
- success: result.status === 0 && !result.error,
151
- output: [result.stdout || '', result.stderr || ''].filter(Boolean).join('\n').trim(),
195
+ success: result.exitCode === 0,
196
+ output,
152
197
  evidence_paths: resolveEvidencePaths(projectRoot, manifest),
153
198
  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}`),
199
+ error: result.exitCode === 0 ? undefined : (result.stderr || result.stdout || `Capability exited with status ${result.exitCode}`),
200
+ };
201
+ },
202
+ async preInvoke(input) {
203
+ if (!manifest.preInvokeHook)
204
+ return { allowed: true };
205
+ const hookPath = path_1.default.isAbsolute(manifest.preInvokeHook)
206
+ ? manifest.preInvokeHook
207
+ : path_1.default.join(manifest.dir, manifest.preInvokeHook);
208
+ const env = {
209
+ ...process.env,
210
+ OXE_CAPABILITY_INPUT: JSON.stringify(input.params || {}),
211
+ OXE_CAPABILITY_RUN_ID: input.run_id,
212
+ OXE_CAPABILITY_WORK_ITEM_ID: input.work_item_id,
213
+ OXE_CAPABILITY_ATTEMPT_ID: input.attempt_id,
214
+ OXE_CAPABILITY_WORKSPACE_ROOT: input.workspace_root,
155
215
  };
216
+ const ext = path_1.default.extname(hookPath).toLowerCase();
217
+ let program = hookPath;
218
+ let args = [];
219
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
220
+ program = process.execPath;
221
+ args = [hookPath];
222
+ }
223
+ else if (ext === '.ps1') {
224
+ program = 'powershell';
225
+ args = ['-File', hookPath];
226
+ }
227
+ const result = await runCapabilityAsync(program, args, env, projectRoot, 10000);
228
+ return result.exitCode === 0
229
+ ? { allowed: true }
230
+ : { allowed: false, reason: result.stderr || result.stdout || `pre_invoke_hook exited with status ${result.exitCode}` };
231
+ },
232
+ async postInvoke(input, _result) {
233
+ if (!manifest.postInvokeHook)
234
+ return;
235
+ const hookPath = path_1.default.isAbsolute(manifest.postInvokeHook)
236
+ ? manifest.postInvokeHook
237
+ : path_1.default.join(manifest.dir, manifest.postInvokeHook);
238
+ const env = {
239
+ ...process.env,
240
+ OXE_CAPABILITY_INPUT: JSON.stringify(input.params || {}),
241
+ OXE_CAPABILITY_RUN_ID: input.run_id,
242
+ OXE_CAPABILITY_WORK_ITEM_ID: input.work_item_id,
243
+ OXE_CAPABILITY_ATTEMPT_ID: input.attempt_id,
244
+ OXE_CAPABILITY_WORKSPACE_ROOT: input.workspace_root,
245
+ OXE_INVOKE_SUCCESS: _result.success ? '1' : '0',
246
+ };
247
+ const ext = path_1.default.extname(hookPath).toLowerCase();
248
+ let program = hookPath;
249
+ let args = [];
250
+ if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
251
+ program = process.execPath;
252
+ args = [hookPath];
253
+ }
254
+ else if (ext === '.ps1') {
255
+ program = 'powershell';
256
+ args = ['-File', hookPath];
257
+ }
258
+ await runCapabilityAsync(program, args, env, projectRoot, 10000).catch(() => { });
156
259
  },
157
260
  };
158
261
  }
@@ -15,12 +15,20 @@ export interface ToolInvocationResult {
15
15
  side_effects_applied: string[];
16
16
  error?: string;
17
17
  }
18
+ export interface PreInvokeResult {
19
+ allowed: boolean;
20
+ reason?: string;
21
+ }
18
22
  export interface ToolProvider {
19
23
  readonly name: string;
20
24
  readonly kind: 'read' | 'mutation' | 'verification' | 'analysis' | 'external_operation';
21
25
  readonly idempotent: boolean;
22
26
  supports(actionType: string): boolean;
23
27
  invoke(input: ToolInvocationInput): Promise<ToolInvocationResult>;
28
+ /** Optional: called before invoke. Return allowed:false to block execution. */
29
+ preInvoke?(input: ToolInvocationInput): Promise<PreInvokeResult>;
30
+ /** Optional: called after invoke. Errors are swallowed — does not affect outcome. */
31
+ postInvoke?(input: ToolInvocationInput, result: ToolInvocationResult): Promise<void>;
24
32
  }
25
33
  export interface WorkspaceProvider extends WorkspaceManager {
26
34
  readonly name: string;
@@ -6,7 +6,8 @@ export declare class PluginRegistry {
6
6
  register(plugin: OxePlugin): void;
7
7
  unregister(name: string): void;
8
8
  loadFromDirectory(dir: string): string[];
9
- toolProviderFor(actionType: string): ToolProvider | null;
9
+ toolProviderFor(actionType: string, required: true): ToolProvider;
10
+ toolProviderFor(actionType: string, required?: false): ToolProvider | null;
10
11
  workspaceProviderFor(strategy: string): WorkspaceProvider | null;
11
12
  verifierProviderFor(checkType: string): VerifierProvider | null;
12
13
  contextProviderFor(name: string): ContextProvider | null;
@@ -54,12 +54,17 @@ class PluginRegistry {
54
54
  }
55
55
  return loaded;
56
56
  }
57
- toolProviderFor(actionType) {
57
+ toolProviderFor(actionType, required = false) {
58
58
  for (const plugin of this.plugins) {
59
59
  const provider = plugin.toolProviders?.find((p) => p.supports(actionType));
60
60
  if (provider)
61
61
  return provider;
62
62
  }
63
+ if (required) {
64
+ const loaded = this.plugins.map(p => p.name).join(', ') || '(none)';
65
+ throw new Error(`[plugin-registry] No provider supports action type "${actionType}". ` +
66
+ `Loaded plugins: [${loaded}]. Load errors: ${this.loadErrors.length}.`);
67
+ }
63
68
  return null;
64
69
  }
65
70
  workspaceProviderFor(strategy) {
@@ -31,6 +31,38 @@ function createEmptyRunState() {
31
31
  function reduce(events) {
32
32
  return events.reduce(applyEvent, createEmptyRunState());
33
33
  }
34
+ const VALID_WORK_ITEM_TRANSITIONS = {
35
+ pending: ['ready'],
36
+ ready: ['running', 'completed', 'failed', 'blocked'],
37
+ running: ['completed', 'failed', 'blocked'],
38
+ failed: ['ready'], // retry path
39
+ completed: [], // terminal
40
+ blocked: [], // terminal
41
+ skipped: [], // terminal
42
+ };
43
+ const VALID_RUN_TRANSITIONS = {
44
+ planned: ['running'],
45
+ running: ['paused', 'failed', 'completed', 'aborted', 'cancelled', 'waiting_approval'],
46
+ paused: ['running', 'cancelled'],
47
+ waiting_approval: ['running', 'cancelled'],
48
+ failed: ['replaying'],
49
+ replaying: ['running', 'failed', 'completed'],
50
+ completed: [],
51
+ aborted: [],
52
+ cancelled: [],
53
+ };
54
+ function assertWorkItemTransition(itemId, from, to, eventType) {
55
+ const allowed = VALID_WORK_ITEM_TRANSITIONS[from] ?? [];
56
+ if (!allowed.includes(to)) {
57
+ throw new Error(`[state-machine] Invalid work item transition for "${itemId}": ${from} → ${to} (event: ${eventType})`);
58
+ }
59
+ }
60
+ function assertRunTransition(from, to, eventType) {
61
+ const allowed = VALID_RUN_TRANSITIONS[from] ?? [];
62
+ if (!allowed.includes(to)) {
63
+ throw new Error(`[state-machine] Invalid run transition: ${from} → ${to} (event: ${eventType})`);
64
+ }
65
+ }
34
66
  function applyEvent(state, event) {
35
67
  switch (event.type) {
36
68
  case 'RunStarted': {
@@ -41,6 +73,7 @@ function applyEvent(state, event) {
41
73
  if (!state.run)
42
74
  return state;
43
75
  const status = event.payload.status ?? 'completed';
76
+ assertRunTransition(state.run.status, status, event.type);
44
77
  return {
45
78
  ...state,
46
79
  run: { ...state.run, status, ended_at: event.timestamp },
@@ -92,8 +125,10 @@ function applyEvent(state, event) {
92
125
  return state;
93
126
  const workItems = new Map(state.workItems);
94
127
  const item = workItems.get(event.work_item_id);
95
- if (item)
128
+ if (item) {
129
+ assertWorkItemTransition(event.work_item_id, item.status, 'completed', event.type);
96
130
  workItems.set(event.work_item_id, { ...item, status: 'completed' });
131
+ }
97
132
  const completedWorkItems = new Set(state.completedWorkItems);
98
133
  completedWorkItems.add(event.work_item_id);
99
134
  // Collect evidence refs from payload
@@ -111,8 +146,10 @@ function applyEvent(state, event) {
111
146
  return state;
112
147
  const workItems = new Map(state.workItems);
113
148
  const item = workItems.get(event.work_item_id);
114
- if (item)
149
+ if (item) {
150
+ assertWorkItemTransition(event.work_item_id, item.status, 'blocked', event.type);
115
151
  workItems.set(event.work_item_id, { ...item, status: 'blocked' });
152
+ }
116
153
  const blockedWorkItems = new Set(state.blockedWorkItems);
117
154
  blockedWorkItems.add(event.work_item_id);
118
155
  return { ...state, workItems, blockedWorkItems };