agentic-orchestrator 0.1.13 → 0.1.15

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 (108) hide show
  1. package/AGENTS.md +139 -0
  2. package/CLAUDE.md +12 -0
  3. package/agentic/orchestrator/agents.yaml +3 -0
  4. package/agentic/orchestrator/defaults/policy.defaults.yaml +3 -0
  5. package/agentic/orchestrator/policy.yaml +3 -0
  6. package/agentic/orchestrator/schemas/agents.schema.json +15 -0
  7. package/agentic/orchestrator/schemas/policy.schema.json +14 -0
  8. package/apps/control-plane/src/cli/cli-argument-parser.ts +7 -0
  9. package/apps/control-plane/src/cli/help-command-handler.ts +8 -0
  10. package/apps/control-plane/src/cli/init-command-handler.ts +6 -0
  11. package/apps/control-plane/src/cli/resume-command-handler.ts +31 -2
  12. package/apps/control-plane/src/cli/run-command-handler.ts +31 -3
  13. package/apps/control-plane/src/cli/types.ts +1 -0
  14. package/apps/control-plane/src/core/error-codes.ts +4 -0
  15. package/apps/control-plane/src/core/kernel.ts +3 -0
  16. package/apps/control-plane/src/index.ts +14 -0
  17. package/apps/control-plane/src/interfaces/cli/bootstrap.ts +25 -3
  18. package/apps/control-plane/src/providers/api-worker-provider.ts +115 -0
  19. package/apps/control-plane/src/providers/cli-worker-provider.ts +385 -0
  20. package/apps/control-plane/src/providers/output-parsers/generic-output-parser.ts +100 -0
  21. package/apps/control-plane/src/providers/output-parsers/index.ts +11 -0
  22. package/apps/control-plane/src/providers/output-parsers/types.ts +23 -0
  23. package/apps/control-plane/src/providers/providers.ts +19 -0
  24. package/apps/control-plane/src/providers/worker-provider-factory.ts +198 -0
  25. package/apps/control-plane/src/supervisor/build-wave-executor.ts +140 -3
  26. package/apps/control-plane/src/supervisor/planning-wave-executor.ts +125 -5
  27. package/apps/control-plane/src/supervisor/qa-wave-executor.ts +144 -2
  28. package/apps/control-plane/src/supervisor/runtime.ts +24 -0
  29. package/apps/control-plane/src/supervisor/worker-decision-loop.ts +134 -12
  30. package/apps/control-plane/test/cli.unit.spec.ts +36 -0
  31. package/apps/control-plane/test/dashboard-api.integration.spec.ts +2 -2
  32. package/apps/control-plane/test/resume-command.spec.ts +31 -1
  33. package/apps/control-plane/test/worker-decision-loop.spec.ts +3 -0
  34. package/apps/control-plane/test/worker-execution-policy.spec.ts +284 -0
  35. package/apps/control-plane/test/worker-provider-adapters.spec.ts +440 -0
  36. package/apps/control-plane/test/worker-provider-factory.spec.ts +151 -0
  37. package/config/agentic/orchestrator/agents.yaml +3 -0
  38. package/dist/apps/control-plane/cli/cli-argument-parser.js +7 -0
  39. package/dist/apps/control-plane/cli/cli-argument-parser.js.map +1 -1
  40. package/dist/apps/control-plane/cli/help-command-handler.js +8 -0
  41. package/dist/apps/control-plane/cli/help-command-handler.js.map +1 -1
  42. package/dist/apps/control-plane/cli/init-command-handler.js +6 -0
  43. package/dist/apps/control-plane/cli/init-command-handler.js.map +1 -1
  44. package/dist/apps/control-plane/cli/resume-command-handler.d.ts +3 -0
  45. package/dist/apps/control-plane/cli/resume-command-handler.js +18 -2
  46. package/dist/apps/control-plane/cli/resume-command-handler.js.map +1 -1
  47. package/dist/apps/control-plane/cli/run-command-handler.d.ts +3 -1
  48. package/dist/apps/control-plane/cli/run-command-handler.js +17 -3
  49. package/dist/apps/control-plane/cli/run-command-handler.js.map +1 -1
  50. package/dist/apps/control-plane/cli/types.d.ts +1 -0
  51. package/dist/apps/control-plane/core/error-codes.d.ts +4 -0
  52. package/dist/apps/control-plane/core/error-codes.js +4 -0
  53. package/dist/apps/control-plane/core/error-codes.js.map +1 -1
  54. package/dist/apps/control-plane/core/kernel.d.ts +3 -0
  55. package/dist/apps/control-plane/core/kernel.js.map +1 -1
  56. package/dist/apps/control-plane/index.d.ts +2 -0
  57. package/dist/apps/control-plane/index.js +1 -0
  58. package/dist/apps/control-plane/index.js.map +1 -1
  59. package/dist/apps/control-plane/interfaces/cli/bootstrap.js +14 -2
  60. package/dist/apps/control-plane/interfaces/cli/bootstrap.js.map +1 -1
  61. package/dist/apps/control-plane/providers/api-worker-provider.d.ts +31 -0
  62. package/dist/apps/control-plane/providers/api-worker-provider.js +73 -0
  63. package/dist/apps/control-plane/providers/api-worker-provider.js.map +1 -0
  64. package/dist/apps/control-plane/providers/cli-worker-provider.d.ts +46 -0
  65. package/dist/apps/control-plane/providers/cli-worker-provider.js +274 -0
  66. package/dist/apps/control-plane/providers/cli-worker-provider.js.map +1 -0
  67. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.d.ts +10 -0
  68. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js +79 -0
  69. package/dist/apps/control-plane/providers/output-parsers/generic-output-parser.js.map +1 -0
  70. package/dist/apps/control-plane/providers/output-parsers/index.d.ts +2 -0
  71. package/dist/apps/control-plane/providers/output-parsers/index.js +2 -0
  72. package/dist/apps/control-plane/providers/output-parsers/index.js.map +1 -0
  73. package/dist/apps/control-plane/providers/output-parsers/types.d.ts +21 -0
  74. package/dist/apps/control-plane/providers/output-parsers/types.js +2 -0
  75. package/dist/apps/control-plane/providers/output-parsers/types.js.map +1 -0
  76. package/dist/apps/control-plane/providers/providers.d.ts +4 -0
  77. package/dist/apps/control-plane/providers/providers.js +15 -0
  78. package/dist/apps/control-plane/providers/providers.js.map +1 -1
  79. package/dist/apps/control-plane/providers/worker-provider-factory.d.ts +41 -0
  80. package/dist/apps/control-plane/providers/worker-provider-factory.js +104 -0
  81. package/dist/apps/control-plane/providers/worker-provider-factory.js.map +1 -0
  82. package/dist/apps/control-plane/supervisor/build-wave-executor.d.ts +13 -0
  83. package/dist/apps/control-plane/supervisor/build-wave-executor.js +92 -3
  84. package/dist/apps/control-plane/supervisor/build-wave-executor.js.map +1 -1
  85. package/dist/apps/control-plane/supervisor/planning-wave-executor.d.ts +12 -0
  86. package/dist/apps/control-plane/supervisor/planning-wave-executor.js +83 -5
  87. package/dist/apps/control-plane/supervisor/planning-wave-executor.js.map +1 -1
  88. package/dist/apps/control-plane/supervisor/qa-wave-executor.d.ts +13 -0
  89. package/dist/apps/control-plane/supervisor/qa-wave-executor.js +91 -2
  90. package/dist/apps/control-plane/supervisor/qa-wave-executor.js.map +1 -1
  91. package/dist/apps/control-plane/supervisor/runtime.js +19 -0
  92. package/dist/apps/control-plane/supervisor/runtime.js.map +1 -1
  93. package/dist/apps/control-plane/supervisor/worker-decision-loop.d.ts +10 -0
  94. package/dist/apps/control-plane/supervisor/worker-decision-loop.js +113 -12
  95. package/dist/apps/control-plane/supervisor/worker-decision-loop.js.map +1 -1
  96. package/package.json +2 -2
  97. package/packages/web-dashboard/next-env.d.ts +2 -1
  98. package/packages/web-dashboard/src/app/api/features/[id]/checkout/route.ts +4 -3
  99. package/packages/web-dashboard/src/app/api/features/[id]/diff/route.ts +6 -2
  100. package/packages/web-dashboard/src/app/api/features/[id]/evidence/[artifact]/route.ts +6 -5
  101. package/packages/web-dashboard/src/app/api/features/[id]/review/route.ts +5 -4
  102. package/packages/web-dashboard/src/app/api/features/[id]/route.ts +7 -3
  103. package/packages/web-dashboard/src/lib/aop-client.ts +2 -2
  104. package/packages/web-dashboard/src/lib/orchestrator-tools.ts +1 -1
  105. package/packages/web-dashboard/tsconfig.json +1 -0
  106. package/spec-files/outstanding/agentic_orchestrator_human_input_interaction_protocol_spec.md +590 -0
  107. package/spec-files/outstanding/agentic_orchestrator_real_worker_provider_execution_spec.md +616 -0
  108. package/spec-files/progress.md +91 -0
