@the-open-engine/zeroshot 6.31.3 → 6.32.1

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 (47) hide show
  1. package/README.md +66 -98
  2. package/cli/index.js +251 -252
  3. package/cli/lib/setup-provider-readiness.js +86 -0
  4. package/cli/lib/setup-scanner-worker.js +120 -0
  5. package/cli/lib/setup-scanner.js +185 -0
  6. package/cli/lib/setup-wizard-input.js +146 -0
  7. package/cli/lib/setup-wizard-model.js +205 -0
  8. package/cli/lib/setup-wizard-plan-view.js +157 -0
  9. package/cli/lib/setup-wizard-scan-view.js +144 -0
  10. package/cli/lib/setup-wizard-terminal.js +237 -0
  11. package/cli/lib/setup-wizard-view.js +180 -0
  12. package/cli/lib/setup-wizard.js +281 -0
  13. package/cli/message-formatters-normal.js +14 -18
  14. package/cli/message-formatters-watch.js +53 -141
  15. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/adapters/codex.js +8 -2
  17. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  18. package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
  19. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  20. package/lib/agent-cli-provider/provider-registry.js +13 -3
  21. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  22. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  23. package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
  24. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  25. package/lib/agent-cli-provider/types.d.ts +2 -0
  26. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/types.js.map +1 -1
  28. package/lib/completion.js +102 -153
  29. package/lib/settings.js +10 -2
  30. package/lib/setup-apply.js +62 -55
  31. package/lib/setup-plan.js +32 -52
  32. package/lib/start-cluster.js +65 -25
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +3 -3
  35. package/scripts/postinstall.js +54 -0
  36. package/src/agent/agent-lifecycle.js +11 -1
  37. package/src/agent/agent-task-executor.js +25 -7
  38. package/src/agent/structured-output-error.js +42 -0
  39. package/src/agent-cli-provider/adapters/codex.ts +8 -12
  40. package/src/agent-cli-provider/provider-registry.ts +15 -3
  41. package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
  42. package/src/agent-cli-provider/types.ts +2 -0
  43. package/src/preflight.js +27 -1
  44. package/src/status-footer.js +19 -12
  45. package/task-lib/commands/list.js +90 -78
  46. package/task-lib/commands/status.js +97 -40
  47. package/task-lib/effective-status.js +52 -0
@@ -14,7 +14,7 @@
14
14
  const { resolveDecisionPath, domainFor, isConsumedPath, CONSUMED_PATHS } = require('./setup-plan');
15
15
  const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
16
16
  const { VALID_PROVIDERS } = require('./provider-names');
17
- const { VALID_MODELS, mapLegacyModelToLevel } = require('./settings');
17
+ const VALID_LEVELS = Object.freeze(['level1', 'level2', 'level3']);
18
18
  const {
19
19
  loadJournal,
20
20
  saveJournal,
@@ -38,10 +38,8 @@ function domainError(decisionId, value) {
38
38
  );
39
39
  }
40
40
 
