@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.
@@ -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 userConfig = await loadUserConfig(actualEnv);
341
- const fallbackConfig = manifest.pipeline_id === 'rlm' && userConfig?.source === 'repo'
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
- async loadDesignConfig(rootDir) {
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
- logger.warn(`[design-config] ${warning}`);
31
+ this.logWarn(`[design-config] ${warning}`, quiet);
16
32
  }
17
33
  }
18
34
  return designConfig;
19
35
  }
20
36
  async resolve(env, options) {
21
- logger.info(`PipelineResolver.resolve start for ${options.pipelineId ?? '<default>'}`);
22
- const designConfig = await this.loadDesignConfig(env.repoRoot);
23
- logger.info(`PipelineResolver.resolve loaded design config from ${designConfig.path}`);
24
- const userConfig = await loadUserConfig(env);
25
- logger.info(`PipelineResolver.resolve loaded user config`);
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
- logger.warn(`[pipeline] ${resolvedAlias.aliasId} is deprecated; use ${requestedPipelineId} with CODEX_REVIEW_DEVTOOLS=1.`);
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
- logger.info(`PipelineResolver.resolve selected pipeline ${pipeline.id}`);
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
- const packageConfig = await loadPackageConfig(env);
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
- logger.info(`PipelineResolver.resolve selected package pipeline ${pipeline.id}`);
52
- return { pipeline, userConfig: packageConfig, designConfig, source, envOverrides };
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
- logger.error(`PipelineResolver.resolve failed for ${requestedPipelineId ?? '<default>'}: ${error.message}`);
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) && process.env.DESIGN_PIPELINE === undefined) {
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: [command, ...args].join(' '),
81
+ commandLine: buildCommandPreview(command, args),
81
82
  configSnippet: DEVTOOLS_CONFIG_SNIPPET
82
83
  };
83
84
  }