@@ -0,0 +1,385 @@
1
+ import crypto from 'node:crypto';
2
+ import { ERROR_CODES } from '../core/error-codes.js';
3
+ import {
4
+ NodeProviderCommandRunner,
5
+ type AppError,
6
+ type ProviderCommandResult,
7
+ type ProviderCommandRunner,
8
+ type ProviderSelection,
9
+ type WorkerProvider,
10
+ type WorkerSession,
11
+ } from './providers.js';
12
+ import type { ProviderOutputParser } from './output-parsers/types.js';
13
+
14
+ interface ProviderCommandTemplate {
15
+ command: string;
16
+ args: string[];
17
+ }
18
+
19
+ interface CliWorkerProviderOptions {
20
+ outputParser: ProviderOutputParser;
21
+ commandRunner?: ProviderCommandRunner;
22
+ workerResponseTimeoutMs: number;
23
+ }
24
+
25
+ interface CommandTemplateValues {
26
+ sessionId: string;
27
+ prompt: string;
28
+ featureId: string;
29
+ role: string;
30
+ message?: string;
31
+ }
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
35
+ }
36
+
37
+ function readStringArray(value: unknown): string[] | null {
38
+ if (!Array.isArray(value)) {
39
+ return null;
40
+ }
41
+ const strings = value.filter((item): item is string => typeof item === 'string');
42
+ return strings.length === value.length ? strings : null;
43
+ }
44
+
45
+ function asNonEmptyString(value: unknown): string | null {
46
+ if (typeof value !== 'string') {
47
+ return null;
48
+ }
49
+ const trimmed = value.trim();
50
+ return trimmed.length > 0 ? trimmed : null;
51
+ }
52
+
53
+ function templateFromConfig(
54
+ config: Record<string, unknown> | null | undefined,
55
+ kind: 'run' | 'attach' | 'send',
56
+ fallback: ProviderCommandTemplate | undefined,
57
+ ): ProviderCommandTemplate | undefined {
58
+ const nested = isRecord(config?.[kind]) ? config?.[kind] : null;
59
+ const nestedCommand = asNonEmptyString(nested?.command);
60
+ const nestedArgs = readStringArray(nested?.args);
61
+
62
+ const explicitCommand = asNonEmptyString(config?.[`${kind}_command`]);
63
+ const explicitArgs = readStringArray(config?.[`${kind}_args`]);
64
+
65
+ const command = nestedCommand ?? explicitCommand ?? fallback?.command;
66
+ const args = nestedArgs ?? explicitArgs ?? fallback?.args;
67
+ if (!command || !args) {
68
+ return undefined;
69
+ }
70
+
71
+ return { command, args };
72
+ }
73
+
74
+ function resolveRunTemplate(selection: ProviderSelection): ProviderCommandTemplate | undefined {
75
+ const defaults: Partial<Record<string, ProviderCommandTemplate>> = {
76
+ codex: {
77
+ command: 'codex',
78
+ args: ['chat', '--session', '{session_id}', '--message', '{prompt}'],
79
+ },
80
+ claude: {
81
+ command: 'claude-code',
82
+ args: ['chat', '--session', '{session_id}', '--message', '{prompt}'],
83
+ },
84
+ 'kiro-cli': {
85
+ command: 'kiro-cli',
86
+ args: ['chat', '--session', '{session_id}', '--message', '{prompt}'],
87
+ },
88
+ copilot: {
89
+ command: 'copilot',
90
+ args: ['chat', '--session', '{session_id}', '--message', '{prompt}'],
91
+ },
92
+ };
93
+
94
+ const topLevelCommand = asNonEmptyString(selection.agent_config?.command);
95
+ const topLevelArgs = readStringArray(selection.agent_config?.args);
96
+ const topLevelTemplate =
97
+ topLevelCommand && topLevelArgs ? { command: topLevelCommand, args: topLevelArgs } : undefined;
98
+
99
+ return templateFromConfig(
100
+ selection.agent_config,
101
+ 'run',
102
+ topLevelTemplate ?? defaults[selection.provider],
103
+ );
104
+ }
105
+
106
+ function resolveAttachTemplate(selection: ProviderSelection): ProviderCommandTemplate | undefined {
107
+ const defaults: Partial<Record<string, ProviderCommandTemplate>> = {
108
+ codex: {
109
+ command: 'codex',
110
+ args: ['chat', '--session', '{session_id}'],
111
+ },
112
+ claude: {
113
+ command: 'claude-code',
114
+ args: ['chat', '--session', '{session_id}'],
115
+ },
116
+ };
117
+ return templateFromConfig(selection.agent_config, 'attach', defaults[selection.provider]);
118
+ }
119
+
120
+ function resolveSendTemplate(selection: ProviderSelection): ProviderCommandTemplate | undefined {
121
+ const defaults: Partial<Record<string, ProviderCommandTemplate>> = {
122
+ codex: {
123
+ command: 'codex',
124
+ args: ['chat', '--session', '{session_id}', '--message', '{message}'],
125
+ },
126
+ claude: {
127
+ command: 'claude-code',
128
+ args: ['chat', '--session', '{session_id}', '--message', '{message}'],
129
+ },
130
+ };
131
+ return templateFromConfig(selection.agent_config, 'send', defaults[selection.provider]);
132
+ }
133
+
134
+ function applyTemplateArgs(args: string[], values: CommandTemplateValues): string[] {
135
+ return args.map((arg) => {
136
+ if (arg === '{session_id}') {
137
+ return values.sessionId;
138
+ }
139
+ if (arg === '{prompt}') {
140
+ return values.prompt;
141
+ }
142
+ if (arg === '{feature_id}') {
143
+ return values.featureId;
144
+ }
145
+ if (arg === '{role}') {
146
+ return values.role;
147
+ }
148
+ if (arg === '{message}') {
149
+ return values.message ?? '';
150
+ }
151
+ return arg;
152
+ });
153
+ }
154
+
155
+ function providerError(code: string, message: string, details: Record<string, unknown>): AppError {
156
+ const error = new Error(message) as AppError;
157
+ error.code = code;
158
+ error.details = details;
159
+ return error;
160
+ }
161
+
162
+ function buildRunCommandEnv(selection: ProviderSelection): NodeJS.ProcessEnv {
163
+ const env: NodeJS.ProcessEnv = { ...process.env };
164
+ if (selection.provider_config_env && selection.provider_config_ref) {
165
+ env[selection.provider_config_env] = selection.provider_config_ref;
166
+ }
167
+ return env;
168
+ }
169
+
170
+ export class CliWorkerProvider implements WorkerProvider {
171
+ readonly mode = 'live' as const;
172
+ readonly selection: ProviderSelection;
173
+ sendMessage?: (sessionId: string, message: string) => Promise<void>;
174
+ attachToSession?: (sessionId: string) => Promise<void>;
175
+ getSessionInfo?: (sessionId: string) => Promise<{ active: boolean; provider: string }>;
176
+
177
+ private readonly outputParser: ProviderOutputParser;
178
+ private readonly commandRunner: ProviderCommandRunner;
179
+ private readonly workerResponseTimeoutMs: number;
180
+ private readonly runTemplate: ProviderCommandTemplate | undefined;
181
+ private readonly attachTemplate: ProviderCommandTemplate | undefined;
182
+ private readonly sendTemplate: ProviderCommandTemplate | undefined;
183
+ private readonly sessionsByRoleFeature = new Map<string, string>();
184
+
185
+ constructor(selection: ProviderSelection, options: CliWorkerProviderOptions) {
186
+ this.selection = selection;
187
+ this.outputParser = options.outputParser;
188
+ this.commandRunner = options.commandRunner ?? new NodeProviderCommandRunner();
189
+ this.workerResponseTimeoutMs = options.workerResponseTimeoutMs;
190
+ this.runTemplate = resolveRunTemplate(selection);
191
+ this.attachTemplate = resolveAttachTemplate(selection);
192
+ this.sendTemplate = resolveSendTemplate(selection);
193
+
194
+ if (this.attachTemplate) {
195
+ this.attachToSession = async (sessionId: string) => {
196
+ await this.executeControlCommand('attach', sessionId);
197
+ };
198
+ this.getSessionInfo = (sessionId: string) =>
199
+ Promise.resolve({
200
+ active: Boolean(sessionId) && sessionId !== 'unknown' && sessionId !== 'unassigned',
201
+ provider: this.selection.provider,
202
+ });
203
+ }
204
+
205
+ if (this.sendTemplate) {
206
+ this.sendMessage = async (sessionId: string, message: string) => {
207
+ await this.executeControlCommand('send', sessionId, message);
208
+ };
209
+ }
210
+ }
211
+
212
+ createSession(
213
+ role: string,
214
+ featureId: string,
215
+ systemPrompt: string | null,
216
+ ): Promise<WorkerSession> {
217
+ const sessionId = `${role}-${featureId}-${crypto.randomUUID()}`;
218
+ this.sessionsByRoleFeature.set(this.sessionKey(role, featureId), sessionId);
219
+ return Promise.resolve({
220
+ session_id: sessionId,
221
+ role,
222
+ feature_id: featureId,
223
+ system_prompt_loaded: Boolean(systemPrompt),
224
+ });
225
+ }
226
+
227
+ reattachSession(sessionId: string): Promise<WorkerSession | null> {
228
+ if (!sessionId || sessionId === 'unknown') {
229
+ return Promise.resolve(null);
230
+ }
231
+ return Promise.resolve({
232
+ session_id: sessionId,
233
+ role: 'orchestrator',
234
+ feature_id: 'global',
235
+ system_prompt_loaded: false,
236
+ });
237
+ }
238
+
239
+ closeSession(sessionId: string): Promise<{ closed: true }> {
240
+ for (const [key, activeSessionId] of this.sessionsByRoleFeature.entries()) {
241
+ if (activeSessionId === sessionId) {
242
+ this.sessionsByRoleFeature.delete(key);
243
+ }
244
+ }
245
+ return Promise.resolve({ closed: true });
246
+ }
247
+
248
+ async runWorker(input: {
249
+ role: string;
250
+ feature_id: string;
251
+ context_bundle?: Record<string, unknown>;
252
+ instructions?: string;
253
+ last_tool_results?: Array<Record<string, unknown>>;
254
+ runtime_selection?: {
255
+ provider: string;
256
+ model: string;
257
+ provider_config_ref: string | null;
258
+ };
259
+ }): Promise<Record<string, unknown>> {
260
+ if (!this.runTemplate) {
261
+ throw providerError(
262
+ ERROR_CODES.PROVIDER_RUNTIME_UNAVAILABLE,
263
+ 'Live worker provider is missing run command configuration',
264
+ {
265
+ provider: this.selection.provider,
266
+ reason: 'missing_run_command_template',
267
+ },
268
+ );
269
+ }
270
+
271
+ const sessionId =
272
+ this.sessionsByRoleFeature.get(this.sessionKey(input.role, input.feature_id)) ??
273
+ `${input.role}-${input.feature_id}-unassigned`;
274
+
275
+ const promptPayload = {
276
+ role: input.role,
277
+ feature_id: input.feature_id,
278
+ context_bundle: input.context_bundle ?? {},
279
+ instructions: input.instructions ?? '',
280
+ last_tool_results: input.last_tool_results ?? [],
281
+ runtime_selection: input.runtime_selection ?? null,
282
+ session_id: sessionId,
283
+ };
284
+ const prompt = JSON.stringify(promptPayload);
285
+
286
+ const args = applyTemplateArgs(this.runTemplate.args, {
287
+ sessionId,
288
+ prompt,
289
+ featureId: input.feature_id,
290
+ role: input.role,
291
+ });
292
+
293
+ const runResult = await this.commandRunner.run(this.runTemplate.command, args, {
294
+ stdin: prompt,
295
+ timeoutMs: this.workerResponseTimeoutMs,
296
+ env: buildRunCommandEnv(this.selection),
297
+ });
298
+
299
+ this.ensureRunSucceeded(runResult, this.runTemplate.command, args);
300
+
301
+ return this.outputParser.parse(runResult.stdout ?? '', {
302
+ sessionId,
303
+ role: input.role,
304
+ featureId: input.feature_id,
305
+ provider: this.selection.provider,
306
+ model: this.selection.model,
307
+ });
308
+ }
309
+
310
+ private sessionKey(role: string, featureId: string): string {
311
+ return `${role}::${featureId}`;
312
+ }
313
+
314
+ private ensureRunSucceeded(result: ProviderCommandResult, command: string, args: string[]): void {
315
+ if (result.exitCode === 0) {
316
+ return;
317
+ }
318
+
319
+ throw providerError(
320
+ ERROR_CODES.PROVIDER_RUNTIME_UNAVAILABLE,
321
+ 'Live worker provider command failed',
322
+ {
323
+ provider: this.selection.provider,
324
+ command,
325
+ args,
326
+ exit_code: result.exitCode,
327
+ signal: result.signal,
328
+ error_code: result.errorCode,
329
+ stderr: result.stderr,
330
+ },
331
+ );
332
+ }
333
+
334
+ private async executeControlCommand(
335
+ kind: 'attach' | 'send',
336
+ sessionId: string,
337
+ message?: string,
338
+ ): Promise<void> {
339
+ const template = kind === 'attach' ? this.attachTemplate : this.sendTemplate;
340
+ if (!template) {
341
+ throw providerError(
342
+ ERROR_CODES.UNSUPPORTED_AGENT_PROVIDER,
343
+ `Provider does not support ${kind}`,
344
+ {
345
+ provider: this.selection.provider,
346
+ kind,
347
+ },
348
+ );
349
+ }
350
+
351
+ const args = applyTemplateArgs(template.args, {
352
+ sessionId,
353
+ prompt: '',
354
+ featureId: 'global',
355
+ role: 'orchestrator',
356
+ message,
357
+ });
358
+
359
+ const result = await this.commandRunner.run(template.command, args, {
360
+ interactive: kind === 'attach',
361
+ timeoutMs: kind === 'send' ? 15_000 : undefined,
362
+ env: buildRunCommandEnv(this.selection),
363
+ });
364
+
365
+ if (result.exitCode === 0) {
366
+ return;
367
+ }
368
+
369
+ throw providerError(
370
+ result.exitCode === 127 || result.errorCode === 'ENOENT'
371
+ ? ERROR_CODES.UNSUPPORTED_AGENT_PROVIDER
372
+ : ERROR_CODES.INTERNAL_ERROR,
373
+ `Provider ${kind} command failed`,
374
+ {
375
+ provider: this.selection.provider,
376
+ kind,
377
+ command: template.command,
378
+ args,
379
+ exit_code: result.exitCode,
380
+ signal: result.signal,
381
+ error_code: result.errorCode,
382
+ },
383
+ );
384
+ }
385
+ }
@@ -0,0 +1,100 @@
1
+ import { ERROR_CODES } from '../../core/error-codes.js';
2
+ import type {
3
+ ProviderOutputParser,
4
+ ProviderOutputParserContext,
5
+ WorkerOutputEnvelope,
6
+ } from './types.js';
7
+
8
+ interface ParserError extends Error {
9
+ code?: string;
10
+ details?: Record<string, unknown>;
11
+ }
12
+
13
+ function isRecord(value: unknown): value is Record<string, unknown> {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+
17
+ function asOutputArray(value: unknown): Array<Record<string, unknown>> {
18
+ if (Array.isArray(value)) {
19
+ return value.filter(isRecord);
20
+ }
21
+ if (isRecord(value)) {
22
+ return [value];
23
+ }
24
+ return [];
25
+ }
26
+
27
+ function parserError(message: string, details: Record<string, unknown>): ParserError {
28
+ const error = new Error(message) as ParserError;
29
+ error.code = ERROR_CODES.PROVIDER_OUTPUT_INVALID;
30
+ error.details = details;
31
+ return error;
32
+ }
33
+
34
+ function parseJsonOutput(raw: string): unknown {
35
+ try {
36
+ return JSON.parse(raw);
37
+ } catch {
38
+ throw parserError('Provider output must be valid JSON', {
39
+ reason: 'invalid_json',
40
+ raw_preview: raw.slice(0, 512),
41
+ });
42
+ }
43
+ }
44
+
45
+ export class GenericCliOutputParser implements ProviderOutputParser {
46
+ parse(raw: string, context: ProviderOutputParserContext): WorkerOutputEnvelope {
47
+ const trimmed = raw.trim();
48
+ if (trimmed.length === 0) {
49
+ throw parserError('Provider output was empty', {
50
+ reason: 'empty_output',
51
+ role: context.role,
52
+ feature_id: context.featureId,
53
+ });
54
+ }
55
+
56
+ const parsed = parseJsonOutput(trimmed);
57
+ const parsedRecord = isRecord(parsed) ? parsed : null;
58
+
59
+ const outputs =
60
+ parsedRecord && Array.isArray(parsedRecord.outputs)
61
+ ? asOutputArray(parsedRecord.outputs)
62
+ : parsedRecord && typeof parsedRecord.type === 'string'
63
+ ? [parsedRecord]
64
+ : [];
65
+
66
+ if (!parsedRecord || outputs.length === 0) {
67
+ throw parserError('Provider output did not include any parseable outputs', {
68
+ reason: 'missing_outputs',
69
+ role: context.role,
70
+ feature_id: context.featureId,
71
+ });
72
+ }
73
+
74
+ return {
75
+ session_id:
76
+ typeof parsedRecord.session_id === 'string' && parsedRecord.session_id.length > 0
77
+ ? parsedRecord.session_id
78
+ : context.sessionId,
79
+ role:
80
+ typeof parsedRecord.role === 'string' && parsedRecord.role.length > 0
81
+ ? parsedRecord.role
82
+ : context.role,
83
+ feature_id:
84
+ typeof parsedRecord.feature_id === 'string' && parsedRecord.feature_id.length > 0
85
+ ? parsedRecord.feature_id
86
+ : context.featureId,
87
+ outputs,
88
+ provider_meta: {
89
+ provider: context.provider,
90
+ model: context.model,
91
+ },
92
+ };
93
+ }
94
+ }
95
+
96
+ export class CodexOutputParser extends GenericCliOutputParser {}
97
+
98
+ export class ClaudeOutputParser extends GenericCliOutputParser {}
99
+
100
+ export class GeminiOutputParser extends GenericCliOutputParser {}
@@ -0,0 +1,11 @@
1
+ export {
2
+ GenericCliOutputParser,
3
+ CodexOutputParser,
4
+ ClaudeOutputParser,
5
+ GeminiOutputParser,
6
+ } from './generic-output-parser.js';
7
+ export type {
8
+ ProviderOutputParser,
9
+ ProviderOutputParserContext,
10
+ WorkerOutputEnvelope,
11
+ } from './types.js';
@@ -0,0 +1,23 @@
1
+ export interface ProviderOutputParserContext {
2
+ sessionId: string;
3
+ role: string;
4
+ featureId: string;
5
+ provider: string;
6
+ model: string;
7
+ }
8
+
9
+ export interface WorkerOutputEnvelope {
10
+ [key: string]: unknown;
11
+ session_id: string;
12
+ role: string;
13
+ feature_id: string;
14
+ outputs: Array<Record<string, unknown>>;
15
+ provider_meta: {
16
+ provider: string;
17
+ model: string;
18
+ };
19
+ }
20
+
21
+ export interface ProviderOutputParser {
22
+ parse(raw: string, context: ProviderOutputParserContext): WorkerOutputEnvelope;
23
+ }
@@ -212,6 +212,7 @@ export interface WorkerSession {
212
212
  }
213
213
 
214
214
  export interface WorkerProvider {
215
+ mode: 'live' | 'stub';
215
216
  selection: ProviderSelection;
216
217
  createSession(
217
218
  role: string,
@@ -241,6 +242,8 @@ export interface ProviderCommandResult {
241
242
  exitCode: number;
242
243
  signal: NodeJS.Signals | null;
243
244
  errorCode?: number | string;
245
+ stdout?: string;
246
+ stderr?: string;
244
247
  }
245
248
 
246
249
  export interface ProviderCommandRunnerOptions {
@@ -281,10 +284,23 @@ export class NodeProviderCommandRunner implements ProviderCommandRunner {
281
284
  }
282
285
 
283
286
  let spawnErrorCode: number | string | undefined;
287
+ let stdout = '';
288
+ let stderr = '';
284
289
  child.on('error', (error: NodeJS.ErrnoException) => {
285
290
  spawnErrorCode = error.code;
286
291
  });
287
292
 
293
+ if (!options.interactive && child.stdout) {
294
+ child.stdout.on('data', (chunk: Buffer | string) => {
295
+ stdout += chunk.toString();
296
+ });
297
+ }
298
+ if (!options.interactive && child.stderr) {
299
+ child.stderr.on('data', (chunk: Buffer | string) => {
300
+ stderr += chunk.toString();
301
+ });
302
+ }
303
+
288
304
  if (!options.interactive && typeof options.stdin === 'string' && child.stdin) {
289
305
  child.stdin.write(options.stdin);
290
306
  }
@@ -307,6 +323,8 @@ export class NodeProviderCommandRunner implements ProviderCommandRunner {
307
323
  exitCode: code ?? (spawnErrorCode === 'ENOENT' ? 127 : 1),
308
324
  signal,
309
325
  errorCode: spawnErrorCode,
326
+ stdout,
327
+ stderr,
310
328
  });
311
329
  });
312
330
  });
@@ -422,6 +440,7 @@ function commandFailureError(
422
440
  }
423
441
 
424
442
  export class NullWorkerProvider implements WorkerProvider {
443
+ readonly mode = 'stub' as const;
425
444
  selection: ProviderSelection;
426
445
  sendMessage?: (sessionId: string, message: string) => Promise<void>;
427
446
  attachToSession?: (sessionId: string) => Promise<void>;