@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
@@ -9,6 +9,8 @@ import type {
9
9
  ProviderDocsMetadata,
10
10
  ProviderInvokeSpec,
11
11
  ProviderRegistryEntry,
12
+ StructuredOutputProviderRegistryEntry,
13
+ UnstructuredOutputProviderRegistryEntry,
12
14
  } from './provider-registry';
13
15
 
14
16
  export type ProviderId = (typeof providerIds)[number];
@@ -27,8 +29,9 @@ export type {
27
29
  ProviderDocsMetadata,
28
30
  ProviderInvokeSpec,
29
31
  ProviderRegistryEntry,
32
+ StructuredOutputProviderRegistryEntry,
33
+ UnstructuredOutputProviderRegistryEntry,
30
34
  };
31
-
32
35
  export interface AgentCliProviderHelperMetadata {
33
36
  readonly packageName: '@the-open-engine/zeroshot';
34
37
  readonly buildOutputDir: 'lib/agent-cli-provider';
@@ -105,6 +108,9 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
105
108
  readonly supportsSettings: boolean;
106
109
  readonly supportsMcpConfig: boolean;
107
110
  readonly supportsResume: boolean;
111
+ readonly supportsTools: boolean;
112
+ readonly supportsStrictMcpConfig: boolean;
113
+ readonly supportsNoSessionPersistence: boolean;
108
114
  }
109
115
 
