@kbediako/codex-orchestrator 0.1.22 → 0.1.23
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 +7 -1
- package/dist/bin/codex-orchestrator.js +75 -5
- package/dist/orchestrator/src/cli/delegationSetup.js +239 -0
- package/dist/orchestrator/src/cli/doctor.js +2 -1
- package/dist/orchestrator/src/cli/doctorUsage.js +210 -0
- package/dist/orchestrator/src/cli/init.js +1 -1
- package/docs/README.md +2 -1
- package/package.json +1 -1
- package/skills/delegation-usage/DELEGATION_GUIDE.md +2 -2
- package/skills/delegation-usage/SKILL.md +6 -4
package/README.md
CHANGED
|
@@ -152,11 +152,16 @@ Bundled skills (may vary by release):
|
|
|
152
152
|
|
|
153
153
|
## DevTools readiness
|
|
154
154
|
|
|
155
|
-
Check
|
|
155
|
+
Check readiness (deps + capability wiring):
|
|
156
156
|
```bash
|
|
157
157
|
codex-orchestrator doctor --format json
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
Usage snapshot (scans local `.runs/`):
|
|
161
|
+
```bash
|
|
162
|
+
codex-orchestrator doctor --usage
|
|
163
|
+
```
|
|
164
|
+
|
|
160
165
|
Print DevTools MCP setup guidance:
|
|
161
166
|
```bash
|
|
162
167
|
codex-orchestrator devtools setup
|
|
@@ -171,6 +176,7 @@ codex-orchestrator devtools setup
|
|
|
171
176
|
- `codex-orchestrator init codex --codex-cli --yes --codex-source <path>` — optionally provision a CO-managed Codex CLI binary (build-from-source default; set `CODEX_CLI_SOURCE` to avoid passing `--codex-source` every time).
|
|
172
177
|
- `codex-orchestrator init codex --codex-cli --yes --codex-download-url <url> --codex-download-sha256 <sha>` — opt-in to a prebuilt Codex CLI download.
|
|
173
178
|
- `codex-orchestrator codex setup` — plan/apply a CO-managed Codex CLI install (optional managed/pinned path; use `--download-url` + `--download-sha256` for prebuilts).
|
|
179
|
+
- `codex-orchestrator delegation setup --yes` — configure delegation MCP server wiring.
|
|
174
180
|
- `codex-orchestrator self-check --format json` — JSON health payload.
|
|
175
181
|
- `codex-orchestrator mcp serve` — Codex MCP stdio server.
|
|
176
182
|
|
|
@@ -14,8 +14,10 @@ import { evaluateInteractiveGate } from '../orchestrator/src/cli/utils/interacti
|
|
|
14
14
|
import { buildSelfCheckResult } from '../orchestrator/src/cli/selfCheck.js';
|
|
15
15
|
import { initCodexTemplates, formatInitSummary } from '../orchestrator/src/cli/init.js';
|
|
16
16
|
import { runDoctor, formatDoctorSummary } from '../orchestrator/src/cli/doctor.js';
|
|
17
|
+
import { formatDoctorUsageSummary, runDoctorUsage } from '../orchestrator/src/cli/doctorUsage.js';
|
|
17
18
|
import { formatDevtoolsSetupSummary, runDevtoolsSetup } from '../orchestrator/src/cli/devtoolsSetup.js';
|
|
18
19
|
import { formatCodexCliSetupSummary, runCodexCliSetup } from '../orchestrator/src/cli/codexCliSetup.js';
|
|
20
|
+
import { formatDelegationSetupSummary, runDelegationSetup } from '../orchestrator/src/cli/delegationSetup.js';
|
|
19
21
|
import { formatSkillsInstallSummary, installSkills } from '../orchestrator/src/cli/skills.js';
|
|
20
22
|
import { loadPackageInfo } from '../orchestrator/src/cli/utils/packageInfo.js';
|
|
21
23
|
import { slugify } from '../orchestrator/src/cli/utils/strings.js';
|
|
@@ -85,6 +87,9 @@ async function main() {
|
|
|
85
87
|
case 'delegation-server':
|
|
86
88
|
await handleDelegationServer(args);
|
|
87
89
|
break;
|
|
90
|
+
case 'delegation':
|
|
91
|
+
await handleDelegation(args);
|
|
92
|
+
break;
|
|
88
93
|
case 'version':
|
|
89
94
|
printVersion();
|
|
90
95
|
break;
|
|
@@ -332,6 +337,16 @@ async function handleRlm(orchestrator, rawArgs) {
|
|
|
332
337
|
process.env.MCP_RUNNER_TASK_ID = taskId;
|
|
333
338
|
applyRlmEnvOverrides(flags, goal);
|
|
334
339
|
console.log(`Task: ${taskId}`);
|
|
340
|
+
const collabUserChoice = flags['collab'] !== undefined || process.env.RLM_SYMBOLIC_COLLAB !== undefined;
|
|
341
|
+
if (!collabUserChoice) {
|
|
342
|
+
const doctor = runDoctor();
|
|
343
|
+
if (doctor.collab.status === 'ok') {
|
|
344
|
+
console.log('Tip: collab is enabled. Try: codex-orchestrator rlm --collab auto \"<goal>\"');
|
|
345
|
+
}
|
|
346
|
+
else if (doctor.collab.status === 'disabled') {
|
|
347
|
+
console.log('Tip: collab is available but disabled. Enable with: codex features enable collab');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
335
350
|
let startResult = null;
|
|
336
351
|
await withRunUi(flags, 'text', async (runEvents) => {
|
|
337
352
|
startResult = await orchestrator.start({
|
|
@@ -544,15 +559,38 @@ async function handleInit(rawArgs) {
|
|
|
544
559
|
async function handleDoctor(rawArgs) {
|
|
545
560
|
const { flags } = parseArgs(rawArgs);
|
|
546
561
|
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
547
|
-
const
|
|
562
|
+
const includeUsage = Boolean(flags['usage']);
|
|
563
|
+
const windowDaysRaw = readStringFlag(flags, 'window-days');
|
|
564
|
+
let windowDays = undefined;
|
|
565
|
+
if (windowDaysRaw) {
|
|
566
|
+
if (!/^\d+$/u.test(windowDaysRaw)) {
|
|
567
|
+
throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
|
|
568
|
+
}
|
|
569
|
+
const parsed = Number(windowDaysRaw);
|
|
570
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
571
|
+
throw new Error(`Invalid --window-days value '${windowDaysRaw}'. Expected a positive integer.`);
|
|
572
|
+
}
|
|
573
|
+
windowDays = parsed;
|
|
574
|
+
}
|
|
575
|
+
const taskFilter = readStringFlag(flags, 'task') ?? null;
|
|
576
|
+
const doctorResult = runDoctor();
|
|
577
|
+
const usageResult = includeUsage ? await runDoctorUsage({ windowDays, taskFilter }) : null;
|
|
548
578
|
if (format === 'json') {
|
|
549
|
-
|
|
579
|
+
if (usageResult) {
|
|
580
|
+
console.log(JSON.stringify({ ...doctorResult, usage: usageResult }, null, 2));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
console.log(JSON.stringify(doctorResult, null, 2));
|
|
550
584
|
return;
|
|
551
585
|
}
|
|
552
|
-
const
|
|
553
|
-
for (const line of summary) {
|
|
586
|
+
for (const line of formatDoctorSummary(doctorResult)) {
|
|
554
587
|
console.log(line);
|
|
555
588
|
}
|
|
589
|
+
if (usageResult) {
|
|
590
|
+
for (const line of formatDoctorUsageSummary(usageResult)) {
|
|
591
|
+
console.log(line);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
556
594
|
}
|
|
557
595
|
async function handleDevtools(rawArgs) {
|
|
558
596
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
@@ -578,6 +616,30 @@ async function handleDevtools(rawArgs) {
|
|
|
578
616
|
console.log(line);
|
|
579
617
|
}
|
|
580
618
|
}
|
|
619
|
+
async function handleDelegation(rawArgs) {
|
|
620
|
+
const { positionals, flags } = parseArgs(rawArgs);
|
|
621
|
+
const subcommand = positionals.shift();
|
|
622
|
+
if (!subcommand) {
|
|
623
|
+
throw new Error('delegation requires a subcommand (setup).');
|
|
624
|
+
}
|
|
625
|
+
if (subcommand !== 'setup') {
|
|
626
|
+
throw new Error(`Unknown delegation subcommand: ${subcommand}`);
|
|
627
|
+
}
|
|
628
|
+
const format = flags['format'] === 'json' ? 'json' : 'text';
|
|
629
|
+
const apply = Boolean(flags['yes']);
|
|
630
|
+
if (format === 'json' && apply) {
|
|
631
|
+
throw new Error('delegation setup does not support --format json with --yes.');
|
|
632
|
+
}
|
|
633
|
+
const repoRoot = readStringFlag(flags, 'repo') ?? process.cwd();
|
|
634
|
+
const result = await runDelegationSetup({ apply, repoRoot });
|
|
635
|
+
if (format === 'json') {
|
|
636
|
+
console.log(JSON.stringify(result, null, 2));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
for (const line of formatDelegationSetupSummary(result)) {
|
|
640
|
+
console.log(line);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
581
643
|
async function handleCodex(rawArgs) {
|
|
582
644
|
const { positionals, flags } = parseArgs(rawArgs);
|
|
583
645
|
const subcommand = positionals.shift();
|
|
@@ -961,7 +1023,11 @@ Commands:
|
|
|
961
1023
|
--codex-download-sha256 <sha> Expected SHA256 for the prebuilt download.
|
|
962
1024
|
--codex-force Overwrite existing CO-managed codex binary.
|
|
963
1025
|
--yes Apply codex CLI setup (otherwise plan only).
|
|
964
|
-
doctor [--format json]
|
|
1026
|
+
doctor [--format json] [--usage] [--window-days <n>] [--task <id>]
|
|
1027
|
+
--usage Include a local usage snapshot (scans .runs/).
|
|
1028
|
+
--window-days <n> Window for --usage (default 30).
|
|
1029
|
+
--task <id> Limit --usage scan to a specific task directory.
|
|
1030
|
+
--format json Emit machine-readable output.
|
|
965
1031
|
codex setup
|
|
966
1032
|
--source <path> Build from local Codex repo (or git URL).
|
|
967
1033
|
--ref <ref> Git ref (branch/tag/sha) when building from repo.
|
|
@@ -973,6 +1039,10 @@ Commands:
|
|
|
973
1039
|
devtools setup Print DevTools MCP setup instructions.
|
|
974
1040
|
--yes Apply setup by running "codex mcp add ...".
|
|
975
1041
|
--format json Emit machine-readable output (dry-run only).
|
|
1042
|
+
delegation setup Configure delegation MCP server wiring.
|
|
1043
|
+
--repo <path> Repo root for delegation server (default cwd).
|
|
1044
|
+
--yes Apply setup by running "codex mcp add ...".
|
|
1045
|
+
--format json Emit machine-readable output (dry-run only).
|
|
976
1046
|
skills install Install bundled skills into $CODEX_HOME/skills.
|
|
977
1047
|
--force Overwrite existing skill files.
|
|
978
1048
|
--only <skills> Install only selected skills (comma-separated).
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { resolveCodexCliBin } from './utils/codexCli.js';
|
|
6
|
+
import { resolveCodexHome } from './utils/codexPaths.js';
|
|
7
|
+
export async function runDelegationSetup(options = {}) {
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
10
|
+
const codexBin = resolveCodexCliBin(env);
|
|
11
|
+
const codexHome = resolveCodexHome(env);
|
|
12
|
+
const configPath = join(codexHome, 'config.toml');
|
|
13
|
+
const plan = {
|
|
14
|
+
codexBin,
|
|
15
|
+
codexHome,
|
|
16
|
+
repoRoot,
|
|
17
|
+
commandLine: `"${codexBin}" mcp add delegation -- codex-orchestrator delegate-server`
|
|
18
|
+
};
|
|
19
|
+
const probe = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env });
|
|
20
|
+
const readiness = { configured: probe.configured, configPath };
|
|
21
|
+
if (!options.apply) {
|
|
22
|
+
return { status: 'planned', plan, readiness };
|
|
23
|
+
}
|
|
24
|
+
if (probe.configured) {
|
|
25
|
+
return { status: 'skipped', reason: probe.reason ?? 'Delegation MCP is already configured.', plan, readiness };
|
|
26
|
+
}
|
|
27
|
+
await applyDelegationSetup({ codexBin, removeExisting: probe.removeExisting, envVars: probe.envVars }, env);
|
|
28
|
+
const configuredAfter = inspectDelegationReadiness({ codexBin, configPath, repoRoot, env }).configured;
|
|
29
|
+
return {
|
|
30
|
+
status: 'applied',
|
|
31
|
+
reason: probe.reason,
|
|
32
|
+
plan,
|
|
33
|
+
readiness: { ...readiness, configured: configuredAfter }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function formatDelegationSetupSummary(result) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`Delegation setup: ${result.status}`);
|
|
39
|
+
if (result.reason) {
|
|
40
|
+
lines.push(`Note: ${result.reason}`);
|
|
41
|
+
}
|
|
42
|
+
lines.push(`- Codex home: ${result.plan.codexHome}`);
|
|
43
|
+
lines.push(`- Config: ${result.readiness.configured ? 'ok' : 'missing'} (${result.readiness.configPath})`);
|
|
44
|
+
lines.push(`- Command: ${result.plan.commandLine}`);
|
|
45
|
+
if (result.status === 'planned') {
|
|
46
|
+
lines.push('Run with --yes to apply this setup.');
|
|
47
|
+
}
|
|
48
|
+
return lines;
|
|
49
|
+
}
|
|
50
|
+
function inspectDelegationReadiness(options) {
|
|
51
|
+
const requestedRepo = resolve(options.repoRoot);
|
|
52
|
+
const existing = readDelegationMcpServer(options.codexBin, options.env);
|
|
53
|
+
if (existing) {
|
|
54
|
+
const envVars = existing.envVars;
|
|
55
|
+
const isDelegationServer = existing.args.includes('delegate-server') || existing.args.includes('delegation-server');
|
|
56
|
+
if (!isDelegationServer) {
|
|
57
|
+
return {
|
|
58
|
+
configured: false,
|
|
59
|
+
removeExisting: true,
|
|
60
|
+
envVars,
|
|
61
|
+
reason: 'Existing delegation MCP entry does not point to codex-orchestrator delegate-server; reconfiguring.'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (existing.pinnedRepo) {
|
|
65
|
+
const pinnedRepo = resolve(existing.pinnedRepo);
|
|
66
|
+
if (pinnedRepo !== requestedRepo) {
|
|
67
|
+
return {
|
|
68
|
+
configured: false,
|
|
69
|
+
removeExisting: true,
|
|
70
|
+
envVars,
|
|
71
|
+
reason: `Existing delegation MCP entry is pinned to ${existing.pinnedRepo}; reconfiguring.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
configured: true,
|
|
76
|
+
removeExisting: false,
|
|
77
|
+
envVars,
|
|
78
|
+
reason: `Delegation MCP is already configured (pinned to ${existing.pinnedRepo}).`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
configured: true,
|
|
83
|
+
removeExisting: false,
|
|
84
|
+
envVars,
|
|
85
|
+
reason: 'Delegation MCP is already configured.'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Fall back to directly scanning config.toml when the Codex CLI probe is unavailable.
|
|
89
|
+
const configured = isDelegationConfiguredFallback(options.configPath);
|
|
90
|
+
return { configured, removeExisting: false, envVars: {} };
|
|
91
|
+
}
|
|
92
|
+
function applyDelegationSetup(plan, env) {
|
|
93
|
+
const envFlags = [];
|
|
94
|
+
for (const [key, value] of Object.entries(plan.envVars ?? {})) {
|
|
95
|
+
envFlags.push('--env', `${key}=${value}`);
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const runAdd = () => {
|
|
99
|
+
const child = spawn(plan.codexBin, ['mcp', 'add', 'delegation', ...envFlags, '--', 'codex-orchestrator', 'delegate-server'], { stdio: 'inherit', env });
|
|
100
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
101
|
+
child.once('exit', (code) => {
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
reject(new Error(`codex mcp add exited with code ${code ?? 'unknown'}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
if (!plan.removeExisting) {
|
|
111
|
+
runAdd();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const child = spawn(plan.codexBin, ['mcp', 'remove', 'delegation'], { stdio: 'inherit', env });
|
|
115
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
116
|
+
child.once('exit', (code) => {
|
|
117
|
+
if (code === 0) {
|
|
118
|
+
runAdd();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
reject(new Error(`codex mcp remove exited with code ${code ?? 'unknown'}`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function readDelegationMcpServer(codexBin, env) {
|
|
127
|
+
const result = spawnSync(codexBin, ['mcp', 'get', 'delegation', '--json'], {
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
timeout: 5000,
|
|
131
|
+
env
|
|
132
|
+
});
|
|
133
|
+
if (result.error || result.status !== 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const stdout = String(result.stdout ?? '').trim();
|
|
137
|
+
if (!stdout) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(stdout);
|
|
142
|
+
const transport = parsed.transport;
|
|
143
|
+
const args = Array.isArray(transport?.args)
|
|
144
|
+
? transport.args.filter((value) => typeof value === 'string')
|
|
145
|
+
: [];
|
|
146
|
+
const envVars = {};
|
|
147
|
+
const envRecord = transport?.env;
|
|
148
|
+
if (envRecord && typeof envRecord === 'object' && !Array.isArray(envRecord)) {
|
|
149
|
+
for (const [key, value] of Object.entries(envRecord)) {
|
|
150
|
+
if (typeof value === 'string') {
|
|
151
|
+
envVars[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const pinnedRepo = readPinnedRepo(args);
|
|
156
|
+
return { args, pinnedRepo, envVars };
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function readPinnedRepo(args) {
|
|
163
|
+
const index = args.indexOf('--repo');
|
|
164
|
+
if (index === -1) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const candidate = args[index + 1];
|
|
168
|
+
return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate.trim() : null;
|
|
169
|
+
}
|
|
170
|
+
function isDelegationConfiguredFallback(configPath) {
|
|
171
|
+
if (!existsSync(configPath)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
// Keep parsing loose; we only need to know whether a delegation entry exists.
|
|
176
|
+
const raw = readFileSync(configPath, 'utf8');
|
|
177
|
+
return hasMcpServerEntry(raw, 'delegation');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function hasMcpServerEntry(raw, serverName) {
|
|
184
|
+
const lines = raw.split('\n');
|
|
185
|
+
let currentTable = null;
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const trimmed = stripTomlComment(line).trim();
|
|
188
|
+
if (!trimmed) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const tableMatch = trimmed.match(/^\[(.+)\]$/u);
|
|
192
|
+
if (tableMatch) {
|
|
193
|
+
currentTable = tableMatch[1]?.trim() ?? null;
|
|
194
|
+
if (currentTable === `mcp_servers.${serverName}` ||
|
|
195
|
+
currentTable === `mcp_servers."${serverName}"` ||
|
|
196
|
+
currentTable === `mcp_servers.'${serverName}'`) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (trimmed.startsWith('mcp_servers.')) {
|
|
202
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}".`)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'.`)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}.`)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (trimmed.startsWith(`mcp_servers."${serverName}"=`)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (trimmed.startsWith(`mcp_servers.'${serverName}'=`)) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (trimmed.startsWith(`mcp_servers.${serverName}=`)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (currentTable === 'mcp_servers') {
|
|
222
|
+
const entryPattern = new RegExp(`^"?${escapeRegExp(serverName)}"?\\s*=`, 'u');
|
|
223
|
+
if (entryPattern.test(trimmed)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
function stripTomlComment(line) {
|
|
231
|
+
const index = line.indexOf('#');
|
|
232
|
+
if (index === -1) {
|
|
233
|
+
return line;
|
|
234
|
+
}
|
|
235
|
+
return line.slice(0, index);
|
|
236
|
+
}
|
|
237
|
+
function escapeRegExp(value) {
|
|
238
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
239
|
+
}
|
|
@@ -115,7 +115,8 @@ export function runDoctor(cwd = process.cwd()) {
|
|
|
115
115
|
status: delegationStatus,
|
|
116
116
|
config: delegationConfig,
|
|
117
117
|
enablement: [
|
|
118
|
-
|
|
118
|
+
'Run: codex-orchestrator delegation setup --yes',
|
|
119
|
+
'Or manually: codex mcp add delegation -- codex-orchestrator delegate-server',
|
|
119
120
|
"Enable for a run with: codex -c 'mcp_servers.delegation.enabled=true' ...",
|
|
120
121
|
'See: codex-orchestrator init codex'
|
|
121
122
|
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { collectManifests, findSubagentManifests, parseRunIdTimestamp, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
|
|
6
|
+
import { normalizeTaskKey as normalizeTaskKeyAny } from '../../../scripts/lib/docs-helpers.js';
|
|
7
|
+
const normalizeTaskKey = normalizeTaskKeyAny;
|
|
8
|
+
export async function runDoctorUsage(options = {}) {
|
|
9
|
+
const windowDays = clampInt(options.windowDays ?? 30, 1, 3650);
|
|
10
|
+
const cutoffMs = Date.now() - windowDays * 24 * 60 * 60 * 1000;
|
|
11
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
12
|
+
const env = resolveEnvironmentPaths();
|
|
13
|
+
const manifestPaths = await collectManifests(env.runsRoot, options.taskFilter ?? undefined);
|
|
14
|
+
const seenRunIds = new Set();
|
|
15
|
+
const pipelines = new Map();
|
|
16
|
+
const cloudByStatus = {};
|
|
17
|
+
const statusCounts = { total: 0, succeeded: 0, failed: 0, cancelled: 0, other: 0 };
|
|
18
|
+
let cloudRuns = 0;
|
|
19
|
+
let rlmRuns = 0;
|
|
20
|
+
let collabRunsWithToolCalls = 0;
|
|
21
|
+
let collabTotalToolCalls = 0;
|
|
22
|
+
const collabCaptureDisabled = String(process.env.CODEX_ORCHESTRATOR_COLLAB_MAX_EVENTS ?? '').trim() === '0';
|
|
23
|
+
const activeIndexTasks = new Set();
|
|
24
|
+
const taskKeys = readTaskIndexKeys(env.repoRoot);
|
|
25
|
+
for (const manifestPath of manifestPaths) {
|
|
26
|
+
const runIdFromPath = extractRunIdFromManifestPath(manifestPath);
|
|
27
|
+
if (!runIdFromPath) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (seenRunIds.has(runIdFromPath)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const timestamp = parseRunIdTimestamp(runIdFromPath);
|
|
34
|
+
if (timestamp && timestamp.getTime() < cutoffMs) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let manifest;
|
|
38
|
+
try {
|
|
39
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
40
|
+
manifest = JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const runId = typeof manifest.run_id === 'string' && manifest.run_id ? manifest.run_id : runIdFromPath;
|
|
46
|
+
if (seenRunIds.has(runId)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
seenRunIds.add(runId);
|
|
50
|
+
const startedAtMs = Date.parse(manifest.started_at ?? '') || timestamp?.getTime() || 0;
|
|
51
|
+
if (!startedAtMs || startedAtMs < cutoffMs) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
statusCounts.total += 1;
|
|
55
|
+
if (manifest.status === 'succeeded') {
|
|
56
|
+
statusCounts.succeeded += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (manifest.status === 'failed') {
|
|
59
|
+
statusCounts.failed += 1;
|
|
60
|
+
}
|
|
61
|
+
else if (manifest.status === 'cancelled') {
|
|
62
|
+
statusCounts.cancelled += 1;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
statusCounts.other += 1;
|
|
66
|
+
}
|
|
67
|
+
const pipelineId = typeof manifest.pipeline_id === 'string' && manifest.pipeline_id ? manifest.pipeline_id : 'unknown';
|
|
68
|
+
pipelines.set(pipelineId, (pipelines.get(pipelineId) ?? 0) + 1);
|
|
69
|
+
if (pipelineId === 'rlm') {
|
|
70
|
+
rlmRuns += 1;
|
|
71
|
+
}
|
|
72
|
+
if (manifest.cloud_execution) {
|
|
73
|
+
cloudRuns += 1;
|
|
74
|
+
const status = (manifest.cloud_execution.status ?? 'unknown').trim() || 'unknown';
|
|
75
|
+
cloudByStatus[status] = (cloudByStatus[status] ?? 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(manifest.collab_tool_calls) && manifest.collab_tool_calls.length > 0) {
|
|
78
|
+
collabRunsWithToolCalls += 1;
|
|
79
|
+
collabTotalToolCalls += manifest.collab_tool_calls.length;
|
|
80
|
+
}
|
|
81
|
+
if (taskKeys.has(manifest.task_id)) {
|
|
82
|
+
activeIndexTasks.add(manifest.task_id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const pipelineTop = [...pipelines.entries()]
|
|
86
|
+
.sort((a, b) => b[1] - a[1])
|
|
87
|
+
.slice(0, 10)
|
|
88
|
+
.map(([id, runs]) => ({ id, runs }));
|
|
89
|
+
const delegationErrors = [];
|
|
90
|
+
let activeWithSubagents = 0;
|
|
91
|
+
let totalSubagentManifests = 0;
|
|
92
|
+
const activeTasks = [...activeIndexTasks];
|
|
93
|
+
const subagentResults = await Promise.all(activeTasks.map(async (taskId) => {
|
|
94
|
+
const result = await findSubagentManifests(env.runsRoot, taskId);
|
|
95
|
+
if (result.error) {
|
|
96
|
+
delegationErrors.push(result.error);
|
|
97
|
+
}
|
|
98
|
+
return { taskId, found: result.found };
|
|
99
|
+
}));
|
|
100
|
+
for (const item of subagentResults) {
|
|
101
|
+
totalSubagentManifests += item.found.length;
|
|
102
|
+
if (item.found.length > 0) {
|
|
103
|
+
activeWithSubagents += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
window_days: windowDays,
|
|
108
|
+
cutoff_iso: cutoffIso,
|
|
109
|
+
runs: statusCounts,
|
|
110
|
+
cloud: {
|
|
111
|
+
runs: cloudRuns,
|
|
112
|
+
by_status: cloudByStatus
|
|
113
|
+
},
|
|
114
|
+
rlm: {
|
|
115
|
+
runs: rlmRuns
|
|
116
|
+
},
|
|
117
|
+
collab: {
|
|
118
|
+
runs_with_tool_calls: collabRunsWithToolCalls,
|
|
119
|
+
total_tool_calls: collabTotalToolCalls,
|
|
120
|
+
capture_disabled: collabCaptureDisabled
|
|
121
|
+
},
|
|
122
|
+
delegation: {
|
|
123
|
+
active_top_level_tasks: activeTasks.length,
|
|
124
|
+
active_with_subagents: activeWithSubagents,
|
|
125
|
+
total_subagent_manifests: totalSubagentManifests,
|
|
126
|
+
errors: delegationErrors
|
|
127
|
+
},
|
|
128
|
+
pipelines: {
|
|
129
|
+
total: pipelines.size,
|
|
130
|
+
top: pipelineTop
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function formatDoctorUsageSummary(result) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`Usage (last ${result.window_days}d, cutoff ${result.cutoff_iso})`);
|
|
137
|
+
lines.push(` - runs: ${result.runs.total} (ok=${result.runs.succeeded}, failed=${result.runs.failed}, cancelled=${result.runs.cancelled}, other=${result.runs.other})`);
|
|
138
|
+
lines.push(` - cloud: ${result.cloud.runs} (${formatPercent(result.cloud.runs, result.runs.total)})${formatCloudStatuses(result.cloud.by_status)}`);
|
|
139
|
+
lines.push(` - rlm: ${result.rlm.runs} (${formatPercent(result.rlm.runs, result.runs.total)})`);
|
|
140
|
+
const collabSuffix = result.collab.capture_disabled ? ' (capture disabled)' : '';
|
|
141
|
+
lines.push(` - collab: ${result.collab.runs_with_tool_calls} (${formatPercent(result.collab.runs_with_tool_calls, result.runs.total)})${collabSuffix}`);
|
|
142
|
+
if (result.delegation.active_top_level_tasks > 0) {
|
|
143
|
+
lines.push(` - delegation: ${result.delegation.active_with_subagents}/${result.delegation.active_top_level_tasks} top-level tasks have subagent manifests`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(' - delegation: no top-level tasks detected in tasks/index.json for this window');
|
|
147
|
+
}
|
|
148
|
+
if (result.pipelines.top.length > 0) {
|
|
149
|
+
lines.push('Top pipelines:');
|
|
150
|
+
for (const entry of result.pipelines.top) {
|
|
151
|
+
lines.push(` - ${entry.id}: ${entry.runs}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (result.delegation.errors.length > 0) {
|
|
155
|
+
lines.push('Delegation scan warnings:');
|
|
156
|
+
for (const warning of result.delegation.errors.slice(0, 3)) {
|
|
157
|
+
lines.push(` - ${warning}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
function extractRunIdFromManifestPath(manifestPath) {
|
|
163
|
+
if (!manifestPath) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// .../<run-id>/manifest.json
|
|
167
|
+
const dir = manifestPath.endsWith('manifest.json') ? basename(dirname(manifestPath)) : null;
|
|
168
|
+
return dir && dir !== '..' ? dir : null;
|
|
169
|
+
}
|
|
170
|
+
function clampInt(value, min, max) {
|
|
171
|
+
const rounded = Math.floor(value);
|
|
172
|
+
if (!Number.isFinite(rounded)) {
|
|
173
|
+
return min;
|
|
174
|
+
}
|
|
175
|
+
return Math.max(min, Math.min(max, rounded));
|
|
176
|
+
}
|
|
177
|
+
function formatPercent(numerator, denominator) {
|
|
178
|
+
if (!denominator) {
|
|
179
|
+
return '0%';
|
|
180
|
+
}
|
|
181
|
+
const pct = (numerator / denominator) * 100;
|
|
182
|
+
return `${Math.round(pct * 10) / 10}%`;
|
|
183
|
+
}
|
|
184
|
+
function formatCloudStatuses(byStatus) {
|
|
185
|
+
const entries = Object.entries(byStatus);
|
|
186
|
+
if (entries.length === 0) {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
190
|
+
const top = entries
|
|
191
|
+
.slice(0, 3)
|
|
192
|
+
.map(([status, count]) => `${status}=${count}`)
|
|
193
|
+
.join(', ');
|
|
194
|
+
return ` [${top}]`;
|
|
195
|
+
}
|
|
196
|
+
function readTaskIndexKeys(repoRoot) {
|
|
197
|
+
const indexPath = join(repoRoot, 'tasks', 'index.json');
|
|
198
|
+
try {
|
|
199
|
+
const raw = readFileSync(indexPath, 'utf8');
|
|
200
|
+
const parsed = JSON.parse(raw);
|
|
201
|
+
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
202
|
+
const keys = items
|
|
203
|
+
.map((item) => normalizeTaskKey(item))
|
|
204
|
+
.filter((key) => typeof key === 'string' && key.length > 0);
|
|
205
|
+
return new Set(keys);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return new Set();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -61,7 +61,7 @@ export function formatInitSummary(result, cwd) {
|
|
|
61
61
|
lines.push('No files written.');
|
|
62
62
|
}
|
|
63
63
|
lines.push('Next steps (recommended):');
|
|
64
|
-
lines.push(
|
|
64
|
+
lines.push(' - codex-orchestrator delegation setup --yes');
|
|
65
65
|
lines.push(' - codex-orchestrator codex setup # optional: managed/pinned Codex CLI (stock CLI works by default)');
|
|
66
66
|
return lines;
|
|
67
67
|
}
|
package/docs/README.md
CHANGED
|
@@ -101,8 +101,9 @@ Use `npx @kbediako/codex-orchestrator resume --run <run-id>` to continue interru
|
|
|
101
101
|
## Companion Package Commands
|
|
102
102
|
- `codex-orchestrator mcp serve [--repo <path>] [--dry-run] [-- <extra args>]`: launch the MCP stdio server (delegates to `codex mcp-server`; stdout guard keeps protocol-only output, logs to stderr).
|
|
103
103
|
- `codex-orchestrator init codex [--cwd <path>] [--force]`: copy starter templates into a repo (includes `mcp-client.json` and `AGENTS.md`; no overwrite unless `--force`).
|
|
104
|
-
- `codex-orchestrator doctor [--format json]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands.
|
|
104
|
+
- `codex-orchestrator doctor [--format json] [--usage]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands. `--usage` appends a local usage snapshot (scans `.runs/`).
|
|
105
105
|
- `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
|
|
106
|
+
- `codex-orchestrator delegation setup [--yes]`: configure delegation MCP wiring (`--yes` applies `codex mcp add ...`).
|
|
106
107
|
- `codex-orchestrator skills install [--force] [--only <skills>] [--codex-home <path>]`: install bundled skills into `$CODEX_HOME/skills` (global skills remain the primary reference when installed).
|
|
107
108
|
- `codex-orchestrator self-check --format json`: emit a safe JSON health payload for smoke tests.
|
|
108
109
|
- `codex-orchestrator --version`: print the package version.
|
package/package.json
CHANGED
|
@@ -63,7 +63,7 @@ Fix by re-registering the server with a TOML-quoted override:
|
|
|
63
63
|
codex mcp remove delegation
|
|
64
64
|
codex mcp add delegation \
|
|
65
65
|
--env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' \
|
|
66
|
-
-- codex-orchestrator delegate-server
|
|
66
|
+
-- codex-orchestrator delegate-server
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Server mode vs child mode (don’t mix them up)
|
|
@@ -98,7 +98,7 @@ If you want deeper recursion or longer wall-clock time for delegated runs, set R
|
|
|
98
98
|
```bash
|
|
99
99
|
codex mcp add delegation \
|
|
100
100
|
--env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' \
|
|
101
|
-
-- codex-orchestrator delegate-server
|
|
101
|
+
-- codex-orchestrator delegate-server
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
For the `rlm` pipeline specifically, use:
|
|
@@ -35,16 +35,16 @@ codex exec \
|
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Optional (only if you need it):
|
|
38
|
-
- Add `--repo /path/to/repo`
|
|
38
|
+
- Add `--repo /path/to/repo` only when you want to pin the server to a repo even if Codex is launched outside that repo (default uses cwd).
|
|
39
39
|
- Add `-c 'features.skills=false'` for a minimal, deterministic background run.
|
|
40
40
|
- Add `-c 'delegate.mode=question_only'` when the child only needs `delegate.question.*` (and optional `delegate.status`).
|
|
41
41
|
- Add `-c 'delegate.mode=full'` when the child needs `delegate.spawn/pause/cancel` (nested delegation / run control).
|
|
42
42
|
- If the task needs external docs or APIs, enable only the relevant MCP server for that environment.
|
|
43
43
|
- If `delegate.spawn` is missing, re-register the MCP server with full mode (server config controls tool surface):
|
|
44
44
|
- `codex mcp remove delegation`
|
|
45
|
-
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server
|
|
45
|
+
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=delegate.mode="full"' -- codex-orchestrator delegate-server`
|
|
46
46
|
- To raise RLM budgets for delegated runs, re-register with an override (TOML-quoted):
|
|
47
|
-
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server
|
|
47
|
+
- `codex mcp add delegation --env 'CODEX_MCP_CONFIG_OVERRIDES=rlm.max_subcall_depth=8;rlm.wall_clock_timeout_ms=14400000' -- codex-orchestrator delegate-server`
|
|
48
48
|
|
|
49
49
|
For deeper background patterns and troubleshooting, see `DELEGATION_GUIDE.md`.
|
|
50
50
|
For runner + delegation coordination (short `--task` flow), see `docs/delegation-runner-workflow.md`.
|
|
@@ -64,8 +64,10 @@ For runner + delegation coordination (short `--task` flow), see `docs/delegation
|
|
|
64
64
|
### 0) One-time setup (register the MCP server)
|
|
65
65
|
|
|
66
66
|
- Register the delegation server once:
|
|
67
|
+
- Preferred: `codex-orchestrator delegation setup --yes`
|
|
68
|
+
- This wraps `codex mcp add delegation ...` and keeps wiring discoverable via `codex-orchestrator doctor`.
|
|
67
69
|
- `codex mcp add delegation -- codex-orchestrator delegate-server`
|
|
68
|
-
- Optional
|
|
70
|
+
- Optional: append `--repo /path/to/repo` to pin the server to one repo (not recommended if you work across repos).
|
|
69
71
|
- `delegate-server` is the canonical name; `delegation-server` is supported as an alias.
|
|
70
72
|
- Per-run `-c 'mcp_servers.delegation.enabled=true'` only works **after** registration.
|
|
71
73
|
- If `delegate.*` tools are missing mid-task, start a new run with:
|