@the-open-engine/zeroshot 6.4.0 → 6.6.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 (146) hide show
  1. package/cli/commands/providers.js +11 -5
  2. package/cli/index.js +79 -16
  3. package/cli/lib/first-run.js +14 -3
  4. package/docker/zeroshot-cluster/Dockerfile +11 -1
  5. package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
  6. package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
  7. package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
  8. package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
  9. package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
  10. package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
  11. package/lib/agent-cli-provider/adapters/acp.js +373 -0
  12. package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
  13. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/adapters/claude.js +2 -29
  15. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  16. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
  17. package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
  18. package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
  19. package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
  20. package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
  21. package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
  22. package/lib/agent-cli-provider/adapters/copilot.js +176 -0
  23. package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
  24. package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
  25. package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
  26. package/lib/agent-cli-provider/adapters/gateway.js +161 -0
  27. package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
  28. package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
  29. package/lib/agent-cli-provider/adapters/gemini.js +9 -2
  30. package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
  31. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  32. package/lib/agent-cli-provider/adapters/index.js +6 -34
  33. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  34. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  35. package/lib/agent-cli-provider/adapters/opencode.js +5 -1
  36. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  37. package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
  38. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
  39. package/lib/agent-cli-provider/adapters/pi.js +331 -0
  40. package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
  41. package/lib/agent-cli-provider/claude-command.d.ts +6 -0
  42. package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
  43. package/lib/agent-cli-provider/claude-command.js +43 -0
  44. package/lib/agent-cli-provider/claude-command.js.map +1 -0
  45. package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
  46. package/lib/agent-cli-provider/contract-actions.js +13 -17
  47. package/lib/agent-cli-provider/contract-actions.js.map +1 -1
  48. package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
  49. package/lib/agent-cli-provider/contract-invoke.js +10 -3
  50. package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
  51. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  52. package/lib/agent-cli-provider/contract-options.js +57 -2
  53. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  54. package/lib/agent-cli-provider/contract-support.d.ts +1 -0
  55. package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
  56. package/lib/agent-cli-provider/contract-support.js +25 -3
  57. package/lib/agent-cli-provider/contract-support.js.map +1 -1
  58. package/lib/agent-cli-provider/errors.d.ts +1 -0
  59. package/lib/agent-cli-provider/errors.d.ts.map +1 -1
  60. package/lib/agent-cli-provider/errors.js +14 -5
  61. package/lib/agent-cli-provider/errors.js.map +1 -1
  62. package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
  63. package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
  64. package/lib/agent-cli-provider/gateway-client.js +135 -0
  65. package/lib/agent-cli-provider/gateway-client.js.map +1 -0
  66. package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
  67. package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
  68. package/lib/agent-cli-provider/gateway-runner.js +294 -0
  69. package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
  70. package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
  71. package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
  72. package/lib/agent-cli-provider/gateway-tools.js +495 -0
  73. package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
  74. package/lib/agent-cli-provider/index.d.ts +3 -2
  75. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  76. package/lib/agent-cli-provider/index.js +14 -1
  77. package/lib/agent-cli-provider/index.js.map +1 -1
  78. package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
  79. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  80. package/lib/agent-cli-provider/invoke-evidence.js +17 -3
  81. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  82. package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
  83. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
  84. package/lib/agent-cli-provider/provider-registry.js +411 -0
  85. package/lib/agent-cli-provider/provider-registry.js.map +1 -0
  86. package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
  87. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  88. package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
  89. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  90. package/lib/agent-cli-provider/types.d.ts +106 -4
  91. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  92. package/lib/agent-cli-provider/types.js.map +1 -1
  93. package/lib/docker-config.js +24 -13
  94. package/lib/provider-defaults.js +15 -4
  95. package/lib/provider-detection.js +2 -0
  96. package/lib/provider-names.js +32 -12
  97. package/lib/repo-settings.js +15 -1
  98. package/lib/settings/claude-auth.js +3 -6
  99. package/lib/settings.js +17 -2
  100. package/lib/setup-apply.js +300 -0
  101. package/lib/setup-journal.js +109 -0
  102. package/lib/setup-plan.js +406 -0
  103. package/lib/setup-undo.js +88 -0
  104. package/lib/start-cluster.js +12 -1
  105. package/lib/stream-json-parser.js +7 -5
  106. package/package.json +31 -1
  107. package/scripts/assert-release-published.js +53 -0
  108. package/scripts/live-provider-smoke.js +221 -0
  109. package/scripts/release-preflight.js +236 -0
  110. package/src/agent/agent-lifecycle.js +5 -4
  111. package/src/agent/agent-quality-gate-schema.js +2 -2
  112. package/src/agent/agent-task-executor.js +41 -2
  113. package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
  114. package/src/agent-cli-provider/adapters/acp.ts +493 -0
  115. package/src/agent-cli-provider/adapters/claude.ts +2 -32
  116. package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
  117. package/src/agent-cli-provider/adapters/copilot.ts +231 -0
  118. package/src/agent-cli-provider/adapters/gateway.ts +186 -0
  119. package/src/agent-cli-provider/adapters/gemini.ts +9 -2
  120. package/src/agent-cli-provider/adapters/index.ts +5 -36
  121. package/src/agent-cli-provider/adapters/opencode.ts +4 -1
  122. package/src/agent-cli-provider/adapters/pi.ts +410 -0
  123. package/src/agent-cli-provider/claude-command.ts +47 -0
  124. package/src/agent-cli-provider/contract-actions.ts +14 -17
  125. package/src/agent-cli-provider/contract-invoke.ts +12 -3
  126. package/src/agent-cli-provider/contract-options.ts +72 -3
  127. package/src/agent-cli-provider/contract-support.ts +30 -3
  128. package/src/agent-cli-provider/errors.ts +14 -4
  129. package/src/agent-cli-provider/gateway-client.ts +170 -0
  130. package/src/agent-cli-provider/gateway-runner.ts +353 -0
  131. package/src/agent-cli-provider/gateway-tools.ts +616 -0
  132. package/src/agent-cli-provider/index.ts +28 -0
  133. package/src/agent-cli-provider/invoke-evidence.ts +24 -3
  134. package/src/agent-cli-provider/provider-registry.ts +515 -0
  135. package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
  136. package/src/agent-cli-provider/types.ts +145 -4
  137. package/src/config-validator.js +33 -15
  138. package/src/isolation-manager.js +124 -19
  139. package/src/orchestrator.js +23 -10
  140. package/src/preflight.js +83 -30
  141. package/src/providers/capabilities.js +15 -42
  142. package/src/providers/index.js +52 -72
  143. package/src/worktree-claude-config.js +17 -0
  144. package/task-lib/commands/run.js +1 -0
  145. package/task-lib/provider-helper-runtime.js +11 -0
  146. package/task-lib/runner.js +7 -0