110
116
  export interface CodexCliFeatures extends BaseCliFeatures {
@@ -118,6 +124,11 @@ export interface CodexCliFeatures extends BaseCliFeatures {
118
124
  readonly supportsSkipGitRepoCheck: boolean;
119
125
  readonly supportsResume: boolean;
120
126
  readonly supportsWebSearch: boolean;
127
+ readonly supportsSandbox: boolean;
128
+ readonly supportsEphemeral: boolean;
129
+ readonly supportsIgnoreUserConfig: boolean;
130
+ readonly supportsIgnoreRules: boolean;
131
+ readonly supportsStrictConfig: boolean;
121
132
  }
122
133
 
123
134
  export interface GeminiCliFeatures extends BaseCliFeatures {
@@ -126,6 +137,7 @@ export interface GeminiCliFeatures extends BaseCliFeatures {
126
137
  readonly supportsAutoApprove: boolean;
127
138
  readonly supportsCwd: boolean;
128
139
  readonly supportsModel: boolean;
140
+ readonly supportsAdminPolicy: boolean;
129
141
  }
130
142
 
131
143
  export interface OpencodeCliFeatures extends BaseCliFeatures {
@@ -138,6 +150,8 @@ export interface OpencodeCliFeatures extends BaseCliFeatures {
138
150
  readonly supportsAutoApprove: false;
139
151
  readonly supportsResume: boolean;
140
152
  readonly supportsWebSearch: boolean;
153
+ readonly supportsAgent: boolean;
154
+ readonly supportsRecoveryIsolation: boolean;
141
155
  }
142
156
 
143
157
  export interface PiCliFeatures extends BaseCliFeatures {
@@ -215,6 +229,9 @@ export interface CliFeatureOverrides {
215
229
  readonly supportsIncludePartials?: boolean;
216
230
  readonly supportsVerbose?: boolean;
217
231
  readonly supportsModel?: boolean;
232
+ readonly supportsTools?: boolean;
233
+ readonly supportsStrictMcpConfig?: boolean;
234
+ readonly supportsNoSessionPersistence?: boolean;
218
235
  readonly supportsEffort?: boolean;
219
236
  readonly supportsSettings?: boolean;
220
237
  readonly supportsJson?: boolean;
@@ -223,12 +240,20 @@ export interface CliFeatureOverrides {
223
240
  readonly supportsCwd?: boolean;
224
241
  readonly supportsConfigOverride?: boolean;
225
242
  readonly supportsSkipGitRepoCheck?: boolean;
243
+ readonly supportsSandbox?: boolean;
244
+ readonly supportsEphemeral?: boolean;
245
+ readonly supportsIgnoreUserConfig?: boolean;
246
+ readonly supportsIgnoreRules?: boolean;
247
+ readonly supportsStrictConfig?: boolean;
226
248
  readonly supportsVariant?: boolean;
249
+ readonly supportsAdminPolicy?: boolean;
227
250
  readonly supportsJsonMode?: boolean;
228
251
  readonly supportsNoSession?: boolean;
229
252
  readonly supportsNoExtensions?: boolean;
230
253
  readonly supportsNoSkills?: boolean;
231
254
  readonly supportsNoPromptTemplates?: boolean;
255
+ readonly supportsAgent?: boolean;
256
+ readonly supportsRecoveryIsolation?: boolean;
232
257
  readonly supportsNoContextFiles?: boolean;
233
258
  readonly supportsNoApprove?: boolean;
234
259
  readonly supportsModeJson?: boolean;
@@ -262,7 +287,7 @@ export interface CleanupMetadata {
262
287
  readonly kind: 'temp-file' | 'temp-directory';
263
288
  readonly provider: ProviderId;
264
289
  readonly path: string;
265
- readonly reason: 'output-schema' | 'settings-overlay';
290
+ readonly reason: 'output-schema' | 'settings-overlay' | 'admin-policy' | 'isolated-config';
266
291
  }
267
292
 
268
293
  export interface WarningMetadata {
@@ -312,6 +337,8 @@ export interface BuildProviderCommandOptions {
312
337
  // one variadic `--mcp-config` followed by file paths; adapters such as Copilot accept repeated
313
338
  // flags with inline JSON so configs survive container path translation.
314
339
  readonly mcpConfig?: readonly string[];
340
+ /** Internal profile for provider-neutral structured-output correction turns. */
341
+ readonly structuredOutputRecovery?: boolean;
315
342
  }
316
343
 
317
344
  export interface TextEvent {
@@ -415,6 +442,13 @@ export interface ProviderAdapter {
415
442
  classifyError(error: unknown): ErrorClassification;
416
443
  }
417
444
 
445
+ export interface StructuredOutputRecoveryAdapter extends ProviderAdapter {
446
+ buildStructuredOutputRecoveryCommand(
447
+ context: string,
448
+ options?: BuildProviderCommandOptions
449
+ ): CommandSpec;
450
+ }
451
+
418
452
  export class InvalidProviderModelError extends Error {
419
453
  readonly permanent = true;
420
454
 
@@ -13,7 +13,7 @@
13
13
  const LogicEngine = require('./logic-engine');
14
14
  const { validateAgentConfig } = require('./agent/agent-config');
15
15
  const { loadSettings, validateModelAgainstMax, VALID_MODELS } = require('../lib/settings');
16
- const { normalizeProviderName } = require('../lib/provider-names');
16
+ const { normalizeProviderName, getDefaultProviderId } = require('../lib/provider-names');
17
17
  const { getProvider } = require('./providers');
18
18
  const { buildContext } = require('./agent/agent-context-builder');
19
19
  const { collectQueuedGuidance } = require('./agent/guidance-queue');
@@ -160,9 +160,9 @@ class AgentWrapper {
160
160
  this.config.provider ||
161
161
  clusterConfig.defaultProvider ||
162
162
  settings.defaultProvider ||
163
- 'claude';
163
+ getDefaultProviderId();
164
164
 
165
- return normalizeProviderName(resolved) || 'claude';
165
+ return normalizeProviderName(resolved) || getDefaultProviderId();
166
166
  }
167
167
 
168
168
  _resolveModelSpec() {
@@ -586,8 +586,8 @@ class AgentWrapper {
586
586
  * Falls back to reformatting if extraction fails
587
587
  * @private
588
588
  */
589
- _parseResultOutput(output) {
590
- return parseResultOutput(this, output);
589
+ _parseResultOutput(output, options = {}) {
590
+ return parseResultOutput(this, output, options);
591
591
  }
592
592
 
593
593
  /**
@@ -9,7 +9,7 @@ const { spawn, spawnSync } = require('child_process');
9
9
  const fs = require('fs');
10
10
  const TaskRunner = require('./task-runner');
11
11
  const { loadSettings } = require('../lib/settings');
12
- const { normalizeProviderName } = require('../lib/provider-names');
12
+ const { normalizeProviderName, getDefaultProviderId } = require('../lib/provider-names');
13
13
  const { getProvider } = require('./providers');
14
14
  const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
15
15
  const { applyDarwinKeychainBoundaryToEnv } = require('./darwin-keychain-boundary');
@@ -200,7 +200,9 @@ class ClaudeTaskRunner extends TaskRunner {
200
200
  } = options;
201
201
 
202
202
  const settings = loadSettings();
203
- const providerName = normalizeProviderName(provider || settings.defaultProvider || 'claude');
203
+ const providerName = normalizeProviderName(
204
+ provider || settings.defaultProvider || getDefaultProviderId()
205
+ );
204
206
  const { providerModule, providerSettings, levelOverrides } = this._getProviderContext(
205
207
  providerName,
206
208
  settings
@@ -736,7 +738,7 @@ class ClaudeTaskRunner extends TaskRunner {
736
738
  rejectCallerSuppliedModelProvenance(options);
737
739
  const {
738
740
  agentId = 'unknown',
739
- provider = 'claude',
741
+ provider = getDefaultProviderId(),
740
742
  model = null,
741
743
  modelLevel = null,
742
744
  modelSpec: explicitModelSpec = null,
@@ -13,6 +13,7 @@ export class ClusterInternalError extends ClusterError {}
13
13
  export class ClusterProtocolError extends ClusterError {}
14
14
  export class ClusterTransportError extends ClusterError {}
15
15
  export class ClusterTimeoutError extends ClusterError {}
16
+ export class ClusterRequestError extends ClusterError {}
16
17
 
17
18
  export function requestAbortError(method: string): DOMException {
18
19
  return new DOMException(
@@ -5,11 +5,15 @@ export {
5
5
  ClusterError,
6
6
  ClusterInternalError,
7
7
  ClusterProtocolError,
8
+ ClusterRequestError,
8
9
  ClusterRpcError,
9
10
  ClusterStateError,
10
11
  ClusterTimeoutError,
11
12
  ClusterTransportError,
12
13
  } from './errors.js';
14
+ export { assertGraphProfile, assertGraphProfileSupported, assertGraphSpec } from './validators.js';
15
+ export * from './payload-value.js';
16
+ export * from './json-source.js';
13
17
  export { CONNECTION_TRANSITIONS, PROTOCOL_DIAGNOSTIC_CAPACITY, Connection } from './connection.js';
14
18
  export type {
15
19
  CallOptions,
@@ -0,0 +1,71 @@
1
+ import { ClusterRequestError } from './errors.js';
2
+ import { MAX_FRAME_BYTES } from './generated/protocol.js';
3
+
4
+ export const MAX_REQUEST_BYTES = MAX_FRAME_BYTES;
5
+
6
+ export function decodeBoundedJson(bytes: Uint8Array): unknown {
7
+ if (bytes.length > MAX_REQUEST_BYTES) {
8
+ throw new ClusterRequestError(
9
+ `request payload of ${bytes.length} bytes exceeds the ${MAX_REQUEST_BYTES} byte limit`,
10
+ 'OVERSIZED_JSON',
11
+ );
12
+ }
13
+ let text: string;
14
+ try {
15
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
16
+ } catch {
17
+ throw new ClusterRequestError('request payload is not valid UTF-8', 'INVALID_UTF8');
18
+ }
19
+ try {
20
+ return JSON.parse(text) as unknown;
21
+ } catch {
22
+ throw new ClusterRequestError('request payload is not valid JSON', 'MALFORMED_JSON');
23
+ }
24
+ }
25
+
26
+ // Callers resolve `--graph`/`--input` specifiers ('-' vs. a file path) to a byte
27
+ // source themselves; opening the filesystem stream stays outside src/cluster so this
28
+ // module keeps no host-specific dependency (not even the `NodeJS` ambient namespace,
29
+ // which isn't resolvable from a consumer's declaration-only install) beyond the
30
+ // standard async-iterable protocol every Node Readable already implements — mirrors
31
+ // how WebSocketLike is injected instead of importing 'ws' directly, see socket.ts.
32
+ export async function readBoundedSource(
33
+ source: AsyncIterable<Uint8Array | string>,
34
+ ): Promise<Uint8Array> {
35
+ const encoder = new TextEncoder();
36
+ const chunks: Uint8Array[] = [];
37
+ let total = 0;
38
+ for await (const chunk of source) {
39
+ const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : new Uint8Array(chunk);
40
+ total += bytes.length;
41
+ if (total > MAX_REQUEST_BYTES) {
42
+ throw new ClusterRequestError(
43
+ `request payload exceeds the ${MAX_REQUEST_BYTES} byte limit`,
44
+ 'OVERSIZED_JSON',
45
+ );
46
+ }
47
+ chunks.push(bytes);
48
+ }
49
+ const assembled = new Uint8Array(total);
50
+ let offset = 0;
51
+ for (const bytes of chunks) {
52
+ assembled.set(bytes, offset);
53
+ offset += bytes.length;
54
+ }
55
+ return assembled;
56
+ }
57
+
58
+ export function assertDistinctRequestSources(
59
+ graph: string,
60
+ input: string | undefined,
61
+ ): asserts input is string {
62
+ if (input === undefined) {
63
+ throw new ClusterRequestError('--input is required for every hosted run', 'MISSING_INPUT');
64
+ }
65
+ if (graph === '-' && input === '-') {
66
+ throw new ClusterRequestError(
67
+ 'graph and input cannot both read from stdin',
68
+ 'AMBIGUOUS_STDIN_SOURCE',
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,84 @@
1
+ import type { PayloadType } from './generated/protocol.js';
2
+ import { ClusterRequestError } from './errors.js';
3
+
4
+ export type InputValidationIssue = {
5
+ readonly path: string;
6
+ readonly code: 'TYPE_MISMATCH' | 'MISSING_REQUIRED_FIELD' | 'UNKNOWN_FIELD' | 'UNKNOWN_ENUM_LABEL';
7
+ };
8
+
9
+ type InternalIssue = InputValidationIssue & { readonly expectedKind: string };
10
+
11
+ function isJsonInteger(value: unknown): boolean {
12
+ return typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value);
13
+ }
14
+
15
+ function evaluate(type: PayloadType, value: unknown, path: string): InternalIssue | null {
16
+ switch (type.kind) {
17
+ case 'null':
18
+ return value === null ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'null' };
19
+ case 'boolean':
20
+ return typeof value === 'boolean' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'boolean' };
21
+ case 'integer':
22
+ return isJsonInteger(value) ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'integer' };
23
+ case 'number':
24
+ return typeof value === 'number' && Number.isFinite(value)
25
+ ? null
26
+ : { path, code: 'TYPE_MISMATCH', expectedKind: 'number' };
27
+ case 'string':
28
+ return typeof value === 'string' ? null : { path, code: 'TYPE_MISMATCH', expectedKind: 'string' };
29
+ case 'enum':
30
+ if (typeof value !== 'string') return { path, code: 'TYPE_MISMATCH', expectedKind: 'enum' };
31
+ return type.values.includes(value)
32
+ ? null
33
+ : { path, code: 'UNKNOWN_ENUM_LABEL', expectedKind: 'enum' };
34
+ case 'array': {
35
+ if (!Array.isArray(value)) return { path, code: 'TYPE_MISMATCH', expectedKind: 'array' };
36
+ for (let index = 0; index < value.length; index += 1) {
37
+ const issue = evaluate(type.items, value[index], `${path}/${index}`);
38
+ if (issue) return issue;
39
+ }
40
+ return null;
41
+ }
42
+ case 'record': {
43
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
44
+ return { path, code: 'TYPE_MISMATCH', expectedKind: 'record' };
45
+ }
46
+ const record = value as Readonly<Record<string, unknown>>;
47
+ const fieldNames = Object.keys(type.fields).sort();
48
+ for (const name of fieldNames) {
49
+ const field = type.fields[name];
50
+ if (!field) continue;
51
+ const fieldPath = `${path}/${name}`;
52
+ if (Object.prototype.hasOwnProperty.call(record, name)) {
53
+ const issue = evaluate(field.type, record[name], fieldPath);
54
+ if (issue) return issue;
55
+ } else if (field.required) {
56
+ return { path: fieldPath, code: 'MISSING_REQUIRED_FIELD', expectedKind: field.type.kind };
57
+ }
58
+ }
59
+ const declared = new Set(fieldNames);
60
+ const unknownField = Object.keys(record).sort().find((name) => !declared.has(name));
61
+ return unknownField
62
+ ? { path: `${path}/${unknownField}`, code: 'UNKNOWN_FIELD', expectedKind: 'record' }
63
+ : null;
64
+ }
65
+ }
66
+ }
67
+
68
+ export function firstInputValidationIssue(
69
+ type: PayloadType,
70
+ value: unknown,
71
+ path = '',
72
+ ): InputValidationIssue | null {
73
+ const issue = evaluate(type, value, path);
74
+ return issue ? { path: issue.path, code: issue.code } : null;
75
+ }
76
+
77
+ export function assertInputValue(type: PayloadType, value: unknown): void {
78
+ const issue = evaluate(type, value, '');
79
+ if (!issue) return;
80
+ throw new ClusterRequestError(
81
+ `input value at '${issue.path || '/'}' failed validation: ${issue.code}, expected ${issue.expectedKind}`,
82
+ 'INVALID_INPUT',
83
+ );
84
+ }
@@ -4,9 +4,9 @@ import {
4
4
  METHOD_RESULT_DEFINITIONS,
5
5
  PROTOCOL_VERSION,
6
6
  } from './generated/protocol.js';
7
- import type { ClusterMethod } from './generated/protocol.js';
7
+ import type { ClusterMethod, GraphProfile, GraphSpec, ServerCapabilities } from './generated/protocol.js';
8
8
  import { CLUSTER_PROTOCOL_SCHEMA } from './generated/protocol-schema.js';
9
- import { ClusterProtocolError } from './errors.js';
9
+ import { ClusterProtocolError, ClusterRequestError } from './errors.js';
10
10
 
11
11
  const ajv = new Ajv2020({
12
12
  allErrors: true,
@@ -54,3 +54,32 @@ export function assertMethodResult(method: ClusterMethod, value: unknown): void
54
54
  throw error;
55
55
  }
56
56
  }
57
+
58
+ function assertRequestDefinition(definition: string, value: unknown, code: string): void {
59
+ const validate = validatorFor(definition);
60
+ if (validate(value)) return;
61
+ const details = (validate.errors ?? []).map((error) =>
62
+ `${error.instancePath || '/'} ${error.message ?? 'is invalid'}`
63
+ ).join('; ');
64
+ throw new ClusterRequestError(`${definition} validation failed: ${details}`, code);
65
+ }
66
+
67
+ export function assertGraphSpec(value: unknown): asserts value is GraphSpec {
68
+ assertRequestDefinition('GraphSpec', value, 'INVALID_GRAPH');
69
+ }
70
+
71
+ export function assertGraphProfile(value: unknown): asserts value is GraphProfile {
72
+ assertRequestDefinition('GraphProfile', value, 'INVALID_GRAPH_PROFILE');
73
+ }
74
+
75
+ export function assertGraphProfileSupported(
76
+ profile: GraphProfile,
77
+ capabilities: ServerCapabilities,
78
+ ): void {
79
+ if (!capabilities.graphProfiles?.includes(profile)) {
80
+ throw new ClusterRequestError(
81
+ `graph profile ${profile} is not among the server's advertised graphProfiles`,
82
+ 'UNSUPPORTED_GRAPH_PROFILE',
83
+ );
84
+ }
85
+ }
@@ -14,6 +14,7 @@
14
14
  const { loadSettings } = require('../lib/settings');
15
15
  const {
16
16
  VALID_PROVIDERS,
17
+ getDefaultProviderId,
17
18
  normalizeProviderName,
18
19
  providerSupportsCapability,
19
20
  } = require('../lib/provider-names');
@@ -1987,8 +1988,8 @@ function resolveProviderName(agent, config, settings) {
1987
1988
  agent.provider ||
1988
1989
  config.defaultProvider ||
1989
1990
  settings.defaultProvider ||
1990
- 'claude';
1991
- return normalizeProviderName(resolved) || 'claude';
1991
+ getDefaultProviderId();
1992
+ return normalizeProviderName(resolved) || getDefaultProviderId();
1992
1993
  }
1993
1994
 
1994
1995
  function validateProviderLevel(provider, requestedLevel, minLevel, maxLevel) {
@@ -16,7 +16,11 @@ const os = require('os');
16
16
  const fs = require('fs');
17
17
  const { loadSettings } = require('../lib/settings');
18
18
  const { CLAUDE_AUTH_ENV_VARS, resolveClaudeAuth } = require('../lib/settings/claude-auth');
19
- const { normalizeProviderName, getProviderMetadata } = require('../lib/provider-names');
19
+ const {
20
+ normalizeProviderName,
21
+ getProviderMetadata,
22
+ getDefaultProviderId,
23
+ } = require('../lib/provider-names');
20
24
  const {
21
25
  MOUNT_PRESETS,
22
26
  resolveMounts,
@@ -236,7 +240,7 @@ class IsolationManager {
236
240
 
237
241
  const settings = loadSettings();
238
242
  const providerName = normalizeProviderName(
239
- config.provider || settings.defaultProvider || 'claude'
243
+ config.provider || settings.defaultProvider || getDefaultProviderId()
240
244
  );
241
245
  const containerHome = config.containerHome || settings.dockerContainerHome || '/root';
242
246
 
@@ -46,7 +46,7 @@ const { generateName } = require('./name-generator');
46
46
  const configValidator = require('./config-validator');
47
47
  const TemplateResolver = require('./template-resolver');
48
48
  const { loadSettings } = require('../lib/settings');
49
- const { normalizeProviderName } = require('../lib/provider-names');
49
+ const { normalizeProviderName, getDefaultProviderId } = require('../lib/provider-names');
50
50
  const { resolveRunPlan } = require('../lib/run-plan');
51
51
  const { isProcessRunning } = require('../lib/process-liveness');
52
52
  const { getProvider } = require('./providers');
@@ -270,6 +270,7 @@ class Orchestrator {
270
270
 
271
271
  // Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
272
272
  this.closed = false;
273
+ this._conductorWatchdogs = new Set();
273
274
 
274
275
  // Track if clusters are loaded (for lazy loading pattern)
275
276
  this._clustersLoaded = options.skipLoad === true;
@@ -312,8 +313,8 @@ class Orchestrator {
312
313
  clusterConfig.forceProvider ||
313
314
  clusterConfig.defaultProvider ||
314
315
  settings.defaultProvider ||
315
- 'claude';
316
- return normalizeProviderName(resolved) || 'claude';
316
+ getDefaultProviderId();
317
+ return normalizeProviderName(resolved) || getDefaultProviderId();
317
318
  }
318
319
 
319
320
  /**
@@ -1654,6 +1655,28 @@ class Orchestrator {
1654
1655
  const timeoutMs = 30000;
1655
1656
  let watchdogTimer = null;
1656
1657
  let completedAt = null;
1658
+ const watchdog = {
1659
+ clear: () => {
1660
+ if (!watchdogTimer) {
1661
+ return;
1662
+ }
1663
+ clearTimeout(watchdogTimer);
1664
+ watchdogTimer = null;
1665
+ this._conductorWatchdogs.delete(watchdog);
1666
+ const elapsed = completedAt ? Date.now() - completedAt : 0;
1667
+ this._log(
1668
+ `✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
1669
+ );
1670
+ },
1671
+ dispose: () => {
1672
+ if (watchdogTimer) {
1673
+ clearTimeout(watchdogTimer);
1674
+ watchdogTimer = null;
1675
+ }
1676
+ this._conductorWatchdogs.delete(watchdog);
1677
+ },
1678
+ };
1679
+ this._conductorWatchdogs.add(watchdog);
1657
1680
 
1658
1681
  this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_LIFECYCLE', (message) => {
1659
1682
  const event = message.content?.data?.event;
@@ -1666,6 +1689,11 @@ class Orchestrator {
1666
1689
  );
1667
1690
 
1668
1691
  watchdogTimer = setTimeout(() => {
1692
+ watchdogTimer = null;
1693
+ this._conductorWatchdogs.delete(watchdog);
1694
+ if (this.closed || messageBus._closed) {
1695
+ return;
1696
+ }
1669
1697
  const clusterOps = messageBus.query({
1670
1698
  cluster_id: clusterId,
1671
1699
  topic: 'CLUSTER_OPERATIONS',
@@ -1700,19 +1728,7 @@ class Orchestrator {
1700
1728
  }
1701
1729
  });
1702
1730
 
1703
- return {
1704
- clear: () => {
1705
- if (!watchdogTimer) {
1706
- return;
1707
- }
1708
- clearTimeout(watchdogTimer);
1709
- watchdogTimer = null;
1710
- const elapsed = completedAt ? Date.now() - completedAt : 0;
1711
- this._log(
1712
- `✅ CLUSTER_OPERATIONS received (${elapsed}ms after conductor completed) - watchdog cleared`
1713
- );
1714
- },
1715
- };
1731
+ return watchdog;
1716
1732
  }
1717
1733
 
1718
1734
  _registerClusterOperationsHandler(
@@ -2414,6 +2430,10 @@ class Orchestrator {
2414
2430
  return;
2415
2431
  }
2416
2432
  this.closed = true;
2433
+ for (const watchdog of this._conductorWatchdogs) {
2434
+ watchdog.dispose();
2435
+ }
2436
+ this._conductorWatchdogs.clear();
2417
2437
 
2418
2438
  for (const cluster of this.clusters.values()) {
2419
2439
  if (typeof cluster.snapshotter?.stop === 'function') {
package/src/preflight.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  const { loadSettings, getClaudeCommand } = require('../lib/settings.js');
22
22
  const {
23
23
  VALID_PROVIDERS,
24
+ getDefaultProviderId,
24
25
  getProviderMetadata,
25
26
  normalizeProviderName,
26
27
  resolveProviderCommand,
@@ -597,7 +598,7 @@ async function runPreflight(options = {}) {
597
598
  };
598
599
  }
599
600
  const providerName = normalizeProviderName(
600
- options.provider || settings.defaultProvider || 'claude'
601
+ options.provider || settings.defaultProvider || getDefaultProviderId()
601
602
  );
602
603
 
603
604
  const providerResult = validateProvider(providerName, options);
@@ -111,6 +111,7 @@ const providerSessionCapture = createProviderSessionCapture({
111
111
  requestedSessionId: persistedTask?.requestedResumeSessionId || null,
112
112
  initialSessionId: persistedTask?.sessionId || null,
113
113
  initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
114
+ disabled: config.structuredOutputRecovery === true,
114
115
  });
115
116
  let outputBuffer = '';
116
117
 
@@ -14,6 +14,10 @@ const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
14
14
  const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
15
15
  const SCHEMA_FILE_PATTERN =
16
16
  /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
17
+ const POLICY_DIRECTORY_PATTERN = /^zeroshot-gemini-policy-[A-Za-z0-9_-]+$/u;
18
+ const POLICY_FILE_PATTERN =
19
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.toml$/u;
20
+ const OPENCODE_CONFIG_DIRECTORY_PATTERN = /^zeroshot-opencode-config-[A-Za-z0-9_-]+$/u;
17
21
 
18
22
  function assertClosedCleanupMetadata(cleanupPath, metadata) {
19
23
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
@@ -59,6 +63,37 @@ function createCleanupPlan(commandSpec) {
59
63
  }
60
64
 
61
65
  function assertOwnedTempDirectory(cleanupPath, metadata) {
66
+ const isOpenCodeConfig =
67
+ metadata.provider === 'opencode' && metadata.reason === 'isolated-config';
68
+ if (isOpenCodeConfig) {
69
+ const canonical =
70
+ isAbsolute(cleanupPath) &&
71
+ resolve(cleanupPath) === cleanupPath &&
72
+ dirname(cleanupPath) === resolve(tmpdir()) &&
73
+ OPENCODE_CONFIG_DIRECTORY_PATTERN.test(basename(cleanupPath));
74
+ if (!canonical) {
75
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
76
+ }
77
+ let stat;
78
+ try {
79
+ stat = lstatSync(cleanupPath);
80
+ } catch (error) {
81
+ if (error?.code === 'ENOENT') return true;
82
+ throw error;
83
+ }
84
+ const tempRoot = realpathSync(tmpdir());
85
+ const realDirectory = realpathSync(cleanupPath);
86
+ if (
87
+ stat.isSymbolicLink() ||
88
+ !stat.isDirectory() ||
89
+ dirname(realDirectory) !== tempRoot ||
90
+ basename(realDirectory) !== basename(cleanupPath)
91
+ ) {
92
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
93
+ }
94
+ return false;
95
+ }
96
+
62
97
  if (
63
98
  metadata.provider !== 'claude' ||
64
99
  metadata.reason !== 'settings-overlay' ||
@@ -79,22 +114,33 @@ function assertOwnedTempDirectory(cleanupPath, metadata) {
79
114
  }
80
115
 
81
116
  function assertCanonicalSchemaPath(cleanupPath, metadata) {
117
+ const isCodexSchema =
118
+ metadata.kind === 'temp-file' &&
119
+ metadata.provider === 'codex' &&
120
+ metadata.reason === 'output-schema';
121
+ const isGeminiPolicy =
122
+ metadata.kind === 'temp-file' &&
123
+ metadata.provider === 'gemini' &&
124
+ metadata.reason === 'admin-policy';
82
125
  if (
83
- metadata.kind !== 'temp-file' ||
84
- metadata.provider !== 'codex' ||
85
- metadata.reason !== 'output-schema' ||
126
+ (!isCodexSchema && !isGeminiPolicy) ||
86
127
  !isAbsolute(cleanupPath) ||
87
128
  resolve(cleanupPath) !== cleanupPath
88
129
  ) {
89
- throw new Error(`Refusing unowned output-schema cleanup: ${cleanupPath}`);
130
+ throw new Error(
131
+ `Refusing unowned ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
132
+ );
90
133
  }
91
- const schemaDirectory = dirname(cleanupPath);
92
- if (
93
- dirname(schemaDirectory) !== resolve(tmpdir()) ||
94
- !SCHEMA_DIRECTORY_PATTERN.test(basename(schemaDirectory)) ||
95
- !SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
96
- ) {
97
- throw new Error(`Refusing non-canonical output-schema cleanup: ${cleanupPath}`);
134
+ const ownedDirectory = dirname(cleanupPath);
135
+ const matchesOwnedName = isCodexSchema
136
+ ? SCHEMA_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
137
+ SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
138
+ : POLICY_DIRECTORY_PATTERN.test(basename(ownedDirectory)) &&
139
+ POLICY_FILE_PATTERN.test(basename(cleanupPath));
140
+ if (dirname(ownedDirectory) !== resolve(tmpdir()) || !matchesOwnedName) {
141
+ throw new Error(
142
+ `Refusing non-canonical ${isGeminiPolicy ? 'admin-policy' : 'output-schema'} cleanup: ${cleanupPath}`
143
+ );
98
144
  }
99
145
  }
100
146
 
@@ -40,6 +40,7 @@ export async function runTask(prompt, options = {}) {
40
40
  jsonSchema,
41
41
  mcpConfig: options.mcpConfig,
42
42
  silentJsonOutput,
43
+ structuredOutputRecovery: options.structuredOutputRecovery === true,
43
44
  });
44
45
 
45
46
  console.log(chalk.green(`\n✓ Task spawned: ${chalk.cyan(task.id)}`));