@kbediako/codex-orchestrator 0.1.1 → 0.1.3
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 +11 -8
- package/dist/bin/codex-orchestrator.js +245 -121
- package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
- package/dist/orchestrator/src/cli/devtoolsSetup.js +66 -0
- package/dist/orchestrator/src/cli/doctor.js +46 -21
- package/dist/orchestrator/src/cli/exec/context.js +5 -2
- package/dist/orchestrator/src/cli/exec/learning.js +5 -3
- package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
- package/dist/orchestrator/src/cli/exec/summary.js +1 -1
- package/dist/orchestrator/src/cli/orchestrator.js +16 -7
- package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
- package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
- package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
- package/dist/orchestrator/src/cli/rlm/types.js +1 -0
- package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
- package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
- package/dist/orchestrator/src/cli/run/environment.js +4 -11
- package/dist/orchestrator/src/cli/run/manifest.js +7 -1
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
- package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
- package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
- package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +178 -0
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
- package/dist/orchestrator/src/cli/utils/strings.js +8 -6
- package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
- package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
- package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
- package/dist/packages/orchestrator/src/index.js +1 -0
- package/dist/packages/shared/design-artifacts/writer.js +4 -14
- package/dist/packages/shared/streams/stdio.js +2 -112
- package/dist/packages/shared/utils/strings.js +17 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
- package/dist/scripts/design/pipeline/context.js +5 -5
- package/dist/scripts/design/pipeline/extract.js +9 -6
- package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
- package/dist/scripts/design/pipeline/permit.js +59 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
- package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
- package/dist/scripts/design/pipeline/visual-regression.js +2 -11
- package/dist/scripts/lib/cli-args.js +53 -0
- package/dist/scripts/lib/docs-helpers.js +111 -0
- package/dist/scripts/lib/npm-pack.js +20 -0
- package/dist/scripts/lib/run-manifests.js +160 -0
- package/package.json +17 -6
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
- package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
- package/dist/orchestrator/src/control-plane/index.js +0 -3
- package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
- package/dist/orchestrator/src/scheduler/index.js +0 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const NODE_PACKAGE_MANAGER_MAP = {
|
|
4
|
+
pnpm: 'pnpm test',
|
|
5
|
+
yarn: 'yarn test',
|
|
6
|
+
npm: 'npm test',
|
|
7
|
+
bun: 'bun test'
|
|
8
|
+
};
|
|
9
|
+
const NODE_LOCKFILES = [
|
|
10
|
+
{ file: 'pnpm-lock.yaml', command: 'pnpm test' },
|
|
11
|
+
{ file: 'yarn.lock', command: 'yarn test' },
|
|
12
|
+
{ file: 'package-lock.json', command: 'npm test' },
|
|
13
|
+
{ file: 'bun.lockb', command: 'bun test' }
|
|
14
|
+
];
|
|
15
|
+
async function fileExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await access(path);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function ecosystemReason(ecosystem, reason) {
|
|
25
|
+
return `${ecosystem}: ${reason}`;
|
|
26
|
+
}
|
|
27
|
+
async function detectNodeCandidates(repoRoot) {
|
|
28
|
+
const candidates = [];
|
|
29
|
+
const packageJsonPath = join(repoRoot, 'package.json');
|
|
30
|
+
if (await fileExists(packageJsonPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(packageJsonPath, 'utf8');
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
const packageManager = parsed.packageManager?.trim();
|
|
35
|
+
if (packageManager) {
|
|
36
|
+
const tool = packageManager.split('@')[0]?.trim();
|
|
37
|
+
const command = tool ? NODE_PACKAGE_MANAGER_MAP[tool] : undefined;
|
|
38
|
+
if (command) {
|
|
39
|
+
candidates.push({
|
|
40
|
+
command,
|
|
41
|
+
reason: ecosystemReason('node', `packageManager=${packageManager}`),
|
|
42
|
+
ecosystem: 'node'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore invalid package.json for auto-detection.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const lockfile of NODE_LOCKFILES) {
|
|
52
|
+
const lockPath = join(repoRoot, lockfile.file);
|
|
53
|
+
if (await fileExists(lockPath)) {
|
|
54
|
+
candidates.push({
|
|
55
|
+
command: lockfile.command,
|
|
56
|
+
reason: ecosystemReason('node', lockfile.file),
|
|
57
|
+
ecosystem: 'node'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return candidates;
|
|
62
|
+
}
|
|
63
|
+
async function detectPythonCandidates(repoRoot) {
|
|
64
|
+
const candidates = [];
|
|
65
|
+
const pyprojectPath = join(repoRoot, 'pyproject.toml');
|
|
66
|
+
if (await fileExists(pyprojectPath)) {
|
|
67
|
+
candidates.push({
|
|
68
|
+
command: 'python -m pytest',
|
|
69
|
+
reason: ecosystemReason('python', 'pyproject.toml'),
|
|
70
|
+
ecosystem: 'python'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const pytestIni = join(repoRoot, 'pytest.ini');
|
|
74
|
+
if (await fileExists(pytestIni)) {
|
|
75
|
+
candidates.push({
|
|
76
|
+
command: 'pytest',
|
|
77
|
+
reason: ecosystemReason('python', 'pytest.ini'),
|
|
78
|
+
ecosystem: 'python'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const requirements = join(repoRoot, 'requirements.txt');
|
|
82
|
+
if (await fileExists(requirements)) {
|
|
83
|
+
candidates.push({
|
|
84
|
+
command: 'pytest',
|
|
85
|
+
reason: ecosystemReason('python', 'requirements.txt'),
|
|
86
|
+
ecosystem: 'python'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
async function detectGoCandidates(repoRoot) {
|
|
92
|
+
const goModPath = join(repoRoot, 'go.mod');
|
|
93
|
+
if (await fileExists(goModPath)) {
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
command: 'go test ./...',
|
|
97
|
+
reason: ecosystemReason('go', 'go.mod'),
|
|
98
|
+
ecosystem: 'go'
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
async function detectRustCandidates(repoRoot) {
|
|
105
|
+
const cargoPath = join(repoRoot, 'Cargo.toml');
|
|
106
|
+
if (await fileExists(cargoPath)) {
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
command: 'cargo test',
|
|
110
|
+
reason: ecosystemReason('rust', 'Cargo.toml'),
|
|
111
|
+
ecosystem: 'rust'
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
export async function detectValidatorCandidates(repoRoot) {
|
|
118
|
+
const [node, python, go, rust] = await Promise.all([
|
|
119
|
+
detectNodeCandidates(repoRoot),
|
|
120
|
+
detectPythonCandidates(repoRoot),
|
|
121
|
+
detectGoCandidates(repoRoot),
|
|
122
|
+
detectRustCandidates(repoRoot)
|
|
123
|
+
]);
|
|
124
|
+
return [...node, ...python, ...go, ...rust];
|
|
125
|
+
}
|
|
126
|
+
export async function detectValidator(repoRoot) {
|
|
127
|
+
const rawCandidates = await detectValidatorCandidates(repoRoot);
|
|
128
|
+
if (rawCandidates.length === 0) {
|
|
129
|
+
return { status: 'missing', command: null, candidates: [] };
|
|
130
|
+
}
|
|
131
|
+
const grouped = new Map();
|
|
132
|
+
for (const candidate of rawCandidates) {
|
|
133
|
+
const entry = grouped.get(candidate.command);
|
|
134
|
+
if (entry) {
|
|
135
|
+
entry.reasons.push(candidate.reason);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
grouped.set(candidate.command, {
|
|
139
|
+
command: candidate.command,
|
|
140
|
+
reasons: [candidate.reason],
|
|
141
|
+
ecosystem: candidate.ecosystem
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const candidates = Array.from(grouped.values()).map((entry) => ({
|
|
146
|
+
command: entry.command,
|
|
147
|
+
reason: entry.reasons.join('; '),
|
|
148
|
+
ecosystem: entry.ecosystem
|
|
149
|
+
}));
|
|
150
|
+
if (candidates.length === 1) {
|
|
151
|
+
return {
|
|
152
|
+
status: 'selected',
|
|
153
|
+
command: candidates[0]?.command ?? null,
|
|
154
|
+
reason: candidates[0]?.reason,
|
|
155
|
+
candidates
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { status: 'ambiguous', command: null, candidates };
|
|
159
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { logger } from '../logger.js';
|
|
10
|
+
import { resolveCodexCommand } from './utils/devtools.js';
|
|
11
|
+
import { detectValidator } from './rlm/validator.js';
|
|
12
|
+
import { buildRlmPrompt } from './rlm/prompt.js';
|
|
13
|
+
import { runRlmLoop } from './rlm/runner.js';
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
const DEFAULT_MAX_ITERATIONS = 88;
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const parsed = {};
|
|
18
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
19
|
+
const token = argv[i] ?? '';
|
|
20
|
+
if (!token.startsWith('--')) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const [flag, inlineValue] = token.slice(2).split('=', 2);
|
|
24
|
+
const nextValue = inlineValue ?? argv[i + 1];
|
|
25
|
+
switch (flag) {
|
|
26
|
+
case 'goal':
|
|
27
|
+
if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
|
|
28
|
+
parsed.goal = nextValue;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
else if (inlineValue) {
|
|
32
|
+
parsed.goal = inlineValue;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'validator':
|
|
36
|
+
if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
|
|
37
|
+
parsed.validator = nextValue;
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
else if (inlineValue) {
|
|
41
|
+
parsed.validator = inlineValue;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case 'max-iterations':
|
|
45
|
+
if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
|
|
46
|
+
parsed.maxIterations = nextValue;
|
|
47
|
+
i += 1;
|
|
48
|
+
}
|
|
49
|
+
else if (inlineValue) {
|
|
50
|
+
parsed.maxIterations = inlineValue;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'max-minutes':
|
|
54
|
+
if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
|
|
55
|
+
parsed.maxMinutes = nextValue;
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
else if (inlineValue) {
|
|
59
|
+
parsed.maxMinutes = inlineValue;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case 'roles':
|
|
63
|
+
if (!inlineValue && nextValue && !nextValue.startsWith('--')) {
|
|
64
|
+
parsed.roles = nextValue;
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
else if (inlineValue) {
|
|
68
|
+
parsed.roles = inlineValue;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
function envFlagEnabled(value) {
|
|
78
|
+
if (!value) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const normalized = value.trim().toLowerCase();
|
|
82
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
83
|
+
}
|
|
84
|
+
function shouldForceNonInteractive(env) {
|
|
85
|
+
const stdinIsTTY = process.stdin?.isTTY === true;
|
|
86
|
+
return (!stdinIsTTY ||
|
|
87
|
+
envFlagEnabled(env.CI) ||
|
|
88
|
+
envFlagEnabled(env.CODEX_REVIEW_NON_INTERACTIVE) ||
|
|
89
|
+
envFlagEnabled(env.CODEX_NON_INTERACTIVE) ||
|
|
90
|
+
envFlagEnabled(env.CODEX_NONINTERACTIVE) ||
|
|
91
|
+
envFlagEnabled(env.CODEX_NO_INTERACTIVE));
|
|
92
|
+
}
|
|
93
|
+
function resolveRepoRoot(env) {
|
|
94
|
+
return env.CODEX_ORCHESTRATOR_ROOT?.trim() || process.cwd();
|
|
95
|
+
}
|
|
96
|
+
function resolveRunIds(env) {
|
|
97
|
+
const taskId = env.CODEX_ORCHESTRATOR_TASK_ID || env.MCP_RUNNER_TASK_ID || 'rlm-adhoc';
|
|
98
|
+
const runId = env.CODEX_ORCHESTRATOR_RUN_ID || 'rlm-adhoc';
|
|
99
|
+
return { taskId, runId };
|
|
100
|
+
}
|
|
101
|
+
function resolveRunsRoot(env, repoRoot) {
|
|
102
|
+
return env.CODEX_ORCHESTRATOR_RUNS_DIR?.trim() || join(repoRoot, '.runs');
|
|
103
|
+
}
|
|
104
|
+
function resolveRunDir(env, repoRoot) {
|
|
105
|
+
const explicit = env.CODEX_ORCHESTRATOR_RUN_DIR?.trim();
|
|
106
|
+
if (explicit) {
|
|
107
|
+
return explicit;
|
|
108
|
+
}
|
|
109
|
+
const { taskId, runId } = resolveRunIds(env);
|
|
110
|
+
const runsRoot = resolveRunsRoot(env, repoRoot);
|
|
111
|
+
return join(runsRoot, taskId, 'cli', runId);
|
|
112
|
+
}
|
|
113
|
+
function parsePositiveInt(value, fallback) {
|
|
114
|
+
if (!value) {
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
const parsed = Number.parseInt(value, 10);
|
|
118
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
function parseRoles(value, fallback) {
|
|
124
|
+
if (!value) {
|
|
125
|
+
return fallback;
|
|
126
|
+
}
|
|
127
|
+
const normalized = value.trim().toLowerCase();
|
|
128
|
+
if (normalized === 'single' || normalized === 'triad') {
|
|
129
|
+
return normalized;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function normalizeValidator(value) {
|
|
134
|
+
if (!value) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const trimmed = value.trim();
|
|
138
|
+
if (!trimmed) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return trimmed;
|
|
142
|
+
}
|
|
143
|
+
async function promptForValidator(candidates) {
|
|
144
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
145
|
+
try {
|
|
146
|
+
console.log('Validator auto-detect found multiple candidates:');
|
|
147
|
+
candidates.forEach((candidate, index) => {
|
|
148
|
+
console.log(` ${index + 1}) ${candidate.command} (${candidate.reason})`);
|
|
149
|
+
});
|
|
150
|
+
console.log(' n) none');
|
|
151
|
+
const answer = (await rl.question('Select validator [1-n or n for none]: ')).trim().toLowerCase();
|
|
152
|
+
if (!answer || answer === 'n' || answer === 'none') {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const selectedIndex = Number.parseInt(answer, 10);
|
|
156
|
+
if (!Number.isFinite(selectedIndex) || selectedIndex < 1 || selectedIndex > candidates.length) {
|
|
157
|
+
throw new Error('Invalid selection');
|
|
158
|
+
}
|
|
159
|
+
return candidates[selectedIndex - 1]?.command ?? null;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
rl.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function promptForValidatorCommand() {
|
|
166
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
167
|
+
try {
|
|
168
|
+
const answer = (await rl.question('Enter validator command (or "none"): ')).trim();
|
|
169
|
+
if (!answer || answer.toLowerCase() === 'none') {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return answer;
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
rl.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function writeTerminalState(runDir, state) {
|
|
179
|
+
const rlmDir = join(runDir, 'rlm');
|
|
180
|
+
await mkdir(rlmDir, { recursive: true });
|
|
181
|
+
await writeFile(join(rlmDir, 'state.json'), JSON.stringify(state, null, 2), 'utf8');
|
|
182
|
+
}
|
|
183
|
+
async function runCodexAgent(input, env, repoRoot, nonInteractive, subagentsEnabled) {
|
|
184
|
+
const prompt = buildRlmPrompt(input);
|
|
185
|
+
const { command, args } = resolveCodexCommand(['exec', prompt], env);
|
|
186
|
+
const childEnv = { ...process.env, ...env };
|
|
187
|
+
if (nonInteractive) {
|
|
188
|
+
childEnv.CODEX_NON_INTERACTIVE = childEnv.CODEX_NON_INTERACTIVE ?? '1';
|
|
189
|
+
childEnv.CODEX_NO_INTERACTIVE = childEnv.CODEX_NO_INTERACTIVE ?? '1';
|
|
190
|
+
childEnv.CODEX_INTERACTIVE = childEnv.CODEX_INTERACTIVE ?? '0';
|
|
191
|
+
}
|
|
192
|
+
if (subagentsEnabled) {
|
|
193
|
+
childEnv.CODEX_SUBAGENTS = childEnv.CODEX_SUBAGENTS ?? '1';
|
|
194
|
+
}
|
|
195
|
+
const child = spawn(command, args, { cwd: repoRoot, env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
196
|
+
let stdout = '';
|
|
197
|
+
let stderr = '';
|
|
198
|
+
child.stdout?.on('data', (chunk) => {
|
|
199
|
+
stdout += chunk.toString();
|
|
200
|
+
process.stdout.write(chunk);
|
|
201
|
+
});
|
|
202
|
+
child.stderr?.on('data', (chunk) => {
|
|
203
|
+
stderr += chunk.toString();
|
|
204
|
+
process.stderr.write(chunk);
|
|
205
|
+
});
|
|
206
|
+
await new Promise((resolvePromise, reject) => {
|
|
207
|
+
child.once('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
208
|
+
child.once('exit', (code) => {
|
|
209
|
+
if (code === 0) {
|
|
210
|
+
resolvePromise();
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
reject(new Error(`codex exec exited with code ${code ?? 'unknown'}`));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
const output = [stdout.trim(), stderr.trim()].filter(Boolean).join('\n');
|
|
218
|
+
return { output };
|
|
219
|
+
}
|
|
220
|
+
function normalizeExitCode(code) {
|
|
221
|
+
if (typeof code === 'number' && Number.isInteger(code)) {
|
|
222
|
+
return code;
|
|
223
|
+
}
|
|
224
|
+
if (typeof code === 'string') {
|
|
225
|
+
const parsed = Number.parseInt(code, 10);
|
|
226
|
+
if (Number.isInteger(parsed)) {
|
|
227
|
+
return parsed;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
async function runValidatorCommand(command, repoRoot) {
|
|
233
|
+
try {
|
|
234
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
235
|
+
cwd: repoRoot,
|
|
236
|
+
env: { ...process.env },
|
|
237
|
+
maxBuffer: 10 * 1024 * 1024
|
|
238
|
+
});
|
|
239
|
+
return { exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' };
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const execError = error;
|
|
243
|
+
const exitCode = normalizeExitCode(execError.code);
|
|
244
|
+
const spawnError = execError.code === 'ENOENT' || exitCode === 127;
|
|
245
|
+
return {
|
|
246
|
+
exitCode,
|
|
247
|
+
stdout: execError.stdout ?? '',
|
|
248
|
+
stderr: execError.stderr ?? execError.message ?? '',
|
|
249
|
+
spawnError
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function main() {
|
|
254
|
+
const env = process.env;
|
|
255
|
+
const repoRoot = resolveRepoRoot(env);
|
|
256
|
+
const runDir = resolveRunDir(env, repoRoot);
|
|
257
|
+
const parsedArgs = parseArgs(process.argv.slice(2));
|
|
258
|
+
const goal = (parsedArgs.goal ?? env.RLM_GOAL)?.trim();
|
|
259
|
+
const roles = parseRoles(parsedArgs.roles ?? env.RLM_ROLES, 'single');
|
|
260
|
+
const maxIterations = parsePositiveInt(parsedArgs.maxIterations ?? env.RLM_MAX_ITERATIONS, DEFAULT_MAX_ITERATIONS);
|
|
261
|
+
const maxMinutes = parsePositiveInt(parsedArgs.maxMinutes ?? env.RLM_MAX_MINUTES, 0);
|
|
262
|
+
if (!goal) {
|
|
263
|
+
const state = {
|
|
264
|
+
goal: '',
|
|
265
|
+
validator: null,
|
|
266
|
+
roles: roles ?? 'single',
|
|
267
|
+
maxIterations: maxIterations ?? DEFAULT_MAX_ITERATIONS,
|
|
268
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
269
|
+
iterations: [],
|
|
270
|
+
final: { status: 'invalid_config', exitCode: 5 }
|
|
271
|
+
};
|
|
272
|
+
await writeTerminalState(runDir, state);
|
|
273
|
+
console.error('RLM goal is required. Set RLM_GOAL or pass --goal.');
|
|
274
|
+
process.exitCode = 5;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!roles) {
|
|
278
|
+
const state = {
|
|
279
|
+
goal,
|
|
280
|
+
validator: null,
|
|
281
|
+
roles: 'single',
|
|
282
|
+
maxIterations: maxIterations ?? DEFAULT_MAX_ITERATIONS,
|
|
283
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
284
|
+
iterations: [],
|
|
285
|
+
final: { status: 'invalid_config', exitCode: 5 }
|
|
286
|
+
};
|
|
287
|
+
await writeTerminalState(runDir, state);
|
|
288
|
+
console.error('Invalid RLM roles value. Use "single" or "triad".');
|
|
289
|
+
process.exitCode = 5;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (maxIterations === null) {
|
|
293
|
+
const state = {
|
|
294
|
+
goal,
|
|
295
|
+
validator: null,
|
|
296
|
+
roles,
|
|
297
|
+
maxIterations: DEFAULT_MAX_ITERATIONS,
|
|
298
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
299
|
+
iterations: [],
|
|
300
|
+
final: { status: 'invalid_config', exitCode: 5 }
|
|
301
|
+
};
|
|
302
|
+
await writeTerminalState(runDir, state);
|
|
303
|
+
console.error('Invalid max iterations value.');
|
|
304
|
+
process.exitCode = 5;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (maxMinutes === null) {
|
|
308
|
+
const state = {
|
|
309
|
+
goal,
|
|
310
|
+
validator: null,
|
|
311
|
+
roles,
|
|
312
|
+
maxIterations,
|
|
313
|
+
maxMinutes: null,
|
|
314
|
+
iterations: [],
|
|
315
|
+
final: { status: 'invalid_config', exitCode: 5 }
|
|
316
|
+
};
|
|
317
|
+
await writeTerminalState(runDir, state);
|
|
318
|
+
console.error('Invalid max minutes value.');
|
|
319
|
+
process.exitCode = 5;
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const validatorOverride = normalizeValidator(parsedArgs.validator ?? env.RLM_VALIDATOR);
|
|
323
|
+
const isInteractive = !shouldForceNonInteractive(env);
|
|
324
|
+
let validatorCommand = null;
|
|
325
|
+
if (validatorOverride && validatorOverride.toLowerCase() !== 'auto') {
|
|
326
|
+
if (validatorOverride.toLowerCase() === 'none') {
|
|
327
|
+
validatorCommand = null;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
validatorCommand = validatorOverride;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const detection = await detectValidator(repoRoot);
|
|
335
|
+
if (detection.status === 'selected' && detection.command) {
|
|
336
|
+
validatorCommand = detection.command;
|
|
337
|
+
console.log(`Validator: ${detection.command} (${detection.reason ?? 'auto-detect'})`);
|
|
338
|
+
}
|
|
339
|
+
else if (detection.status === 'ambiguous') {
|
|
340
|
+
if (isInteractive) {
|
|
341
|
+
validatorCommand = await promptForValidator(detection.candidates);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const candidates = detection.candidates
|
|
345
|
+
.map((candidate) => `- ${candidate.command} (${candidate.reason})`)
|
|
346
|
+
.join('\n');
|
|
347
|
+
const state = {
|
|
348
|
+
goal,
|
|
349
|
+
validator: null,
|
|
350
|
+
roles,
|
|
351
|
+
maxIterations,
|
|
352
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
353
|
+
iterations: [],
|
|
354
|
+
final: { status: 'no_validator', exitCode: 2 }
|
|
355
|
+
};
|
|
356
|
+
await writeTerminalState(runDir, state);
|
|
357
|
+
console.error('Validator auto-detect ambiguous. Provide --validator or --validator none.');
|
|
358
|
+
console.error(candidates);
|
|
359
|
+
process.exitCode = 2;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
if (isInteractive) {
|
|
365
|
+
validatorCommand = await promptForValidatorCommand();
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const state = {
|
|
369
|
+
goal,
|
|
370
|
+
validator: null,
|
|
371
|
+
roles,
|
|
372
|
+
maxIterations,
|
|
373
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
374
|
+
iterations: [],
|
|
375
|
+
final: { status: 'no_validator', exitCode: 2 }
|
|
376
|
+
};
|
|
377
|
+
await writeTerminalState(runDir, state);
|
|
378
|
+
console.error('Validator auto-detect failed. Provide --validator or --validator none.');
|
|
379
|
+
process.exitCode = 2;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (validatorCommand === null) {
|
|
385
|
+
console.log('Validator: none');
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
console.log(`Validator: ${validatorCommand}`);
|
|
389
|
+
}
|
|
390
|
+
const subagentsEnabled = envFlagEnabled(env.CODEX_SUBAGENTS) || envFlagEnabled(env.RLM_SUBAGENTS);
|
|
391
|
+
const nonInteractive = shouldForceNonInteractive(env);
|
|
392
|
+
const result = await runRlmLoop({
|
|
393
|
+
goal,
|
|
394
|
+
validatorCommand,
|
|
395
|
+
maxIterations,
|
|
396
|
+
maxMinutes: maxMinutes && maxMinutes > 0 ? maxMinutes : null,
|
|
397
|
+
roles,
|
|
398
|
+
subagentsEnabled,
|
|
399
|
+
repoRoot,
|
|
400
|
+
runDir: join(runDir, 'rlm'),
|
|
401
|
+
runAgent: (input) => runCodexAgent(input, env, repoRoot, nonInteractive, subagentsEnabled),
|
|
402
|
+
runValidator: (command) => runValidatorCommand(command, repoRoot),
|
|
403
|
+
logger: (line) => logger.info(line)
|
|
404
|
+
});
|
|
405
|
+
const finalStatus = result.state.final?.status ?? 'unknown';
|
|
406
|
+
const iterationCount = result.state.iterations.length;
|
|
407
|
+
console.log(`RLM completed: status=${finalStatus} iterations=${iterationCount} exit=${result.exitCode}`);
|
|
408
|
+
process.exitCode = result.exitCode;
|
|
409
|
+
}
|
|
410
|
+
const entry = process.argv[1] ? resolve(process.argv[1]) : null;
|
|
411
|
+
const self = resolve(fileURLToPath(import.meta.url));
|
|
412
|
+
if (entry && entry === self) {
|
|
413
|
+
main().catch((error) => {
|
|
414
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
415
|
+
process.exitCode = 10;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import { join, resolve } from 'node:path';
|
|
2
1
|
import { sanitizeTaskId } from '../../persistence/sanitizeTaskId.js';
|
|
3
|
-
|
|
4
|
-
export function resolveEnvironment() {
|
|
5
|
-
const repoRoot = resolve(process.env.CODEX_ORCHESTRATOR_ROOT ?? process.cwd());
|
|
6
|
-
const runsRoot = resolve(process.env.CODEX_ORCHESTRATOR_RUNS_DIR ?? join(repoRoot, '.runs'));
|
|
7
|
-
const outRoot = resolve(process.env.CODEX_ORCHESTRATOR_OUT_DIR ?? join(repoRoot, 'out'));
|
|
8
|
-
const rawTaskId = process.env.MCP_RUNNER_TASK_ID ?? DEFAULT_TASK_ID;
|
|
9
|
-
const taskId = normalizeTaskId(rawTaskId);
|
|
10
|
-
return { repoRoot, runsRoot, outRoot, taskId };
|
|
11
|
-
}
|
|
12
|
-
function normalizeTaskId(value) {
|
|
2
|
+
export function normalizeTaskId(value) {
|
|
13
3
|
try {
|
|
14
4
|
return sanitizeTaskId(value);
|
|
15
5
|
}
|
|
@@ -21,4 +11,7 @@ function normalizeTaskId(value) {
|
|
|
21
11
|
throw new Error(`Invalid MCP_RUNNER_TASK_ID: ${message}`);
|
|
22
12
|
}
|
|
23
13
|
}
|
|
14
|
+
export function normalizeEnvironmentPaths(paths) {
|
|
15
|
+
return { ...paths, taskId: normalizeTaskId(paths.taskId) };
|
|
16
|
+
}
|
|
24
17
|
export { sanitizeTaskId };
|
|
@@ -239,7 +239,13 @@ function computeGuardrailStatus(manifest) {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
function selectGuardrailCommands(manifest) {
|
|
242
|
-
return manifest.commands.filter((entry) =>
|
|
242
|
+
return manifest.commands.filter((entry) => {
|
|
243
|
+
const id = entry.id?.toLowerCase() ?? '';
|
|
244
|
+
const title = entry.title?.toLowerCase() ?? '';
|
|
245
|
+
const command = entry.command?.toLowerCase() ?? '';
|
|
246
|
+
const haystack = `${id} ${title} ${command}`;
|
|
247
|
+
return haystack.includes('spec-guard') || haystack.includes('specguardrunner');
|
|
248
|
+
});
|
|
243
249
|
}
|
|
244
250
|
function formatGuardrailSummary(counts) {
|
|
245
251
|
if (counts.total === 0) {
|
|
@@ -129,7 +129,7 @@ export async function runCommandStage(context, hooks = {}) {
|
|
|
129
129
|
CODEX_ORCHESTRATOR_RUN_DIR: paths.runDir,
|
|
130
130
|
CODEX_ORCHESTRATOR_RUNS_DIR: env.runsRoot,
|
|
131
131
|
CODEX_ORCHESTRATOR_OUT_DIR: env.outRoot,
|
|
132
|
-
|
|
132
|
+
CODEX_ORCHESTRATOR_ROOT: env.repoRoot,
|
|
133
133
|
CODEX_ORCHESTRATOR_PACKAGE_ROOT: PACKAGE_ROOT
|
|
134
134
|
};
|
|
135
135
|
const execEnv = { ...baseEnv, ...stage.env };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
import {
|
|
2
|
+
import { ControlPlaneDriftReporter } from '../../control-plane/drift-reporter.js';
|
|
3
|
+
import { buildRunRequestV2 } from '../../control-plane/request-builder.js';
|
|
4
|
+
import { ControlPlaneValidationError, ControlPlaneValidator } from '../../control-plane/validator.js';
|
|
3
5
|
import { relativeToRepo } from '../run/runPaths.js';
|
|
4
6
|
import { appendSummary } from '../run/manifest.js';
|
|
5
7
|
import { persistManifest } from '../run/manifestPersister.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { ExecSessionManager, UnifiedExecRunner, ToolOrchestrator } from '../../../../packages/orchestrator/src/index.js';
|
|
3
|
-
import { RemoteExecHandleService } from '../../../../packages/orchestrator/src/exec/handle-service.js';
|
|
2
|
+
import { ExecSessionManager, UnifiedExecRunner, ToolOrchestrator, RemoteExecHandleService } from '../../../../packages/orchestrator/src/index.js';
|
|
4
3
|
import { PrivacyGuard } from '../../privacy/guard.js';
|
|
5
4
|
import { resolveEnforcementMode } from '../utils/enforcementMode.js';
|
|
6
5
|
class CliExecSessionHandle {
|