41
- // Resolves a submitted decision value into the raw form actually stored at
42
- // its target settings path, validating it against #A's domain first.
43
- // (defaultIsolation's domain is worktree|docker|none, but its target path is
44
- // the boolean settings.defaultDocker — this is where that translation lives.)
41
+ // Resolve a submitted decision into the exact value stored at its canonical
42
+ // settings path, validating its decision domain before any write occurs.
45
43
  function convertDecisionValue({ decisionId, value, globalSettings, deps }) {
46
44
  switch (decisionId) {
47
45
  case 'defaultProvider':
@@ -52,7 +50,7 @@ function convertDecisionValue({ decisionId, value, globalSettings, deps }) {
52
50
 
53
51
  case 'defaultIsolation':
54
52
  if (!['worktree', 'docker', 'none'].includes(value)) throw domainError(decisionId, value);
55
- return value === 'docker';
53
+ return value;
56
54
 
57
55
  case 'allowLocalNoIsolation':
58
56
  if (typeof value !== 'boolean') throw domainError(decisionId, value);
@@ -88,24 +86,34 @@ function convertDecisionValue({ decisionId, value, globalSettings, deps }) {
88
86
  if (!['off', 'notify', 'auto'].includes(value)) throw domainError(decisionId, value);
89
87
  return value;
90
88
 
91
- default:
92
- if (decisionId.startsWith('providerLevel.')) {
93
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
94
- throw domainError(decisionId, value);
95
- }
96
- for (const key of ['min', 'default', 'max']) {
97
- if (!VALID_MODELS.includes(value[key])) throw domainError(decisionId, value);
98
- }
99
- const providerName = decisionId.slice('providerLevel.'.length);
100
- const existing = getNestedValue(globalSettings, `providerSettings.${providerName}`) || {};
101
- return {
102
- ...existing,
103
- minLevel: mapLegacyModelToLevel(value.min),
104
- defaultLevel: mapLegacyModelToLevel(value.default),
105
- maxLevel: mapLegacyModelToLevel(value.max),
106
- };
89
+ default: {
90
+ if (!decisionId.startsWith('providerLevel.')) {
91
+ throw new Error(`Unknown decision ID: ${decisionId}`);
107
92
  }
108
- throw new Error(`Unknown decision ID: ${decisionId}`);
93
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
94
+ throw domainError(decisionId, value);
95
+ }
96
+ const keys = Object.keys(value).sort();
97
+ const expectedKeys = ['defaultLevel', 'maxLevel', 'minLevel'];
98
+ if (
99
+ keys.length !== expectedKeys.length ||
100
+ keys.some((key, index) => key !== expectedKeys[index]) ||
101
+ expectedKeys.some((key) => !VALID_LEVELS.includes(value[key]))
102
+ ) {
103
+ throw domainError(decisionId, value);
104
+ }
105
+ const providerName = decisionId.slice('providerLevel.'.length);
106
+ if (!deps.VALID_PROVIDERS.includes(providerName)) throw domainError(decisionId, value);
107
+ const rank = (level) => VALID_LEVELS.indexOf(level);
108
+ if (
109
+ rank(value.minLevel) > rank(value.defaultLevel) ||
110
+ rank(value.defaultLevel) > rank(value.maxLevel)
111
+ ) {
112
+ throw domainError(decisionId, value);
113
+ }
114
+ const existing = getNestedValue(globalSettings, `providerSettings.${providerName}`) || {};
115
+ return { ...existing, ...value };
116
+ }
109
117
  }
110
118
  }
111
119
 
@@ -194,7 +202,6 @@ function applyWrite(write, { repoRoot, journal, deps }) {
194
202
  return write.scope;
195
203
  }
196
204
 
197
-
198
205
  // Phase 2: resolve each global outcome against the locked, freshly read state.
199
206
  // Repo-local settings remain intentionally outside the global settings lock.
200
207
  function writeResolvedDecisions(
@@ -252,35 +259,20 @@ function writeResolvedDecisions(
252
259
  return resolved.map((decision) => resultsById.get(decision.decisionId));
253
260
  }
254
261
 
255
- /**
256
- * Apply a decisions file `{ "<decisionId>": <value>, ... }` to settings.
257
- *
258
- * @param {Object} params
259
- * @param {string} params.decisionsPath - Path to the decisions JSON file.
260
- * @param {string} params.cwd - Working directory (for repo-scope settings lookup).
261
- * @param {boolean} [params.allowRiskyDefaults] - Required to store defaultDelivery='ship'.
262
- * @param {Object} [params.deps] - Injected dependencies (for testing).
263
- * @returns {Array<{decisionId: string, applied: boolean, from: *, to: *, skippedReason?: string}>}
264
- */
265
- function applyDecisions({ decisionsPath, cwd, allowRiskyDefaults = false, deps = {} }) {
266
- const resolvedDeps = { ...defaultApplyDeps(), ...deps };
267
-
268
- let input;
269
- try {
270
- input = JSON.parse(resolvedDeps.readFile(decisionsPath));
271
- } catch (err) {
272
- throw new Error(`Failed to read decisions file "${decisionsPath}": ${err.message}`);
273
- }
274
- if (!input || typeof input !== 'object' || Array.isArray(input)) {
275
- throw new Error('Decisions file must be a JSON object of { decisionId: value }');
262
+ function assertDecisionObject(decisions, label) {
263
+ if (!decisions || typeof decisions !== 'object' || Array.isArray(decisions)) {
264
+ throw new Error(`${label} must be a JSON object of { decisionId: value }`);
276
265
  }
266
+ }
277
267
 
268
+ /** Apply an in-memory decision object without temporary files. */
269
+ function applyDecisionValues({ decisions, cwd, allowRiskyDefaults = false, deps = {} }) {
270
+ assertDecisionObject(decisions, 'Decisions');
271
+ const resolvedDeps = { ...defaultApplyDeps(), ...deps };
278
272
  const globalSettings = resolvedDeps.loadSettings();
279
273
  const { repoRoot, settings: repoSettingsRaw } = resolvedDeps.readRepoSettings(cwd);
280
274
  const repoSettings = repoSettingsRaw || {};
281
-
282
- const resolved = resolveAndValidateDecisions(input, globalSettings, resolvedDeps);
283
-
275
+ const resolved = resolveAndValidateDecisions(decisions, globalSettings, resolvedDeps);
284
276
  const results = writeResolvedDecisions(resolved, {
285
277
  repoSettings,
286
278
  repoRoot,
@@ -289,22 +281,37 @@ function applyDecisions({ decisionsPath, cwd, allowRiskyDefaults = false, deps =
289
281
  deps: resolvedDeps,
290
282
  });
291
283
 
292
- // Never store provider-auth secrets: print the login command instead.
293
284
  const issueSourceApplied = results.find(
294
- (r) => r.decisionId === 'defaultIssueSource' && r.applied
285
+ (result) => result.decisionId === 'defaultIssueSource' && result.applied
295
286
  );
296
- if (issueSourceApplied && issueSourceApplied.to === 'github') {
287
+ if (issueSourceApplied?.to === 'github') {
297
288
  const auth = resolvedDeps.checkGhAuth();
298
- if (!auth || !auth.authenticated) {
299
- console.log('Run: gh auth login');
300
- }
289
+ if (!auth?.authenticated) console.log('Run: gh auth login');
301
290
  }
302
-
303
291
  return results;
304
292
  }
305
293
 
294
+ /** Apply a decisions JSON file through the shared object API. */
295
+ function applyDecisions({ decisionsPath, cwd, allowRiskyDefaults = false, deps = {} }) {
296
+ const resolvedDeps = { ...defaultApplyDeps(), ...deps };
297
+ let decisions;
298
+ try {
299
+ decisions = JSON.parse(resolvedDeps.readFile(decisionsPath));
300
+ } catch (error) {
301
+ throw new Error(`Failed to read decisions file "${decisionsPath}": ${error.message}`);
302
+ }
303
+ assertDecisionObject(decisions, 'Decisions file');
304
+ return applyDecisionValues({
305
+ decisions,
306
+ cwd,
307
+ allowRiskyDefaults,
308
+ deps: resolvedDeps,
309
+ });
310
+ }
311
+
306
312
  module.exports = {
307
313
  applyDecisions,
314
+ applyDecisionValues,
308
315
  resolveAndValidateDecisions,
309
316
  writeResolvedDecisions,
310
317
  assertSecretSafePath,
package/lib/setup-plan.js CHANGED
@@ -11,15 +11,13 @@
11
11
 
12
12
  const path = require('path');
13
13
 
14
- const SCHEMA_VERSION = 1;
14
+ const SCHEMA_VERSION = 2;
15
15
 
16
- // Canonical settings key each decision maps to. `defaultIsolation` and
17
- // `defaultDelivery` MUST stay pinned to the keys resolveEffectiveRunPlan
18
- // already reads (today: settings.defaultDocker) — see the canonical-path
19
- // rule in issue #605. Never introduce a second parallel key here.
16
+ // Canonical settings keys consumed by resolveEffectiveRunPlan. Never introduce
17
+ // a parallel setting or translate a decision into a differently named key.
20
18
  const DECISION_PATHS = {
21
19
  defaultProvider: { scope: 'global', path: 'defaultProvider' },
22
- defaultIsolation: { scope: 'global', path: 'defaultDocker' },
20
+ defaultIsolation: { scope: 'global', path: 'defaultIsolation' },
23
21
  allowLocalNoIsolation: { scope: 'global', path: 'allowLocalNoIsolation' },
24
22
  defaultDelivery: { scope: 'global', path: 'defaultDelivery' },
25
23
  defaultIssueSource: { scope: 'global', path: 'defaultIssueSource' },
@@ -33,13 +31,11 @@ function providerLevelDecisionId(providerName) {
33
31
  return `providerLevel.${providerName}`;
34
32
  }
35
33
 
36
- // Settings keys a run-mode resolver actually reads today. Shared by
37
- // buildProposedWrites below (never propose a write nobody will read) and by
38
- // lib/setup-apply.js (never perform a write nobody will read) — one
39
- // canonical answer to "is this key consumed?" so the two can't drift.
34
+ // Settings keys consumed by runtime resolvers. Shared by buildProposedWrites
35
+ // and setup apply so neither surface can advertise dead configuration.
40
36
  const CONSUMED_PATHS = new Set([
41
37
  'global:defaultProvider',
42
- 'global:defaultDocker',
38
+ 'global:defaultIsolation',
43
39
  'global:defaultDelivery',
44
40
  'global:defaultIssueSource',
45
41
  'global:dockerMounts',
@@ -77,7 +73,7 @@ function defaultDeps() {
77
73
  const { execSync } = require('../src/lib/safe-exec');
78
74
  const { listProviders, getProvider } = require('../src/providers');
79
75
  const { getProviderDefaults } = require('./provider-defaults');
80
- const { getDefaultProviderId } = require('./provider-names');
76
+ const { getDefaultProviderId, getProviderMetadata } = require('./provider-names');
81
77
  const packageJson = require('../package.json');
82
78
 
83
79
  return {
@@ -90,6 +86,7 @@ function defaultDeps() {
90
86
  getProvider,
91
87
  getProviderDefaults,
92
88
  getDefaultProviderId,
89
+ getProviderMetadata,
93
90
  getNodeVersion: () => process.version,
94
91
  getPackageVersion: () => packageJson.version,
95
92
  };
@@ -98,14 +95,8 @@ function defaultDeps() {
98
95
  function detectInstallSource(cwd, env) {
99
96
  if (env.npm_config_global === 'true') return 'npm-global';
100
97
  if (env.npm_execpath && /_npx|npx/.test(env.npm_execpath)) return 'npx';
101
- try {
102
- const ownNodeModules = path.join(__dirname, '..', 'node_modules');
103
- if (cwd && (cwd === path.join(__dirname, '..') || cwd.startsWith(ownNodeModules))) {
104
- return 'local';
105
- }
106
- } catch {
107
- // fall through to unknown
108
- }
98
+ const ownNodeModules = path.join(__dirname, '..', 'node_modules');
99
+ if (cwd && (cwd === path.join(__dirname, '..') || cwd.startsWith(ownNodeModules))) return 'local';
109
100
  return 'unknown';
110
101
  }
111
102
 
@@ -120,16 +111,20 @@ function buildNodeFacts({ cwd, env, deps }) {
120
111
  function buildProviderFacts(deps) {
121
112
  const providers = {};
122
113
  for (const name of deps.listProviders()) {
123
- let cliCommand = name;
114
+ const metadata = deps.getProviderMetadata(name);
115
+ const provider = deps.getProvider(name);
116
+ let available = false;
124
117
  try {
125
- cliCommand = deps.getProvider(name).cliCommand || name;
118
+ available = provider.isAvailable() === true;
126
119
  } catch {
127
- // Provider metadata unavailable — fall back to the provider name as the CLI command.
120
+ available = false;
128
121
  }
129
- const cliAvailable = deps.commandExists(cliCommand);
122
+ const cliCommand = provider.cliCommand || metadata.binary;
130
123
  providers[name] = {
131
- cliAvailable,
132
- path: cliAvailable ? deps.getCommandPath(cliCommand) : null,
124
+ available,
125
+ displayName: metadata.displayName,
126
+ installInstructions: metadata.installInstructions,
127
+ path: available && deps.commandExists(cliCommand) ? deps.getCommandPath(cliCommand) : null,
133
128
  };
134
129
  }
135
130
  return providers;
@@ -185,30 +180,19 @@ function inferPrBase(cwd, deps) {
185
180
 
186
181
  function buildProviderLevelRecommendation(name, deps) {
187
182
  const providerDefaults = deps.getProviderDefaults()[name] || {};
188
- const minLevel = providerDefaults.minLevel;
189
- const defaultLevel = providerDefaults.defaultLevel;
190
- const maxLevel = providerDefaults.maxLevel;
191
- const overrides = providerDefaults.levelOverrides || {};
192
-
193
- try {
194
- const provider = deps.getProvider(name);
195
- return {
196
- min: provider.resolveModelSpec(minLevel, overrides).model,
197
- default: provider.resolveModelSpec(defaultLevel, overrides).model,
198
- max: provider.resolveModelSpec(maxLevel, overrides).model,
199
- };
200
- } catch {
201
- return { min: null, default: null, max: null };
202
- }
183
+ return {
184
+ minLevel: providerDefaults.minLevel || 'level1',
185
+ defaultLevel: providerDefaults.defaultLevel || 'level2',
186
+ maxLevel: providerDefaults.maxLevel || 'level3',
187
+ };
203
188
  }
204
189
 
205
190
  function buildRecommendedAndRisk({ cwd, facts, env, deps }) {
206
191
  const recommended = {};
207
192
  const risk = {};
208
-
209
193
  const registryDefault = deps.getDefaultProviderId();
210
194
  recommended.defaultProvider = registryDefault;
211
- risk.defaultProvider = facts.providers[registryDefault]?.cliAvailable ? 'low' : 'medium';
195
+ risk.defaultProvider = facts.providers[registryDefault]?.available ? 'low' : 'medium';
212
196
 
213
197
  for (const name of Object.keys(facts.providers)) {
214
198
  recommended[providerLevelDecisionId(name)] = buildProviderLevelRecommendation(name, deps);
@@ -228,7 +212,6 @@ function buildRecommendedAndRisk({ cwd, facts, env, deps }) {
228
212
 
229
213
  recommended.allowLocalNoIsolation = false;
230
214
  risk.allowLocalNoIsolation = 'low';
231
-
232
215
  recommended.defaultDelivery = 'none';
233
216
  risk.defaultDelivery = 'low';
234
217
 
@@ -239,10 +222,8 @@ function buildRecommendedAndRisk({ cwd, facts, env, deps }) {
239
222
  const inferredPrBase = inferPrBase(cwd, deps);
240
223
  recommended.prBase = inferredPrBase || 'main';
241
224
  risk.prBase = inferredPrBase ? 'low' : 'medium';
242
-
243
225
  recommended.dockerMounts = ['gh', 'git', 'ssh'];
244
226
  risk.dockerMounts = 'low';
245
-
246
227
  recommended.dockerEnvPassthrough = [];
247
228
  risk.dockerEnvPassthrough = 'low';
248
229
 
@@ -301,9 +282,11 @@ function buildDecisions({ facts, settings, repoSettings, inferredIssueSource, in
301
282
  }
302
283
 
303
284
  function domainFor(decisionId) {
304
- if (decisionId.startsWith('providerLevel.')) return '{ min, default, max } of haiku|sonnet|opus';
285
+ if (decisionId.startsWith('providerLevel.')) {
286
+ return '{ minLevel, defaultLevel, maxLevel } of level1|level2|level3';
287
+ }
305
288
  const domains = {
306
- defaultProvider: 'claude | codex | gemini | opencode',
289
+ defaultProvider: 'registry provider id',
307
290
  defaultIsolation: 'worktree | docker | none',
308
291
  allowLocalNoIsolation: 'boolean',
309
292
  defaultDelivery: 'none | pr | ship',
@@ -326,10 +309,7 @@ function buildProposedWrites({ decisions, recommended }) {
326
309
  // no resolver reads would advertise a write that apply will always skip —
327
310
  // dead config. Only propose writes apply will actually perform.
328
311
  if (!isConsumedPath(target.scope, target.path)) continue;
329
- let to = recommended[decision.decisionId];
330
- if (decision.decisionId === 'defaultIsolation') {
331
- to = to === 'docker';
332
- }
312
+ const to = recommended[decision.decisionId];
333
313
  if (to === decision.currentValue) continue;
334
314
  writes.push({
335
315
  scope: target.scope,
@@ -198,24 +198,74 @@ function loadClusterConfig(orchestrator, configPath, settings = {}, providerOver
198
198
  return prepareClusterConfig(orchestrator.loadConfig(configPath), settings, providerOverride);
199
199
  }
200
200
 
201
+ const RUN_MODE_KEYS = ['docker', 'worktree', 'pr', 'ship', 'noIsolation', 'isolation'];
202
+
203
+ function hasExplicitRunMode(options) {
204
+ return RUN_MODE_KEYS.some(
205
+ (key) => options[key] === true || (key === 'isolation' && options[key] === false)
206
+ );
207
+ }
208
+
201
209
  function mergeRunOptions(options) {
202
210
  const envRunOptions = parseRunOptionsEnv();
203
- return envRunOptions ? { ...envRunOptions, ...options } : options;
211
+ if (!envRunOptions) return options;
212
+ if (!hasExplicitRunMode(options)) return { ...envRunOptions, ...options };
213
+ const withoutEnvMode = { ...envRunOptions };
214
+ for (const key of RUN_MODE_KEYS) delete withoutEnvMode[key];
215
+ return { ...withoutEnvMode, ...options };
216
+ }
217
+
218
+ function validateSavedRunModes(settings) {
219
+ const isolation = settings.defaultIsolation || 'none';
220
+ const delivery = settings.defaultDelivery || 'none';
221
+ if (!['none', 'worktree', 'docker'].includes(isolation)) {
222
+ throw new Error(`Invalid saved isolation mode: ${isolation}`);
223
+ }
224
+ if (!['none', 'pr', 'ship'].includes(delivery)) {
225
+ throw new Error(`Invalid saved delivery mode: ${delivery}`);
226
+ }
227
+ return { isolation, delivery };
228
+ }
229
+
230
+ function resolveEffectiveIsolation(options, savedIsolation) {
231
+ if (options.noIsolation === true || options.isolation === false) return 'none';
232
+ if (options.docker === true) return 'docker';
233
+ if (options.worktree === true || options.pr === true || options.ship === true) return 'worktree';
234
+ if (resolveEnvBool(process.env.ZEROSHOT_DOCKER) === true) return 'docker';
235
+ if (resolveEnvBool(process.env.ZEROSHOT_WORKTREE) === true) return 'worktree';
236
+ return savedIsolation;
237
+ }
238
+
239
+ function resolveEffectiveDelivery(options, savedDelivery) {
240
+ if (options.ship === true || options.autoMerge === true) return 'ship';
241
+ if (options.pr === true || resolveEnvBool(process.env.ZEROSHOT_PR) === true) return 'pr';
242
+ return savedDelivery;
204
243
  }
205
244
 
206
- // The single producer of the run plan for a cluster start: fold env + settings
207
- // into the flags, then resolve the canonical isolation/delivery/autoMerge plan.
208
- // buildStartOptions reads EVERY mode field off this one plan — no field
209
- // (isolation, worktree, autoPr, autoMerge, runMode) is derived independently.
210
- function resolveEffectiveRunPlan(mergedOptions, settings) {
245
+ function resolveEffectiveRunPlan(options = {}, settings = {}) {
246
+ const mergedOptions = mergeRunOptions(options);
247
+ const noIsolation = mergedOptions.noIsolation === true || mergedOptions.isolation === false;
248
+ const conflicts = ['docker', 'worktree', 'pr', 'ship'].filter(
249
+ (key) => mergedOptions[key] === true
250
+ );
251
+ if (noIsolation && conflicts.length > 0) {
252
+ throw new Error(`--no-isolation conflicts with --${conflicts.join(', --')}`);
253
+ }
254
+
255
+ const saved = validateSavedRunModes(settings);
256
+ let isolation = resolveEffectiveIsolation(mergedOptions, saved.isolation);
257
+ const delivery = resolveEffectiveDelivery(mergedOptions, saved.delivery);
258
+ if (delivery !== 'none' && isolation === 'none') {
259
+ if (noIsolation) {
260
+ throw new Error(`--no-isolation conflicts with saved delivery mode "${delivery}"`);
261
+ }
262
+ isolation = 'worktree';
263
+ }
211
264
  return resolveRunPlan({
212
- ...mergedOptions,
213
- docker: anyTruthy(
214
- mergedOptions.docker,
215
- process.env.ZEROSHOT_DOCKER === '1',
216
- settings.defaultDocker
217
- ),
218
- worktree: anyTruthy(mergedOptions.worktree, process.env.ZEROSHOT_WORKTREE === '1'),
265
+ docker: isolation === 'docker',
266
+ worktree: isolation === 'worktree',
267
+ pr: delivery === 'pr',
268
+ ship: delivery === 'ship',
219
269
  });
220
270
  }
221
271
 
@@ -255,7 +305,7 @@ function buildStartOptions({
255
305
  forceProvider,
256
306
  }) {
257
307
  const mergedOptions = mergeRunOptions(options);
258
- const plan = resolveEffectiveRunPlan(mergedOptions, settings);
308
+ const plan = resolveEffectiveRunPlan(options, settings);
259
309
  return buildStartOptionsFromPlan({
260
310
  clusterId,
261
311
  plan,
@@ -368,16 +418,6 @@ function resolveConfigOrThrow({
368
418
  return loadClusterConfig(orchestrator, resolvedPath, settings, providerOverride);
369
419
  }
370
420
 
371
- function applyDefaultDeliveryOptions(options, settings) {
372
- if (settings.defaultDelivery === 'pr') {
373
- return { ...options, pr: true };
374
- }
375
- if (settings.defaultDelivery === 'ship') {
376
- return { ...options, ship: true };
377
- }
378
- return options;
379
- }
380
-
381
421
  function startClusterWithInput(args, input) {
382
422
  const { orchestrator, config, configPath, configName, settings, providerOverride } = args;
383
423
  if (!orchestrator) {
@@ -393,7 +433,7 @@ function startClusterWithInput(args, input) {
393
433
  });
394
434
  const startOptions = buildStartOptions({
395
435
  clusterId: args.clusterId,
396
- options: applyDefaultDeliveryOptions(args.options || {}, settings),
436
+ options: args.options || {},
397
437
  settings,
398
438
  providerOverride,
399
439
  modelOverride: args.modelOverride,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.31.3",
3
+ "version": "6.32.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@the-open-engine/zeroshot",
9
- "version": "6.31.3",
9
+ "version": "6.32.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.31.3",
4
- "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
3
+ "version": "6.32.1",
4
+ "description": "Independent executor–verifier orchestration for software changes.",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
7
7
  "zeroshot": "./cli/index.js",
@@ -38,7 +38,7 @@
38
38
  "test:all": "npm run test && npm run test:e2e && npm run test:slow",
39
39
  "test:coverage": "c8 npm run test:unit",
40
40
  "test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
41
- "postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
41
+ "postinstall": "node scripts/postinstall.js",
42
42
  "start": "node cli/index.js",
43
43
  "typecheck": "tsc --noEmit && npm run typecheck:cluster && npm run typecheck:hosted-target && npm run typecheck:hosted-session && npm run typecheck:target",
44
44
  "typecheck:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.json",
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+
6
+ const LIFECYCLE_SCRIPTS = ['fix-node-pty-permissions.js', 'check-path.js'];
7
+ const SETUP_INVITATION = 'Run zeroshot to finish setup.\n';
8
+
9
+ function isTruthyEnvironmentFlag(value) {
10
+ if (typeof value !== 'string') return false;
11
+ return !['', '0', 'false'].includes(value.trim().toLowerCase());
12
+ }
13
+
14
+ function shouldPrintSetupInvitation(env) {
15
+ const globalInstall = env.npm_config_global === 'true' || env.npm_config_location === 'global';
16
+ return globalInstall && !isTruthyEnvironmentFlag(env.CI);
17
+ }
18
+
19
+ function runLifecycleScript(scriptName) {
20
+ const result = spawnSync(process.execPath, [path.join(__dirname, scriptName)], {
21
+ stdio: 'inherit',
22
+ });
23
+ if (result.error) throw result.error;
24
+ return result.status ?? 1;
25
+ }
26
+
27
+ function runPostinstall({
28
+ env = process.env,
29
+ stdout = process.stdout,
30
+ runScript = runLifecycleScript,
31
+ } = {}) {
32
+ for (const scriptName of LIFECYCLE_SCRIPTS) {
33
+ const status = runScript(scriptName);
34
+ if (status !== 0) return status;
35
+ }
36
+ if (shouldPrintSetupInvitation(env)) stdout.write(SETUP_INVITATION);
37
+ return 0;
38
+ }
39
+
40
+ if (require.main === module) {
41
+ try {
42
+ process.exitCode = runPostinstall();
43
+ } catch (error) {
44
+ console.warn(`[postinstall] Warning: ${error.message}`);
45
+ process.exitCode = 1;
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ LIFECYCLE_SCRIPTS,
51
+ SETUP_INVITATION,
52
+ runPostinstall,
53
+ shouldPrintSetupInvitation,
54
+ };
@@ -24,6 +24,10 @@ const { findPlatformMismatchReason } = require('./validation-platform');
24
24
  const { calculateRateLimitDelay, isRateLimitError } = require('./rate-limit-backoff');
25
25
  const { updateAgentProviderSession } = require('./provider-session');
26
26
  const { rebuildProviderSessionAfterCommit } = require('./agent-task-executor');
27
+ const {
28
+ buildStructuredOutputClusterFailure,
29
+ isStructuredOutputInvalidError,
30
+ } = require('./structured-output-error');
27
31
  const {
28
32
  commitRecordedOwnership,
29
33
  markCleanupRequired,
@@ -735,6 +739,7 @@ async function handleFinalFailure(agent, triggeringMessage, error, maxRetries) {
735
739
  const failureAttempts = error?.terminationAttempts ?? maxRetries;
736
740
  const unsupportedCapability =
737
741
  error?.code === 'unsupported-capability' && error?.permanent === true;
742
+ const structuredOutputInvalid = isStructuredOutputInvalidError(error);
738
743
  console.error(`
739
744
  ${'='.repeat(80)}`);
740
745
  console.error(`🔴🔴🔴 MAX RETRIES EXHAUSTED - AGENT: ${agent.id} 🔴🔴🔴`);
@@ -810,6 +815,9 @@ ${'='.repeat(80)}`);
810
815
  });
811
816
  }
812
817
 
818
+ if (structuredOutputInvalid) {
819
+ agent._publish(buildStructuredOutputClusterFailure(agent, error));
820
+ }
813
821
  if (unsupportedCapability) {
814
822
  agent._publish({
815
823
  topic: 'CLUSTER_FAILED',
@@ -880,6 +888,7 @@ ${'='.repeat(80)}`);
880
888
  capability: error.capability,
881
889
  }
882
890
  : {}),
891
+ ...(structuredOutputInvalid ? { code: error.code, details: error.details ?? null } : {}),
883
892
  timestamp: Date.now(),
884
893
  };
885
894
 
@@ -910,6 +919,7 @@ ${'='.repeat(80)}`);
910
919
  capability: error.capability,
911
920
  }
912
921
  : {}),
922
+ ...(structuredOutputInvalid ? { code: error.code, details: error.details ?? null } : {}),
913
923
  hookFailureContext: error.message.includes('Hook uses result')
914
924
  ? {
915
925
  taskId: agent.currentTaskId || 'UNKNOWN',
@@ -936,7 +946,7 @@ ${'='.repeat(80)}`);
936
946
  orchestrator: agent.orchestrator,
937
947
  });
938
948
 
939
- if (!error?.terminationExhausted && !unsupportedCapability) {
949
+ if (!error?.terminationExhausted && !unsupportedCapability && !structuredOutputInvalid) {
940
950
  agent.state = 'idle';
941
951
  }
942
952
  }