@the-open-engine/zeroshot 6.17.0 → 6.19.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 (95) hide show
  1. package/cli/index.js +11 -14
  2. package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +30 -0
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
  7. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.js +37 -0
  9. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  10. package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
  11. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/gemini.js +115 -4
  13. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  14. package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
  15. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/opencode.js +108 -0
  17. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/contract-options.js +12 -0
  20. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  21. package/lib/agent-cli-provider/index.d.ts +2 -2
  22. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/index.js +2 -1
  24. package/lib/agent-cli-provider/index.js.map +1 -1
  25. package/lib/agent-cli-provider/omp-release.d.ts +15 -0
  26. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -0
  27. package/lib/agent-cli-provider/omp-release.js +50 -0
  28. package/lib/agent-cli-provider/omp-release.js.map +1 -0
  29. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +38 -0
  30. package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -0
  31. package/lib/agent-cli-provider/omp-rpc-protocol.js +267 -0
  32. package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -0
  33. package/lib/agent-cli-provider/provider-registry.d.ts +35 -8
  34. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/provider-registry.js +29 -1
  36. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  37. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  38. package/lib/agent-cli-provider/single-agent-runtime.js +36 -5
  39. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  40. package/lib/agent-cli-provider/types.d.ts +30 -3
  41. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  42. package/lib/agent-cli-provider/types.js.map +1 -1
  43. package/lib/cluster/errors.cjs +4 -1
  44. package/lib/cluster/errors.d.ts +2 -0
  45. package/lib/cluster/errors.mjs +2 -0
  46. package/lib/cluster/index.cjs +8 -1
  47. package/lib/cluster/index.d.ts +4 -1
  48. package/lib/cluster/index.mjs +4 -1
  49. package/lib/cluster/json-source.cjs +61 -0
  50. package/lib/cluster/json-source.d.ts +4 -0
  51. package/lib/cluster/json-source.mjs +55 -0
  52. package/lib/cluster/payload-value.cjs +76 -0
  53. package/lib/cluster/payload-value.d.ts +7 -0
  54. package/lib/cluster/payload-value.mjs +72 -0
  55. package/lib/cluster/validators.cjs +21 -0
  56. package/lib/cluster/validators.d.ts +4 -1
  57. package/lib/cluster/validators.mjs +19 -1
  58. package/lib/provider-names.js +10 -0
  59. package/lib/settings.js +6 -3
  60. package/lib/setup-plan.js +5 -5
  61. package/lib/start-cluster.js +3 -3
  62. package/package.json +2 -2
  63. package/src/agent/agent-lifecycle.js +4 -4
  64. package/src/agent/agent-task-executor.js +201 -416
  65. package/src/agent/output-extraction.js +39 -14
  66. package/src/agent/output-reformatter.js +154 -111
  67. package/src/agent/pr-verification.js +4 -1
  68. package/src/agent-cli-provider/adapters/claude.ts +39 -2
  69. package/src/agent-cli-provider/adapters/codex.ts +56 -2
  70. package/src/agent-cli-provider/adapters/gemini.ts +102 -6
  71. package/src/agent-cli-provider/adapters/opencode.ts +85 -2
  72. package/src/agent-cli-provider/contract-options.ts +16 -0
  73. package/src/agent-cli-provider/index.ts +4 -0
  74. package/src/agent-cli-provider/omp-release.ts +63 -0
  75. package/src/agent-cli-provider/omp-rpc-protocol.ts +396 -0
  76. package/src/agent-cli-provider/provider-registry.ts +52 -4
  77. package/src/agent-cli-provider/single-agent-runtime.ts +52 -5
  78. package/src/agent-cli-provider/types.ts +36 -2
  79. package/src/agent-wrapper.js +5 -5
  80. package/src/claude-task-runner.js +5 -3
  81. package/src/cluster/errors.ts +1 -0
  82. package/src/cluster/index.ts +4 -0
  83. package/src/cluster/json-source.ts +71 -0
  84. package/src/cluster/payload-value.ts +84 -0
  85. package/src/cluster/validators.ts +31 -2
  86. package/src/config-validator.js +3 -2
  87. package/src/isolation-manager.js +6 -2
  88. package/src/orchestrator.js +36 -16
  89. package/src/preflight.js +2 -1
  90. package/task-lib/attachable-watcher.js +1 -0
  91. package/task-lib/command-spec-cleanup.js +57 -11
  92. package/task-lib/commands/run.js +1 -0
  93. package/task-lib/provider-session-capture.js +8 -0
  94. package/task-lib/runner.js +9 -6
  95. package/task-lib/watcher.js +1 -0
