@kbediako/codex-orchestrator 0.1.33 → 0.1.34
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 +19 -3
- package/codex.orchestrator.json +448 -0
- package/dist/bin/codex-orchestrator.js +365 -78
- package/dist/orchestrator/src/cli/config/repoConfigPolicy.js +22 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +20 -9
- package/dist/orchestrator/src/cli/delegationSetup.js +111 -14
- package/dist/orchestrator/src/cli/doctor.js +82 -5
- package/dist/orchestrator/src/cli/doctorIssueLog.js +350 -0
- package/dist/orchestrator/src/cli/init.js +23 -0
- package/dist/orchestrator/src/cli/orchestrator.js +19 -3
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +70 -18
- package/dist/orchestrator/src/cli/services/runPreparation.js +2 -0
- package/dist/orchestrator/src/cli/utils/commandPreview.js +10 -0
- package/dist/orchestrator/src/cli/utils/devtools.js +2 -1
- package/dist/orchestrator/src/cloud/CodexCloudTaskExecutor.js +21 -0
- package/docs/README.md +12 -7
- package/package.json +2 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { collectManifests, parseRunIdTimestamp } from '../../../scripts/lib/run-manifests.js';
|
|
5
|
+
const ISSUE_LOG_HEADER = `# Codex Orchestrator Issues Log
|
|
6
|
+
|
|
7
|
+
Purpose:
|
|
8
|
+
- Track concrete Codex Orchestrator (CO) friction points observed in this repo so they can be addressed upstream.
|
|
9
|
+
`;
|
|
10
|
+
export async function writeDoctorIssueLog(options) {
|
|
11
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
12
|
+
const env = options.env ?? process.env;
|
|
13
|
+
const repoRoot = resolveIssueLogRepoRoot(cwd, env);
|
|
14
|
+
const runsRoot = resolveIssueLogRootPath(repoRoot, env.CODEX_ORCHESTRATOR_RUNS_DIR, '.runs');
|
|
15
|
+
const outRoot = resolveIssueLogRootPath(repoRoot, env.CODEX_ORCHESTRATOR_OUT_DIR, 'out');
|
|
16
|
+
const defaultTaskId = normalizeIssueLogTaskId(env);
|
|
17
|
+
const capturedAt = new Date().toISOString();
|
|
18
|
+
const issueId = formatIssueId(capturedAt);
|
|
19
|
+
const issueTitle = normalizeText(options.issueTitle) ?? 'Observed Codex Orchestrator issue';
|
|
20
|
+
const issueNotes = normalizeText(options.issueNotes);
|
|
21
|
+
const taskFilter = normalizeText(options.taskFilter);
|
|
22
|
+
const issueLogPath = resolveIssueLogPath(repoRoot, options.issueLogPath);
|
|
23
|
+
const runContext = await resolveLatestRunContext({
|
|
24
|
+
runsRoot,
|
|
25
|
+
repoRoot,
|
|
26
|
+
taskFilter
|
|
27
|
+
});
|
|
28
|
+
const bundleTaskId = normalizeArtifactTaskId(taskFilter ?? runContext?.task_id ?? defaultTaskId);
|
|
29
|
+
const bundleDir = join(outRoot, bundleTaskId, 'doctor', 'issue-bundles');
|
|
30
|
+
await mkdir(bundleDir, { recursive: true });
|
|
31
|
+
const bundlePath = join(bundleDir, `${toCompactTimestamp(capturedAt)}-${slugify(issueTitle)}.json`);
|
|
32
|
+
const bundlePayload = {
|
|
33
|
+
version: 1,
|
|
34
|
+
captured_at: capturedAt,
|
|
35
|
+
issue: {
|
|
36
|
+
id: issueId,
|
|
37
|
+
title: issueTitle,
|
|
38
|
+
notes: issueNotes,
|
|
39
|
+
command: 'codex-orchestrator doctor --issue-log'
|
|
40
|
+
},
|
|
41
|
+
repo: {
|
|
42
|
+
cwd,
|
|
43
|
+
repo_root: repoRoot
|
|
44
|
+
},
|
|
45
|
+
task_filter: taskFilter,
|
|
46
|
+
doctor: options.doctor,
|
|
47
|
+
usage: options.usage ?? null,
|
|
48
|
+
cloud_preflight: options.cloudPreflight ?? null,
|
|
49
|
+
run_context: runContext
|
|
50
|
+
};
|
|
51
|
+
await writeFile(bundlePath, `${JSON.stringify(bundlePayload, null, 2)}\n`, 'utf8');
|
|
52
|
+
await writeIssueLogMarkdown({
|
|
53
|
+
issueLogPath,
|
|
54
|
+
issueId,
|
|
55
|
+
issueTitle,
|
|
56
|
+
issueNotes,
|
|
57
|
+
taskFilter,
|
|
58
|
+
capturedAt,
|
|
59
|
+
doctor: options.doctor,
|
|
60
|
+
cloudPreflight: options.cloudPreflight ?? null,
|
|
61
|
+
runContext,
|
|
62
|
+
bundlePath,
|
|
63
|
+
repoRoot,
|
|
64
|
+
cwd
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
issue_id: issueId,
|
|
68
|
+
issue_title: issueTitle,
|
|
69
|
+
issue_log_path: toDisplayPath(issueLogPath, cwd),
|
|
70
|
+
bundle_path: toDisplayPath(bundlePath, cwd),
|
|
71
|
+
task_filter: taskFilter,
|
|
72
|
+
run_context: runContext
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function formatDoctorIssueLogSummary(result) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push(`Issue log: ${result.issue_id}`);
|
|
78
|
+
lines.push(` - markdown: ${result.issue_log_path}`);
|
|
79
|
+
lines.push(` - bundle: ${result.bundle_path}`);
|
|
80
|
+
if (result.run_context) {
|
|
81
|
+
lines.push(` - run: ${result.run_context.run_id} (${result.run_context.status})`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
lines.push(' - run: <none found>');
|
|
85
|
+
}
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
function resolveIssueLogPath(repoRoot, rawPath) {
|
|
89
|
+
const normalized = normalizeText(rawPath);
|
|
90
|
+
if (!normalized) {
|
|
91
|
+
return join(repoRoot, 'docs', 'codex-orchestrator-issues.md');
|
|
92
|
+
}
|
|
93
|
+
if (isAbsolute(normalized)) {
|
|
94
|
+
return normalized;
|
|
95
|
+
}
|
|
96
|
+
return resolve(repoRoot, normalized);
|
|
97
|
+
}
|
|
98
|
+
function resolveIssueLogRepoRoot(cwd, env) {
|
|
99
|
+
const configuredRoot = normalizeText(env.CODEX_ORCHESTRATOR_ROOT);
|
|
100
|
+
const rootHint = configuredRoot === null
|
|
101
|
+
? cwd
|
|
102
|
+
: isAbsolute(configuredRoot)
|
|
103
|
+
? configuredRoot
|
|
104
|
+
: resolve(cwd, configuredRoot);
|
|
105
|
+
return resolveRepoRootFromHint(rootHint);
|
|
106
|
+
}
|
|
107
|
+
function resolveRepoRootFromHint(rootHint) {
|
|
108
|
+
const normalizedHint = resolve(rootHint);
|
|
109
|
+
const gitBoundary = findNearestGitBoundary(normalizedHint);
|
|
110
|
+
let current = normalizedHint;
|
|
111
|
+
while (current) {
|
|
112
|
+
if (existsSync(join(current, 'tasks', 'index.json'))) {
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
if (gitBoundary && current === gitBoundary) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const parent = dirname(current);
|
|
119
|
+
if (parent === current) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
current = parent;
|
|
123
|
+
}
|
|
124
|
+
return gitBoundary ?? normalizedHint;
|
|
125
|
+
}
|
|
126
|
+
function findNearestGitBoundary(start) {
|
|
127
|
+
let current = resolve(start);
|
|
128
|
+
while (current) {
|
|
129
|
+
if (existsSync(join(current, '.git'))) {
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
const parent = dirname(current);
|
|
133
|
+
if (parent === current) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
current = parent;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function resolveIssueLogRootPath(repoRoot, configuredPath, fallback) {
|
|
141
|
+
const normalized = normalizeText(configuredPath);
|
|
142
|
+
if (!normalized) {
|
|
143
|
+
return resolve(repoRoot, fallback);
|
|
144
|
+
}
|
|
145
|
+
if (isAbsolute(normalized)) {
|
|
146
|
+
return normalized;
|
|
147
|
+
}
|
|
148
|
+
return resolve(repoRoot, normalized);
|
|
149
|
+
}
|
|
150
|
+
function normalizeIssueLogTaskId(env) {
|
|
151
|
+
return normalizeArtifactTaskId(normalizeText(env.MCP_RUNNER_TASK_ID)
|
|
152
|
+
?? normalizeText(env.TASK)
|
|
153
|
+
?? normalizeText(env.CODEX_ORCHESTRATOR_TASK_ID)
|
|
154
|
+
?? '0101');
|
|
155
|
+
}
|
|
156
|
+
async function writeIssueLogMarkdown(options) {
|
|
157
|
+
await mkdir(dirname(options.issueLogPath), { recursive: true });
|
|
158
|
+
let content = ISSUE_LOG_HEADER;
|
|
159
|
+
if (existsSync(options.issueLogPath)) {
|
|
160
|
+
content = await readFile(options.issueLogPath, 'utf8');
|
|
161
|
+
if (!content.trim()) {
|
|
162
|
+
content = ISSUE_LOG_HEADER;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const dateHeading = options.capturedAt.slice(0, 10);
|
|
166
|
+
const dateMarker = `\n## ${dateHeading}\n`;
|
|
167
|
+
if (!content.includes(dateMarker) && !content.endsWith(`## ${dateHeading}`)) {
|
|
168
|
+
content = `${content.replace(/\s*$/u, '')}\n\n## ${dateHeading}\n`;
|
|
169
|
+
}
|
|
170
|
+
const cloudPreflightStatus = options.cloudPreflight
|
|
171
|
+
? options.cloudPreflight.ok
|
|
172
|
+
? 'ok'
|
|
173
|
+
: 'failed'
|
|
174
|
+
: 'not-run';
|
|
175
|
+
const cloudPreflightIssueCodes = options.cloudPreflight
|
|
176
|
+
? options.cloudPreflight.issues.map((issue) => issue.code).filter(Boolean)
|
|
177
|
+
: [];
|
|
178
|
+
const lines = [];
|
|
179
|
+
lines.push(`### ${options.issueId}: ${options.issueTitle}`);
|
|
180
|
+
lines.push('- Logged via: `codex-orchestrator doctor --issue-log`');
|
|
181
|
+
lines.push(`- Captured at: ${options.capturedAt}`);
|
|
182
|
+
lines.push(`- Repo root: \`${toDisplayPath(options.repoRoot, options.cwd)}\``);
|
|
183
|
+
lines.push(`- Task filter: \`${options.taskFilter ?? '<none>'}\``);
|
|
184
|
+
lines.push(`- Doctor status: \`${options.doctor.status}\``);
|
|
185
|
+
lines.push(`- Cloud preflight: \`${cloudPreflightStatus}\``);
|
|
186
|
+
if (cloudPreflightIssueCodes.length > 0) {
|
|
187
|
+
lines.push(`- Cloud preflight issue codes: \`${cloudPreflightIssueCodes.join(', ')}\``);
|
|
188
|
+
}
|
|
189
|
+
if (options.runContext) {
|
|
190
|
+
lines.push(`- Latest run id: \`${options.runContext.run_id}\``);
|
|
191
|
+
lines.push(`- Latest run status: \`${options.runContext.status}\``);
|
|
192
|
+
lines.push(`- Latest run pipeline: \`${options.runContext.pipeline_id}\``);
|
|
193
|
+
lines.push(`- Latest run manifest: \`${options.runContext.manifest_path}\``);
|
|
194
|
+
if (options.runContext.cloud_fallback_reason) {
|
|
195
|
+
lines.push(`- Latest run cloud fallback: \`${options.runContext.cloud_fallback_reason}\``);
|
|
196
|
+
}
|
|
197
|
+
if (options.runContext.cloud_execution_status) {
|
|
198
|
+
const cloudBits = [
|
|
199
|
+
`status=${options.runContext.cloud_execution_status}`,
|
|
200
|
+
options.runContext.cloud_execution_task_id
|
|
201
|
+
? `task=${options.runContext.cloud_execution_task_id}`
|
|
202
|
+
: null,
|
|
203
|
+
options.runContext.cloud_execution_status_url
|
|
204
|
+
? `url=${options.runContext.cloud_execution_status_url}`
|
|
205
|
+
: null
|
|
206
|
+
].filter((item) => Boolean(item));
|
|
207
|
+
lines.push(`- Latest run cloud execution: \`${cloudBits.join(' ')}\``);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
lines.push('- Latest run context: `<none found under .runs>`');
|
|
212
|
+
}
|
|
213
|
+
if (options.issueNotes) {
|
|
214
|
+
lines.push(`- Notes: ${options.issueNotes}`);
|
|
215
|
+
}
|
|
216
|
+
lines.push(`- Bundle JSON: \`${toDisplayPath(options.bundlePath, options.cwd)}\``);
|
|
217
|
+
lines.push('');
|
|
218
|
+
content = `${content.replace(/\s*$/u, '')}\n\n${lines.join('\n')}\n`;
|
|
219
|
+
await writeFile(options.issueLogPath, content, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
async function resolveLatestRunContext(options) {
|
|
222
|
+
const manifestPaths = await collectManifests(options.runsRoot, options.taskFilter ?? undefined);
|
|
223
|
+
let latest = null;
|
|
224
|
+
for (const manifestPath of manifestPaths) {
|
|
225
|
+
const parsed = await parseManifestSnapshot(manifestPath, options.repoRoot);
|
|
226
|
+
if (!parsed) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!latest || parsed.sort_time_ms > latest.sort_time_ms) {
|
|
230
|
+
latest = parsed;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!latest) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
task_id: latest.task_id,
|
|
238
|
+
run_id: latest.run_id,
|
|
239
|
+
pipeline_id: latest.pipeline_id,
|
|
240
|
+
status: latest.status,
|
|
241
|
+
status_detail: latest.status_detail,
|
|
242
|
+
summary: latest.summary,
|
|
243
|
+
manifest_path: latest.manifest_path,
|
|
244
|
+
log_path: latest.log_path,
|
|
245
|
+
cloud_fallback_reason: latest.cloud_fallback_reason,
|
|
246
|
+
cloud_fallback_issue_codes: latest.cloud_fallback_issue_codes,
|
|
247
|
+
cloud_execution_status: latest.cloud_execution_status,
|
|
248
|
+
cloud_execution_task_id: latest.cloud_execution_task_id,
|
|
249
|
+
cloud_execution_status_url: latest.cloud_execution_status_url
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function parseManifestSnapshot(manifestPath, repoRoot) {
|
|
253
|
+
try {
|
|
254
|
+
const raw = await readFile(manifestPath, 'utf8');
|
|
255
|
+
const parsed = JSON.parse(raw);
|
|
256
|
+
const fallbackIssuesRaw = parsed.cloud_fallback && typeof parsed.cloud_fallback === 'object'
|
|
257
|
+
? parsed.cloud_fallback.issues
|
|
258
|
+
: null;
|
|
259
|
+
const fallbackIssueCodes = Array.isArray(fallbackIssuesRaw)
|
|
260
|
+
? fallbackIssuesRaw
|
|
261
|
+
.map((issue) => {
|
|
262
|
+
if (!issue || typeof issue !== 'object') {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const code = issue.code;
|
|
266
|
+
return typeof code === 'string' && code.trim().length > 0 ? code.trim() : null;
|
|
267
|
+
})
|
|
268
|
+
.filter((code) => Boolean(code))
|
|
269
|
+
: [];
|
|
270
|
+
const cloudFallbackReason = parsed.cloud_fallback && typeof parsed.cloud_fallback === 'object'
|
|
271
|
+
? normalizeText(parsed.cloud_fallback.reason)
|
|
272
|
+
: null;
|
|
273
|
+
const cloudExecution = parsed.cloud_execution && typeof parsed.cloud_execution === 'object'
|
|
274
|
+
? parsed.cloud_execution
|
|
275
|
+
: null;
|
|
276
|
+
const runId = normalizeText(parsed.run_id) ?? basename(dirname(manifestPath));
|
|
277
|
+
const startedAtMs = Date.parse(String(parsed.started_at ?? ''));
|
|
278
|
+
const sortTimeMs = Number.isFinite(startedAtMs) && startedAtMs > 0
|
|
279
|
+
? startedAtMs
|
|
280
|
+
: parseRunIdTimestamp(runId)?.getTime() ?? 0;
|
|
281
|
+
return {
|
|
282
|
+
task_id: normalizeText(parsed.task_id) ?? 'unknown-task',
|
|
283
|
+
run_id: runId,
|
|
284
|
+
pipeline_id: normalizeText(parsed.pipeline_id) ?? 'unknown-pipeline',
|
|
285
|
+
status: normalizeText(parsed.status) ?? 'unknown',
|
|
286
|
+
status_detail: normalizeText(parsed.status_detail),
|
|
287
|
+
summary: normalizeText(parsed.summary),
|
|
288
|
+
manifest_path: toRepoRelativePath(manifestPath, repoRoot),
|
|
289
|
+
log_path: normalizeText(parsed.log_path),
|
|
290
|
+
cloud_fallback_reason: cloudFallbackReason,
|
|
291
|
+
cloud_fallback_issue_codes: fallbackIssueCodes,
|
|
292
|
+
cloud_execution_status: normalizeText(cloudExecution?.status),
|
|
293
|
+
cloud_execution_task_id: normalizeText(cloudExecution?.task_id),
|
|
294
|
+
cloud_execution_status_url: normalizeText(cloudExecution?.status_url),
|
|
295
|
+
sort_time_ms: sortTimeMs
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function normalizeArtifactTaskId(value) {
|
|
303
|
+
const normalized = normalizeText(value);
|
|
304
|
+
if (!normalized) {
|
|
305
|
+
return 'issue-log';
|
|
306
|
+
}
|
|
307
|
+
const safe = normalized.replace(/[^A-Za-z0-9._-]+/gu, '-').replace(/^-+|-+$/gu, '');
|
|
308
|
+
return safe.length > 0 ? safe : 'issue-log';
|
|
309
|
+
}
|
|
310
|
+
function formatIssueId(iso) {
|
|
311
|
+
const compact = iso
|
|
312
|
+
.replace(/\.\d{3}Z$/u, '')
|
|
313
|
+
.replace(/[-:]/gu, '')
|
|
314
|
+
.replace('T', '-');
|
|
315
|
+
return `CO-${compact}`;
|
|
316
|
+
}
|
|
317
|
+
function toCompactTimestamp(iso) {
|
|
318
|
+
return iso.replace(/[-:.]/gu, '').replace(/Z$/u, 'Z');
|
|
319
|
+
}
|
|
320
|
+
function slugify(value) {
|
|
321
|
+
const normalized = value
|
|
322
|
+
.toLowerCase()
|
|
323
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
324
|
+
.replace(/^-+|-+$/gu, '');
|
|
325
|
+
if (!normalized) {
|
|
326
|
+
return 'issue';
|
|
327
|
+
}
|
|
328
|
+
return normalized.slice(0, 48);
|
|
329
|
+
}
|
|
330
|
+
function toRepoRelativePath(pathValue, repoRoot) {
|
|
331
|
+
const rel = relative(repoRoot, pathValue);
|
|
332
|
+
if (!rel || rel.startsWith('..')) {
|
|
333
|
+
return pathValue;
|
|
334
|
+
}
|
|
335
|
+
return rel.replace(/\\/gu, '/');
|
|
336
|
+
}
|
|
337
|
+
function toDisplayPath(pathValue, cwd) {
|
|
338
|
+
const rel = relative(cwd, pathValue);
|
|
339
|
+
if (!rel || rel.startsWith('..')) {
|
|
340
|
+
return pathValue;
|
|
341
|
+
}
|
|
342
|
+
return rel.replace(/\\/gu, '/');
|
|
343
|
+
}
|
|
344
|
+
function normalizeText(value) {
|
|
345
|
+
if (typeof value !== 'string') {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const trimmed = value.trim();
|
|
349
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
350
|
+
}
|
|
@@ -2,6 +2,8 @@ import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
4
|
import { findPackageRoot } from './utils/packageInfo.js';
|
|
5
|
+
const CODEX_TEMPLATE = 'codex';
|
|
6
|
+
const CODEX_PIPELINE_CONFIG = 'codex.orchestrator.json';
|
|
5
7
|
export async function initCodexTemplates(options) {
|
|
6
8
|
const root = findPackageRoot();
|
|
7
9
|
const templateRoot = join(root, 'templates', options.template);
|
|
@@ -13,6 +15,13 @@ export async function initCodexTemplates(options) {
|
|
|
13
15
|
written,
|
|
14
16
|
skipped
|
|
15
17
|
});
|
|
18
|
+
if (options.template === CODEX_TEMPLATE) {
|
|
19
|
+
await copyTemplateFile(join(root, CODEX_PIPELINE_CONFIG), join(options.cwd, CODEX_PIPELINE_CONFIG), {
|
|
20
|
+
force: options.force,
|
|
21
|
+
written,
|
|
22
|
+
skipped
|
|
23
|
+
});
|
|
24
|
+
}
|
|
16
25
|
return { written, skipped, templateRoot };
|
|
17
26
|
}
|
|
18
27
|
async function assertDirectory(path) {
|
|
@@ -43,6 +52,19 @@ async function copyTemplateDir(sourceDir, targetDir, options) {
|
|
|
43
52
|
options.written.push(targetPath);
|
|
44
53
|
}
|
|
45
54
|
}
|
|
55
|
+
async function copyTemplateFile(sourcePath, targetPath, options) {
|
|
56
|
+
const info = await stat(sourcePath).catch(() => null);
|
|
57
|
+
if (!info || !info.isFile()) {
|
|
58
|
+
throw new Error(`Template file not found: ${sourcePath}`);
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(targetPath) && !options.force) {
|
|
61
|
+
options.skipped.push(targetPath);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
65
|
+
await copyFile(sourcePath, targetPath);
|
|
66
|
+
options.written.push(targetPath);
|
|
67
|
+
}
|
|
46
68
|
export function formatInitSummary(result, cwd) {
|
|
47
69
|
const lines = [];
|
|
48
70
|
if (result.written.length > 0) {
|
|
@@ -61,6 +83,7 @@ export function formatInitSummary(result, cwd) {
|
|
|
61
83
|
lines.push('No files written.');
|
|
62
84
|
}
|
|
63
85
|
lines.push('Next steps (recommended):');
|
|
86
|
+
lines.push(' - Review codex.orchestrator.json and adjust pipeline commands to your repository toolchain');
|
|
64
87
|
lines.push(' - codex-orchestrator setup --yes # installs bundled skills + configures delegation/devtools wiring');
|
|
65
88
|
lines.push(' - codex-orchestrator codex setup # optional managed/pinned Codex CLI (activate with CODEX_CLI_USE_MANAGED=1; stock codex is default)');
|
|
66
89
|
return lines;
|
|
@@ -24,6 +24,7 @@ import { SchedulerService } from './services/schedulerService.js';
|
|
|
24
24
|
import { applyHandlesToRunSummary, applyPrivacyToRunSummary, applyCloudExecutionToRunSummary, applyCloudFallbackToRunSummary, applyUsageKpiToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
|
|
25
25
|
import { prepareRun, resolvePipelineForResume, overrideTaskEnvironment } from './services/runPreparation.js';
|
|
26
26
|
import { loadPackageConfig, loadUserConfig } from './config/userConfig.js';
|
|
27
|
+
import { formatRepoConfigRequiredError, isRepoConfigRequired } from './config/repoConfigPolicy.js';
|
|
27
28
|
import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
|
|
28
29
|
import { ControlServer } from './control/controlServer.js';
|
|
29
30
|
import { RunEventEmitter, RunEventPublisher, snapshotStages } from './events/runEvents.js';
|
|
@@ -240,6 +241,9 @@ export class CodexOrchestrator {
|
|
|
240
241
|
approvalPolicy: options.approvalPolicy ?? null,
|
|
241
242
|
planTargetId: preparation.planPreview?.targetId ?? preparation.plannerTargetId ?? null
|
|
242
243
|
});
|
|
244
|
+
if (preparation.configNotice) {
|
|
245
|
+
appendSummary(manifest, preparation.configNotice);
|
|
246
|
+
}
|
|
243
247
|
const persister = new ManifestPersister({
|
|
244
248
|
manifest,
|
|
245
249
|
paths,
|
|
@@ -337,8 +341,12 @@ export class CodexOrchestrator {
|
|
|
337
341
|
const actualEnv = overrideTaskEnvironment(env, manifest.task_id);
|
|
338
342
|
const resolver = new PipelineResolver();
|
|
339
343
|
const designConfig = await resolver.loadDesignConfig(actualEnv.repoRoot);
|
|
340
|
-
const
|
|
341
|
-
const
|
|
344
|
+
const repoConfigRequired = isRepoConfigRequired(process.env);
|
|
345
|
+
const userConfig = await loadUserConfig(actualEnv, { allowPackageFallback: !repoConfigRequired });
|
|
346
|
+
if (repoConfigRequired && userConfig?.source !== 'repo') {
|
|
347
|
+
throw new Error(formatRepoConfigRequiredError(actualEnv.repoRoot));
|
|
348
|
+
}
|
|
349
|
+
const fallbackConfig = !repoConfigRequired && manifest.pipeline_id === 'rlm' && userConfig?.source === 'repo'
|
|
342
350
|
? await loadPackageConfig(actualEnv)
|
|
343
351
|
: null;
|
|
344
352
|
const pipeline = resolvePipelineForResume(actualEnv, manifest, userConfig, fallbackConfig);
|
|
@@ -360,6 +368,9 @@ export class CodexOrchestrator {
|
|
|
360
368
|
planTargetFallback: manifest.plan_target_id ?? null,
|
|
361
369
|
envOverrides
|
|
362
370
|
});
|
|
371
|
+
if (preparation.configNotice && !(manifest.summary ?? '').includes(preparation.configNotice)) {
|
|
372
|
+
appendSummary(manifest, preparation.configNotice);
|
|
373
|
+
}
|
|
363
374
|
manifest.plan_target_id = preparation.planPreview?.targetId ?? preparation.plannerTargetId ?? null;
|
|
364
375
|
const persister = new ManifestPersister({
|
|
365
376
|
manifest,
|
|
@@ -1028,7 +1039,12 @@ export class CodexOrchestrator {
|
|
|
1028
1039
|
branch,
|
|
1029
1040
|
enableFeatures,
|
|
1030
1041
|
disableFeatures,
|
|
1031
|
-
env: cloudEnvOverrides
|
|
1042
|
+
env: cloudEnvOverrides,
|
|
1043
|
+
onUpdate: async (cloudExecution) => {
|
|
1044
|
+
manifest.cloud_execution = cloudExecution;
|
|
1045
|
+
targetEntry.log_path = cloudExecution.log_path;
|
|
1046
|
+
await schedulePersist({ manifest: true, force: true });
|
|
1047
|
+
}
|
|
1032
1048
|
});
|
|
1033
1049
|
success = cloudResult.success;
|
|
1034
1050
|
notes.push(...cloudResult.notes);
|
|
@@ -3,64 +3,116 @@ import { loadPackageConfig, loadUserConfig } from '../config/userConfig.js';
|
|
|
3
3
|
import { resolvePipeline } from '../pipelines/index.js';
|
|
4
4
|
import { loadDesignConfig, shouldActivateDesignPipeline, designPipelineId } from '../../../../packages/shared/config/index.js';
|
|
5
5
|
import { logger } from '../../logger.js';
|
|
6
|
+
import { formatRepoConfigRequiredError, isRepoConfigRequired } from '../config/repoConfigPolicy.js';
|
|
6
7
|
const DEVTOOLS_PIPELINE_ALIASES = new Map([
|
|
7
8
|
['implementation-gate-devtools', 'implementation-gate'],
|
|
8
9
|
['frontend-testing-devtools', 'frontend-testing']
|
|
9
10
|
]);
|
|
10
11
|
export class PipelineResolver {
|
|
11
|
-
|
|
12
|
+
logInfo(message, quiet) {
|
|
13
|
+
if (!quiet) {
|
|
14
|
+
logger.info(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
logWarn(message, quiet) {
|
|
18
|
+
if (!quiet) {
|
|
19
|
+
logger.warn(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
logError(message, quiet) {
|
|
23
|
+
if (!quiet) {
|
|
24
|
+
logger.error(message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async loadDesignConfig(rootDir, quiet = false) {
|
|
12
28
|
const designConfig = await loadDesignConfig({ rootDir });
|
|
13
29
|
if (designConfig.warnings.length > 0) {
|
|
14
30
|
for (const warning of designConfig.warnings) {
|
|
15
|
-
|
|
31
|
+
this.logWarn(`[design-config] ${warning}`, quiet);
|
|
16
32
|
}
|
|
17
33
|
}
|
|
18
34
|
return designConfig;
|
|
19
35
|
}
|
|
20
36
|
async resolve(env, options) {
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
37
|
+
const quiet = options.quiet === true;
|
|
38
|
+
const runtimeEnv = options.processEnv ?? process.env;
|
|
39
|
+
this.logInfo(`PipelineResolver.resolve start for ${options.pipelineId ?? '<default>'}`, quiet);
|
|
40
|
+
const designConfig = await this.loadDesignConfig(env.repoRoot, quiet);
|
|
41
|
+
if (designConfig.exists) {
|
|
42
|
+
this.logInfo(`[design-config] loaded repo file at ${designConfig.path}`, quiet);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.logInfo(`[design-config] using defaults (missing file at ${designConfig.path})`, quiet);
|
|
46
|
+
}
|
|
47
|
+
const repoConfigRequired = isRepoConfigRequired(runtimeEnv);
|
|
48
|
+
const userConfig = await loadUserConfig(env, { allowPackageFallback: !repoConfigRequired, quiet });
|
|
49
|
+
if (repoConfigRequired && userConfig?.source !== 'repo') {
|
|
50
|
+
throw new Error(formatRepoConfigRequiredError(env.repoRoot));
|
|
51
|
+
}
|
|
52
|
+
let configNotice = null;
|
|
53
|
+
if (userConfig?.source === 'package') {
|
|
54
|
+
configNotice =
|
|
55
|
+
'Using packaged fallback codex.orchestrator.json (compatibility path). ' +
|
|
56
|
+
'Run `codex-orchestrator init codex` to pin repo-local config.';
|
|
57
|
+
this.logWarn(`[codex-config] ${configNotice}`, quiet);
|
|
58
|
+
}
|
|
59
|
+
else if (userConfig?.source === 'repo') {
|
|
60
|
+
this.logInfo('[codex-config] Using repo-local codex.orchestrator.json.', quiet);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.logWarn('[codex-config] No codex.orchestrator.json found in repo or package.', quiet);
|
|
64
|
+
}
|
|
26
65
|
const pipelineCandidate = options.pipelineId ??
|
|
27
66
|
(shouldActivateDesignPipeline(designConfig) ? designPipelineId(designConfig) : undefined);
|
|
28
67
|
const resolvedAlias = this.resolvePipelineAlias(pipelineCandidate);
|
|
29
68
|
const requestedPipelineId = resolvedAlias.pipelineId;
|
|
30
|
-
const envOverrides = this.resolveDesignEnvOverrides(designConfig, requestedPipelineId);
|
|
69
|
+
const envOverrides = this.resolveDesignEnvOverrides(designConfig, requestedPipelineId, runtimeEnv);
|
|
31
70
|
if (resolvedAlias.devtoolsRequested) {
|
|
32
71
|
envOverrides.CODEX_REVIEW_DEVTOOLS = '1';
|
|
33
|
-
|
|
72
|
+
this.logWarn(`[pipeline] ${resolvedAlias.aliasId} is deprecated; use ${requestedPipelineId} with CODEX_REVIEW_DEVTOOLS=1.`, quiet);
|
|
34
73
|
}
|
|
35
74
|
try {
|
|
36
75
|
const { pipeline, source } = resolvePipeline(env, {
|
|
37
76
|
pipelineId: requestedPipelineId,
|
|
38
77
|
config: userConfig
|
|
39
78
|
});
|
|
40
|
-
|
|
41
|
-
return { pipeline, userConfig, designConfig, source, envOverrides };
|
|
79
|
+
this.logInfo(`PipelineResolver.resolve selected pipeline ${pipeline.id}`, quiet);
|
|
80
|
+
return { pipeline, userConfig, designConfig, source, configNotice, envOverrides };
|
|
42
81
|
}
|
|
43
82
|
catch (error) {
|
|
44
|
-
if (requestedPipelineId === 'rlm' && userConfig?.source === 'repo') {
|
|
45
|
-
|
|
83
|
+
if (requestedPipelineId === 'rlm' && userConfig?.source === 'repo' && repoConfigRequired) {
|
|
84
|
+
throw new Error('Repo-local codex.orchestrator.json is missing the rlm pipeline while strict repo-config mode is enabled.');
|
|
85
|
+
}
|
|
86
|
+
if (requestedPipelineId === 'rlm' && userConfig?.source === 'repo' && !repoConfigRequired) {
|
|
87
|
+
const packageConfig = await loadPackageConfig(env, { quiet });
|
|
46
88
|
if (packageConfig) {
|
|
89
|
+
const fallbackNotice = 'Repo config is missing the rlm pipeline; using packaged fallback pipeline for compatibility. ' +
|
|
90
|
+
'Add rlm to your repo-local codex.orchestrator.json to avoid fallback.';
|
|
91
|
+
this.logWarn(`[codex-config] ${fallbackNotice}`, quiet);
|
|
47
92
|
const { pipeline, source } = resolvePipeline(env, {
|
|
48
93
|
pipelineId: requestedPipelineId,
|
|
49
94
|
config: packageConfig
|
|
50
95
|
});
|
|
51
|
-
|
|
52
|
-
return {
|
|
96
|
+
this.logInfo(`PipelineResolver.resolve selected package pipeline ${pipeline.id}`, quiet);
|
|
97
|
+
return {
|
|
98
|
+
pipeline,
|
|
99
|
+
userConfig: packageConfig,
|
|
100
|
+
designConfig,
|
|
101
|
+
source,
|
|
102
|
+
configNotice: fallbackNotice,
|
|
103
|
+
envOverrides
|
|
104
|
+
};
|
|
53
105
|
}
|
|
54
106
|
}
|
|
55
|
-
|
|
107
|
+
this.logError(`PipelineResolver.resolve failed for ${requestedPipelineId ?? '<default>'}: ${error.message}`, quiet);
|
|
56
108
|
throw error;
|
|
57
109
|
}
|
|
58
110
|
}
|
|
59
|
-
resolveDesignEnvOverrides(designConfig, pipelineId) {
|
|
111
|
+
resolveDesignEnvOverrides(designConfig, pipelineId, runtimeEnv = process.env) {
|
|
60
112
|
const envOverrides = {
|
|
61
113
|
DESIGN_CONFIG_PATH: designConfig.path
|
|
62
114
|
};
|
|
63
|
-
if (pipelineId === designPipelineId(designConfig) &&
|
|
115
|
+
if (pipelineId === designPipelineId(designConfig) && runtimeEnv.DESIGN_PIPELINE === undefined) {
|
|
64
116
|
envOverrides.DESIGN_PIPELINE = '1';
|
|
65
117
|
}
|
|
66
118
|
return envOverrides;
|
|
@@ -36,6 +36,7 @@ export async function prepareRun(options) {
|
|
|
36
36
|
? {
|
|
37
37
|
pipeline: options.pipeline,
|
|
38
38
|
source: options.pipelineSource ?? null,
|
|
39
|
+
configNotice: options.configNotice ?? null,
|
|
39
40
|
envOverrides: options.envOverrides ?? {}
|
|
40
41
|
}
|
|
41
42
|
: await resolver.resolve(env, { pipelineId: options.pipelineId });
|
|
@@ -53,6 +54,7 @@ export async function prepareRun(options) {
|
|
|
53
54
|
env,
|
|
54
55
|
pipeline: resolvedPipeline.pipeline,
|
|
55
56
|
pipelineSource: resolvedPipeline.source ?? null,
|
|
57
|
+
configNotice: resolvedPipeline.configNotice ?? null,
|
|
56
58
|
envOverrides: resolvedPipeline.envOverrides ?? {},
|
|
57
59
|
planner,
|
|
58
60
|
plannerTargetId: planPreview?.targetId ?? targetId,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const SAFE_COMMAND_PREVIEW_TOKEN = /^[A-Za-z0-9_./:@=-]+$/u;
|
|
2
|
+
export function quoteCommandPreviewToken(value) {
|
|
3
|
+
if (SAFE_COMMAND_PREVIEW_TOKEN.test(value)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
export function buildCommandPreview(command, args) {
|
|
9
|
+
return [quoteCommandPreviewToken(command), ...args.map((arg) => quoteCommandPreviewToken(arg))].join(' ');
|
|
10
|
+
}
|
|
@@ -4,6 +4,7 @@ import process from 'node:process';
|
|
|
4
4
|
import { EnvUtils } from '../../../../packages/shared/config/env.js';
|
|
5
5
|
import { resolveCodexCliBin } from './codexCli.js';
|
|
6
6
|
import { resolveCodexHome } from './codexPaths.js';
|
|
7
|
+
import { buildCommandPreview } from './commandPreview.js';
|
|
7
8
|
export const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
|
|
8
9
|
export const DEVTOOLS_CONFIG_OVERRIDE = 'mcp_servers.chrome-devtools.enabled=true';
|
|
9
10
|
const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_MCP_CONFIG_OVERRIDES', 'CODEX_CONFIG_OVERRIDES'];
|
|
@@ -77,7 +78,7 @@ export function buildDevtoolsSetupPlan(env = process.env) {
|
|
|
77
78
|
configPath,
|
|
78
79
|
command,
|
|
79
80
|
args,
|
|
80
|
-
commandLine:
|
|
81
|
+
commandLine: buildCommandPreview(command, args),
|
|
81
82
|
configSnippet: DEVTOOLS_CONFIG_SNIPPET
|
|
82
83
|
};
|
|
83
84
|
}
|