@the-open-engine/zeroshot 6.39.0 → 6.39.2
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/README.md +39 -1
- package/cli/index.js +90 -24
- package/cluster-templates/conductor-bootstrap.json +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +1 -1
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +26 -1
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/omp.js +1 -1
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts +4 -0
- package/lib/agent-cli-provider/omp/sdk-environment-policy.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp/sdk-environment-policy.js +37 -0
- package/lib/agent-cli-provider/omp/sdk-environment-policy.js.map +1 -0
- package/lib/agent-cli-provider/omp/sdk-process-private-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js +4 -15
- package/lib/agent-cli-provider/omp/sdk-process-private-runtime.js.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-process-result.d.ts +2 -0
- package/lib/agent-cli-provider/omp/sdk-process-result.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-process-result.js +2 -2
- package/lib/agent-cli-provider/omp/sdk-process-result.js.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-process-runner.d.ts.map +1 -1
- package/lib/agent-cli-provider/omp/sdk-process-runner.js +6 -1
- package/lib/agent-cli-provider/omp/sdk-process-runner.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +3 -1
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +9 -2
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts +4 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +18 -15
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +3 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster-worker/engine-adapter-common.js +18 -0
- package/lib/cluster-worker/engine-adapter.js +2 -1
- package/lib/detached-startup.d.ts +1 -0
- package/lib/detached-startup.js +2 -1
- package/lib/git-remote-utils.js +22 -5
- package/lib/start-cluster-environment.d.ts +1 -0
- package/lib/start-cluster-run-options.d.ts +1 -0
- package/lib/start-cluster-run-options.js +1 -0
- package/npm-shrinkwrap.json +6 -8
- package/package.json +4 -3
- package/scripts/omp/runtime-identities.js +31 -18
- package/scripts/omp/runtime.js +2 -0
- package/scripts/rust-distribution.js +13 -31
- package/src/agent/agent-lifecycle.js +17 -1
- package/src/agent/output-extraction-failures.js +73 -0
- package/src/agent/output-extraction-failures.ts +100 -0
- package/src/agent/output-extraction-types.ts +5 -0
- package/src/agent/provider-terminal-failure.js +2 -2
- package/src/agent/provider-terminal-failure.ts +4 -2
- package/src/agent-cli-provider/adapters/claude.ts +1 -1
- package/src/agent-cli-provider/adapters/codex.ts +36 -1
- package/src/agent-cli-provider/adapters/omp.ts +2 -1
- package/src/agent-cli-provider/omp/sdk-environment-policy.ts +38 -0
- package/src/agent-cli-provider/omp/sdk-process-private-runtime.ts +4 -16
- package/src/agent-cli-provider/omp/sdk-process-result.ts +4 -2
- package/src/agent-cli-provider/omp/sdk-process-runner.ts +7 -1
- package/src/agent-cli-provider/provider-registry.ts +10 -2
- package/src/agent-cli-provider/single-agent-runtime.ts +24 -15
- package/src/agent-cli-provider/types.ts +3 -0
- package/src/agents/git-pusher-template.js +78 -105
- package/src/attach/socket-paths.js +1 -1
- package/src/attach/socket-paths.ts +1 -1
- package/src/copy-containment.js +191 -0
- package/src/copy-containment.ts +259 -0
- package/src/copy-worker.js +29 -30
- package/src/copy-worker.ts +43 -30
- package/src/foreground-benchmark-run.js +24 -12
- package/src/isolation-manager.js +59 -20
- package/src/legacy-lib/detached-startup.ts +3 -1
- package/src/legacy-lib/git-remote-utils.ts +23 -5
- package/src/legacy-lib/start-cluster-environment.ts +1 -0
- package/src/legacy-lib/start-cluster-run-options.ts +2 -0
- package/src/omp-session-verifier.js +8 -5
- package/src/orchestrator.js +55 -3
- package/src/pr-body-template.js +71 -0
- package/src/preflight.js +31 -6
- package/src/providers/index.js +3 -3
- package/src/task-execution-context.js +22 -0
- package/src/watcher-prompt-channel.js +1 -1
- package/src/worktree-claude-config.js +25 -0
- package/src/worktree-claude-config.ts +28 -0
- package/task-lib/runner.js +57 -22
|
@@ -4,6 +4,7 @@ const { writeBenchmarkResultBundle } = require('./foreground-benchmark-files');
|
|
|
4
4
|
const TERMINAL_TOPICS = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED'];
|
|
5
5
|
const VERIFIER_ELIGIBLE = new Set(['completed', 'task_failure']);
|
|
6
6
|
const EXIT_CODES = Object.freeze({
|
|
7
|
+
task_failure: 23,
|
|
7
8
|
provider_failure: 20,
|
|
8
9
|
engine_failure: 21,
|
|
9
10
|
cancelled: 22,
|
|
@@ -40,20 +41,24 @@ function requireSettledStatus(orchestrator, clusterId) {
|
|
|
40
41
|
return status;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
44
|
+
function buildForegroundResult({ orchestrator, cluster, clusterId, cancelled }) {
|
|
45
|
+
const finalRun = orchestrator.getFinalRun?.(clusterId);
|
|
46
|
+
const status = finalRun?.status || requireSettledStatus(orchestrator, clusterId);
|
|
47
|
+
const terminals = finalRun?.terminalMessages || terminalMessages(cluster, clusterId);
|
|
47
48
|
if (cancelled && terminals.length === 0) {
|
|
48
|
-
|
|
49
|
-
} else {
|
|
50
|
-
result = buildBenchmarkResult({
|
|
51
|
-
runId: clusterId,
|
|
52
|
-
terminalMessages: terminals,
|
|
53
|
-
agents: status.agents,
|
|
54
|
-
});
|
|
49
|
+
return buildCancelledResult({ runId: clusterId, agents: status.agents });
|
|
55
50
|
}
|
|
56
|
-
|
|
51
|
+
return buildBenchmarkResult({
|
|
52
|
+
runId: clusterId,
|
|
53
|
+
terminalMessages: terminals,
|
|
54
|
+
agents: status.agents,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
|
|
59
|
+
const result = buildForegroundResult({ orchestrator, cluster, clusterId, cancelled });
|
|
60
|
+
const snapshot =
|
|
61
|
+
orchestrator.getFinalRun?.(clusterId)?.snapshot || cluster.messageBus.readSnapshot(clusterId);
|
|
57
62
|
return writeBenchmarkResultBundle(resultPath, result, snapshot);
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -64,8 +69,15 @@ function exitCodeForResult(result) {
|
|
|
64
69
|
return exitCode;
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
function exitCodeForForegroundResult(result) {
|
|
73
|
+
if (result.outcome === 'completed') return 0;
|
|
74
|
+
return EXIT_CODES[result.outcome] ?? exitCodeForResult(result);
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
module.exports = {
|
|
78
|
+
buildForegroundResult,
|
|
68
79
|
exitCodeForResult,
|
|
80
|
+
exitCodeForForegroundResult,
|
|
69
81
|
isForegroundStatusSettled,
|
|
70
82
|
terminalMessages,
|
|
71
83
|
writeForegroundResult,
|
package/src/isolation-manager.js
CHANGED
|
@@ -34,6 +34,14 @@ const { resolveProviderCredentialPaths } = require('../lib/provider-credential-p
|
|
|
34
34
|
const { getProvider } = require('./providers');
|
|
35
35
|
const { readRepoSettings } = require('../lib/repo-settings');
|
|
36
36
|
const { provisionClaudeCredentials } = require('./claude-credentials');
|
|
37
|
+
const {
|
|
38
|
+
CopyContainmentError,
|
|
39
|
+
copyErrorFromPayload,
|
|
40
|
+
createCopyBoundary,
|
|
41
|
+
resolveCopyPath,
|
|
42
|
+
resolveSourcePath,
|
|
43
|
+
validateRelativePath,
|
|
44
|
+
} = require('./copy-containment');
|
|
37
45
|
|
|
38
46
|
const DEFAULT_WORKTREE_SETUP_TIMEOUT_MS = 15 * 60 * 1000;
|
|
39
47
|
const FRESH_BASE_REF_PREFIX = 'refs/zeroshot/base-fetch';
|
|
@@ -1298,6 +1306,7 @@ class IsolationManager {
|
|
|
1298
1306
|
// Phase 1: Collect all files and directories
|
|
1299
1307
|
const files = [];
|
|
1300
1308
|
const directories = new Set();
|
|
1309
|
+
const copyBoundary = createCopyBoundary(src, dest);
|
|
1301
1310
|
|
|
1302
1311
|
const shouldIgnoreFsError = (err) =>
|
|
1303
1312
|
err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'ENOENT';
|
|
@@ -1328,12 +1337,13 @@ class IsolationManager {
|
|
|
1328
1337
|
}
|
|
1329
1338
|
};
|
|
1330
1339
|
|
|
1331
|
-
function handleEntry(entry,
|
|
1340
|
+
function handleEntry(entry, relPath, relativePath, ancestorDirectories) {
|
|
1332
1341
|
if (entry.isSymbolicLink()) {
|
|
1333
|
-
const
|
|
1342
|
+
const resolvedSourcePath = resolveSourcePath(copyBoundary, relPath);
|
|
1343
|
+
const targetStats = fs.statSync(resolvedSourcePath);
|
|
1334
1344
|
if (targetStats.isDirectory()) {
|
|
1335
1345
|
directories.add(relPath);
|
|
1336
|
-
collectFiles(
|
|
1346
|
+
collectFiles(relPath, ancestorDirectories);
|
|
1337
1347
|
return;
|
|
1338
1348
|
}
|
|
1339
1349
|
|
|
@@ -1344,7 +1354,7 @@ class IsolationManager {
|
|
|
1344
1354
|
|
|
1345
1355
|
if (entry.isDirectory()) {
|
|
1346
1356
|
directories.add(relPath);
|
|
1347
|
-
collectFiles(
|
|
1357
|
+
collectFiles(relPath, ancestorDirectories);
|
|
1348
1358
|
return;
|
|
1349
1359
|
}
|
|
1350
1360
|
|
|
@@ -1352,7 +1362,15 @@ class IsolationManager {
|
|
|
1352
1362
|
ensureParentDirTracked(relativePath);
|
|
1353
1363
|
}
|
|
1354
1364
|
|
|
1355
|
-
function collectFiles(
|
|
1365
|
+
function collectFiles(relativePath = '', ancestorDirectories = new Set()) {
|
|
1366
|
+
const currentSrc = relativePath
|
|
1367
|
+
? resolveSourcePath(copyBoundary, relativePath)
|
|
1368
|
+
: copyBoundary.sourceRoot.canonicalPath;
|
|
1369
|
+
if (ancestorDirectories.has(currentSrc)) {
|
|
1370
|
+
throw new CopyContainmentError(relativePath, 'source directory symlink creates a cycle');
|
|
1371
|
+
}
|
|
1372
|
+
const childAncestors = new Set(ancestorDirectories);
|
|
1373
|
+
childAncestors.add(currentSrc);
|
|
1356
1374
|
const entries = readEntries(currentSrc);
|
|
1357
1375
|
|
|
1358
1376
|
for (const entry of entries) {
|
|
@@ -1360,11 +1378,11 @@ class IsolationManager {
|
|
|
1360
1378
|
continue;
|
|
1361
1379
|
}
|
|
1362
1380
|
|
|
1363
|
-
const
|
|
1364
|
-
const relPath = relativePath ? path.join(relativePath,
|
|
1381
|
+
const entryName = validateRelativePath(entry.name);
|
|
1382
|
+
const relPath = relativePath ? path.join(relativePath, entryName) : entryName;
|
|
1365
1383
|
|
|
1366
1384
|
try {
|
|
1367
|
-
handleEntry(entry,
|
|
1385
|
+
handleEntry(entry, relPath, relativePath, childAncestors);
|
|
1368
1386
|
} catch (err) {
|
|
1369
1387
|
if (shouldIgnoreFsError(err)) {
|
|
1370
1388
|
continue;
|
|
@@ -1374,7 +1392,7 @@ class IsolationManager {
|
|
|
1374
1392
|
}
|
|
1375
1393
|
}
|
|
1376
1394
|
|
|
1377
|
-
collectFiles(
|
|
1395
|
+
collectFiles();
|
|
1378
1396
|
|
|
1379
1397
|
// Phase 2: Create directory structure (sequential - must exist before file copy)
|
|
1380
1398
|
// Sort directories by depth to ensure parents are created before children
|
|
@@ -1385,7 +1403,7 @@ class IsolationManager {
|
|
|
1385
1403
|
});
|
|
1386
1404
|
|
|
1387
1405
|
for (const dir of sortedDirs) {
|
|
1388
|
-
const destDir =
|
|
1406
|
+
const { destinationPath: destDir } = resolveCopyPath(copyBoundary, dir);
|
|
1389
1407
|
try {
|
|
1390
1408
|
fs.mkdirSync(destDir, { recursive: true });
|
|
1391
1409
|
} catch (err) {
|
|
@@ -1399,10 +1417,9 @@ class IsolationManager {
|
|
|
1399
1417
|
// For small file counts (<100), use synchronous copy (worker overhead not worth it)
|
|
1400
1418
|
if (files.length < 100) {
|
|
1401
1419
|
for (const relPath of files) {
|
|
1402
|
-
const srcPath = path.join(src, relPath);
|
|
1403
|
-
const destPath = path.join(dest, relPath);
|
|
1404
1420
|
try {
|
|
1405
|
-
|
|
1421
|
+
const { sourcePath, destinationPath } = resolveCopyPath(copyBoundary, relPath);
|
|
1422
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
1406
1423
|
} catch (err) {
|
|
1407
1424
|
if (err.code !== 'EACCES' && err.code !== 'EPERM' && err.code !== 'ENOENT') {
|
|
1408
1425
|
throw err;
|
|
@@ -1424,18 +1441,22 @@ class IsolationManager {
|
|
|
1424
1441
|
}
|
|
1425
1442
|
|
|
1426
1443
|
// Spawn workers and wait for completion
|
|
1444
|
+
const workers = [];
|
|
1427
1445
|
const workerPromises = chunks.map((chunk) => {
|
|
1428
1446
|
return new Promise((resolve, reject) => {
|
|
1429
1447
|
const worker = new Worker(workerPath, {
|
|
1430
1448
|
workerData: {
|
|
1431
1449
|
files: chunk,
|
|
1432
|
-
sourceBase:
|
|
1433
|
-
destBase:
|
|
1450
|
+
sourceBase: copyBoundary.sourceRoot.canonicalPath,
|
|
1451
|
+
destBase: copyBoundary.destinationRoot.canonicalPath,
|
|
1452
|
+
expectedBoundary: copyBoundary,
|
|
1434
1453
|
},
|
|
1435
1454
|
});
|
|
1455
|
+
workers.push(worker);
|
|
1456
|
+
let result = null;
|
|
1436
1457
|
|
|
1437
|
-
worker.on('message', (
|
|
1438
|
-
|
|
1458
|
+
worker.on('message', (workerResult) => {
|
|
1459
|
+
result = workerResult;
|
|
1439
1460
|
});
|
|
1440
1461
|
|
|
1441
1462
|
worker.on('error', (err) => {
|
|
@@ -1445,6 +1466,12 @@ class IsolationManager {
|
|
|
1445
1466
|
worker.on('exit', (code) => {
|
|
1446
1467
|
if (code !== 0) {
|
|
1447
1468
|
reject(new Error(`Worker exited with code ${code}`));
|
|
1469
|
+
} else if (!result) {
|
|
1470
|
+
reject(new Error('Copy worker exited without reporting a result'));
|
|
1471
|
+
} else if (result.error) {
|
|
1472
|
+
reject(copyErrorFromPayload(result.error));
|
|
1473
|
+
} else {
|
|
1474
|
+
resolve(result);
|
|
1448
1475
|
}
|
|
1449
1476
|
});
|
|
1450
1477
|
});
|
|
@@ -1453,7 +1480,12 @@ class IsolationManager {
|
|
|
1453
1480
|
// Wait for all workers to complete (proper async/await - no busy-wait!)
|
|
1454
1481
|
// FIX: Previous version used busy-wait which blocked the event loop,
|
|
1455
1482
|
// preventing worker thread messages from being processed (timeout bug)
|
|
1456
|
-
|
|
1483
|
+
try {
|
|
1484
|
+
await Promise.all(workerPromises);
|
|
1485
|
+
} catch (err) {
|
|
1486
|
+
await Promise.all(workers.map((worker) => worker.terminate().catch(() => undefined)));
|
|
1487
|
+
throw err;
|
|
1488
|
+
}
|
|
1457
1489
|
}
|
|
1458
1490
|
|
|
1459
1491
|
/**
|
|
@@ -2383,8 +2415,15 @@ class IsolationManager {
|
|
|
2383
2415
|
// NEVER pass --volumes (irreversible data loss) and NEVER tear down a pinned/shared
|
|
2384
2416
|
// Compose project — only a project scoped to the worktree directory basename, which is
|
|
2385
2417
|
// the only kind zeroshot could itself have created, is touched.
|
|
2386
|
-
|
|
2387
|
-
|
|
2418
|
+
let teardown = { shouldTeardown: false };
|
|
2419
|
+
try {
|
|
2420
|
+
const { resolveWorktreeComposeTeardown } = require('../lib/compose-utils');
|
|
2421
|
+
teardown = resolveWorktreeComposeTeardown(worktreeInfo.path);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
console.warn(
|
|
2424
|
+
`[IsolationManager] Skipping Docker Compose teardown in ${worktreeInfo.path}: ${error.message}`
|
|
2425
|
+
);
|
|
2426
|
+
}
|
|
2388
2427
|
if (teardown.shouldTeardown) {
|
|
2389
2428
|
try {
|
|
2390
2429
|
runSync('docker', teardown.args, {
|
|
@@ -26,6 +26,7 @@ interface RunOptions extends Record<string, unknown> {
|
|
|
26
26
|
mergeQueue?: unknown;
|
|
27
27
|
pr?: unknown;
|
|
28
28
|
prBase?: unknown;
|
|
29
|
+
prBody?: unknown;
|
|
29
30
|
ship?: unknown;
|
|
30
31
|
worktree?: unknown;
|
|
31
32
|
}
|
|
@@ -236,9 +237,10 @@ async function registerDetachedSetupCluster({
|
|
|
236
237
|
setupStartedAt: Date.now(),
|
|
237
238
|
setupStage: 'starting',
|
|
238
239
|
autoPr: plan.delivery !== 'none',
|
|
239
|
-
prOptions:
|
|
240
|
+
prOptions: plan.delivery !== 'none'
|
|
240
241
|
? {
|
|
241
242
|
prBase: runOptions.prBase,
|
|
243
|
+
prBody: typeof runOptions.prBody === 'string' ? runOptions.prBody : null,
|
|
242
244
|
mergeQueue: runOptions.mergeQueue || false,
|
|
243
245
|
closeIssue: runOptions.closeIssue || null,
|
|
244
246
|
autoMerge: plan.autoMerge,
|
|
@@ -181,6 +181,25 @@ function parseGitRemoteUrl(remoteUrl: unknown): GitContext | null {
|
|
|
181
181
|
return null;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
function parseFetchRemoteLine(line: string): readonly [string, string] | null {
|
|
185
|
+
// Partial clones append their filter after the fetch marker, for example
|
|
186
|
+
// `origin https://github.com/org/repo.git (fetch) [blob:none]`.
|
|
187
|
+
const fetchMarker = ' (fetch)';
|
|
188
|
+
const fetchMarkerIndex = line.lastIndexOf(fetchMarker);
|
|
189
|
+
if (fetchMarkerIndex < 1) return null;
|
|
190
|
+
const suffix = line.slice(fetchMarkerIndex + fetchMarker.length);
|
|
191
|
+
if (suffix.length > 0 && !(suffix.startsWith(' [') && suffix.endsWith(']'))) return null;
|
|
192
|
+
|
|
193
|
+
const remoteLine = line.slice(0, fetchMarkerIndex);
|
|
194
|
+
const separatorIndex = remoteLine.search(/\s/);
|
|
195
|
+
if (separatorIndex < 1) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const remoteUrl = remoteLine.slice(separatorIndex).trim();
|
|
200
|
+
return remoteUrl.length > 0 ? [remoteLine.slice(0, separatorIndex), remoteUrl] : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
184
203
|
/**
|
|
185
204
|
* Detect git repository context from current working directory.
|
|
186
205
|
* Returns provider context extracted from git remote URL.
|
|
@@ -205,15 +224,14 @@ function detectGitContext(cwd = process.cwd()): GitContextWithRemote | null {
|
|
|
205
224
|
|
|
206
225
|
const supportedRemotes = new Map<string, GitContextWithRemote>();
|
|
207
226
|
for (const line of remoteOutput.split(/\r?\n/)) {
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
227
|
+
const remoteParts = parseFetchRemoteLine(line);
|
|
228
|
+
if (!remoteParts) {
|
|
210
229
|
continue;
|
|
211
230
|
}
|
|
212
231
|
|
|
213
|
-
const remoteCandidate =
|
|
214
|
-
const remoteUrl = match[2];
|
|
232
|
+
const [remoteCandidate, remoteUrl] = remoteParts;
|
|
215
233
|
const remote = normalizeGitRemoteName(remoteCandidate);
|
|
216
|
-
if (!remote
|
|
234
|
+
if (!remote) {
|
|
217
235
|
continue;
|
|
218
236
|
}
|
|
219
237
|
const context = parseGitRemoteUrl(remoteUrl);
|
|
@@ -28,6 +28,7 @@ interface RunOptions extends Record<string, unknown> {
|
|
|
28
28
|
noMounts?: unknown;
|
|
29
29
|
pr?: unknown;
|
|
30
30
|
prBase?: unknown;
|
|
31
|
+
prBody?: unknown;
|
|
31
32
|
preparedWorktree?: unknown;
|
|
32
33
|
requiredQualityGates?: unknown;
|
|
33
34
|
ship?: unknown;
|
|
@@ -201,6 +202,7 @@ function buildStartOptionsFromPlan({
|
|
|
201
202
|
containerHome: optionalValue(options.containerHome),
|
|
202
203
|
forceProvider: optionalValue(forceProvider),
|
|
203
204
|
prBase: environment ? resolvePrBase(options) : optionalValue(options.prBase),
|
|
205
|
+
prBody: typeof options.prBody === 'string' ? options.prBody : undefined,
|
|
204
206
|
mergeQueue: environment ? resolveMergeQueue(options) : optionalValue(options.mergeQueue),
|
|
205
207
|
closeIssue: environment ? resolveCloseIssue(options) : optionalValue(options.closeIssue),
|
|
206
208
|
ship: plan.delivery === 'ship',
|
|
@@ -399,7 +399,10 @@ function streamSessionJsonl(fd, describePath, limits) {
|
|
|
399
399
|
`${describePath} record ${records} is not valid JSON: ${error.message}`
|
|
400
400
|
);
|
|
401
401
|
}
|
|
402
|
-
|
|
402
|
+
// OMP 17.2.1 may reserve its first JSONL record for mutable title metadata before writing the
|
|
403
|
+
// authoritative session record. Accept exactly that one known preamble position; every other
|
|
404
|
+
// ordering still fails closed in parseSessionHeader below.
|
|
405
|
+
if (header === null && !(records === 1 && parsed?.type === 'title')) header = parsed;
|
|
403
406
|
collectCanonicalBlobRefs(parsed, blobRefs, limits);
|
|
404
407
|
}
|
|
405
408
|
|
|
@@ -430,9 +433,9 @@ function streamSessionJsonl(fd, describePath, limits) {
|
|
|
430
433
|
};
|
|
431
434
|
}
|
|
432
435
|
|
|
433
|
-
/** The session
|
|
434
|
-
*
|
|
435
|
-
* to disk and its `cwd` is the workspace the session belongs to. */
|
|
436
|
+
/** The session header (`{type:"session", version, id, cwd, ...}`) is either the first record or
|
|
437
|
+
* follows OMP 17.2.1's single leading title-metadata record. Its `id` is the authoritative session
|
|
438
|
+
* identity written to disk and its `cwd` is the workspace the session belongs to. */
|
|
436
439
|
function parseSessionHeader(header, describePath) {
|
|
437
440
|
if (!header || typeof header !== 'object' || Array.isArray(header)) {
|
|
438
441
|
fail('session-header-missing', `${describePath} has no session header record.`);
|
|
@@ -440,7 +443,7 @@ function parseSessionHeader(header, describePath) {
|
|
|
440
443
|
if (header.type !== 'session') {
|
|
441
444
|
fail(
|
|
442
445
|
'session-header-invalid',
|
|
443
|
-
`${describePath} first record is type ${JSON.stringify(header.type)}, not "session".`
|
|
446
|
+
`${describePath} first non-title-preamble record is type ${JSON.stringify(header.type)}, not "session".`
|
|
444
447
|
);
|
|
445
448
|
}
|
|
446
449
|
if (typeof header.id !== 'string' || header.id.length === 0) {
|
package/src/orchestrator.js
CHANGED
|
@@ -262,6 +262,7 @@ function buildPrOptions(options, requiredQualityGates) {
|
|
|
262
262
|
prBase: options.prBase || null,
|
|
263
263
|
mergeQueue: options.mergeQueue || false,
|
|
264
264
|
closeIssue: options.closeIssue || null,
|
|
265
|
+
prBody: typeof options.prBody === 'string' ? options.prBody : null,
|
|
265
266
|
gitRemote: options.gitRemote || null,
|
|
266
267
|
autoMerge,
|
|
267
268
|
...(requiredQualityGates.length > 0 ? { requiredQualityGates } : {}),
|
|
@@ -306,6 +307,9 @@ class Orchestrator {
|
|
|
306
307
|
this.closed = false;
|
|
307
308
|
this._conductorWatchdogs = new Set();
|
|
308
309
|
this._clusterRunBoundaries = new Map();
|
|
310
|
+
// Successful auto-cleanup closes and removes the live cluster before a foreground caller can
|
|
311
|
+
// build its authoritative result. Preserve only the bounded terminal handoff for this process.
|
|
312
|
+
this._finalRuns = new Map();
|
|
309
313
|
|
|
310
314
|
// Track if clusters are loaded (for lazy loading pattern)
|
|
311
315
|
this._clustersLoaded = options.skipLoad === true;
|
|
@@ -2038,6 +2042,7 @@ class Orchestrator {
|
|
|
2038
2042
|
prBase: options.prBase,
|
|
2039
2043
|
mergeQueue: options.mergeQueue,
|
|
2040
2044
|
closeIssue: options.closeIssue,
|
|
2045
|
+
prBody: options.prBody,
|
|
2041
2046
|
requiredQualityGates: options.requiredQualityGates,
|
|
2042
2047
|
autoMerge: resolveRunPlan(options).autoMerge,
|
|
2043
2048
|
gitRemote: gitContext?.remote || options.gitRemote,
|
|
@@ -2155,8 +2160,16 @@ class Orchestrator {
|
|
|
2155
2160
|
// NEVER pass --volumes (irreversible data loss) and NEVER tear down a pinned/shared
|
|
2156
2161
|
// Compose project — only a project scoped to the worktree directory basename, which is
|
|
2157
2162
|
// the only kind zeroshot could itself have created, is touched.
|
|
2158
|
-
|
|
2159
|
-
|
|
2163
|
+
let teardown;
|
|
2164
|
+
try {
|
|
2165
|
+
const { resolveWorktreeComposeTeardown } = require('../lib/compose-utils');
|
|
2166
|
+
teardown = resolveWorktreeComposeTeardown(worktreePath);
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
this._log(
|
|
2169
|
+
`[Orchestrator] Skipping Docker Compose teardown in ${worktreePath}: ${error.message}`
|
|
2170
|
+
);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2160
2173
|
if (!teardown.shouldTeardown) {
|
|
2161
2174
|
if (teardown.composePath) {
|
|
2162
2175
|
this._log(
|
|
@@ -2420,6 +2433,7 @@ class Orchestrator {
|
|
|
2420
2433
|
cluster.pid = null; // Clear PID - cluster is no longer running
|
|
2421
2434
|
|
|
2422
2435
|
if (shouldAutoCleanWorktree) {
|
|
2436
|
+
this._captureFinalRun(clusterId, cluster);
|
|
2423
2437
|
// Close message bus and ledger (same as kill() path)
|
|
2424
2438
|
cluster.messageBus.close();
|
|
2425
2439
|
}
|
|
@@ -2552,6 +2566,35 @@ class Orchestrator {
|
|
|
2552
2566
|
cluster.messageBus.close();
|
|
2553
2567
|
}
|
|
2554
2568
|
}
|
|
2569
|
+
this._finalRuns.clear();
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
_captureFinalRun(clusterId, cluster) {
|
|
2573
|
+
const snapshot = cluster.messageBus.readSnapshot(clusterId);
|
|
2574
|
+
const terminalMessages = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED']
|
|
2575
|
+
.flatMap((topic) => cluster.messageBus.query({ cluster_id: clusterId, topic }))
|
|
2576
|
+
.sort((left, right) => {
|
|
2577
|
+
const a = BigInt(left.sequence);
|
|
2578
|
+
const b = BigInt(right.sequence);
|
|
2579
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2580
|
+
});
|
|
2581
|
+
this._finalRuns.set(clusterId, {
|
|
2582
|
+
snapshot,
|
|
2583
|
+
terminalMessages,
|
|
2584
|
+
status: {
|
|
2585
|
+
id: clusterId,
|
|
2586
|
+
state: cluster.state,
|
|
2587
|
+
isZombie: false,
|
|
2588
|
+
pid: null,
|
|
2589
|
+
createdAt: cluster.createdAt,
|
|
2590
|
+
agents: cluster.agents.map((agent) => agent.getState()),
|
|
2591
|
+
messageCount: snapshot.messageCount,
|
|
2592
|
+
setupLogPath: cluster.setupLogPath || null,
|
|
2593
|
+
setupStage: cluster.setupStage || null,
|
|
2594
|
+
failureInfo: cluster.failureInfo || null,
|
|
2595
|
+
runMode: cluster.runMode || null,
|
|
2596
|
+
},
|
|
2597
|
+
});
|
|
2555
2598
|
}
|
|
2556
2599
|
|
|
2557
2600
|
/**
|
|
@@ -4217,7 +4260,7 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
4217
4260
|
|
|
4218
4261
|
// Get issue context from ledger
|
|
4219
4262
|
const issueMsg = cluster.messageBus.ledger.findLast({ topic: 'ISSUE_OPENED' });
|
|
4220
|
-
const issueNumber = issueMsg?.content?.data?.
|
|
4263
|
+
const issueNumber = issueMsg?.content?.data?.issue_number || 'unknown';
|
|
4221
4264
|
const issueTitle = issueMsg?.content?.data?.title || 'Implementation';
|
|
4222
4265
|
|
|
4223
4266
|
// Generate the final prompt in one typed assembly pass. Issue values are
|
|
@@ -4290,11 +4333,16 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
4290
4333
|
* @returns {Object} Cluster status
|
|
4291
4334
|
*/
|
|
4292
4335
|
getStatus(clusterId) {
|
|
4336
|
+
const finalRun = this._finalRuns.get(clusterId);
|
|
4337
|
+
if (finalRun) return finalRun.status;
|
|
4293
4338
|
const cluster = this.clusters.get(clusterId);
|
|
4294
4339
|
if (!cluster) {
|
|
4295
4340
|
throw new Error(`Cluster ${clusterId} not found`);
|
|
4296
4341
|
}
|
|
4342
|
+
return this._getLiveStatus(clusterId, cluster);
|
|
4343
|
+
}
|
|
4297
4344
|
|
|
4345
|
+
_getLiveStatus(clusterId, cluster) {
|
|
4298
4346
|
// Detect zombie clusters: state=running but no backing process
|
|
4299
4347
|
let state = cluster.state;
|
|
4300
4348
|
let isZombie = false;
|
|
@@ -4378,6 +4426,10 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
4378
4426
|
return this.clusters.get(clusterId);
|
|
4379
4427
|
}
|
|
4380
4428
|
|
|
4429
|
+
getFinalRun(clusterId) {
|
|
4430
|
+
return this._finalRuns.get(clusterId) || null;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4381
4433
|
/**
|
|
4382
4434
|
* Export cluster conversation
|
|
4383
4435
|
* @param {String} clusterId - Cluster ID
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const MAX_PR_BODY_LENGTH = 65536;
|
|
2
|
+
|
|
3
|
+
function normalizeIssueNumber(value) {
|
|
4
|
+
const candidate = typeof value === 'number' ? String(value) : value;
|
|
5
|
+
if (typeof candidate !== 'string') return 'unknown';
|
|
6
|
+
const trimmed = candidate.trim();
|
|
7
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) ? trimmed : 'unknown';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeIssueTitle(value) {
|
|
11
|
+
if (typeof value !== 'string' || value.trim() === '') return 'Implementation';
|
|
12
|
+
const normalized = [...value]
|
|
13
|
+
.map((character) => {
|
|
14
|
+
const codePoint = character.codePointAt(0);
|
|
15
|
+
return codePoint < 0x20 || codePoint === 0x7f ? ' ' : character;
|
|
16
|
+
})
|
|
17
|
+
.join('')
|
|
18
|
+
.trim();
|
|
19
|
+
return normalized || 'Implementation';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveIssueContext(options = {}) {
|
|
23
|
+
const issueNumber = normalizeIssueNumber(options.issueNumber);
|
|
24
|
+
const issueTitle = normalizeIssueTitle(options.issueTitle);
|
|
25
|
+
const issueReference =
|
|
26
|
+
options.includeIssueReference === false || issueNumber === 'unknown'
|
|
27
|
+
? ''
|
|
28
|
+
: `Closes #${issueNumber}`;
|
|
29
|
+
return { issueNumber, issueTitle, issueReference };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePrBodyTemplate(value) {
|
|
33
|
+
if (value === undefined || value === null) return null;
|
|
34
|
+
if (typeof value !== 'string') throw new TypeError('PR body template must be a string');
|
|
35
|
+
if (value.includes('\0')) throw new TypeError('PR body template must not contain NUL bytes');
|
|
36
|
+
if (value.length > MAX_PR_BODY_LENGTH) {
|
|
37
|
+
throw new TypeError(`PR body template must not exceed ${MAX_PR_BODY_LENGTH} characters`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render a bounded PR body. Missing issue metadata expands to empty text so
|
|
44
|
+
* manual tasks never leak internal sentinel values into pull requests.
|
|
45
|
+
*/
|
|
46
|
+
function renderPullRequestBody(template, options = {}) {
|
|
47
|
+
const issueContext = resolveIssueContext(options);
|
|
48
|
+
const normalizedTemplate = normalizePrBodyTemplate(template);
|
|
49
|
+
if (normalizedTemplate === null) return issueContext.issueReference;
|
|
50
|
+
|
|
51
|
+
const hasIssue = issueContext.issueNumber !== 'unknown';
|
|
52
|
+
const substitutions = {
|
|
53
|
+
'{{issue_number}}': hasIssue ? issueContext.issueNumber : '',
|
|
54
|
+
'{{issue_title}}': hasIssue ? issueContext.issueTitle : '',
|
|
55
|
+
'{{issue_reference}}': hasIssue ? issueContext.issueReference : '',
|
|
56
|
+
};
|
|
57
|
+
let rendered = normalizedTemplate;
|
|
58
|
+
for (const [token, replacement] of Object.entries(substitutions)) {
|
|
59
|
+
rendered = rendered.replaceAll(token, replacement);
|
|
60
|
+
}
|
|
61
|
+
if (rendered.length > MAX_PR_BODY_LENGTH) {
|
|
62
|
+
throw new TypeError(`Rendered PR body must not exceed ${MAX_PR_BODY_LENGTH} characters`);
|
|
63
|
+
}
|
|
64
|
+
return rendered;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
MAX_PR_BODY_LENGTH,
|
|
69
|
+
renderPullRequestBody,
|
|
70
|
+
resolveIssueContext,
|
|
71
|
+
};
|
package/src/preflight.js
CHANGED
|
@@ -29,6 +29,10 @@ const {
|
|
|
29
29
|
} = require('../lib/provider-names');
|
|
30
30
|
const { detectGitContext } = require('../lib/git-remote-utils');
|
|
31
31
|
const { readKeychainCredentials } = require('./claude-credentials');
|
|
32
|
+
const {
|
|
33
|
+
TASK_EXECUTION_CONTEXT_ENV,
|
|
34
|
+
resolveTaskExecutionContext,
|
|
35
|
+
} = require('./task-execution-context');
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
38
|
* Validation result
|
|
@@ -164,6 +168,13 @@ function checkClaudeAuth() {
|
|
|
164
168
|
...(method && { method }),
|
|
165
169
|
});
|
|
166
170
|
|
|
171
|
+
// Claude Code supports gateway bearer authentication independently of Anthropic API keys.
|
|
172
|
+
// This is the documented path for OpenRouter and enterprise LLM gateways, where
|
|
173
|
+
// ANTHROPIC_API_KEY is intentionally present but empty to avoid credential conflicts.
|
|
174
|
+
if (process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
175
|
+
return authResult(true, null, 'auth_token');
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
// Check for Bedrock bearer token (highest priority)
|
|
168
179
|
if (process.env.AWS_BEARER_TOKEN_BEDROCK) {
|
|
169
180
|
if (!process.env.AWS_REGION) {
|
|
@@ -481,7 +492,7 @@ function validateProviderExecutionSettings(providerName, settings, options) {
|
|
|
481
492
|
return [];
|
|
482
493
|
}
|
|
483
494
|
|
|
484
|
-
const executionContext = options
|
|
495
|
+
const executionContext = resolvePreflightExecutionContext(options);
|
|
485
496
|
const error = metadata.settingsValidator(providerSettings, { executionContext });
|
|
486
497
|
if (!error) return [];
|
|
487
498
|
|
|
@@ -519,11 +530,20 @@ function validateProvider(providerName, options) {
|
|
|
519
530
|
return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
|
|
520
531
|
}
|
|
521
532
|
|
|
522
|
-
const result = validateRegistryCliProvider(
|
|
533
|
+
const result = validateRegistryCliProvider(
|
|
534
|
+
providerName,
|
|
535
|
+
resolvePreflightExecutionContext(options)
|
|
536
|
+
);
|
|
523
537
|
return { errors: [...isolationErrors, ...result.errors], warnings: result.warnings };
|
|
524
538
|
}
|
|
525
539
|
|
|
526
|
-
function
|
|
540
|
+
function resolvePreflightExecutionContext(options) {
|
|
541
|
+
const executionContext =
|
|
542
|
+
options.executionContext ?? (options.requireDocker ? 'docker' : 'detached');
|
|
543
|
+
return resolveTaskExecutionContext({ [TASK_EXECUTION_CONTEXT_ENV]: executionContext });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function validateRegistryCliProvider(providerName, executionContext) {
|
|
527
547
|
const metadata = getProviderMetadata(providerName);
|
|
528
548
|
const { command } = resolveProviderCommand(providerName);
|
|
529
549
|
const installSteps = metadata.installInstructions.split('\n').filter(Boolean);
|
|
@@ -537,7 +557,7 @@ function validateRegistryCliProvider(providerName) {
|
|
|
537
557
|
}
|
|
538
558
|
|
|
539
559
|
const { getProvider } = require('./providers');
|
|
540
|
-
if (getProvider(providerName).isAvailable()) {
|
|
560
|
+
if (getProvider(providerName).isAvailable({ executionContext })) {
|
|
541
561
|
return { errors: [], warnings: [] };
|
|
542
562
|
}
|
|
543
563
|
|
|
@@ -642,6 +662,7 @@ function validateGitRequirement() {
|
|
|
642
662
|
* @param {boolean} options.quiet - Suppress success messages
|
|
643
663
|
* @param {string} options.claudeCommand - Custom Claude command (from settings)
|
|
644
664
|
* @param {string} options.provider - Provider override
|
|
665
|
+
* @param {'host'|'detached'|'docker'|'benchmark'} options.executionContext - Provider execution boundary
|
|
645
666
|
* @returns {ValidationResult}
|
|
646
667
|
*/
|
|
647
668
|
async function runPreflight(options = {}) {
|
|
@@ -670,10 +691,14 @@ async function runPreflight(options = {}) {
|
|
|
670
691
|
const providerName = normalizeProviderName(
|
|
671
692
|
options.provider || settings.defaultProvider || getDefaultProviderId()
|
|
672
693
|
);
|
|
694
|
+
const providerOptions = {
|
|
695
|
+
...options,
|
|
696
|
+
executionContext: resolvePreflightExecutionContext(options),
|
|
697
|
+
};
|
|
673
698
|
|
|
674
|
-
const providerResult = validateProvider(providerName,
|
|
699
|
+
const providerResult = validateProvider(providerName, providerOptions);
|
|
675
700
|
errors.push(...providerResult.errors);
|
|
676
|
-
errors.push(...validateProviderExecutionSettings(providerName, settings,
|
|
701
|
+
errors.push(...validateProviderExecutionSettings(providerName, settings, providerOptions));
|
|
677
702
|
warnings.push(...providerResult.warnings);
|
|
678
703
|
|
|
679
704
|
// 4. Check issue provider CLI (if required)
|
package/src/providers/index.js
CHANGED
|
@@ -32,13 +32,13 @@ class RuntimeProvider extends BaseProvider {
|
|
|
32
32
|
return this._adapter;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
isAvailable() {
|
|
35
|
+
isAvailable(context = {}) {
|
|
36
36
|
if (this._usesBundledRunner()) {
|
|
37
|
-
return helper.probeRuntimeProviderCli(this.name).available;
|
|
37
|
+
return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
|
|
38
38
|
}
|
|
39
39
|
const { command } = resolveProviderCommand(this.name);
|
|
40
40
|
if (!commandExists(command)) return false;
|
|
41
|
-
return helper.probeRuntimeProviderCli(this.name).available;
|
|
41
|
+
return helper.probeRuntimeProviderCli(this.name, undefined, undefined, context).available;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
getCliPath() {
|