@@ -140,6 +140,28 @@ function extractFromTextEvents(output, providerName) {
140
140
  return null;
141
141
  }
142
142
 
143
+ /**
144
+ * Assemble assistant text through the active provider parser. Recovery prompts
145
+ * use this instead of exposing raw event envelopes when model text exists.
146
+ *
147
+ * @param {string} output
148
+ * @param {string} providerName
149
+ * @returns {string|null}
150
+ */
151
+ function extractModelTextFromOutput(output, providerName) {
152
+ if (!output || typeof output !== 'string') return null;
153
+ const normalized = output
154
+ .split('\n')
155
+ .map((line) => stripTimestamp(line))
156
+ .filter(Boolean)
157
+ .join('\n');
158
+ const text = parseProviderChunk(providerName, normalized)
159
+ .filter((event) => event.type === 'text' && typeof event.text === 'string')
160
+ .map((event) => event.text)
161
+ .join('');
162
+ return text.trim() ? text : null;
163
+ }
164
+
143
165
  /**
144
166
  * Strategy 3: Extract JSON from markdown code block
145
167
  * Handles: ```json\n{...}\n```
@@ -280,13 +302,13 @@ function extractClaudeVertexModelError(output, { useVertex = false } = {}) {
280
302
  * Provider error formats:
281
303
  * - Claude: {type:"result", is_error:true, errors:["msg"]} or {type:"result", subtype:"error"}
282
304
  * - Codex: {type:"turn.failed", error:{message:"msg"}}
283
- * - Gemini: {type:"result", success:false, error:"msg"}
305
+ * - Gemini: {type:"result", status:"error", error:{message:"msg"}} or {type:"error", severity:"error"}
284
306
  * - Opencode: {type:"session.error", error:{message:"msg"}}
285
307
  *
286
- * @param {string} output - Raw CLI output
308
+ * @param {string} providerName - Active provider whose terminal errors may be inspected
287
309
  * @returns {{error: string, provider: string}|null} Error info or null
288
310
  */
