@the-open-engine/zeroshot 6.31.3 → 6.32.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.
@@ -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.0",
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.0",
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.0",
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
+ };
package/src/preflight.js CHANGED
@@ -468,6 +468,31 @@ function validateProviderIsolationCapabilities(providerName, options) {
468
468
  return errors;
469
469
  }
470
470
 
471
+ function validateProviderExecutionSettings(providerName, settings, options) {
472
+ const metadata = getProviderMetadata(providerName);
473
+ if (!metadata.settingsValidator) return [];
474
+
475
+ const providerSettings = settings.providerSettings?.[providerName];
476
+ if (
477
+ !providerSettings ||
478
+ typeof providerSettings !== 'object' ||
479
+ Array.isArray(providerSettings)
480
+ ) {
481
+ return [];
482
+ }
483
+
484
+ const executionContext = options.requireDocker ? 'docker' : 'detached';
485
+ const error = metadata.settingsValidator(providerSettings, { executionContext });
486
+ if (!error) return [];
487
+
488
+ return [
489
+ formatError(`${metadata.displayName} configuration cannot run cluster agents`, error, [
490
+ metadata.authInstructions,
491
+ 'Or select a different provider with: zeroshot providers set-default <provider>',
492
+ ]),
493
+ ];
494
+ }
495
+
471
496
  function validateProvider(providerName, options) {
472
497
  let metadata;
473
498
  try {
@@ -623,7 +648,7 @@ async function runPreflight(options = {}) {
623
648
  const errors = [];
624
649
  const warnings = [];
625
650
 
626
- const settings = loadSettings();
651
+ const settings = options.settings || loadSettings();
627
652
 
628
653
  if (process.platform === 'win32') {
629
654
  return {
@@ -648,6 +673,7 @@ async function runPreflight(options = {}) {
648
673
 
649
674
  const providerResult = validateProvider(providerName, options);
650
675
  errors.push(...providerResult.errors);
676
+ errors.push(...validateProviderExecutionSettings(providerName, settings, options));
651
677
  warnings.push(...providerResult.warnings);
652
678
 
653
679
  // 4. Check issue provider CLI (if required)
@@ -151,21 +151,29 @@ class StatusFooter {
151
151
  }
152
152
 
153
153
  /**
154
- * Print text to stdout, coordinating with the render cycle.
155
- * When a render is in progress, queues output to prevent cursor corruption.
156
- * When no render is active, writes immediately.
157
- *
158
- * MUST be used instead of console.log() when status footer is active.
159
- * @param {string} text - Text to print (newline will be added)
154
+ * Print one logical line while coordinating with footer rendering.
155
+ * The caller owns line normalization; this method appends one newline.
156
+ * @param {string} text
160
157
  */
161
158
  print(text) {
159
+ this._queueOrWrite(`${text}\n`);
160
+ }
161
+
162
+ /**
163
+ * Write an exact streaming chunk while coordinating with footer rendering.
164
+ * @param {string} text
165
+ */
166
+ write(text) {
167
+ this._queueOrWrite(String(text));
168
+ }
169
+
170
+ /** @private */
171
+ _queueOrWrite(text) {
162
172
  if (this.isRendering) {
163
- // Queue for later - render() will flush after restoring cursor
164
173
  this.printQueue.push(text);
165
- } else {
166
- // Write immediately - no render in progress
167
- process.stdout.write(text + '\n');
174
+ return;
168
175
  }
176
+ process.stdout.write(text);
169
177
  }
170
178
 
171
179
  /**
@@ -176,8 +184,7 @@ class StatusFooter {
176
184
  _flushPrintQueue() {
177
185
  if (this.printQueue.length === 0) return;
178
186
 
179
- // Write all queued output
180
- const output = this.printQueue.map((text) => text + '\n').join('');
187
+ const output = this.printQueue.join('');
181
188
  this.printQueue = [];
182
189
  process.stdout.write(output);
183
190
  }