@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
@@ -11,17 +11,14 @@
11
11
  */
12
12
 
13
13
  const { spawn, spawnSync } = require('child_process');
14
- const { randomUUID } = require('crypto');
15
14
  const path = require('path');
16
15
  const fs = require('fs');
17
- const {
18
- getNestedExecutionRegistry,
19
- TaskExecutionHandle,
20
- } = require('./task-execution-handle');
16
+ const { getNestedExecutionRegistry, TaskExecutionHandle } = require('./task-execution-handle');
21
17
  const os = require('os');
22
18
  const { parseProviderChunk, getProvider } = require('../providers');
23
19
  const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.js');
24
20
  const { loadSettings } = require('../../lib/settings.js');
21
+ const { getDefaultProviderId } = require('../../lib/provider-names');
25
22
  const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
26
23
  const { prependWorktreeToolBinToEnv } = require('../worktree-tooling-env.js');
27
24
  const { applyDarwinKeychainBoundaryToEnv } = require('../darwin-keychain-boundary.js');
@@ -54,207 +51,6 @@ const {
54
51
  } = require('./provider-session');
55
52
  const { extractClaudeVertexModelError } = require('./output-extraction');
56
53
  const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
57
- const OPENCODE_CONFIG_CONTENT_ENV = 'OPENCODE_CONFIG_CONTENT';
58
- const OPENCODE_AGENT_ENV = 'ZEROSHOT_OPENCODE_AGENT';
59
- const REFORMATTER_AGENT_PREFIX = 'zeroshot-output-reformatter-';
60
-
61
- function parseOpenCodeJsonc(content) {
62
- let withoutComments = '';
63
- let inString = false;
64
- let escaped = false;
65
- let cursor = 0;
66
- while (cursor < content.length) {
67
- const char = content[cursor];
68
- const next = content[cursor + 1];
69
- if (inString) {
70
- withoutComments += char;
71
- if (escaped) {
72
- escaped = false;
73
- } else if (char === '\\') {
74
- escaped = true;
75
- } else if (char === '"') {
76
- inString = false;
77
- }
78
- cursor += 1;
79
- continue;
80
- }
81
- if (char === '"') {
82
- inString = true;
83
- withoutComments += char;
84
- cursor += 1;
85
- continue;
86
- }
87
- if (char === '/' && next === '/') {
88
- cursor += 2;
89
- while (cursor < content.length && content[cursor] !== '\n') cursor += 1;
90
- withoutComments += '\n';
91
- cursor += 1;
92
- continue;
93
- }
94
- if (char === '/' && next === '*') {
95
- cursor += 2;
96
- while (
97
- cursor < content.length &&
98
- !(content[cursor] === '*' && content[cursor + 1] === '/')
99
- ) {
100
- if (content[cursor] === '\n') withoutComments += '\n';
101
- cursor += 1;
102
- }
103
- if (cursor >= content.length) {
104
- throw new SyntaxError('Unterminated block comment in OpenCode inline config');
105
- }
106
- cursor += 2;
107
- continue;
108
- }
109
- withoutComments += char;
110
- cursor += 1;
111
- }
112
-
113
- let withoutTrailingCommas = '';
114
- inString = false;
115
- escaped = false;
116
- for (let index = 0; index < withoutComments.length; index++) {
117
- const char = withoutComments[index];
118
- if (inString) {
119
- withoutTrailingCommas += char;
120
- if (escaped) {
121
- escaped = false;
122
- } else if (char === '\\') {
123
- escaped = true;
124
- } else if (char === '"') {
125
- inString = false;
126
- }
127
- continue;
128
- }
129
- if (char === '"') {
130
- inString = true;
131
- withoutTrailingCommas += char;
132
- continue;
133
- }
134
- if (char === ',') {
135
- let nextIndex = index + 1;
136
- while (/\s/.test(withoutComments[nextIndex] || '')) nextIndex++;
137
- if (withoutComments[nextIndex] === '}' || withoutComments[nextIndex] === ']') {
138
- continue;
139
- }
140
- }
141
- withoutTrailingCommas += char;
142
- }
143
- return JSON.parse(withoutTrailingCommas);
144
- }
145
-
146
- function ensureFormatterLaunchOptions(providerName, options) {
147
- if (providerName !== 'opencode' || options.disableTools !== true) return options;
148
- if (options.formatterAgentName) return options;
149
- return {
150
- ...options,
151
- formatterAgentName: `${REFORMATTER_AGENT_PREFIX}${randomUUID()}`,
152
- };
153
- }
154
-
155
- function applyOpenCodeToolBoundary(env, providerName, options = {}) {
156
- if (providerName !== 'opencode' || options.disableTools !== true) return env;
157
- if (!options.formatterAgentName) {
158
- throw new Error('Tool-disabled OpenCode formatter launch is missing its unique agent identity');
159
- }
160
-
161
- let config = {};
162
- const existingContent = env[OPENCODE_CONFIG_CONTENT_ENV];
163
- if (existingContent) {
164
- try {
165
- config = parseOpenCodeJsonc(existingContent);
166
- } catch (error) {
167
- throw new Error(
168
- `Cannot install tool-disabled OpenCode formatter profile: invalid ${OPENCODE_CONFIG_CONTENT_ENV}: ${error.message}`
169
- );
170
- }
171
- }
172
-
173
- const existingAgents =
174
- config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
175
- ? config.agent
176
- : {};
177
- const existingModes =
178
- config.mode && typeof config.mode === 'object' && !Array.isArray(config.mode)
179
- ? config.mode
180
- : {};
181
- const formatterProfile = {
182
- description: 'Convert supplied text to schema-valid JSON without external actions',
183
- mode: 'primary',
184
- permission: { '*': 'deny' },
185
- tools: { '*': false },
186
- };
187
- env[OPENCODE_AGENT_ENV] = options.formatterAgentName;
188
- env[OPENCODE_CONFIG_CONTENT_ENV] = JSON.stringify({
189
- ...config,
190
- default_agent: options.formatterAgentName,
191
- permission: 'deny',
192
- tools: { '*': false },
193
- agent: {
194
- ...existingAgents,
195
- [options.formatterAgentName]: formatterProfile,
196
- },
197
- mode: {
198
- ...existingModes,
199
- [options.formatterAgentName]: formatterProfile,
200
- },
201
- });
202
- return env;
203
- }
204
-
205
- function resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName) {
206
- if (
207
- providerName === 'opencode' &&
208
- typeof manager.getContainerEnvironmentValue === 'function'
209
- ) {
210
- return manager.getContainerEnvironmentValue(clusterId, OPENCODE_CONFIG_CONTENT_ENV);
211
- }
212
- return process.env[OPENCODE_CONFIG_CONTENT_ENV] || null;
213
- }
214
-
215
- async function resolveIsolatedOpenCodeConfigUnderOwnership({
216
- manager,
217
- clusterId,
218
- providerName,
219
- executionHandle,
220
- }) {
221
- if (!executionHandle) {
222
- return resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName);
223
- }
224
-
225
- let setupPending = true;
226
- let rejectSetup;
227
- const setupFailure = new Promise((_resolve, reject) => {
228
- rejectSetup = reject;
229
- });
230
- executionHandle.setCancelAction((reason, details = {}) => {
231
- if (setupPending) {
232
- const error = new Error(reason || 'Nested task setup cancelled');
233
- error.code = details.code || 'REFORMAT_CANCELLED';
234
- error.nestedExecutionCancellation = true;
235
- error.nestedExecutionLifecycle = true;
236
- rejectSetup(error);
237
- }
238
- return { forced: true, beforeLaunch: true };
239
- });
240
- executionHandle.setFailClosedAction((error) => {
241
- if (setupPending) rejectSetup(error);
242
- });
243
-
244
- try {
245
- const config = await Promise.race([
246
- Promise.resolve(resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName)),
247
- setupFailure,
248
- ]);
249
- if (executionHandle.isCancelled) {
250
- throw createNestedCancellationError(executionHandle);
251
- }
252
- return config;
253
- } finally {
254
- setupPending = false;
255
- }
256
- }
257
-
258
54
  function runCommandWithTimeout(command, args, options = {}, callback = null) {
259
55
  const timeout = options.timeout ?? 30000;
260
56
  if (timeout <= 0) {
@@ -326,9 +122,6 @@ function runCommandSync(command, args, options = {}) {
326
122
  return result.stdout?.toString() || '';
327
123
  }
328
124
 
329
- // Schema utilities for normalizing LLM output
330
- const { normalizeEnumValues } = require('./schema-utils');
331
-
332
125
  /**
333
126
  * Build Claude-specific environment variables for task spawning
334
127
  * Consolidates auth resolution and model mapping logic used by both isolated and non-isolated modes
@@ -607,7 +400,7 @@ function extractErrorContext({ output, statusOutput, taskId, isNotFound = false,
607
400
  * @param {string} output - Full NDJSON output from Claude CLI
608
401
  * @returns {Object|null} Token usage data or null if not found
609
402
  */
610
- function extractTokenUsage(output, providerName = 'claude') {
403
+ function extractTokenUsage(output, providerName = getDefaultProviderId()) {
611
404
  if (!output) return null;
612
405
 
613
406
  const events = parseProviderChunk(providerName, output);
@@ -729,7 +522,7 @@ async function settleRegisteredNestedHandle(registry, handle) {
729
522
  }
730
523
 
731
524
  async function spawnClaudeTask(agent, context, options = {}) {
732
- const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
525
+ const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
733
526
  const modelSpec = resolveAgentModelSpec(agent);
734
527
 
735
528
  const ctPath = agent.taskCliPath || getClaudeTasksPath();
@@ -740,6 +533,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
740
533
  providerName,
741
534
  modelSpec,
742
535
  runOutputFormat,
536
+ structuredOutputRecovery: options.structuredOutputRecovery === true,
743
537
  });
744
538
 
745
539
  maybeLogStreamJsonNotice(agent, runOutputFormat);
@@ -766,7 +560,6 @@ async function spawnClaudeTask(agent, context, options = {}) {
766
560
  if (agent.isolation?.enabled) {
767
561
  return spawnClaudeTaskIsolated(agent, context, options);
768
562
  }
769
- options = ensureFormatterLaunchOptions(providerName, options);
770
563
 
771
564
  const claudeSettingsPath =
772
565
  providerName === 'claude'
@@ -781,11 +574,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
781
574
  let pendingLaunch;
782
575
  try {
783
576
  try {
784
- const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, {
785
- claudeSettingsPath,
786
- disableTools: options.disableTools === true,
787
- formatterAgentName: options.formatterAgentName,
788
- });
577
+ const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, { claudeSettingsPath });
789
578
  taskId = await spawnTaskProcess({
790
579
  agent,
791
580
  ctPath,
@@ -866,11 +655,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
866
655
  if (error.terminationExhausted === true && error.retainTaskHandle === true) {
867
656
  throw error;
868
657
  }
869
- if (
870
- nested &&
871
- handle.isCancelled &&
872
- handle.cancelDetails.code !== 'NESTED_SETUP_FAILED'
873
- ) {
658
+ if (nested && handle.isCancelled && handle.cancelDetails.code !== 'NESTED_SETUP_FAILED') {
874
659
  const termination = await handle.waitForCancellation();
875
660
  const cancellationError = createNestedCancellationError(handle);
876
661
  if (!isTerminationConfirmed(termination)) {
@@ -907,28 +692,32 @@ function resolveOutputFormatConfig(agent) {
907
692
  return { desiredOutputFormat, strictSchema, runOutputFormat };
908
693
  }
909
694
 
910
- function buildTaskRunArgs({ agent, providerName, modelSpec, runOutputFormat }) {
695
+ function buildTaskRunArgs({
696
+ agent,
697
+ providerName,
698
+ modelSpec,
699
+ runOutputFormat,
700
+ structuredOutputRecovery = false,
701
+ }) {
911
702
  const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
912
703
  const modelSpecSource = agent._resolveModelSpecSource
913
704
  ? agent._resolveModelSpecSource()
914
705
  : 'direct';
915
706
  appendTaskRunModelArgs(args, modelSpec, modelSpecSource);
916
707
 
917
- // Add verification mode flag if configured
918
708
  if (agent.config.verificationMode) {
919
709
  args.push('-v');
920
710
  }
921
711
 
922
- // Add JSON schema if specified in agent config.
923
- // If we are running stream-json for live logs (strictSchema=false), do NOT pass schema to CLI.
924
712
  if (agent.config.jsonSchema && runOutputFormat === 'json') {
925
- const schema = JSON.stringify(agent.config.jsonSchema);
926
- args.push('--json-schema', schema);
713
+ args.push('--json-schema', JSON.stringify(agent.config.jsonSchema));
714
+ }
715
+
716
+ if (structuredOutputRecovery) {
717
+ args.push('--structured-output-recovery');
718
+ return args;
927
719
  }
928
720
 
929
- // MCP servers: explicitly forward the repo config through the task CLI.
930
- // Claude receives the path; providers such as Copilot receive inlined JSON
931
- // so it survives container path translation.
932
721
  for (const mcpArg of resolveMcpConfigArgs(agent, providerName)) {
933
722
  args.push(mcpArg);
934
723
  }
@@ -1056,7 +845,7 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
1056
845
  worktreePath: agent.worktree?.path || null,
1057
846
  });
1058
847
 
1059
- return applyOpenCodeToolBoundary(spawnEnv, providerName, options);
848
+ return spawnEnv;
1060
849
  }
1061
850
 
1062
851
  function parseTaskIdFromOutput(stdout) {
@@ -1073,6 +862,27 @@ function assignDurableTaskId(agent, taskId) {
1073
862
  });
1074
863
  }
1075
864
 
865
+ async function terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed) {
866
+ if (isWrapperClosed()) return;
867
+ if (!proc.pid) {
868
+ await new Promise((resolve) => {
869
+ let settled = false;
870
+ const finish = () => {
871
+ if (settled) return;
872
+ settled = true;
873
+ proc.off('spawn', finish);
874
+ proc.off('error', finish);
875
+ resolve();
876
+ };
877
+ proc.once('spawn', finish);
878
+ proc.once('error', finish);
879
+ if (proc.pid || isWrapperClosed()) finish();
880
+ });
881
+ }
882
+ if (!isWrapperClosed()) proc.kill('SIGKILL');
883
+ await waitForWrapperClose;
884
+ }
885
+
1076
886
  function createPendingTaskLaunchHandle({
1077
887
  agent,
1078
888
  proc,
@@ -1112,10 +922,7 @@ function createPendingTaskLaunchHandle({
1112
922
  };
1113
923
  if (taskId) await cancelTask(taskId);
1114
924
 
1115
- if (!isWrapperClosed()) {
1116
- proc.kill('SIGKILL');
1117
- await waitForWrapperClose;
1118
- }
925
+ await terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed);
1119
926
 
1120
927
  taskId = taskId || (await findLateTaskId());
1121
928
  if (taskId && !commandAttempted) await cancelTask(taskId);
@@ -1165,6 +972,7 @@ function spawnTaskProcess({
1165
972
  spawnTimeoutMs = 30000,
1166
973
  handle = null,
1167
974
  nested = false,
975
+ spawnProcess = spawn,
1168
976
  }) {
1169
977
  // Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it.
1170
978
  const SPAWN_TIMEOUT_MS = spawnTimeoutMs;
@@ -1174,7 +982,7 @@ function spawnTaskProcess({
1174
982
  const findPersistedTaskId = () => getTaskBySpawnOwnershipToken(ownershipToken)?.id || null;
1175
983
 
1176
984
  return new Promise((resolve, reject) => {
1177
- const proc = spawn(ctPath, safeArgs, {
985
+ const proc = spawnProcess(ctPath, safeArgs, {
1178
986
  cwd,
1179
987
  stdio: ['ignore', 'pipe', 'pipe'],
1180
988
  env: { ...spawnEnv, [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken },
@@ -1543,7 +1351,7 @@ function requiresStructuredResult(agent) {
1543
1351
  return outputFormat !== 'text' || !!agent?.config?.jsonSchema;
1544
1352
  }
1545
1353
 
1546
- async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
1354
+ async function evaluateStructuredSuccess({ agent, taskId, state, success, allowRecovery }) {
1547
1355
  if (!success || !requiresStructuredResult(agent)) {
1548
1356
  return { success, error: null };
1549
1357
  }
@@ -1556,10 +1364,16 @@ async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
1556
1364
  // Cache the validated parsed object so completion hooks and {{result.*}}
1557
1365
  // substitution consume it directly instead of re-parsing (which could
1558
1366
  // trigger a second recovery model call).
1559
- state._cachedParsedResult = await agent._parseResultOutput(state.output);
1367
+ state._cachedParsedResult = await agent._parseResultOutput(state.output, { allowRecovery });
1560
1368
  return { success: true, error: null };
1561
1369
  } catch (error) {
1562
- if (isNestedLifecycleError(error)) throw error;
1370
+ if (
1371
+ isNestedLifecycleError(error) ||
1372
+ error?.permanent === true ||
1373
+ error?.recoveryAbort === true
1374
+ ) {
1375
+ throw error;
1376
+ }
1563
1377
  const errorContext = sanitizeErrorMessage(error.message);
1564
1378
  console.warn(
1565
1379
  `[Agent ${agent.id}] Task ${taskId} reported completed but produced invalid structured output; ` +
@@ -1595,9 +1409,16 @@ async function buildCompletionResult({
1595
1409
  success,
1596
1410
  taskInfo = getTask(taskId),
1597
1411
  }) {
1412
+ const { isCompleted } = parseStatusFlags(stripAnsiCodes(stdout));
1598
1413
  const classified = state.skipStructuredResultCheck
1599
1414
  ? { success, error: null }
1600
- : await evaluateStructuredSuccess({ agent, taskId, state, success });
1415
+ : await evaluateStructuredSuccess({
1416
+ agent,
1417
+ taskId,
1418
+ state,
1419
+ success,
1420
+ allowRecovery: isCompleted,
1421
+ });
1601
1422
  const resumeIdentityError = classified.success ? validateCompletedResumeIdentity(taskInfo) : null;
1602
1423
  if (resumeIdentityError) {
1603
1424
  classified.success = false;
@@ -1802,18 +1623,7 @@ function handleStatusCompletion({
1802
1623
  success,
1803
1624
  })
1804
1625
  .then(resolve)
1805
- .catch((error) => {
1806
- if (isNestedLifecycleError(error)) {
1807
- reject(error);
1808
- return;
1809
- }
1810
- resolve({
1811
- success: false,
1812
- output: state.output,
1813
- error: sanitizeErrorMessage(error.message),
1814
- tokenUsage: extractTokenUsage(state.output, providerName),
1815
- });
1816
- });
1626
+ .catch(reject);
1817
1627
  }, 500);
1818
1628
 
1819
1629
  return true;
@@ -1939,7 +1749,7 @@ function createLogFollower({
1939
1749
  function followClaudeTaskLogs(agent, taskId, options = {}) {
1940
1750
  const fsModule = require('fs');
1941
1751
  const ctPath = agent.taskCliPath || getClaudeTasksPath();
1942
- const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
1752
+ const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
1943
1753
 
1944
1754
  return createLogFollower({
1945
1755
  agent,
@@ -2033,8 +1843,7 @@ async function spawnClaudeTaskIsolated(agent, context, options = {}) {
2033
1843
 
2034
1844
  async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2035
1845
  const { manager, clusterId } = agent.isolation;
2036
- const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
2037
- options = ensureFormatterLaunchOptions(providerName, options);
1846
+ const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
2038
1847
  const modelSpec = resolveAgentModelSpec(agent);
2039
1848
  const modelSpecSource = agent._resolveModelSpecSource
2040
1849
  ? agent._resolveModelSpecSource()
@@ -2050,6 +1859,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2050
1859
  providerName,
2051
1860
  modelSpec,
2052
1861
  runOutputFormat,
1862
+ structuredOutputRecovery: options.structuredOutputRecovery === true,
2053
1863
  }),
2054
1864
  ];
2055
1865
  maybeLogStreamJsonNotice(agent, runOutputFormat);
@@ -2074,26 +1884,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2074
1884
  const ownershipToken = createTaskSpawnOwnershipToken();
2075
1885
  // Auth env vars are injected by IsolationManager; the launch token is the
2076
1886
  // only authoritative bridge back to the detached task row in the container.
2077
- const effectiveOpenCodeConfig =
2078
- options.disableTools === true
2079
- ? await resolveIsolatedOpenCodeConfigUnderOwnership({
2080
- manager,
2081
- clusterId,
2082
- providerName,
2083
- executionHandle: options.executionHandle,
2084
- })
2085
- : null;
2086
- const isolatedEnv = applyOpenCodeToolBoundary(
2087
- {
2088
- ...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
2089
- ...(effectiveOpenCodeConfig
2090
- ? { [OPENCODE_CONFIG_CONTENT_ENV]: effectiveOpenCodeConfig }
2091
- : {}),
2092
- [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
2093
- },
2094
- providerName,
2095
- options
2096
- );
1887
+ const isolatedEnv = {
1888
+ ...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
1889
+ [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
1890
+ };
2097
1891
 
2098
1892
  if (options.executionHandle?.isCancelled) {
2099
1893
  throw createNestedCancellationError(options.executionHandle);
@@ -2156,8 +1950,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2156
1950
  rejection.terminationExhausted = true;
2157
1951
  rejection.terminationAttempts = 1;
2158
1952
  if (options.nested) options.executionHandle?.retainOwnership();
2159
- rejection.taskId =
2160
- isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
1953
+ rejection.taskId = isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
2161
1954
  } else if (!options.nested && agent.currentTask === isolatedPendingLaunch) {
2162
1955
  agent.currentTask = null;
2163
1956
  }
@@ -2184,14 +1977,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2184
1977
  commandError = error;
2185
1978
  }
2186
1979
 
2187
- if (!wrapperClosed) {
2188
- proc.kill('SIGKILL');
2189
- await waitForWrapperClose;
2190
- }
1980
+ await terminatePendingTaskWrapper(proc, waitForWrapperClose, () => wrapperClosed);
2191
1981
 
2192
1982
  try {
2193
- const persistedTaskId =
2194
- isolatedTaskId || (await findLatePersistedTaskId());
1983
+ const persistedTaskId = isolatedTaskId || (await findLatePersistedTaskId());
2195
1984
  if (persistedTaskId && !termination) {
2196
1985
  termination = await terminateIsolatedTask(manager, clusterId, persistedTaskId);
2197
1986
  commandError = null;
@@ -2214,8 +2003,8 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2214
2003
  if (!resolved) {
2215
2004
  const error =
2216
2005
  (sourceError instanceof Error &&
2217
- sourceError.code === 'unsupported-capability' &&
2218
- sourceError.permanent === true
2006
+ sourceError.code === 'unsupported-capability' &&
2007
+ sourceError.permanent === true
2219
2008
  ? sourceError
2220
2009
  : null) ||
2221
2010
  timeoutError ||
@@ -2456,8 +2245,7 @@ async function terminateIsolatedTask(manager, clusterId, taskId) {
2456
2245
  );
2457
2246
  }
2458
2247
  return {
2459
- alreadyTerminal:
2460
- Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
2248
+ alreadyTerminal: Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
2461
2249
  forced: !beforeStatus && afterStatus === 'killed',
2462
2250
  status: beforeStatus || afterStatus,
2463
2251
  };
@@ -2526,29 +2314,49 @@ function settleIsolatedTerminalStatus({
2526
2314
  useVertex: process.env.CLAUDE_CODE_USE_VERTEX === '1',
2527
2315
  })
2528
2316
  : null;
2529
- const success = status === 'completed' && !vertexModelError;
2530
- const errorContext = !success
2531
- ? extractErrorContext({
2532
- output: state.fullOutput,
2533
- statusOutput: status ? `Status: ${status}` : '',
2534
- taskId,
2535
- isNotFound,
2536
- debug: {
2537
- agentId: agent.id,
2538
- providerName,
2539
- pid: agent.processPid,
2540
- cwd: agent.config.cwd || process.cwd(),
2541
- worktreePath: agent.worktree?.path || null,
2542
- isolation: true,
2543
- clusterId,
2544
- logFilePath,
2545
- },
2546
- })
2547
- : null;
2548
- const parsedResult =
2549
- state.skipStructuredResultCheck || vertexModelError
2550
- ? null
2317
+ const staleCandidate =
2318
+ status === 'stale' &&
2319
+ !vertexModelError &&
2320
+ determineStaleSuccess({ agent, output: state.fullOutput, providerName, taskId });
2321
+ let success = (status === 'completed' || staleCandidate) && !vertexModelError;
2322
+ let structuredError = null;
2323
+ if (staleCandidate && !state.skipStructuredResultCheck) {
2324
+ const evaluated = await evaluateStructuredSuccess({
2325
+ agent,
2326
+ taskId,
2327
+ state,
2328
+ success,
2329
+ allowRecovery: false,
2330
+ });
2331
+ success = evaluated.success;
2332
+ structuredError = evaluated.error;
2333
+ }
2334
+ const errorContext =
2335
+ structuredError ||
2336
+ (!success
2337
+ ? extractErrorContext({
2338
+ output: state.fullOutput,
2339
+ statusOutput: status ? `Status: ${status}` : '',
2340
+ taskId,
2341
+ isNotFound,
2342
+ debug: {
2343
+ agentId: agent.id,
2344
+ providerName,
2345
+ pid: agent.processPid,
2346
+ cwd: agent.config.cwd || process.cwd(),
2347
+ worktreePath: agent.worktree?.path || null,
2348
+ isolation: true,
2349
+ clusterId,
2350
+ logFilePath,
2351
+ },
2352
+ })
2353
+ : null);
2354
+ let parsedResult = null;
2355
+ if (success && !state.skipStructuredResultCheck && !vertexModelError) {
2356
+ parsedResult = staleCandidate
2357
+ ? state._cachedParsedResult
2551
2358
  : await agent._parseResultOutput(state.fullOutput);
2359
+ }
2552
2360
 
2553
2361
  settleIsolatedFollower({
2554
2362
  agent,
@@ -2836,7 +2644,7 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
2836
2644
 
2837
2645
  const manager = isolation.manager;
2838
2646
  const clusterId = isolation.clusterId;
2839
- const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
2647
+ const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
2840
2648
 
2841
2649
  return new Promise((resolve, reject) => {
2842
2650
  const state = createIsolatedLogState(
@@ -2844,8 +2652,7 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
2844
2652
  options.nested === true
2845
2653
  );
2846
2654
  const cleanup = buildIsolatedCleanup(state);
2847
- const onLine = (line) =>
2848
- broadcastIsolatedLine({ agent, providerName, taskId, state, line });
2655
+ const onLine = (line) => broadcastIsolatedLine({ agent, providerName, taskId, state, line });
2849
2656
  state.lifecycleHandle = buildIsolatedLifecycleHandle({
2850
2657
  agent,
2851
2658
  manager,
@@ -2962,65 +2769,72 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
2962
2769
  * @param {String} output - Raw output from agent
2963
2770
  * @returns {Promise<Object>} Parsed result data
2964
2771
  */
2965
- async function parseResultOutput(agent, output) {
2966
- // Empty outputs = FAIL
2772
+ async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
2967
2773
  if (!output || !output.trim()) {
2968
2774
  throw new Error('Task execution failed - no output');
2969
2775
  }
2970
2776
 
2971
- const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
2777
+ const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
2972
2778
  const {
2973
- extractJsonFromOutput,
2974
2779
  extractCliError,
2780
+ extractJsonFromOutput,
2781
+ extractModelTextFromOutput,
2975
2782
  hasFatalStandaloneOutput,
2976
2783
  } = require('./output-extraction');
2977
-
2978
- // Check for CLI errors FIRST - surface the actual error message
2979
- const cliError = extractCliError(output);
2784
+ const cliError = extractCliError(output, providerName);
2980
2785
  if (cliError) {
2981
2786
  throw new Error(`CLI error (${cliError.provider}): ${cliError.error}`);
2982
2787
  }
2983
2788
 
2984
- // Use clean extraction pipeline
2985
- let parsed = extractJsonFromOutput(output, providerName);
2789
+ const parsed = extractJsonFromOutput(output, providerName);
2790
+ const schema = agent.config.jsonSchema;
2791
+ if (!schema) {
2792
+ if (parsed) return parsed;
2793
+ throw new Error(`Agent ${agent.id} output missing required JSON block`);
2794
+ }
2986
2795
 
2987
- // If extraction failed but we have a schema, attempt reformatting
2988
- if (!parsed && agent.config.jsonSchema) {
2989
- const { reformatOutput } = require('./output-reformatter');
2796
+ const { createStructuredOutputValidator, reformatOutput } = require('./output-reformatter');
2797
+ const validateCandidate = createStructuredOutputValidator(schema);
2798
+ const directValidation = parsed ? validateCandidate(parsed) : null;
2799
+ if (directValidation?.valid) {
2800
+ return directValidation.value;
2801
+ }
2990
2802
 
2991
- try {
2992
- parsed = await reformatOutput({
2993
- rawOutput: output,
2994
- schema: agent.config.jsonSchema,
2995
- providerName,
2996
- isCancelled: () => agent.running === false || agent.state === 'stopped',
2997
- runReformat: (prompt) =>
2998
- agent._spawnClaudeTask(prompt, {
2999
- skipStructuredResultCheck: true,
3000
- nested: true,
3001
- disableTools: true,
3002
- }),
3003
- onAttempt: (attempt, lastError) => {
3004
- if (lastError) {
3005
- console.warn(`[Agent ${agent.id}] Reformat attempt ${attempt}: ${lastError}`);
3006
- } else {
3007
- console.warn(
3008
- `[Agent ${agent.id}] JSON extraction failed, reformatting (attempt ${attempt})...`
3009
- );
3010
- }
3011
- },
3012
- });
3013
- } catch (reformatError) {
3014
- if (
3015
- reformatError.code === 'REFORMAT_CANCELLED' ||
3016
- reformatError.code === 'AGENT_TASK_TIMEOUT' ||
3017
- reformatError.permanent === true ||
3018
- isNestedLifecycleError(reformatError)
3019
- ) {
3020
- throw reformatError;
3021
- }
3022
- // Reformatting failed - fall through to error below
3023
- console.error(`[Agent ${agent.id}] Reformatting failed: ${reformatError.message}`);
2803
+ const schemaIsNonEmpty =
2804
+ typeof schema === 'string'
2805
+ ? schema.trim() !== '' && schema.trim() !== '{}'
2806
+ : typeof schema === 'object' && schema !== null && Object.keys(schema).length > 0;
2807
+ const { providerSupportsOutputReformatting } = require('../../lib/provider-names');
2808
+ let recovery = null;
2809
+ if (
2810
+ schemaIsNonEmpty &&
2811
+ !hasFatalStandaloneOutput(output) &&
2812
+ allowRecovery &&
2813
+ providerSupportsOutputReformatting(providerName)
2814
+ ) {
2815
+ const modelText = extractModelTextFromOutput(output, providerName);
2816
+ recovery = await reformatOutput({
2817
+ rawOutput: modelText || output,
2818
+ schema,
2819
+ providerName,
2820
+ initialError: directValidation?.error || null,
2821
+ validateCandidate,
2822
+ isCancelled: () => agent.running === false || agent.state === 'stopped',
2823
+ runReformat: (prompt) =>
2824
+ agent._spawnClaudeTask(prompt, {
2825
+ skipStructuredResultCheck: true,
2826
+ nested: true,
2827
+ structuredOutputRecovery: true,
2828
+ }),
2829
+ onAttempt: (attempt, lastError) => {
2830
+ const reason = lastError ? `: ${lastError}` : '';
2831
+ console.warn(
2832
+ `[Agent ${agent.id}] Correcting structured output (attempt ${attempt})${reason}`
2833
+ );
2834
+ },
2835
+ });
2836
+ if (recovery.status === 'recovered') {
2837
+ return recovery.value;
3024
2838
  }
3025
2839
  }
3026
2840
 
@@ -3028,69 +2842,40 @@ async function parseResultOutput(agent, output) {
3028
2842
  if (hasFatalStandaloneOutput(output)) {
3029
2843
  throw new Error('Task execution failed - no output');
3030
2844
  }
3031
- const trimmedOutput = output.trim();
3032
- console.error(`\n${'='.repeat(80)}`);
3033
- console.error(`🔴 AGENT OUTPUT MISSING REQUIRED JSON BLOCK`);
3034
- console.error(`${'='.repeat(80)}`);
3035
- console.error(`Agent: ${agent.id}, Role: ${agent.role}, Provider: ${providerName}`);
3036
- console.error(`Output (last 500 chars): ${trimmedOutput.slice(-500)}`);
3037
- console.error(`${'='.repeat(80)}\n`);
3038
- throw new Error(`Agent ${agent.id} output missing required JSON block`);
2845
+ const recoveryDetail = recovery?.lastError ? ` Recovery exhausted: ${recovery.lastError}.` : '';
2846
+ throw new Error(`Agent ${agent.id} output missing required JSON block.${recoveryDetail}`);
3039
2847
  }
3040
2848
 
3041
- // If a JSON schema is configured, validate parsed output locally.
3042
- // This preserves schema enforcement even when we run stream-json for live logs.
3043
- // IMPORTANT: For non-validator agents we warn but do not fail the cluster.
3044
- if (agent.config.jsonSchema) {
3045
- // Normalize enum values BEFORE validation (handles case mismatches, common variations)
3046
- // This is provider-agnostic - works for Claude CLI, Gemini, Codex, etc.
3047
- normalizeEnumValues(parsed, agent.config.jsonSchema);
3048
-
3049
- const Ajv = require('ajv');
3050
- const ajv = new Ajv({
3051
- allErrors: true,
3052
- strict: false,
3053
- coerceTypes: false, // STRICT: Reject type mismatches (e.g., null instead of array)
3054
- useDefaults: true,
3055
- removeAdditional: true,
3056
- });
3057
- const validate = ajv.compile(agent.config.jsonSchema);
3058
- const valid = validate(parsed);
3059
- if (!valid) {
3060
- const errorList = (validate.errors || [])
3061
- .slice(0, 5)
3062
- .map((e) => `${e.instancePath || e.schemaPath} ${e.message}`)
3063
- .join('; ');
3064
- const msg =
3065
- `Agent ${agent.id} output failed JSON schema validation: ` +
3066
- (errorList || 'unknown schema error');
3067
-
3068
- // Validators stay strict (they already have auto-approval fallback on crash).
3069
- if (agent.role === 'validator') {
3070
- throw new Error(msg);
3071
- }
2849
+ if (!allowRecovery) {
2850
+ const detail = directValidation?.error || 'required structured output is missing';
2851
+ throw new Error(`Agent ${agent.id} output is not valid for non-completed task: ${detail}`);
2852
+ }
3072
2853
 
3073
- // Non-validators: emit warning and continue with best-effort parsed data.
3074
- console.warn(`⚠️ ${msg}`);
3075
- agent._publish({
3076
- topic: 'AGENT_SCHEMA_WARNING',
3077
- receiver: 'broadcast',
3078
- content: {
3079
- text: msg,
3080
- data: {
3081
- agent: agent.id,
3082
- role: agent.role,
3083
- iteration: agent.iteration,
3084
- errors: validate.errors || [],
3085
- },
3086
- },
3087
- });
3088
- }
2854
+ const errorDetail = directValidation?.error || 'unknown schema error';
2855
+ const message = `Agent ${agent.id} output failed JSON schema validation: ${errorDetail}`;
2856
+ if (agent.role === 'validator') {
2857
+ throw new Error(
2858
+ recovery?.status === 'exhausted'
2859
+ ? `${message}. Recovery exhausted after ${recovery.attempts} attempts: ${recovery.lastError}`
2860
+ : message
2861
+ );
3089
2862
  }
3090
2863
 
3091
- // Return whatever the agent produced - no hardcoded field requirements
3092
- // Template substitution will validate that required fields exist
3093
- return parsed;
2864
+ console.warn(`⚠️ ${message}`);
2865
+ agent._publish({
2866
+ topic: 'AGENT_SCHEMA_WARNING',
2867
+ receiver: 'broadcast',
2868
+ content: {
2869
+ text: message,
2870
+ data: {
2871
+ agent: agent.id,
2872
+ role: agent.role,
2873
+ iteration: agent.iteration,
2874
+ errors: directValidation?.errors || [],
2875
+ },
2876
+ },
2877
+ });
2878
+ return directValidation?.value || parsed;
3094
2879
  }
3095
2880
 
3096
2881
  /**