289
- function extractCliError(output) {
311
+ function extractCliError(output, providerName = 'claude') {
290
312
  if (!output || typeof output !== 'string') return null;
291
313
 
292
314
  const lines = output.split('\n');
@@ -302,33 +324,35 @@ function extractCliError(output) {
302
324
  continue;
303
325
  }
304
326
 
305
- // Claude: {type:"result", is_error:true, errors:[...]}
306
- if (obj.type === 'result' && obj.is_error === true) {
327
+ if (providerName === 'claude' && obj.type === 'result' && obj.is_error === true) {
307
328
  const errorMsg = Array.isArray(obj.errors)
308
329
  ? obj.errors.join('; ')
309
330
  : obj.error || obj.result || 'Unknown CLI error';
310
331
  return { error: errorMsg, provider: 'claude' };
311
332
  }
312
333
 
313
- // Claude: {type:"result", subtype:"error"}
314
- if (obj.type === 'result' && obj.subtype === 'error') {
334
+ if (providerName === 'claude' && obj.type === 'result' && obj.subtype === 'error') {
315
335
  const errorMsg = obj.error || obj.result || 'CLI returned error';
316
336
  return { error: errorMsg, provider: 'claude' };
317
337
  }
318
338
 
319
- // Codex: {type:"turn.failed", error:{message:"..."}}
320
- if (obj.type === 'turn.failed') {
339
+ if (providerName === 'codex' && obj.type === 'turn.failed') {
321
340
  const errorMsg = obj.error?.message || obj.error || 'Turn failed';
322
341
  return { error: errorMsg, provider: 'codex' };
323
342
  }
324
343
 
325
- // Gemini: {type:"result", success:false, error:"..."}
326
- if (obj.type === 'result' && obj.success === false && obj.error) {
327
- return { error: obj.error, provider: 'gemini' };
344
+ if (
345
+ providerName === 'gemini' &&
346
+ ((obj.type === 'result' && obj.status === 'error') ||
347
+ (obj.type === 'error' && obj.severity === 'error'))
348
+ ) {
349
+ return {
350
+ error: obj.error?.message || obj.message || 'Gemini CLI error',
351
+ provider: 'gemini',
352
+ };
328
353
  }
329
354
 
330
- // Opencode: {type:"session.error", error:{...}}
331
- if (obj.type === 'session.error') {
355
+ if (providerName === 'opencode' && (obj.type === 'session.error' || obj.type === 'error')) {
332
356
  const errorMsg =
333
357
  obj.error?.data?.message || obj.error?.message || obj.error?.name || 'Session error';
334
358
  return { error: errorMsg, provider: 'opencode' };
@@ -396,6 +420,7 @@ function extractJsonFromOutput(output, providerName = 'claude') {
396
420
 
397
421
  module.exports = {
398
422
  extractJsonFromOutput,
423
+ extractModelTextFromOutput,
399
424
  extractCliError,
400
425
  extractClaudeVertexModelError,
401
426
  extractFromResultWrapper,
@@ -1,49 +1,30 @@
1
- /**
2
- * Output Reformatter - Convert non-JSON output to valid JSON
3
- *
4
- * When an LLM outputs markdown/text instead of JSON despite schema instructions,
5
- * this module attempts to extract/reformat the content into valid JSON.
6
- *
7
- * Opencode output can be reformatted through its CLI when direct JSON extraction fails.
8
- * Other providers remain on their own extraction paths and never depend on the opencode binary.
9
- */
1
+ const Ajv = require('ajv');
2
+ const { getProvider } = require('../providers');
3
+ const { normalizeEnumValues } = require('./schema-utils');
10
4
 
11
5
  const DEFAULT_MAX_ATTEMPTS = 3;
12
-
6
+ const MAX_REFORMAT_INPUT_BYTES = 65_536;
7
+ const MAX_CONFIGURED_ATTEMPTS = 10;
13
8
 
14
9
  function createCancellationError() {
15
10
  const error = new Error('Output reformatting cancelled');
16
11
  error.code = 'REFORMAT_CANCELLED';
12
+ error.recoveryAbort = true;
17
13
  return error;
18
14
  }
19
15
 
20
-
21
- /**
22
- * Build the reformatting prompt
23
- *
24
- * @param {string} rawOutput - The non-JSON output to reformat
25
- * @param {Object} schema - Target JSON schema
26
- * @param {string|null} previousError - Error from previous attempt (for feedback)
27
- * @returns {string} The prompt for the reformatting model
28
- */
29
16
  function buildReformatPrompt(rawOutput, schema, previousError = null) {
30
- const schemaStr = JSON.stringify(schema, null, 2);
31
- // Truncate long outputs to avoid context limits
32
- const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
33
-
34
17
  let prompt = `CRITICAL: Do NOT use any tools. Do NOT read, write, or edit any files. Do NOT explore the codebase. This is a pure text-to-JSON transformation — respond with JSON only.
35
18
 
36
- Convert this text into a JSON object matching the schema.
19
+ Convert the JSON-encoded source text into a JSON object matching the schema.
37
20
 
38
21
  ## SCHEMA
39
22
  \`\`\`json
40
- ${schemaStr}
23
+ ${JSON.stringify(schema, null, 2)}
41
24
  \`\`\`
42
25
 
43
- ## TEXT TO CONVERT
44
- \`\`\`
45
- ${truncatedOutput}
46
- \`\`\`
26
+ ## JSON-ENCODED SOURCE TEXT
27
+ ${JSON.stringify(rawOutput)}
47
28
 
48
29
  ## RULES
49
30
  - Output ONLY the JSON object
@@ -55,134 +36,196 @@ ${truncatedOutput}
55
36
  if (previousError) {
56
37
  prompt += `
57
38
 
58
- ## PREVIOUS ATTEMPT FAILED
59
- Error: ${previousError}
39
+ ## PREVIOUS CANDIDATE FAILED
40
+ ${previousError}
60
41
  Fix this issue in your response.`;
61
42
  }
62
43
 
63
44
  return prompt;
64
45
  }
65
46
 
66
- /**
67
- * This fallback is intentionally scoped to opencode agents. It participates in agent
68
- * cancellation so retries cannot outlive cluster shutdown.
69
- *
70
- * @param {Object} options
71
- * @param {string} options.rawOutput - The non-JSON output to reformat
72
- * @param {Object} options.schema - Target JSON schema
73
- * @param {string} options.providerName - Active provider name
74
- * @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
75
- * @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
76
- * @param {Function} [options.isCancelled] - Returns true after agent cancellation
77
- * @param {Function} options.runReformat - Runs the prompt in the active agent execution context
78
- * @returns {Promise<Object>} The reformatted JSON object
79
- * @throws {Error} If the provider is unsupported, cancellation occurs, or attempts fail
80
- */
47
+ function formatValidationErrors(errors, limit = 5) {
48
+ return errors
49
+ .slice(0, limit)
50
+ .map((error) => `${error.instancePath || error.schemaPath || '#'} ${error.message}`)
51
+ .join('; ');
52
+ }
53
+
54
+ function createStructuredOutputValidator(schema) {
55
+ const ajv = new Ajv({
56
+ allErrors: true,
57
+ strict: false,
58
+ coerceTypes: false,
59
+ useDefaults: true,
60
+ removeAdditional: true,
61
+ });
62
+ const validate = ajv.compile(schema);
63
+
64
+ return (candidate) => {
65
+ normalizeEnumValues(candidate, schema);
66
+ const valid = validate(candidate);
67
+ const errors = (validate.errors || []).map((error) => ({ ...error }));
68
+ return {
69
+ valid,
70
+ value: candidate,
71
+ errors,
72
+ error: valid ? null : formatValidationErrors(errors) || 'Schema validation failed',
73
+ };
74
+ };
75
+ }
76
+
77
+ function validateAgainstSchema(parsed, schema) {
78
+ return createStructuredOutputValidator(schema)(parsed).error;
79
+ }
80
+
81
+ function assertReformatRequest(rawOutput, maxAttempts) {
82
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > MAX_CONFIGURED_ATTEMPTS) {
83
+ const error = new RangeError(
84
+ `Output reformatting maxAttempts must be an integer from 1 through ${MAX_CONFIGURED_ATTEMPTS}`
85
+ );
86
+ error.code = 'REFORMAT_INVALID_ATTEMPT_LIMIT';
87
+ error.permanent = true;
88
+ throw error;
89
+ }
90
+ if (Buffer.byteLength(rawOutput || '', 'utf8') > MAX_REFORMAT_INPUT_BYTES) {
91
+ const error = new Error(
92
+ `Output reformatting input exceeds ${MAX_REFORMAT_INPUT_BYTES} UTF-8 bytes`
93
+ );
94
+ error.code = 'REFORMAT_INPUT_TOO_LARGE';
95
+ error.permanent = true;
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ function invocationError(result) {
101
+ const error =
102
+ result?.error instanceof Error
103
+ ? result.error
104
+ : new Error(result?.error || 'Structured-output recovery task failed');
105
+ for (const field of [
106
+ 'code',
107
+ 'permanent',
108
+ 'provider',
109
+ 'capability',
110
+ 'nestedExecutionCancellation',
111
+ 'nestedExecutionLifecycle',
112
+ 'retainTaskHandle',
113
+ 'terminationExhausted',
114
+ 'taskId',
115
+ ]) {
116
+ if (result?.[field] !== undefined && error[field] === undefined) {
117
+ error[field] = result[field];
118
+ }
119
+ }
120
+ return error;
121
+ }
122
+
123
+ function isImmediateRecoveryFailure(error, providerName) {
124
+ if (
125
+ error?.code === 'REFORMAT_CANCELLED' ||
126
+ error?.code === 'AGENT_TASK_TIMEOUT' ||
127
+ error?.nestedExecutionCancellation === true ||
128
+ error?.nestedExecutionLifecycle === true ||
129
+ error?.retainTaskHandle === true ||
130
+ error?.permanent === true ||
131
+ error?.terminationExhausted === true
132
+ ) {
133
+ return true;
134
+ }
135
+ return !getProvider(providerName).isRetryableError(error);
136
+ }
137
+
138
+ function markImmediateRecoveryFailure(error, providerName) {
139
+ if (!isImmediateRecoveryFailure(error, providerName)) return false;
140
+ error.recoveryAbort = true;
141
+ const operationalControl =
142
+ error.code === 'REFORMAT_CANCELLED' ||
143
+ error.code === 'AGENT_TASK_TIMEOUT' ||
144
+ error.nestedExecutionCancellation === true ||
145
+ error.nestedExecutionLifecycle === true ||
146
+ error.retainTaskHandle === true ||
147
+ error.terminationExhausted === true;
148
+ if (!operationalControl && error.permanent !== true) error.permanent = true;
149
+ return true;
150
+ }
151
+
81
152
  async function reformatOutput({
82
153
  rawOutput,
83
154
  schema,
84
155
  providerName,
85
156
  maxAttempts = DEFAULT_MAX_ATTEMPTS,
157
+ initialError = null,
158
+ validateCandidate = createStructuredOutputValidator(schema),
86
159
  onAttempt,
87
160
  isCancelled = () => false,
88
161
  runReformat,
89
162
  }) {
90
- if (providerName !== 'opencode') {
91
- throw new Error(
92
- `Output reformatting not available for provider "${providerName}". ` +
93
- `Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
94
- );
95
- }
163
+ assertReformatRequest(rawOutput, maxAttempts);
96
164
  if (typeof runReformat !== 'function') {
97
165
  throw new Error('Output reformatting requires the active agent execution context');
98
166
  }
99
167
 
100
-
101
- const { extractJsonFromOutput } = require('./output-extraction');
102
- let lastError = null;
168
+ const { extractCliError, extractJsonFromOutput } = require('./output-extraction');
169
+ let lastError = initialError;
103
170
 
104
171
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
105
172
  if (isCancelled()) throw createCancellationError();
106
- if (onAttempt) {
107
- onAttempt(attempt, lastError);
108
- }
109
-
173
+ onAttempt?.(attempt, lastError);
110
174
  const prompt = buildReformatPrompt(rawOutput, schema, lastError);
111
175
 
112
176
  try {
113
177
  const result = await runReformat(prompt);
114
178
  if (isCancelled()) throw createCancellationError();
115
179
  if (!result?.success) {
116
- lastError = result?.error || 'reformat task failed';
180
+ const error = invocationError(result);
181
+ if (markImmediateRecoveryFailure(error, providerName)) throw error;
182
+ lastError = error.message;
117
183
  continue;
118
184
  }
119
- const output = result.output;
120
- if (!output) {
121
- lastError = 'reformat task returned no output';
185
+ if (!result.output) {
186
+ lastError = 'Recovery task returned no output';
122
187
  continue;
123
188
  }
124
-
125
- const parsed = extractJsonFromOutput(output, 'opencode');
126
- if (!parsed) {
127
- lastError = 'Could not extract JSON from reformatted output';
189
+ const terminalError = extractCliError(result.output, providerName);
190
+ if (terminalError) {
191
+ const error = new Error(terminalError.error);
192
+ error.provider = terminalError.provider;
193
+ if (markImmediateRecoveryFailure(error, providerName)) throw error;
194
+ lastError = error.message;
128
195
  continue;
129
196
  }
130
197
 
131
- const validationError = validateAgainstSchema(parsed, schema);
132
- if (validationError) {
133
- lastError = validationError;
198
+ const parsed = extractJsonFromOutput(result.output, providerName);
199
+ if (!parsed) {
200
+ lastError = 'Could not extract JSON from recovery output';
134
201
  continue;
135
202
  }
136
-
137
- return parsed;
138
- } catch (err) {
139
- if (
140
- err.code === 'REFORMAT_CANCELLED' ||
141
- err.code === 'AGENT_TASK_TIMEOUT' ||
142
- err.nestedExecutionLifecycle === true ||
143
- err.retainTaskHandle === true ||
144
- err.permanent === true ||
145
- err.terminationExhausted === true
146
- ) {
147
- throw err;
203
+ const validation = validateCandidate(parsed);
204
+ if (!validation.valid) {
205
+ lastError = validation.error;
206
+ continue;
148
207
  }
149
- lastError = err.message;
208
+ return { status: 'recovered', value: validation.value, attempts: attempt };
209
+ } catch (error) {
210
+ if (isCancelled()) throw createCancellationError();
211
+ if (markImmediateRecoveryFailure(error, providerName)) throw error;
212
+ lastError = error.message;
150
213
  }
151
214
  }
152
215
 
153
- throw new Error(
154
- `Failed to reformat output after ${maxAttempts} attempts (provider "${providerName}"). ` +
155
- `Last error: ${lastError}. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
156
- );
157
- }
158
-
159
- /**
160
- * Validate parsed output against JSON schema
161
- *
162
- * @param {Object} parsed - Parsed JSON object
163
- * @param {Object} schema - JSON schema to validate against
164
- * @returns {string|null} Error message if validation failed, null if valid
165
- */
166
- function validateAgainstSchema(parsed, schema) {
167
- const Ajv = require('ajv');
168
- const ajv = new Ajv({ allErrors: true, strict: false });
169
- const validate = ajv.compile(schema);
170
- const valid = validate(parsed);
171
-
172
- if (!valid) {
173
- const errors = (validate.errors || [])
174
- .slice(0, 3)
175
- .map((e) => `${e.instancePath || '#'} ${e.message}`)
176
- .join('; ');
177
- return errors || 'Schema validation failed';
178
- }
179
-
180
- return null;
216
+ return {
217
+ status: 'exhausted',
218
+ attempts: maxAttempts,
219
+ lastError: lastError || 'Recovery attempts produced no schema-valid JSON object',
220
+ };
181
221
  }
182
222
 
183
223
  module.exports = {
184
224
  reformatOutput,
185
225
  buildReformatPrompt,
226
+ createStructuredOutputValidator,
186
227
  validateAgainstSchema,
187
228
  DEFAULT_MAX_ATTEMPTS,
229
+ MAX_REFORMAT_INPUT_BYTES,
230
+ MAX_CONFIGURED_ATTEMPTS,
188
231
  };
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const { spawnSync } = require('child_process');
6
+ const { getDefaultProviderId } = require('../../lib/provider-names');
6
7
 
7
8
  const DEFAULT_VERIFICATION_PLATFORM = 'github';
8
9
 
@@ -588,7 +589,9 @@ async function verifyPullRequest({ result, agent, autoMerge }) {
588
589
  const platform = resolveVerificationPlatform(agent);
589
590
  const adapter = getVerificationAdapter(platform);
590
591
  const providerName =
591
- typeof agent?._resolveProvider === 'function' ? agent._resolveProvider() : 'claude';
592
+ typeof agent?._resolveProvider === 'function'
593
+ ? agent._resolveProvider()
594
+ : getDefaultProviderId();
592
595
  const claims = resolvePrClaimsFromOutput({
593
596
  output: result.output,
594
597
  parsedResult: result.parsedResult,
@@ -1,4 +1,5 @@
1
1
  import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
2
+ import { UnsupportedProviderCapabilityError } from '../errors';
2
3
  import { contractError } from '../contract-errors';
3
4
  import {
4
5
  type BuildProviderCommandOptions,
@@ -9,7 +10,7 @@ import {
9
10
  type LevelOverrides,
10
11
  type ModelCatalogEntry,
11
12
  type ModelLevel,
12
- type ProviderAdapter,
13
+ type StructuredOutputRecoveryAdapter,
13
14
  type ResolvedModelSpec,
14
15
  type WarningMetadata,
15
16
  } from '../types';
@@ -72,6 +73,9 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
72
73
  supportsSettings: !unknown && /--settings/.test(help),
73
74
  supportsMcpConfig: !unknown && /--mcp-config/.test(help),
74
75
  supportsResume: unknown ? true : /--resume/.test(help),
76
+ supportsTools: !unknown && /--tools\b/.test(help),
77
+ supportsStrictMcpConfig: !unknown && /--strict-mcp-config\b/.test(help),
78
+ supportsNoSessionPersistence: !unknown && /--no-session-persistence\b/.test(help),
75
79
  unknown,
76
80
  };
77
81
  }
@@ -252,6 +256,38 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
252
256
  });
253
257
  }
254
258
 
259
+ function assertStructuredOutputRecoveryFeatures(options: BuildProviderCommandOptions): void {
260
+ const features = optionFeatures(options);
261
+ const missing = [
262
+ ['--tools', features.supportsTools],
263
+ ['--strict-mcp-config', features.supportsStrictMcpConfig],
264
+ ['--no-session-persistence', features.supportsNoSessionPersistence],
265
+ ].filter(([, supported]) => supported !== true);
266
+ if (missing.length === 0) return;
267
+ throw new UnsupportedProviderCapabilityError(
268
+ 'claude',
269
+ 'structuredOutputRecovery',
270
+ `Claude structured-output recovery requires CLI evidence for ${missing.map(([flag]) => flag).join(', ')}. Upgrade Claude Code before retrying.`
271
+ );
272
+ }
273
+
274
+ function buildStructuredOutputRecoveryCommand(
275
+ context: string,
276
+ options: BuildProviderCommandOptions = {}
277
+ ): CommandSpec {
278
+ assertStructuredOutputRecoveryFeatures(options);
279
+ const recoveryOptions = { ...options };
280
+ delete recoveryOptions.resumeSessionId;
281
+ delete recoveryOptions.continueSession;
282
+ delete recoveryOptions.mcpConfig;
283
+ const spec = buildCommand(context, { ...recoveryOptions, autoApprove: false });
284
+ const args = [...spec.args];
285
+ const prompt = args.pop();
286
+ if (prompt === undefined) throw new Error('Claude recovery command is missing its prompt');
287
+ args.push('--tools', '', '--strict-mcp-config', '--no-session-persistence', prompt);
288
+ return { ...spec, args };
289
+ }
290
+
255
291
  function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
256
292
  return resolveModelSpecWithConfig({
257
293
  mapping: LEVEL_MAPPING,
@@ -274,7 +310,7 @@ function classifyError(error: unknown): ErrorClassification {
274
310
  );
275
311
  }
276
312
 
277
- export const claudeAdapter: ProviderAdapter = {
313
+ export const claudeAdapter: StructuredOutputRecoveryAdapter = {
278
314
  id: 'claude',
279
315
  displayName: 'Claude',
280
316
  binary: 'claude',
@@ -287,6 +323,7 @@ export const claudeAdapter: ProviderAdapter = {
287
323
  defaultMinLevel: 'level1',
288
324
  detectCliFeatures,
289
325
  buildCommand,
326
+ buildStructuredOutputRecoveryCommand,
290
327
  extractSessionId,
291
328
  parseEvent: parseClaudeEvent,
292
329
  createParserState: () => createParserState('claude'),
@@ -10,7 +10,7 @@ import {
10
10
  type LevelOverrides,
11
11
  type ModelCatalogEntry,
12
12
  type ModelLevel,
13
- type ProviderAdapter,
13
+ type StructuredOutputRecoveryAdapter,
14
14
  type ResolvedModelSpec,
15
15
  type WarningMetadata,
16
16
  } from '../types';
@@ -67,6 +67,11 @@ function detectCliFeatures(
67
67
  supportsResume: supports(help, /\bresume\b/),
68
68
  supportsWebSearch:
69
69
  !unknown && supportsConfigOverride && isCliVersionAtLeast(versionText, '0.146.0'),
70
+ supportsSandbox: !unknown && /--sandbox\b/.test(help),
71
+ supportsEphemeral: !unknown && /--ephemeral\b/.test(help),
72
+ supportsIgnoreUserConfig: !unknown && /--ignore-user-config\b/.test(help),
73
+ supportsIgnoreRules: !unknown && /--ignore-rules\b/.test(help),
74
+ supportsStrictConfig: !unknown && /--strict-config\b/.test(help),
70
75
  unknown,
71
76
  };
72
77
  }
@@ -245,6 +250,54 @@ function buildCommand(context: string, options: BuildProviderCommandOptions = {}
245
250
  });
246
251
  }
247
252
 
253
+ function assertStructuredOutputRecoveryFeatures(options: BuildProviderCommandOptions): void {
254
+ const features = optionFeatures(options);
255
+ const missing = [
256
+ ['--sandbox', features.supportsSandbox],
257
+ ['--ephemeral', features.supportsEphemeral],
258
+ ['--ignore-user-config', features.supportsIgnoreUserConfig],
259
+ ['--ignore-rules', features.supportsIgnoreRules],
260
+ ['--strict-config', features.supportsStrictConfig],
261
+ ['--config', features.supportsConfigOverride],
262
+ ].filter(([, supported]) => supported !== true);
263
+ if (missing.length === 0) return;
264
+ throw new UnsupportedProviderCapabilityError(
265
+ 'codex',
266
+ 'structuredOutputRecovery',
267
+ `Codex structured-output recovery requires CLI evidence for ${missing.map(([flag]) => flag).join(', ')}. Upgrade @openai/codex before retrying.`
268
+ );
269
+ }
270
+
271
+ function buildStructuredOutputRecoveryCommand(
272
+ context: string,
273
+ options: BuildProviderCommandOptions = {}
274
+ ): CommandSpec {
275
+ assertStructuredOutputRecoveryFeatures(options);
276
+ const recoveryOptions = { ...options };
277
+ delete recoveryOptions.resumeSessionId;
278
+ delete recoveryOptions.continueSession;
279
+ const spec = buildCommand(context, {
280
+ ...recoveryOptions,
281
+ autoApprove: false,
282
+ webSearch: false,
283
+ });
284
+ const args = [...spec.args];
285
+ const prompt = args.pop();
286
+ if (prompt === undefined) throw new Error('Codex recovery command is missing its prompt');
287
+ args.push(
288
+ '--sandbox',
289
+ 'read-only',
290
+ '--ephemeral',
291
+ '--ignore-user-config',
292
+ '--ignore-rules',
293
+ '--strict-config',
294
+ '--config',
295
+ 'web_search="disabled"',
296
+ prompt
297
+ );
298
+ return { ...spec, args };
299
+ }
300
+
248
301
  function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
249
302
  return resolveModelSpecWithConfig({
250
303
  mapping: LEVEL_MAPPING,
@@ -267,7 +320,7 @@ function classifyError(error: unknown): ErrorClassification {
267
320
  );
268
321
  }
269
322
 
270
- export const codexAdapter: ProviderAdapter = {
323
+ export const codexAdapter: StructuredOutputRecoveryAdapter = {
271
324
  id: 'codex',
272
325
  displayName: 'Codex',
273
326
  binary: 'codex',
@@ -280,6 +333,7 @@ export const codexAdapter: ProviderAdapter = {
280
333
  defaultMinLevel: 'level1',
281
334
  detectCliFeatures,
282
335
  buildCommand,
336
+ buildStructuredOutputRecoveryCommand,
283
337
  extractSessionId,
284
338
  parseEvent: parseCodexEvent,
285
339
  createParserState: () => createParserState('codex'),