@kbediako/codex-orchestrator 0.1.11 → 0.1.12
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 +42 -12
- package/dist/orchestrator/src/cli/control/controlServer.js +1 -4
- package/dist/orchestrator/src/cli/delegationServer.js +294 -19
- package/dist/orchestrator/src/cli/rlm/context.js +330 -0
- package/dist/orchestrator/src/cli/rlm/runner.js +4 -0
- package/dist/orchestrator/src/cli/rlm/symbolic.js +644 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +340 -101
- package/docs/README.md +270 -0
- package/docs/assets/setup.gif +0 -0
- package/package.json +2 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +57 -1
- package/skills/delegation-usage/SKILL.md +22 -12
- package/skills/docs-first/SKILL.md +42 -0
- package/skills/standalone-review/SKILL.md +62 -0
- package/templates/codex/AGENTS.md +29 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Codex Orchestrator
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Codex Orchestrator is the CLI + runtime that coordinates Codex-driven runs, pipelines, and delegation MCP tooling. The npm release focuses on running pipelines locally, emitting auditable manifests, and hosting the delegation server.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
@@ -45,33 +47,57 @@ codex-orchestrator delegate-server --repo /path/to/repo
|
|
|
45
47
|
```
|
|
46
48
|
Optional: add `--mode question_only` to disable `delegate.spawn/pause/cancel`, keeping only `delegate.question.*` + `delegate.status` in the delegate namespace. GitHub tools remain available when GitHub integration is enabled.
|
|
47
49
|
|
|
48
|
-
Register it with Codex once
|
|
50
|
+
Register it with Codex once. Delegation MCP is enabled by default (the only MCP enabled by default). To override the default or re-enable after disabling:
|
|
49
51
|
```bash
|
|
50
52
|
codex mcp add delegation -- codex-orchestrator delegate-server --repo /path/to/repo
|
|
51
53
|
codex -c 'mcp_servers.delegation.enabled=true' ...
|
|
52
54
|
```
|
|
53
55
|
`delegate-server` is the canonical name; `delegation-server` is supported as an alias (older docs may use it).
|
|
54
56
|
|
|
55
|
-
## Delegation flow
|
|
57
|
+
## Delegation + RLM flow
|
|
58
|
+
|
|
59
|
+
RLM (Recursive Language Model) is the long-horizon loop used by the `rlm` pipeline (`codex-orchestrator rlm "<goal>"` or `codex-orchestrator start rlm --goal "<goal>"`). Delegated runs only enter RLM when the child is launched with the `rlm` pipeline (or the rlm runner directly). In auto mode it resolves to symbolic when delegated, when `RLM_CONTEXT_PATH` is set, or when the context exceeds `RLM_SYMBOLIC_MIN_BYTES`; otherwise it stays iterative. The runner writes state to `.runs/<task-id>/cli/<run-id>/rlm/state.json` and stops when the validator passes or budgets are exhausted.
|
|
56
60
|
|
|
61
|
+
### Delegation flow
|
|
57
62
|
```mermaid
|
|
58
|
-
flowchart
|
|
59
|
-
A["Parent
|
|
60
|
-
B["Background Codex run\n(delegation enabled)"]
|
|
63
|
+
flowchart TB
|
|
64
|
+
A["Parent run<br/>(delegation MCP enabled)"]
|
|
61
65
|
C["Delegation MCP server"]
|
|
62
66
|
D["delegate.spawn"]
|
|
63
|
-
E["Child run"]
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
E["Child run<br/>(pipeline resolved)"]
|
|
68
|
+
N{Pipeline = rlm?}
|
|
69
|
+
P["Standard pipeline<br/>(plan/build/test/review)"]
|
|
70
|
+
RLM["RLM pipeline<br/>(see next chart)"]
|
|
71
|
+
|
|
72
|
+
A --> C --> D --> E --> N
|
|
73
|
+
N -- yes --> RLM
|
|
74
|
+
N -- no --> P
|
|
75
|
+
E -. optional .-> Q["delegate.question.enqueue/poll"] -.-> A
|
|
76
|
+
```
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
### RLM loop
|
|
79
|
+
```mermaid
|
|
80
|
+
flowchart TB
|
|
81
|
+
F["Resolve mode<br/>(auto -> iterative/symbolic)"]
|
|
82
|
+
G{Symbolic?}
|
|
83
|
+
H["Context store<br/>(chunk + search)"]
|
|
84
|
+
I["Planner JSON<br/>(select subcalls)"]
|
|
85
|
+
J["Subcalls<br/>(tool + edits)"]
|
|
86
|
+
K["Validator<br/>(test command)"]
|
|
87
|
+
L["State + artifacts<br/>.runs/<task-id>/cli/<run-id>/rlm/state.json"]
|
|
88
|
+
M["Exit status"]
|
|
89
|
+
|
|
90
|
+
F --> G
|
|
91
|
+
G -- yes --> H --> I --> J --> K
|
|
92
|
+
G -- no --> J
|
|
93
|
+
J --> K
|
|
94
|
+
K --> L --> M
|
|
95
|
+
K -- fail & budget left --> F
|
|
70
96
|
```
|
|
71
97
|
|
|
72
98
|
## Skills (bundled)
|
|
73
99
|
|
|
74
|
-
The release ships skills under `skills
|
|
100
|
+
The release ships skills under `skills/` for downstream packaging. If you already have global skills installed, treat those as the primary reference and use bundled skills as the shipped fallback. Install bundled skills into `$CODEX_HOME/skills`:
|
|
75
101
|
```bash
|
|
76
102
|
codex-orchestrator skills install
|
|
77
103
|
```
|
|
@@ -82,6 +108,8 @@ Options:
|
|
|
82
108
|
|
|
83
109
|
Bundled skills (may vary by release):
|
|
84
110
|
- `delegation-usage`
|
|
111
|
+
- `standalone-review`
|
|
112
|
+
- `docs-first`
|
|
85
113
|
|
|
86
114
|
## DevTools readiness
|
|
87
115
|
|
|
@@ -100,6 +128,7 @@ codex-orchestrator devtools setup
|
|
|
100
128
|
- `codex-orchestrator start <pipeline>` — run a pipeline.
|
|
101
129
|
- `codex-orchestrator plan <pipeline>` — preview pipeline stages.
|
|
102
130
|
- `codex-orchestrator exec <cmd>` — run a one-off command with the exec runtime.
|
|
131
|
+
- `codex-orchestrator init codex` — install starter templates (`mcp-client.json`, `AGENTS.md`) into a repo.
|
|
103
132
|
- `codex-orchestrator self-check --format json` — JSON health payload.
|
|
104
133
|
- `codex-orchestrator mcp serve` — Codex MCP stdio server.
|
|
105
134
|
|
|
@@ -114,3 +143,4 @@ codex-orchestrator devtools setup
|
|
|
114
143
|
|
|
115
144
|
Repo internals, development workflows, and deeper architecture notes live in the GitHub repository:
|
|
116
145
|
- `docs/README.md`
|
|
146
|
+
- `docs/diagnostics-prompt-guide.md` (first-run diagnostics prompt + expected outputs)
|
|
@@ -1306,10 +1306,7 @@ function assertRunManifestPath(pathname, label) {
|
|
|
1306
1306
|
}
|
|
1307
1307
|
const taskDir = dirname(cliDir);
|
|
1308
1308
|
const runsDir = dirname(taskDir);
|
|
1309
|
-
if (basename(
|
|
1310
|
-
throw new Error(`${label} invalid`);
|
|
1311
|
-
}
|
|
1312
|
-
if (!basename(runDir) || !basename(taskDir)) {
|
|
1309
|
+
if (!basename(runDir) || !basename(taskDir) || !basename(runsDir)) {
|
|
1313
1310
|
throw new Error(`${label} invalid`);
|
|
1314
1311
|
}
|
|
1315
1312
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
|
-
import { chmod, readFile } from 'node:fs/promises';
|
|
5
|
-
import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { access, chmod, readFile, readdir, stat } from 'node:fs/promises';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
|
|
8
8
|
import { logger } from '../logger.js';
|
|
@@ -11,6 +11,8 @@ const PROTOCOL_VERSION = '2024-11-05';
|
|
|
11
11
|
const QUESTION_POLL_INTERVAL_MS = 500;
|
|
12
12
|
const MAX_QUESTION_POLL_WAIT_MS = 10_000;
|
|
13
13
|
const DEFAULT_SPAWN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
|
+
const DEFAULT_SPAWN_START_TIMEOUT_MS = 10_000;
|
|
15
|
+
const DEFAULT_SPAWN_START_POLL_INTERVAL_MS = 200;
|
|
14
16
|
const DEFAULT_GH_TIMEOUT_MS = 60_000;
|
|
15
17
|
const DEFAULT_DELEGATION_TOKEN_RETRY_MS = 2000;
|
|
16
18
|
const DEFAULT_DELEGATION_TOKEN_RETRY_INTERVAL_MS = 200;
|
|
@@ -94,7 +96,8 @@ function buildToolList(options) {
|
|
|
94
96
|
parent_run_id: { type: 'string' },
|
|
95
97
|
parent_manifest_path: { type: 'string' },
|
|
96
98
|
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
97
|
-
delegate_mode: { type: 'string', enum: ['full', 'question_only'] }
|
|
99
|
+
delegate_mode: { type: 'string', enum: ['full', 'question_only'] },
|
|
100
|
+
start_only: { type: 'boolean' }
|
|
98
101
|
},
|
|
99
102
|
required: ['pipeline', 'repo']
|
|
100
103
|
}));
|
|
@@ -322,10 +325,16 @@ async function handleDelegateSpawn(input, repoRoot, allowNested, allowedRoots, a
|
|
|
322
325
|
const pipeline = requireString(readStringValue(input, 'pipeline'), 'pipeline');
|
|
323
326
|
const repo = readStringValue(input, 'repo') ?? repoRoot ?? process.cwd();
|
|
324
327
|
const resolvedRepo = resolve(repo);
|
|
328
|
+
const resolvedRepoRoot = realpathSafe(resolvedRepo);
|
|
325
329
|
if (!isPathWithinRoots(resolvedRepo, allowedRoots)) {
|
|
326
330
|
throw new Error('repo_not_permitted');
|
|
327
331
|
}
|
|
328
332
|
const taskId = readStringValue(input, 'task_id', 'taskId');
|
|
333
|
+
const startOnly = readBooleanValue(input, 'start_only', 'startOnly');
|
|
334
|
+
const resolvedStartOnly = startOnly ?? true;
|
|
335
|
+
if (resolvedStartOnly && !taskId) {
|
|
336
|
+
throw new Error('task_id is required when start_only=true');
|
|
337
|
+
}
|
|
329
338
|
const args = ['start', pipeline, '--format', 'json', '--no-interactive'];
|
|
330
339
|
if (taskId) {
|
|
331
340
|
args.push('--task', taskId);
|
|
@@ -347,19 +356,120 @@ async function handleDelegateSpawn(input, repoRoot, allowNested, allowedRoots, a
|
|
|
347
356
|
...(parentManifestPath ? { CODEX_DELEGATION_PARENT_MANIFEST_PATH: parentManifestPath } : {}),
|
|
348
357
|
...(mcpOverrides.length > 0 ? { CODEX_MCP_CONFIG_OVERRIDES: mcpOverrides.join(';') } : {})
|
|
349
358
|
};
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
if (!envOverrides || !Object.prototype.hasOwnProperty.call(envOverrides, 'CODEX_ORCHESTRATOR_ROOT')) {
|
|
360
|
+
childEnv.CODEX_ORCHESTRATOR_ROOT = resolvedRepoRoot;
|
|
361
|
+
}
|
|
362
|
+
if (!resolvedStartOnly) {
|
|
363
|
+
const child = spawn('codex-orchestrator', args, { cwd: resolvedRepo, env: childEnv });
|
|
364
|
+
const output = await collectOutput(child, DEFAULT_SPAWN_TIMEOUT_MS);
|
|
365
|
+
const parsedRecord = parseSpawnOutput(output.stdout);
|
|
366
|
+
const manifestPath = readStringValue(parsedRecord, 'manifest');
|
|
367
|
+
if (!manifestPath) {
|
|
368
|
+
return { status: 'spawn_failed', stdout: output.stdout.trim(), stderr: output.stderr.trim() };
|
|
369
|
+
}
|
|
370
|
+
const runId = readStringValue(parsedRecord, 'run_id', 'runId');
|
|
371
|
+
const logPath = readStringValue(parsedRecord, 'log_path', 'logPath');
|
|
372
|
+
const resolvedManifestPath = resolveSpawnManifestPath(manifestPath, resolvedRepo, allowedRoots);
|
|
373
|
+
if (!resolvedManifestPath) {
|
|
374
|
+
return { status: 'spawn_failed', stdout: output.stdout.trim(), stderr: output.stderr.trim() };
|
|
375
|
+
}
|
|
376
|
+
const eventsPath = `${dirname(resolvedManifestPath)}/events.jsonl`;
|
|
377
|
+
await persistDelegationToken(resolvedManifestPath, delegationToken, {
|
|
378
|
+
parentRunId: parentRunId ?? null,
|
|
379
|
+
childRunId: runId ?? null
|
|
380
|
+
});
|
|
381
|
+
if (parentManifestPath && parentRunId && runId) {
|
|
382
|
+
try {
|
|
383
|
+
await callControlEndpoint(parentManifestPath, '/delegation/register', {
|
|
384
|
+
token: delegationToken,
|
|
385
|
+
parent_run_id: parentRunId,
|
|
386
|
+
child_run_id: runId
|
|
387
|
+
}, undefined, { allowedHosts });
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
logger.warn(`Failed to register delegation token: ${error?.message ?? error}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
run_id: runId,
|
|
395
|
+
manifest_path: resolvedManifestPath,
|
|
396
|
+
log_path: logPath,
|
|
397
|
+
events_path: eventsPath
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const runsRepoRoot = resolveRepoRootForRuns(resolvedRepo, childEnv);
|
|
401
|
+
const runsRoot = resolveRunsRoot(runsRepoRoot, childEnv);
|
|
402
|
+
if (!isPathWithinRoots(runsRoot, allowedRoots)) {
|
|
403
|
+
throw new Error('runs_root not permitted');
|
|
404
|
+
}
|
|
405
|
+
const taskRunsRoot = join(runsRoot, taskId, 'cli');
|
|
406
|
+
const baselineRuns = await snapshotRunManifests(taskRunsRoot);
|
|
407
|
+
const spawnStart = Date.now();
|
|
408
|
+
const child = spawn('codex-orchestrator', args, {
|
|
409
|
+
cwd: resolvedRepo,
|
|
410
|
+
env: childEnv,
|
|
411
|
+
detached: true,
|
|
412
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
413
|
+
});
|
|
414
|
+
let spawnError = null;
|
|
415
|
+
const recordSpawnError = (message) => {
|
|
416
|
+
if (!spawnError) {
|
|
417
|
+
spawnError = new Error(message);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
child.once('error', (error) => {
|
|
421
|
+
spawnError = error instanceof Error ? error : new Error(String(error));
|
|
422
|
+
});
|
|
423
|
+
child.once('exit', (code, signal) => {
|
|
424
|
+
if (code === 0 && !signal) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
recordSpawnError(`delegate.spawn child exited with code ${code ?? 'null'} (${signal ?? 'no signal'})`);
|
|
428
|
+
});
|
|
429
|
+
child.once('close', (code, signal) => {
|
|
430
|
+
if (code === 0 && !signal) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
recordSpawnError(`delegate.spawn child closed with code ${code ?? 'null'} (${signal ?? 'no signal'})`);
|
|
434
|
+
});
|
|
435
|
+
child.unref();
|
|
436
|
+
const timeoutMs = resolveSpawnStartTimeoutMs(childEnv);
|
|
437
|
+
const manifestInfo = await pollForSpawnManifest({
|
|
438
|
+
taskId: taskId,
|
|
439
|
+
taskRunsRoot,
|
|
440
|
+
baselineRuns,
|
|
441
|
+
spawnStart,
|
|
442
|
+
timeoutMs,
|
|
443
|
+
intervalMs: DEFAULT_SPAWN_START_POLL_INTERVAL_MS,
|
|
444
|
+
getSpawnError: () => spawnError
|
|
445
|
+
});
|
|
446
|
+
if (!manifestInfo) {
|
|
447
|
+
const candidates = await collectSpawnCandidates(taskRunsRoot, taskId);
|
|
448
|
+
return {
|
|
449
|
+
status: 'spawn_failed',
|
|
450
|
+
task_id: taskId,
|
|
451
|
+
runs_root: runsRoot,
|
|
452
|
+
expected_manifest_glob: join(taskRunsRoot, '*', 'manifest.json'),
|
|
453
|
+
candidates,
|
|
454
|
+
error: spawnError?.message ?? null
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const resolvedManifestPath = resolveSpawnManifestPath(manifestInfo.manifestPath, resolvedRepo, allowedRoots);
|
|
360
458
|
if (!resolvedManifestPath) {
|
|
361
|
-
return {
|
|
459
|
+
return {
|
|
460
|
+
status: 'spawn_failed',
|
|
461
|
+
task_id: taskId,
|
|
462
|
+
runs_root: runsRoot,
|
|
463
|
+
expected_manifest_glob: join(taskRunsRoot, '*', 'manifest.json'),
|
|
464
|
+
candidates: [
|
|
465
|
+
{
|
|
466
|
+
path: manifestInfo.manifestPath,
|
|
467
|
+
reason: 'manifest_path not permitted'
|
|
468
|
+
}
|
|
469
|
+
]
|
|
470
|
+
};
|
|
362
471
|
}
|
|
472
|
+
const runId = manifestInfo.runId;
|
|
363
473
|
const eventsPath = `${dirname(resolvedManifestPath)}/events.jsonl`;
|
|
364
474
|
await persistDelegationToken(resolvedManifestPath, delegationToken, {
|
|
365
475
|
parentRunId: parentRunId ?? null,
|
|
@@ -380,7 +490,7 @@ async function handleDelegateSpawn(input, repoRoot, allowNested, allowedRoots, a
|
|
|
380
490
|
return {
|
|
381
491
|
run_id: runId,
|
|
382
492
|
manifest_path: resolvedManifestPath,
|
|
383
|
-
log_path: logPath,
|
|
493
|
+
log_path: manifestInfo.logPath ?? null,
|
|
384
494
|
events_path: eventsPath
|
|
385
495
|
};
|
|
386
496
|
}
|
|
@@ -1035,6 +1145,174 @@ function clampQuestionPollWaitMs(value) {
|
|
|
1035
1145
|
function delay(ms) {
|
|
1036
1146
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1037
1147
|
}
|
|
1148
|
+
function resolveRepoRootForRuns(repoRoot, env) {
|
|
1149
|
+
const configured = env.CODEX_ORCHESTRATOR_ROOT?.trim();
|
|
1150
|
+
if (!configured) {
|
|
1151
|
+
return repoRoot;
|
|
1152
|
+
}
|
|
1153
|
+
return isAbsolute(configured) ? configured : resolve(repoRoot, configured);
|
|
1154
|
+
}
|
|
1155
|
+
function resolveRunsRoot(repoRoot, env) {
|
|
1156
|
+
const configured = env.CODEX_ORCHESTRATOR_RUNS_DIR?.trim();
|
|
1157
|
+
if (!configured) {
|
|
1158
|
+
return resolve(repoRoot, '.runs');
|
|
1159
|
+
}
|
|
1160
|
+
return isAbsolute(configured) ? configured : resolve(repoRoot, configured);
|
|
1161
|
+
}
|
|
1162
|
+
function resolveSpawnStartTimeoutMs(env) {
|
|
1163
|
+
const raw = env.CODEX_DELEGATION_SPAWN_START_TIMEOUT_MS ?? env.DELEGATION_SPAWN_START_TIMEOUT_MS;
|
|
1164
|
+
if (!raw) {
|
|
1165
|
+
return DEFAULT_SPAWN_START_TIMEOUT_MS;
|
|
1166
|
+
}
|
|
1167
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1168
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1169
|
+
return DEFAULT_SPAWN_START_TIMEOUT_MS;
|
|
1170
|
+
}
|
|
1171
|
+
return parsed;
|
|
1172
|
+
}
|
|
1173
|
+
async function snapshotRunManifests(taskRunsRoot) {
|
|
1174
|
+
const snapshot = new Map();
|
|
1175
|
+
let entries;
|
|
1176
|
+
try {
|
|
1177
|
+
entries = await readdir(taskRunsRoot, { withFileTypes: true });
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
return snapshot;
|
|
1181
|
+
}
|
|
1182
|
+
await Promise.all(entries.map(async (entry) => {
|
|
1183
|
+
if (!entry.isDirectory()) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const runId = entry.name;
|
|
1187
|
+
const manifestPath = join(taskRunsRoot, runId, 'manifest.json');
|
|
1188
|
+
let exists = false;
|
|
1189
|
+
try {
|
|
1190
|
+
await access(manifestPath);
|
|
1191
|
+
exists = true;
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
exists = false;
|
|
1195
|
+
}
|
|
1196
|
+
snapshot.set(runId, { manifestExists: exists });
|
|
1197
|
+
}));
|
|
1198
|
+
return snapshot;
|
|
1199
|
+
}
|
|
1200
|
+
async function findSpawnManifest(params) {
|
|
1201
|
+
let entries;
|
|
1202
|
+
try {
|
|
1203
|
+
entries = await readdir(params.taskRunsRoot, { withFileTypes: true });
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
const candidates = [];
|
|
1209
|
+
for (const entry of entries) {
|
|
1210
|
+
if (!entry.isDirectory()) {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const runId = entry.name;
|
|
1214
|
+
const manifestPath = join(params.taskRunsRoot, runId, 'manifest.json');
|
|
1215
|
+
let stats;
|
|
1216
|
+
try {
|
|
1217
|
+
stats = await stat(manifestPath);
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const baseline = params.baselineRuns.get(runId);
|
|
1223
|
+
if (baseline?.manifestExists) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (stats.mtimeMs < params.spawnStart) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
candidates.push({ runId, manifestPath, mtimeMs: stats.mtimeMs });
|
|
1230
|
+
}
|
|
1231
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1232
|
+
for (const candidate of candidates) {
|
|
1233
|
+
try {
|
|
1234
|
+
const raw = await readFile(candidate.manifestPath, 'utf8');
|
|
1235
|
+
const parsed = JSON.parse(raw);
|
|
1236
|
+
if (parsed.task_id && parsed.task_id !== params.taskId) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const runId = typeof parsed.run_id === 'string' && parsed.run_id.trim()
|
|
1240
|
+
? parsed.run_id.trim()
|
|
1241
|
+
: candidate.runId;
|
|
1242
|
+
if (!runId) {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const logPath = typeof parsed.log_path === 'string' && parsed.log_path.trim().length > 0
|
|
1246
|
+
? parsed.log_path.trim()
|
|
1247
|
+
: null;
|
|
1248
|
+
return { runId, manifestPath: candidate.manifestPath, logPath };
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
async function pollForSpawnManifest(params) {
|
|
1257
|
+
const deadline = Date.now() + params.timeoutMs;
|
|
1258
|
+
while (Date.now() <= deadline) {
|
|
1259
|
+
if (params.getSpawnError()) {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
const manifest = await findSpawnManifest(params);
|
|
1263
|
+
if (manifest) {
|
|
1264
|
+
return manifest;
|
|
1265
|
+
}
|
|
1266
|
+
await delay(params.intervalMs);
|
|
1267
|
+
}
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1270
|
+
async function collectSpawnCandidates(taskRunsRoot, taskId) {
|
|
1271
|
+
let entries;
|
|
1272
|
+
try {
|
|
1273
|
+
entries = await readdir(taskRunsRoot, { withFileTypes: true });
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return [];
|
|
1277
|
+
}
|
|
1278
|
+
const candidates = [];
|
|
1279
|
+
for (const entry of entries) {
|
|
1280
|
+
if (!entry.isDirectory()) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const manifestPath = join(taskRunsRoot, entry.name, 'manifest.json');
|
|
1284
|
+
try {
|
|
1285
|
+
await access(manifestPath);
|
|
1286
|
+
}
|
|
1287
|
+
catch {
|
|
1288
|
+
candidates.push({ path: manifestPath, reason: 'manifest.json missing' });
|
|
1289
|
+
if (candidates.length >= 3) {
|
|
1290
|
+
return candidates;
|
|
1291
|
+
}
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
1296
|
+
const parsed = JSON.parse(raw);
|
|
1297
|
+
if (parsed.task_id && parsed.task_id !== taskId) {
|
|
1298
|
+
candidates.push({ path: manifestPath, reason: 'task_id mismatch' });
|
|
1299
|
+
}
|
|
1300
|
+
else if (!parsed.run_id) {
|
|
1301
|
+
candidates.push({ path: manifestPath, reason: 'run_id missing' });
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
candidates.push({ path: manifestPath, reason: 'manifest present but not selected' });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
candidates.push({ path: manifestPath, reason: 'manifest unreadable' });
|
|
1309
|
+
}
|
|
1310
|
+
if (candidates.length >= 3) {
|
|
1311
|
+
return candidates;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return candidates;
|
|
1315
|
+
}
|
|
1038
1316
|
async function collectOutput(child, timeoutMs) {
|
|
1039
1317
|
let stdout = '';
|
|
1040
1318
|
let stderr = '';
|
|
@@ -1315,10 +1593,7 @@ function assertRunManifestPath(pathname, label) {
|
|
|
1315
1593
|
}
|
|
1316
1594
|
const taskDir = dirname(cliDir);
|
|
1317
1595
|
const runsDir = dirname(taskDir);
|
|
1318
|
-
if (basename(
|
|
1319
|
-
throw new Error(`${label} invalid`);
|
|
1320
|
-
}
|
|
1321
|
-
if (!basename(runDir) || !basename(taskDir)) {
|
|
1596
|
+
if (!basename(runDir) || !basename(taskDir) || !basename(runsDir)) {
|
|
1322
1597
|
throw new Error(`${label} invalid`);
|
|
1323
1598
|
}
|
|
1324
1599
|
}
|