@the-open-engine/zeroshot 6.3.0 → 6.5.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.
- package/cli/commands/providers.js +11 -5
- package/cli/index.js +83 -17
- package/cli/lib/first-run.js +14 -3
- package/docker/zeroshot-cluster/Dockerfile +11 -1
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.js +373 -0
- package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +2 -29
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.js +176 -0
- package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.js +161 -0
- package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +9 -2
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +6 -34
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +5 -1
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/pi.js +331 -0
- package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
- package/lib/agent-cli-provider/claude-command.d.ts +6 -0
- package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
- package/lib/agent-cli-provider/claude-command.js +43 -0
- package/lib/agent-cli-provider/claude-command.js.map +1 -0
- package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-actions.js +13 -17
- package/lib/agent-cli-provider/contract-actions.js.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.js +10 -3
- package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +57 -2
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/contract-support.d.ts +1 -0
- package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-support.js +25 -3
- package/lib/agent-cli-provider/contract-support.js.map +1 -1
- package/lib/agent-cli-provider/errors.d.ts +1 -0
- package/lib/agent-cli-provider/errors.d.ts.map +1 -1
- package/lib/agent-cli-provider/errors.js +14 -5
- package/lib/agent-cli-provider/errors.js.map +1 -1
- package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
- package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-client.js +135 -0
- package/lib/agent-cli-provider/gateway-client.js.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.js +294 -0
- package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.js +495 -0
- package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
- package/lib/agent-cli-provider/index.d.ts +3 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +14 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
- package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.js +17 -3
- package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
- package/lib/agent-cli-provider/provider-registry.js +411 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +106 -4
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/detached-startup.js +4 -2
- package/lib/docker-config.js +24 -13
- package/lib/provider-defaults.js +15 -4
- package/lib/provider-detection.js +2 -0
- package/lib/provider-names.js +32 -12
- package/lib/repo-settings.js +15 -1
- package/lib/run-mode.js +18 -6
- package/lib/run-plan.js +32 -0
- package/lib/settings/claude-auth.js +3 -6
- package/lib/settings.js +17 -2
- package/lib/setup-apply.js +300 -0
- package/lib/setup-journal.js +109 -0
- package/lib/setup-plan.js +406 -0
- package/lib/setup-undo.js +88 -0
- package/lib/start-cluster.js +34 -9
- package/lib/stream-json-parser.js +7 -5
- package/package.json +33 -3
- package/scripts/live-provider-smoke.js +221 -0
- package/src/agent/agent-lifecycle.js +7 -4
- package/src/agent/agent-quality-gate-schema.js +2 -2
- package/src/agent/agent-task-executor.js +41 -2
- package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
- package/src/agent-cli-provider/adapters/acp.ts +493 -0
- package/src/agent-cli-provider/adapters/claude.ts +2 -32
- package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
- package/src/agent-cli-provider/adapters/copilot.ts +231 -0
- package/src/agent-cli-provider/adapters/gateway.ts +186 -0
- package/src/agent-cli-provider/adapters/gemini.ts +9 -2
- package/src/agent-cli-provider/adapters/index.ts +5 -36
- package/src/agent-cli-provider/adapters/opencode.ts +4 -1
- package/src/agent-cli-provider/adapters/pi.ts +410 -0
- package/src/agent-cli-provider/claude-command.ts +47 -0
- package/src/agent-cli-provider/contract-actions.ts +14 -17
- package/src/agent-cli-provider/contract-invoke.ts +12 -3
- package/src/agent-cli-provider/contract-options.ts +72 -3
- package/src/agent-cli-provider/contract-support.ts +30 -3
- package/src/agent-cli-provider/errors.ts +14 -4
- package/src/agent-cli-provider/gateway-client.ts +170 -0
- package/src/agent-cli-provider/gateway-runner.ts +353 -0
- package/src/agent-cli-provider/gateway-tools.ts +616 -0
- package/src/agent-cli-provider/index.ts +28 -0
- package/src/agent-cli-provider/invoke-evidence.ts +24 -3
- package/src/agent-cli-provider/provider-registry.ts +515 -0
- package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
- package/src/agent-cli-provider/types.ts +145 -4
- package/src/config-validator.js +33 -15
- package/src/isolation-manager.js +124 -19
- package/src/ledger.js +3 -0
- package/src/message-bus.js +12 -2
- package/src/orchestrator.js +37 -17
- package/src/preflight.js +83 -30
- package/src/providers/capabilities.js +15 -42
- package/src/providers/index.js +52 -72
- package/src/worktree-claude-config.js +17 -0
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-helper-runtime.js +11 -0
- package/task-lib/runner.js +7 -0
package/src/isolation-manager.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
370
|
-
|
|
371
|
-
|
|
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 (
|
|
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(
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
/**
|
package/src/ledger.js
CHANGED
package/src/message-bus.js
CHANGED
|
@@ -24,12 +24,14 @@ class MessageBus extends EventEmitter {
|
|
|
24
24
|
this.setMaxListeners(MAX_LISTENERS);
|
|
25
25
|
this.ledger = ledger || new Ledger();
|
|
26
26
|
this.wsClients = new Set();
|
|
27
|
+
this._closed = false;
|
|
27
28
|
|
|
28
29
|
// Forward ledger events
|
|
29
|
-
this.
|
|
30
|
+
this._forwardLedgerMessage = (message) => {
|
|
30
31
|
this.emit('message', message);
|
|
31
32
|
this._broadcastToWebSocket(message);
|
|
32
|
-
}
|
|
33
|
+
};
|
|
34
|
+
this.ledger.on('message', this._forwardLedgerMessage);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -241,6 +243,14 @@ class MessageBus extends EventEmitter {
|
|
|
241
243
|
* Close the message bus
|
|
242
244
|
*/
|
|
243
245
|
close() {
|
|
246
|
+
if (this._closed) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this._closed = true;
|
|
250
|
+
|
|
251
|
+
this.ledger.off('message', this._forwardLedgerMessage);
|
|
252
|
+
this.removeAllListeners();
|
|
253
|
+
|
|
244
254
|
// Close all WebSocket connections
|
|
245
255
|
for (const ws of this.wsClients) {
|
|
246
256
|
try {
|
package/src/orchestrator.js
CHANGED
|
@@ -47,6 +47,7 @@ const configValidator = require('./config-validator');
|
|
|
47
47
|
const TemplateResolver = require('./template-resolver');
|
|
48
48
|
const { loadSettings } = require('../lib/settings');
|
|
49
49
|
const { normalizeProviderName } = require('../lib/provider-names');
|
|
50
|
+
const { resolveRunPlan } = require('../lib/run-plan');
|
|
50
51
|
const { getProvider } = require('./providers');
|
|
51
52
|
const StateSnapshotter = require('./state-snapshotter');
|
|
52
53
|
const { resolveClusterRequiredQualityGates } = require('./quality-gates');
|
|
@@ -212,14 +213,11 @@ function applyPushBlockedRepairTriggers(config) {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
function resolveAutoMerge(options) {
|
|
216
|
-
return Boolean(options.ship) || Boolean(options.autoMerge);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
216
|
function buildPrOptions(options, requiredQualityGates) {
|
|
220
217
|
// autoMerge must always be persisted (even when no other PR fields are set) so that
|
|
221
218
|
// `zeroshot run --pr` (autoMerge=false) vs `--ship` (autoMerge=true) survives resume.
|
|
222
|
-
|
|
219
|
+
// Derived from the canonical run plan — never recomputed from ship/pr here.
|
|
220
|
+
const autoMerge = resolveRunPlan(options).autoMerge;
|
|
223
221
|
|
|
224
222
|
return {
|
|
225
223
|
prBase: options.prBase || null,
|
|
@@ -577,6 +575,12 @@ class Orchestrator {
|
|
|
577
575
|
// missing - a ledger write. Read-only orchestrators must never write, so they
|
|
578
576
|
// skip it; they only need to read existing snapshots, not produce new ones.
|
|
579
577
|
if (!this.readonly) {
|
|
578
|
+
this._registerClusterSubscriptions({
|
|
579
|
+
messageBus,
|
|
580
|
+
clusterId,
|
|
581
|
+
isolationManager,
|
|
582
|
+
containerId: isolation?.containerId || null,
|
|
583
|
+
});
|
|
580
584
|
this._startSnapshotter(clusterContext);
|
|
581
585
|
}
|
|
582
586
|
this._log(`[Orchestrator] Loaded cluster: ${clusterId} with ${agents.length} agents`);
|
|
@@ -1134,11 +1138,12 @@ class Orchestrator {
|
|
|
1134
1138
|
const messageBus = new MessageBus(ledger);
|
|
1135
1139
|
|
|
1136
1140
|
// Handle isolation mode (Docker container OR git worktree)
|
|
1137
|
-
const {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1141
|
+
const {
|
|
1142
|
+
isolationManager,
|
|
1143
|
+
containerId,
|
|
1144
|
+
worktreeInfo,
|
|
1145
|
+
image: resolvedIsolationImage,
|
|
1146
|
+
} = await this._initializeIsolation(options, config, clusterId);
|
|
1142
1147
|
let requiredQualityGates = resolveClusterRequiredQualityGates(config, options);
|
|
1143
1148
|
let commandProofs = resolveClusterCommandProofs(config, options);
|
|
1144
1149
|
options.requiredQualityGates = requiredQualityGates;
|
|
@@ -1184,7 +1189,9 @@ class Orchestrator {
|
|
|
1184
1189
|
? {
|
|
1185
1190
|
enabled: true,
|
|
1186
1191
|
containerId,
|
|
1187
|
-
image
|
|
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',
|
|
1188
1195
|
manager: isolationManager,
|
|
1189
1196
|
workDir: options.cwd || process.cwd(), // Persisted for resume
|
|
1190
1197
|
}
|
|
@@ -1725,20 +1732,30 @@ class Orchestrator {
|
|
|
1725
1732
|
let isolationManager = null;
|
|
1726
1733
|
let containerId = null;
|
|
1727
1734
|
let worktreeInfo = null;
|
|
1735
|
+
let isolationImage = null;
|
|
1728
1736
|
|
|
1729
1737
|
if (options.isolation) {
|
|
1730
1738
|
if (!IsolationManager.isDockerAvailable()) {
|
|
1731
1739
|
throw new Error('Docker is not available. Install Docker to use --docker mode.');
|
|
1732
1740
|
}
|
|
1733
1741
|
|
|
1734
|
-
|
|
1735
|
-
|
|
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;
|
|
1736
1754
|
|
|
1737
1755
|
isolationManager = new IsolationManager({ image });
|
|
1738
1756
|
this._log(`[Orchestrator] Starting cluster in isolation mode (image: ${image})`);
|
|
1739
1757
|
|
|
1740
1758
|
const workDir = options.cwd || process.cwd();
|
|
1741
|
-
const providerName = this._resolveClusterProvider(config);
|
|
1742
1759
|
containerId = await isolationManager.createContainer(clusterId, {
|
|
1743
1760
|
workDir,
|
|
1744
1761
|
image,
|
|
@@ -1766,7 +1783,7 @@ class Orchestrator {
|
|
|
1766
1783
|
this._log(`[Orchestrator] Branch: ${worktreeInfo.branch}`);
|
|
1767
1784
|
}
|
|
1768
1785
|
|
|
1769
|
-
return { isolationManager, containerId, worktreeInfo };
|
|
1786
|
+
return { isolationManager, containerId, worktreeInfo, image: isolationImage };
|
|
1770
1787
|
}
|
|
1771
1788
|
|
|
1772
1789
|
_applyAutoPrConfig(config, inputData, options) {
|
|
@@ -1825,7 +1842,7 @@ class Orchestrator {
|
|
|
1825
1842
|
mergeQueue: options.mergeQueue,
|
|
1826
1843
|
closeIssue: options.closeIssue,
|
|
1827
1844
|
requiredQualityGates: options.requiredQualityGates,
|
|
1828
|
-
autoMerge:
|
|
1845
|
+
autoMerge: resolveRunPlan(options).autoMerge,
|
|
1829
1846
|
cwd: options.cwd,
|
|
1830
1847
|
});
|
|
1831
1848
|
|
|
@@ -2316,6 +2333,9 @@ class Orchestrator {
|
|
|
2316
2333
|
* Call before deleting storageDir to prevent ENOENT race conditions during cleanup
|
|
2317
2334
|
*/
|
|
2318
2335
|
close() {
|
|
2336
|
+
if (this.closed) {
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2319
2339
|
this.closed = true;
|
|
2320
2340
|
}
|
|
2321
2341
|
|
|
@@ -4103,7 +4123,7 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
4103
4123
|
|
|
4104
4124
|
// Exported for testing (PR options persistence, e.g. autoMerge for --pr vs --ship).
|
|
4105
4125
|
Orchestrator.buildPrOptions = buildPrOptions;
|
|
4106
|
-
Orchestrator.
|
|
4126
|
+
Orchestrator.resolveRunPlan = resolveRunPlan;
|
|
4107
4127
|
|
|
4108
4128
|
module.exports = Orchestrator;
|
|
4109
4129
|
module.exports.DuplicateClusterError = DuplicateClusterError;
|
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 {
|
|
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
|
-
|
|
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
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
'
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
450
|
+
`Use ${VALID_PROVIDERS.join(', ')}`,
|
|
435
451
|
]),
|
|
436
452
|
],
|
|
437
453
|
warnings: [],
|
|
438
454
|
};
|
|
439
455
|
}
|
|
440
456
|
|
|
441
|
-
|
|
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 {
|
|
1
|
+
const {
|
|
2
|
+
PROVIDER_CAPABILITIES,
|
|
3
|
+
normalizeProviderName,
|
|
4
|
+
providerSupportsCapability,
|
|
5
|
+
} = require('../../lib/provider-names');
|
|
2
6
|
|
|
3
|
-
const CAPABILITIES =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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) {
|