@@ -16,8 +16,13 @@ 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 } = require('../lib/provider-names');
20
- const { resolveMounts, resolveEnvs, expandEnvPatterns } = require('../lib/docker-config');
19
+ const { normalizeProviderName, getProviderMetadata } = require('../lib/provider-names');
20
+ const {
21
+ MOUNT_PRESETS,
22
+ resolveMounts,
23
+ resolveEnvs,
24
+ expandEnvPatterns,
25
+ } = require('../lib/docker-config');
21
26
  const { getProvider } = require('./providers');
22
27
  const { readRepoSettings } = require('../lib/repo-settings');
23
28
  const { provisionClaudeCredentials } = require('./claude-credentials');
@@ -98,6 +103,24 @@ function resolveWorktreeSetupTimeoutMs(repoSettings = {}, options = {}) {
98
103
 
99
104
  const DEFAULT_IMAGE = 'zeroshot-cluster-base';
100
105
 
106
+ /**
107
+ * Shell command that installs a provider's CLI inside the cluster image, or null when the
108
+ * provider is baked into the base image (e.g. Claude) or has no single-command installer.
109
+ * Sourced from the provider registry (docker.install) so nothing here is provider-specific.
110
+ * @param {string} providerName
111
+ * @returns {string|null}
112
+ */
113
+ function providerDockerInstall(providerName) {
114
+ if (!providerName) return null;
115
+ try {
116
+ const metadata = getProviderMetadata(providerName);
117
+ const install = metadata && metadata.docker && metadata.docker.install;
118
+ return typeof install === 'string' && install.trim() ? install.trim() : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
101
124
  class IsolationManager {
102
125
  constructor(options = {}) {
103
126
  this.image = options.image || DEFAULT_IMAGE;
@@ -171,7 +194,13 @@ class IsolationManager {
171
194
  clusterConfigDir,
172
195
  });
173
196
 
174
- const mountedHosts = this._applyCredentialMounts(args, config, settings, containerHome);
197
+ const mountedHosts = this._applyCredentialMounts(
198
+ args,
199
+ config,
200
+ settings,
201
+ containerHome,
202
+ providerName
203
+ );
175
204
  this._warnMissingProviderCredentials(providerName, mountedHosts, config, containerHome);
176
205
 
177
206
  args.push('-w', '/workspace', image, 'tail', '-f', '/dev/null');
@@ -285,13 +314,25 @@ class IsolationManager {
285
314
  return settings.dockerMounts;
286
315
  }
287
316
 
288
- _applyCredentialMounts(args, config, settings, containerHome) {
317
+ // Auto-activate the running provider's own credential preset (mount + env) so `--docker` works
318
+ // without listing it in dockerMounts. Claude is mounted separately, so skip it.
319
+ _withActiveProviderPreset(mountConfig, providerName) {
320
+ if (!providerName || providerName === 'claude') return mountConfig;
321
+ if (!MOUNT_PRESETS[providerName]) return mountConfig;
322
+ if (mountConfig.some((item) => item === providerName)) return mountConfig;
323
+ return [...mountConfig, providerName];
324
+ }
325
+
326
+ _applyCredentialMounts(args, config, settings, containerHome, providerName) {
289
327
  const mountedHosts = [];
290
328
  if (config.noMounts) {
291
329
  return mountedHosts;
292
330
  }
293
331
 
294
- const mountConfig = this._resolveMountConfig(config, settings);
332
+ const mountConfig = this._withActiveProviderPreset(
333
+ this._resolveMountConfig(config, settings),
334
+ providerName
335
+ );
295
336
  const mounts = resolveMounts(mountConfig, { containerHome });
296
337
  const claudeContainerPath = path.posix.join(containerHome, '.claude');
297
338
 
@@ -363,16 +404,40 @@ class IsolationManager {
363
404
  return;
364
405
  }
365
406
 
407
+ const metadata = getProviderMetadata(providerName);
366
408
  const provider = getProvider(providerName);
409
+
410
+ // An env token (e.g. COPILOT_GITHUB_TOKEN) is a complete credential on its own.
411
+ const credentialEnvKeys = metadata.credentialEnvKeys || [];
412
+ if (credentialEnvKeys.some((key) => process.env[key])) {
413
+ return;
414
+ }
415
+
416
+ // A mount only counts if it carries the secret (credentialInMount !== false).
417
+ const credentialInMount = metadata.docker && metadata.docker.credentialInMount === false;
367
418
  const credentialPaths = provider.getCredentialPaths ? provider.getCredentialPaths() : [];
368
419
  const expandedCreds = credentialPaths.map((cred) => expandHomePath(cred));
369
- const hasCredentialMount = mountedHosts.some((hostPath) =>
370
- expandedCreds.some(
371
- (credPath) => pathContains(hostPath, credPath) || pathContains(credPath, hostPath)
372
- )
373
- );
420
+ const hasCredentialMount =
421
+ !credentialInMount &&
422
+ mountedHosts.some((hostPath) =>
423
+ expandedCreds.some(
424
+ (credPath) => pathContains(hostPath, credPath) || pathContains(credPath, hostPath)
425
+ )
426
+ );
427
+ if (hasCredentialMount) {
428
+ return;
429
+ }
430
+
431
+ if (credentialInMount && credentialEnvKeys.length > 0) {
432
+ console.warn(
433
+ `[IsolationManager] ⚠️ ${provider.displayName} could not find credentials for Docker. ` +
434
+ `Its login token is not stored in a mountable file — export one of ` +
435
+ `${credentialEnvKeys.join(', ')} before running with --docker.`
436
+ );
437
+ return;
438
+ }
374
439
 
375
- if (!hasCredentialMount && expandedCreds.length > 0) {
440
+ if (expandedCreds.length > 0) {
376
441
  const exampleHost = credentialPaths[0];
377
442
  const exampleContainer = exampleHost.replace(/^~(?=\/|$)/, containerHome);
378
443
  const mountNote = config.noMounts ? 'Credential mounts are disabled. ' : '';
@@ -1230,13 +1295,40 @@ class IsolationManager {
1230
1295
  }
1231
1296
  }
1232
1297
 
1298
+ /**
1299
+ * Resolve the cluster image tag for a provider. Providers baked into the base image (e.g.
1300
+ * Claude) run on the base image directly; providers with a `docker.install` command get a
1301
+ * per-provider image variant `<baseImage>-<providerId>` whose install step is a Docker-cached
1302
+ * layer (built once, reused thereafter).
1303
+ * @param {string} providerName
1304
+ * @param {string} [baseImage]
1305
+ * @returns {string}
1306
+ */
1307
+ static imageForProvider(providerName, baseImage = DEFAULT_IMAGE) {
1308
+ if (!providerDockerInstall(providerName)) {
1309
+ return baseImage;
1310
+ }
1311
+ return `${baseImage}-${normalizeProviderName(providerName)}`;
1312
+ }
1313
+
1314
+ /**
1315
+ * Docker `--build-arg` values that install a provider's CLI into its image variant, or [] when
1316
+ * the provider is baked into the base image or has no installer.
1317
+ * @param {string} providerName
1318
+ * @returns {string[]}
1319
+ */
1320
+ static providerBuildArgs(providerName) {
1321
+ const install = providerDockerInstall(providerName);
1322
+ return install ? [`PROVIDER_INSTALL=${install}`] : [];
1323
+ }
1324
+
1233
1325
  /**
1234
1326
  * Build the Docker image with retry logic
1235
1327
  * @param {string} [image] - Image name to build
1236
1328
  * @param {number} [maxRetries=3] - Maximum retry attempts
1237
1329
  * @returns {Promise<void>}
1238
1330
  */
1239
- static async buildImage(image = DEFAULT_IMAGE, maxRetries = 3) {
1331
+ static async buildImage(image = DEFAULT_IMAGE, maxRetries = 3, buildArgs = []) {
1240
1332
  // Repository root is one level up from src/
1241
1333
  const repoRoot = path.join(__dirname, '..');
1242
1334
  const dockerfilePath = path.join(repoRoot, 'docker', 'zeroshot-cluster', 'Dockerfile');
@@ -1245,6 +1337,12 @@ class IsolationManager {
1245
1337
  throw new Error(`Dockerfile not found at ${dockerfilePath}`);
1246
1338
  }
1247
1339
 
1340
+ // Each buildArg becomes a `--build-arg KEY=VALUE` pair (e.g. the per-provider install command).
1341
+ const buildArgFlags = [];
1342
+ for (const arg of buildArgs) {
1343
+ buildArgFlags.push('--build-arg', arg);
1344
+ }
1345
+
1248
1346
  console.log(`[IsolationManager] Building Docker image '${image}'...`);
1249
1347
 
1250
1348
  const baseDelay = 3000; // 3 seconds
@@ -1253,11 +1351,18 @@ class IsolationManager {
1253
1351
  try {
1254
1352
  // CRITICAL: Run from repo root so build context includes package.json and src/
1255
1353
  // Use -f flag to specify Dockerfile location
1256
- runSync('docker', ['build', '-f', 'docker/zeroshot-cluster/Dockerfile', '-t', image, '.'], {
1257
- cwd: repoRoot,
1258
- encoding: 'utf8',
1259
- stdio: 'inherit',
1260
- });
1354
+ runSync(
1355
+ 'docker',
1356
+ ['build', '-f', 'docker/zeroshot-cluster/Dockerfile', ...buildArgFlags, '-t', image, '.'],
1357
+ {
1358
+ cwd: repoRoot,
1359
+ encoding: 'utf8',
1360
+ stdio: 'inherit',
1361
+ // No timeout: image builds legitimately take many minutes (apt, tool downloads,
1362
+ // provider install). runSync's 30s default would kill every build (ETIMEDOUT).
1363
+ timeout: 0,
1364
+ }
1365
+ );
1261
1366
 
1262
1367
  console.log(`[IsolationManager] ✓ Image '${image}' built successfully`);
1263
1368
  return;
@@ -1284,7 +1389,7 @@ class IsolationManager {
1284
1389
  * @param {boolean} [autoBuild=true] - Auto-build if missing
1285
1390
  * @returns {Promise<void>}
1286
1391
  */
1287
- static async ensureImage(image = DEFAULT_IMAGE, autoBuild = true) {
1392
+ static async ensureImage(image = DEFAULT_IMAGE, autoBuild = true, buildArgs = []) {
1288
1393
  if (this.imageExists(image)) {
1289
1394
  return;
1290
1395
  }
@@ -1297,7 +1402,7 @@ class IsolationManager {
1297
1402
  }
1298
1403
 
1299
1404
  console.log(`[IsolationManager] Image '${image}' not found, building automatically...`);
1300
- await this.buildImage(image);
1405
+ await this.buildImage(image, 3, buildArgs);
1301
1406
  }
1302
1407
 
1303
1408
  /**
@@ -1138,11 +1138,12 @@ class Orchestrator {
1138
1138
  const messageBus = new MessageBus(ledger);
1139
1139
 
1140
1140
  // Handle isolation mode (Docker container OR git worktree)
1141
- const { isolationManager, containerId, worktreeInfo } = await this._initializeIsolation(
1142
- options,
1143
- config,
1144
- clusterId
1145
- );
1141
+ const {
1142
+ isolationManager,
1143
+ containerId,
1144
+ worktreeInfo,
1145
+ image: resolvedIsolationImage,
1146
+ } = await this._initializeIsolation(options, config, clusterId);
1146
1147
  let requiredQualityGates = resolveClusterRequiredQualityGates(config, options);
1147
1148
  let commandProofs = resolveClusterCommandProofs(config, options);
1148
1149
  options.requiredQualityGates = requiredQualityGates;
@@ -1188,7 +1189,9 @@ class Orchestrator {
1188
1189
  ? {
1189
1190
  enabled: true,
1190
1191
  containerId,
1191
- image: options.isolationImage || 'zeroshot-cluster-base',
1192
+ // Provider-specific image variant (resolved in _initializeIsolation) so resume
1193
+ // recreates the container on the same image that has the provider CLI installed.
1194
+ image: resolvedIsolationImage || options.isolationImage || 'zeroshot-cluster-base',
1192
1195
  manager: isolationManager,
1193
1196
  workDir: options.cwd || process.cwd(), // Persisted for resume
1194
1197
  }
@@ -1729,20 +1732,30 @@ class Orchestrator {
1729
1732
  let isolationManager = null;
1730
1733
  let containerId = null;
1731
1734
  let worktreeInfo = null;
1735
+ let isolationImage = null;
1732
1736
 
1733
1737
  if (options.isolation) {
1734
1738
  if (!IsolationManager.isDockerAvailable()) {
1735
1739
  throw new Error('Docker is not available. Install Docker to use --docker mode.');
1736
1740
  }
1737
1741
 
1738
- const image = options.isolationImage || 'zeroshot-cluster-base';
1739
- await IsolationManager.ensureImage(image);
1742
+ // Resolve the provider first so the cluster runs on that provider's image variant, which
1743
+ // installs the provider CLI as a Docker-cached layer. Providers baked into the base image
1744
+ // (e.g. Claude) resolve back to the base image unchanged.
1745
+ const providerName = this._resolveClusterProvider(config);
1746
+ const baseImage = options.isolationImage || 'zeroshot-cluster-base';
1747
+ const image = IsolationManager.imageForProvider(providerName, baseImage);
1748
+ await IsolationManager.ensureImage(
1749
+ image,
1750
+ true,
1751
+ IsolationManager.providerBuildArgs(providerName)
1752
+ );
1753
+ isolationImage = image;
1740
1754
 
1741
1755
  isolationManager = new IsolationManager({ image });
1742
1756
  this._log(`[Orchestrator] Starting cluster in isolation mode (image: ${image})`);
1743
1757
 
1744
1758
  const workDir = options.cwd || process.cwd();
1745
- const providerName = this._resolveClusterProvider(config);
1746
1759
  containerId = await isolationManager.createContainer(clusterId, {
1747
1760
  workDir,
1748
1761
  image,
@@ -1770,7 +1783,7 @@ class Orchestrator {
1770
1783
  this._log(`[Orchestrator] Branch: ${worktreeInfo.branch}`);
1771
1784
  }
1772
1785
 
1773
- return { isolationManager, containerId, worktreeInfo };
1786
+ return { isolationManager, containerId, worktreeInfo, image: isolationImage };
1774
1787
  }
1775
1788
 
1776
1789
  _applyAutoPrConfig(config, inputData, options) {
package/src/preflight.js CHANGED
@@ -19,7 +19,12 @@ const {
19
19
  resolveClaudeAuth,
20
20
  } = require('../lib/settings/claude-auth.js');
21
21
  const { loadSettings, getClaudeCommand } = require('../lib/settings.js');
22
- const { normalizeProviderName } = require('../lib/provider-names');
22
+ const {
23
+ VALID_PROVIDERS,
24
+ getProviderMetadata,
25
+ normalizeProviderName,
26
+ resolveProviderCommand,
27
+ } = require('../lib/provider-names');
23
28
  const { detectGitContext } = require('../lib/git-remote-utils');
24
29
  const { readKeychainCredentials } = require('./claude-credentials');
25
30
 
@@ -343,10 +348,21 @@ function buildClaudeCommand(options) {
343
348
  return options.claudeCommand || [command, ...args].join(' ');
344
349
  }
345
350
 
351
+ function buildRegistryInstallRecovery(metadata, command) {
352
+ return [
353
+ ...metadata.installInstructions.split('\n').filter(Boolean),
354
+ `Then run: ${command} --version`,
355
+ ];
356
+ }
357
+
346
358
  function validateClaudeProvider(options) {
347
359
  const errors = [];
348
360
  const warnings = [];
361
+ const metadata = getProviderMetadata('claude');
349
362
  const claudeCommand = buildClaudeCommand(options);
363
+ const claudeCommandParts = claudeCommand.trim().split(/\s+/);
364
+ const usesDefaultClaudeCommand =
365
+ claudeCommandParts.length === 1 && claudeCommandParts[0] === metadata.binary;
350
366
 
351
367
  const claude = getClaudeVersion(claudeCommand);
352
368
  if (!claude.installed) {
@@ -354,12 +370,8 @@ function validateClaudeProvider(options) {
354
370
  formatError(
355
371
  'Claude command not available',
356
372
  claude.error,
357
- claudeCommand === 'claude'
358
- ? [
359
- 'Install Claude CLI: npm install -g @anthropic-ai/claude-code',
360
- 'Or: brew install claude (macOS)',
361
- 'Then run: claude --version',
362
- ]
373
+ usesDefaultClaudeCommand
374
+ ? buildRegistryInstallRecovery(metadata, metadata.binary)
363
375
  : [
364
376
  `Command '${claudeCommand}' not found`,
365
377
  'Check settings: zeroshot settings',
@@ -404,41 +416,82 @@ function validateCliProvider(command, title, detail, recovery) {
404
416
  return { errors, warnings: [] };
405
417
  }
406
418
 
407
- function validateProvider(providerName, options) {
408
- const validatorByProvider = {
409
- claude: () => validateClaudeProvider(options),
410
- codex: () =>
411
- validateCliProvider('codex', 'Codex CLI not available', 'Command "codex" not installed', [
412
- 'Install Codex CLI: npm install -g @openai/codex',
413
- 'Then run: codex --version',
414
- ]),
415
- gemini: () =>
416
- validateCliProvider('gemini', 'Gemini CLI not available', 'Command "gemini" not installed', [
417
- 'Install Gemini CLI: npm install -g @google/gemini-cli',
418
- 'Then run: gemini --version',
419
- ]),
420
- opencode: () =>
421
- validateCliProvider(
422
- 'opencode',
423
- 'Opencode CLI not available',
424
- 'Command "opencode" not installed',
425
- ['Install Opencode CLI: see https://opencode.ai', 'Then run: opencode --version']
419
+ function validateGatewayProvider() {
420
+ const { getProvider } = require('./providers');
421
+ const provider = getProvider('gateway');
422
+ if (provider.isAvailable()) {
423
+ return { errors: [], warnings: [] };
424
+ }
425
+ return {
426
+ errors: [
427
+ formatError(
428
+ 'Gateway provider not configured',
429
+ 'providerSettings.gateway must define baseUrl, apiKey, model, and toolPolicy before gateway can run.',
430
+ [
431
+ 'Run: zeroshot settings',
432
+ 'Set providerSettings.gateway.baseUrl to your OpenAI-compatible endpoint base URL',
433
+ 'Set providerSettings.gateway.apiKey and providerSettings.gateway.model',
434
+ 'Set providerSettings.gateway.toolPolicy.roots and toolPolicy.commands explicitly',
435
+ ]
426
436
  ),
437
+ ],
438
+ warnings: [],
427
439
  };
440
+ }
428
441
 
429
- const validator = validatorByProvider[providerName];
430
- if (!validator) {
442
+ function validateProvider(providerName, options) {
443
+ let metadata;
444
+ try {
445
+ metadata = getProviderMetadata(providerName);
446
+ } catch {
431
447
  return {
432
448
  errors: [
433
449
  formatError('Unknown provider', `Provider "${providerName}" is not supported`, [
434
- 'Use claude, codex, gemini, or opencode',
450
+ `Use ${VALID_PROVIDERS.join(', ')}`,
435
451
  ]),
436
452
  ],
437
453
  warnings: [],
438
454
  };
439
455
  }
440
456
 
441
- return validator();
457
+ if (metadata.command.kind === 'configured-claude') {
458
+ return validateClaudeProvider(options);
459
+ }
460
+ if (providerName === 'gateway') {
461
+ return validateGatewayProvider();
462
+ }
463
+
464
+ return validateRegistryCliProvider(providerName);
465
+ }
466
+
467
+ function validateRegistryCliProvider(providerName) {
468
+ const metadata = getProviderMetadata(providerName);
469
+ const { command } = resolveProviderCommand(providerName);
470
+ const installSteps = metadata.installInstructions.split('\n').filter(Boolean);
471
+ if (!commandExists(command)) {
472
+ return validateCliProvider(
473
+ command,
474
+ `${metadata.displayName} CLI not available`,
475
+ `Command "${command}" not installed`,
476
+ [...installSteps, `Then run: ${command} --version`]
477
+ );
478
+ }
479
+
480
+ const { getProvider } = require('./providers');
481
+ if (getProvider(providerName).isAvailable()) {
482
+ return { errors: [], warnings: [] };
483
+ }
484
+
485
+ return {
486
+ errors: [
487
+ formatError(
488
+ `${metadata.displayName} CLI not available`,
489
+ `Command "${command}" is installed but did not produce usable --help/--version output`,
490
+ [...installSteps, `Then run: ${command} --version`]
491
+ ),
492
+ ],
493
+ warnings: [],
494
+ };
442
495
  }
443
496
 
444
497
  function validateGhRequirement() {
@@ -1,48 +1,21 @@
1
- const { normalizeProviderName } = require('../../lib/provider-names');
1
+ const {
2
+ PROVIDER_CAPABILITIES,
3
+ normalizeProviderName,
4
+ providerSupportsCapability,
5
+ } = require('../../lib/provider-names');
2
6
 
3
- const CAPABILITIES = {
4
- claude: {
5
- dockerIsolation: true,
6
- worktreeIsolation: true,
7
- mcpServers: true,
8
- jsonSchema: true,
9
- streamJson: true,
10
- thinkingMode: true,
11
- reasoningEffort: false,
12
- },
13
- codex: {
14
- dockerIsolation: true,
15
- worktreeIsolation: true,
16
- mcpServers: true,
17
- jsonSchema: true,
18
- streamJson: true,
19
- thinkingMode: true,
20
- reasoningEffort: true,
21
- },
22
- gemini: {
23
- dockerIsolation: true,
24
- worktreeIsolation: true,
25
- mcpServers: true,
26
- jsonSchema: 'experimental',
27
- streamJson: true,
28
- thinkingMode: true,
29
- reasoningEffort: false,
30
- },
31
- opencode: {
32
- dockerIsolation: true,
33
- worktreeIsolation: true,
34
- mcpServers: true,
35
- jsonSchema: 'experimental',
36
- streamJson: true,
37
- thinkingMode: true,
38
- reasoningEffort: true,
39
- },
40
- };
7
+ const CAPABILITIES = Object.freeze(
8
+ Object.fromEntries(
9
+ Object.entries(PROVIDER_CAPABILITIES).map(([provider, capabilities]) => [
10
+ provider,
11
+ Object.freeze({ ...capabilities }),
12
+ ])
13
+ )
14
+ );
41
15
 
42
16
  function checkCapability(provider, capability) {
43
- const caps = CAPABILITIES[normalizeProviderName(provider)];
44
- if (!caps) return false;
45
- return caps[capability] === true;
17
+ if (!provider) return false;
18
+ return providerSupportsCapability(provider, capability);
46
19
  }
47
20
 
48
21
  function warnIfExperimental(provider, capability) {