@kbediako/codex-orchestrator 0.1.2 → 0.1.4

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.
Files changed (73) hide show
  1. package/README.md +15 -8
  2. package/dist/bin/codex-orchestrator.js +252 -121
  3. package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
  4. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  5. package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
  6. package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
  7. package/dist/orchestrator/src/cli/control/controlState.js +46 -0
  8. package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
  9. package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
  10. package/dist/orchestrator/src/cli/control/questions.js +106 -0
  11. package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
  12. package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
  13. package/dist/orchestrator/src/cli/exec/context.js +9 -3
  14. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  15. package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
  16. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  17. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
  18. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
  19. package/dist/orchestrator/src/cli/orchestrator.js +233 -47
  20. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  21. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  22. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  23. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  24. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  25. package/dist/orchestrator/src/cli/rlmRunner.js +440 -0
  26. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  27. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  28. package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
  29. package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
  30. package/dist/orchestrator/src/cli/services/commandRunner.js +2 -2
  31. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  32. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  33. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  34. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  35. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  36. package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
  37. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  38. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  39. package/dist/orchestrator/src/persistence/ExperienceStore.js +115 -58
  40. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
  41. package/dist/orchestrator/src/persistence/TaskStateStore.js +3 -2
  42. package/dist/orchestrator/src/persistence/lockFile.js +26 -1
  43. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  44. package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
  45. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  46. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  47. package/dist/packages/orchestrator/src/index.js +1 -0
  48. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
  49. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  50. package/dist/packages/shared/streams/stdio.js +2 -112
  51. package/dist/packages/shared/utils/strings.js +17 -0
  52. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  53. package/dist/scripts/design/pipeline/context.js +5 -5
  54. package/dist/scripts/design/pipeline/extract.js +9 -6
  55. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  56. package/dist/scripts/design/pipeline/permit.js +59 -0
  57. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  58. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  59. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  60. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  61. package/dist/scripts/lib/cli-args.js +53 -0
  62. package/dist/scripts/lib/docs-helpers.js +111 -0
  63. package/dist/scripts/lib/npm-pack.js +20 -0
  64. package/dist/scripts/lib/run-manifests.js +160 -0
  65. package/package.json +7 -2
  66. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  67. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  68. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  69. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  70. package/dist/orchestrator/src/control-plane/index.js +0 -3
  71. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  72. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  73. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -0,0 +1,485 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { logger } from '../../logger.js';
7
+ const require = createRequire(import.meta.url);
8
+ const toml = require('@iarna/toml');
9
+ const DEFAULT_ALLOWED_RUN_ROOTS = [];
10
+ const DEFAULT_CONFIG = {
11
+ delegate: {
12
+ allowNested: false,
13
+ toolProfile: [],
14
+ allowedToolServers: [],
15
+ mode: 'question_only',
16
+ expiryFallback: 'pause'
17
+ },
18
+ rlm: {
19
+ policy: 'always',
20
+ environment: 'docker',
21
+ allowedEnvironments: ['docker'],
22
+ maxIterations: 50,
23
+ maxSubcalls: 200,
24
+ maxSubcallDepth: 1,
25
+ wallClockTimeoutMs: 30 * 60 * 1000,
26
+ budgetUsd: 0,
27
+ budgetTokens: 0,
28
+ rootModel: '',
29
+ subModel: ''
30
+ },
31
+ runner: {
32
+ mode: 'prod',
33
+ allowedModes: ['prod']
34
+ },
35
+ ui: {
36
+ controlEnabled: false,
37
+ bindHost: '127.0.0.1',
38
+ allowedBindHosts: ['127.0.0.1'],
39
+ allowedRunRoots: DEFAULT_ALLOWED_RUN_ROOTS
40
+ },
41
+ github: {
42
+ enabled: false,
43
+ operations: []
44
+ },
45
+ paths: {
46
+ allowedRoots: []
47
+ },
48
+ confirm: {
49
+ autoPause: true,
50
+ maxPending: 3,
51
+ expiresInMs: 15 * 60 * 1000
52
+ },
53
+ sandbox: {
54
+ network: false
55
+ }
56
+ };
57
+ const SOURCE_PRIORITY = {
58
+ global: 1,
59
+ repo: 2,
60
+ env: 3,
61
+ cli: 4
62
+ };
63
+ export async function loadDelegationConfigFiles(options) {
64
+ const env = options.env ?? process.env;
65
+ const codexHome = options.codexHome ?? resolveCodexHome(env);
66
+ const globalPath = join(codexHome, 'config.toml');
67
+ const repoPath = join(options.repoRoot, '.codex', 'orchestrator.toml');
68
+ const globalRaw = await readTomlFile(globalPath);
69
+ const repoRaw = await readTomlFile(repoPath);
70
+ return {
71
+ global: globalRaw ? normalizeLayer(globalRaw, 'global') : null,
72
+ repo: repoRaw ? normalizeLayer(repoRaw, 'repo') : null
73
+ };
74
+ }
75
+ export function computeEffectiveDelegationConfig(options) {
76
+ const sorted = [...options.layers].sort((a, b) => SOURCE_PRIORITY[a.source] - SOURCE_PRIORITY[b.source]);
77
+ const merged = sorted.reduce((acc, layer) => mergeLayer(acc, layer), {
78
+ source: 'global'
79
+ });
80
+ const repoLayer = sorted.find((layer) => layer.source === 'repo');
81
+ const defaults = structuredClone(DEFAULT_CONFIG);
82
+ const effective = {
83
+ delegate: {
84
+ ...defaults.delegate,
85
+ ...(merged.delegate ?? {})
86
+ },
87
+ rlm: {
88
+ ...defaults.rlm,
89
+ ...(merged.rlm ?? {})
90
+ },
91
+ runner: {
92
+ ...defaults.runner,
93
+ ...(merged.runner ?? {})
94
+ },
95
+ ui: {
96
+ ...defaults.ui,
97
+ ...(merged.ui ?? {})
98
+ },
99
+ github: {
100
+ ...defaults.github
101
+ },
102
+ paths: {
103
+ ...defaults.paths,
104
+ ...(merged.paths ?? {})
105
+ },
106
+ confirm: {
107
+ ...defaults.confirm,
108
+ ...(merged.confirm ?? {})
109
+ },
110
+ sandbox: {
111
+ ...defaults.sandbox,
112
+ ...(merged.sandbox ?? {})
113
+ }
114
+ };
115
+ if (effective.delegate.mode !== 'full' && effective.delegate.mode !== 'question_only') {
116
+ effective.delegate.mode = defaults.delegate.mode;
117
+ }
118
+ const repoAllowedRoots = typeof repoLayer?.paths?.allowedRoots !== 'undefined' ? repoLayer.paths.allowedRoots : [options.repoRoot];
119
+ const repoCapRoots = normalizeRoots(repoAllowedRoots);
120
+ const requestedRoots = typeof merged.paths?.allowedRoots !== 'undefined' ? merged.paths.allowedRoots : repoCapRoots;
121
+ const candidateRoots = normalizeRoots(requestedRoots);
122
+ effective.paths.allowedRoots = intersectRoots(repoCapRoots, candidateRoots);
123
+ const repoToolCap = repoLayer?.delegate?.allowedToolServers ?? [];
124
+ const requestedToolProfile = merged.delegate?.toolProfile ?? repoToolCap;
125
+ effective.delegate.allowedToolServers = [...repoToolCap];
126
+ effective.delegate.toolProfile = intersectExact(repoToolCap, requestedToolProfile);
127
+ const repoAllowedModes = repoLayer?.runner?.allowedModes ?? defaults.runner.allowedModes;
128
+ effective.runner.allowedModes = repoAllowedModes;
129
+ const requestedMode = merged.runner?.mode ?? defaults.runner.mode;
130
+ effective.runner.mode = repoAllowedModes.includes(requestedMode) ? requestedMode : repoAllowedModes[0] ?? defaults.runner.mode;
131
+ const repoAllowedEnvs = repoLayer?.rlm?.allowedEnvironments ?? defaults.rlm.allowedEnvironments;
132
+ effective.rlm.allowedEnvironments = repoAllowedEnvs;
133
+ const requestedEnv = merged.rlm?.environment ?? defaults.rlm.environment;
134
+ effective.rlm.environment = repoAllowedEnvs.includes(requestedEnv) ? requestedEnv : repoAllowedEnvs[0] ?? defaults.rlm.environment;
135
+ const repoAllowedBindHosts = repoLayer?.ui?.allowedBindHosts ?? defaults.ui.allowedBindHosts;
136
+ effective.ui.allowedBindHosts = repoAllowedBindHosts;
137
+ const requestedBindHost = merged.ui?.bindHost ?? defaults.ui.bindHost;
138
+ effective.ui.bindHost = repoAllowedBindHosts.includes(requestedBindHost)
139
+ ? requestedBindHost
140
+ : repoAllowedBindHosts[0] ?? defaults.ui.bindHost;
141
+ const repoAllowedRunRoots = typeof repoLayer?.ui?.allowedRunRoots !== 'undefined' ? repoLayer.ui.allowedRunRoots : [options.repoRoot];
142
+ const repoCapRunRoots = normalizeRoots(repoAllowedRunRoots);
143
+ const hasRunRootsOverride = typeof repoLayer?.ui?.allowedRunRoots !== 'undefined' || typeof merged.ui?.allowedRunRoots !== 'undefined';
144
+ const requestedRunRoots = typeof merged.ui?.allowedRunRoots !== 'undefined' ? merged.ui.allowedRunRoots : repoCapRunRoots;
145
+ const candidateRunRoots = normalizeRoots(requestedRunRoots);
146
+ effective.ui.allowedRunRoots = intersectRoots(repoCapRunRoots, candidateRunRoots);
147
+ if (hasRunRootsOverride && effective.ui.allowedRunRoots.length === 0 && repoCapRunRoots.length > 0) {
148
+ logger.warn('ui.allowedRunRoots override produced empty intersection with repo cap; UI run access disabled.');
149
+ }
150
+ const repoAllowNetwork = Boolean(repoLayer?.sandbox?.network ?? defaults.sandbox.network);
151
+ effective.sandbox.network = repoAllowNetwork && Boolean(merged.sandbox?.network ?? defaults.sandbox.network);
152
+ const githubEnabled = Boolean(repoLayer?.github?.enabled ?? false);
153
+ effective.github.enabled = githubEnabled;
154
+ effective.github.operations = githubEnabled ? [...(repoLayer?.github?.operations ?? [])] : [];
155
+ if (!hasRunRootsOverride && effective.ui.allowedRunRoots.length === 0) {
156
+ effective.ui.allowedRunRoots = [options.repoRoot];
157
+ }
158
+ return effective;
159
+ }
160
+ function mergeLayer(base, update) {
161
+ const merged = {
162
+ ...base,
163
+ source: update.source
164
+ };
165
+ merged.delegate = mergeSection(base.delegate, update.delegate);
166
+ merged.rlm = mergeSection(base.rlm, update.rlm);
167
+ merged.runner = mergeSection(base.runner, update.runner);
168
+ merged.ui = mergeSection(base.ui, update.ui);
169
+ merged.github = mergeSection(base.github, update.github);
170
+ merged.paths = mergeSection(base.paths, update.paths);
171
+ merged.confirm = mergeSection(base.confirm, update.confirm);
172
+ merged.sandbox = mergeSection(base.sandbox, update.sandbox);
173
+ return merged;
174
+ }
175
+ function mergeSection(base, update) {
176
+ if (!update) {
177
+ return base;
178
+ }
179
+ if (!base) {
180
+ const cleaned = {};
181
+ for (const [key, value] of Object.entries(update)) {
182
+ if (typeof value === 'undefined') {
183
+ continue;
184
+ }
185
+ if (Array.isArray(value)) {
186
+ cleaned[key] = [...value];
187
+ continue;
188
+ }
189
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
190
+ cleaned[key] = { ...value };
191
+ continue;
192
+ }
193
+ cleaned[key] = value;
194
+ }
195
+ return cleaned;
196
+ }
197
+ const merged = { ...base };
198
+ for (const [key, value] of Object.entries(update)) {
199
+ if (typeof value === 'undefined') {
200
+ continue;
201
+ }
202
+ if (Array.isArray(value)) {
203
+ merged[key] = [...value];
204
+ continue;
205
+ }
206
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
207
+ merged[key] = { ...merged[key], ...value };
208
+ continue;
209
+ }
210
+ merged[key] = value;
211
+ }
212
+ return merged;
213
+ }
214
+ async function readTomlFile(path) {
215
+ try {
216
+ const raw = await readFile(path, 'utf8');
217
+ const parsed = toml.parse(raw);
218
+ return parsed;
219
+ }
220
+ catch (error) {
221
+ if (error.code === 'ENOENT') {
222
+ return null;
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+ function normalizeLayer(raw, source) {
228
+ return {
229
+ source,
230
+ delegate: normalizeDelegate(raw.delegate),
231
+ rlm: normalizeRlm(raw.rlm),
232
+ runner: normalizeRunner(raw.runner),
233
+ ui: normalizeUi(raw.ui),
234
+ github: normalizeGithub(raw.github),
235
+ paths: normalizePaths(raw.paths),
236
+ confirm: normalizeConfirm(raw.confirm),
237
+ sandbox: normalizeSandbox(raw.sandbox)
238
+ };
239
+ }
240
+ function normalizeDelegate(value) {
241
+ const record = asRecord(value);
242
+ if (!record)
243
+ return undefined;
244
+ return {
245
+ allowNested: asBoolean(record.allow_nested ?? record.allowNested),
246
+ toolProfile: asStringArray(record.tool_profile ?? record.toolProfile),
247
+ allowedToolServers: asStringArray(record.allowed_tool_servers ?? record.allowedToolServers),
248
+ mode: asString(record.mode),
249
+ expiryFallback: asString(record.question_expiry_fallback ?? record.expiryFallback)
250
+ };
251
+ }
252
+ function normalizeRlm(value) {
253
+ const record = asRecord(value);
254
+ if (!record)
255
+ return undefined;
256
+ return {
257
+ policy: asString(record.policy),
258
+ environment: asString(record.environment) ?? asString(record.env),
259
+ allowedEnvironments: asStringArray(record.allowed_environments ?? record.allowedEnvironments),
260
+ maxIterations: asNumber(record.max_iterations ?? record.maxIterations),
261
+ maxSubcalls: asNumber(record.max_subcalls ?? record.maxSubcalls),
262
+ maxSubcallDepth: asNumber(record.max_subcall_depth ?? record.maxSubcallDepth),
263
+ wallClockTimeoutMs: asNumber(record.wall_clock_timeout_ms ?? record.wallClockTimeoutMs),
264
+ budgetUsd: asNumber(record.budget_usd ?? record.budgetUsd),
265
+ budgetTokens: asNumber(record.budget_tokens ?? record.budgetTokens),
266
+ rootModel: asString(record.root_model ?? record.rootModel),
267
+ subModel: asString(record.sub_model ?? record.subModel)
268
+ };
269
+ }
270
+ function normalizeRunner(value) {
271
+ const record = asRecord(value);
272
+ if (!record)
273
+ return undefined;
274
+ return {
275
+ mode: asString(record.mode),
276
+ allowedModes: asStringArray(record.allowed_modes ?? record.allowedModes)
277
+ };
278
+ }
279
+ function normalizeUi(value) {
280
+ const record = asRecord(value);
281
+ if (!record)
282
+ return undefined;
283
+ return {
284
+ controlEnabled: asBoolean(record.control_enabled ?? record.controlEnabled),
285
+ bindHost: asString(record.bind_host ?? record.bindHost),
286
+ allowedBindHosts: asStringArray(record.allowed_bind_hosts ?? record.allowedBindHosts),
287
+ allowedRunRoots: asStringArray(record.allowed_run_roots ?? record.allowedRunRoots)
288
+ };
289
+ }
290
+ function normalizeGithub(value) {
291
+ const record = asRecord(value);
292
+ if (!record)
293
+ return undefined;
294
+ return {
295
+ enabled: asBoolean(record.enabled),
296
+ operations: asStringArray(record.operations)
297
+ };
298
+ }
299
+ function normalizePaths(value) {
300
+ const record = asRecord(value);
301
+ if (!record)
302
+ return undefined;
303
+ return {
304
+ allowedRoots: asStringArray(record.allowed_roots ?? record.allowedRoots)
305
+ };
306
+ }
307
+ function normalizeConfirm(value) {
308
+ const record = asRecord(value);
309
+ if (!record)
310
+ return undefined;
311
+ return {
312
+ autoPause: asBoolean(record.auto_pause ?? record.autoPause),
313
+ maxPending: asNumber(record.max_pending ?? record.maxPending),
314
+ expiresInMs: asNumber(record.expires_in_ms ?? record.expiresInMs)
315
+ };
316
+ }
317
+ function normalizeSandbox(value) {
318
+ const record = asRecord(value);
319
+ if (!record)
320
+ return undefined;
321
+ return {
322
+ network: asBoolean(record.network)
323
+ };
324
+ }
325
+ function asRecord(value) {
326
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
327
+ return null;
328
+ }
329
+ return value;
330
+ }
331
+ function asBoolean(value) {
332
+ if (typeof value === 'boolean') {
333
+ return value;
334
+ }
335
+ return undefined;
336
+ }
337
+ function asString(value) {
338
+ if (typeof value === 'string' && value.trim().length > 0) {
339
+ return value.trim();
340
+ }
341
+ return undefined;
342
+ }
343
+ function asStringArray(value) {
344
+ if (value === null || typeof value === 'undefined')
345
+ return undefined;
346
+ if (Array.isArray(value)) {
347
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0).map((entry) => entry.trim());
348
+ }
349
+ if (typeof value === 'string') {
350
+ const trimmed = value.trim();
351
+ return trimmed.length > 0 ? [trimmed] : [];
352
+ }
353
+ return undefined;
354
+ }
355
+ function asNumber(value) {
356
+ if (typeof value === 'number' && Number.isFinite(value)) {
357
+ return value;
358
+ }
359
+ return undefined;
360
+ }
361
+ function resolveCodexHome(env) {
362
+ const override = env.CODEX_HOME?.trim();
363
+ if (override) {
364
+ return isAbsolute(override) ? override : resolve(process.cwd(), override);
365
+ }
366
+ return join(homedir(), '.codex');
367
+ }
368
+ function normalizeRoots(roots) {
369
+ const normalized = roots
370
+ .filter((root) => typeof root === 'string')
371
+ .map((root) => realpathSafe(resolve(root)))
372
+ .filter((root) => root.length > 0);
373
+ return Array.from(new Set(normalized));
374
+ }
375
+ function intersectExact(cap, requested) {
376
+ if (cap.length === 0 || requested.length === 0) {
377
+ return [];
378
+ }
379
+ const set = new Set(cap);
380
+ return requested.filter((entry) => set.has(entry));
381
+ }
382
+ function intersectRoots(cap, requested) {
383
+ if (cap.length === 0 || requested.length === 0) {
384
+ return [];
385
+ }
386
+ const resolvedCap = cap.map((root) => realpathSafe(resolve(root)));
387
+ return requested
388
+ .map((root) => realpathSafe(resolve(root)))
389
+ .filter((candidate) => resolvedCap.some((allowed) => isWithinRoot(allowed, candidate)));
390
+ }
391
+ function isWithinRoot(root, candidate) {
392
+ const normalizedRoot = normalizePath(realpathSafe(resolve(root)));
393
+ const normalizedCandidate = normalizePath(realpathSafe(resolve(candidate)));
394
+ if (normalizedRoot === normalizedCandidate) {
395
+ return true;
396
+ }
397
+ const relativePath = relative(normalizedRoot, normalizedCandidate);
398
+ if (!relativePath) {
399
+ return true;
400
+ }
401
+ if (isAbsolute(relativePath)) {
402
+ return false;
403
+ }
404
+ return !relativePath.startsWith(`..${sep}`) && relativePath !== '..';
405
+ }
406
+ function realpathSafe(pathname) {
407
+ try {
408
+ return realpathSync(pathname);
409
+ }
410
+ catch {
411
+ return pathname;
412
+ }
413
+ }
414
+ function normalizePath(pathname) {
415
+ return process.platform === 'win32' ? pathname.toLowerCase() : pathname;
416
+ }
417
+ export function resolveConfigDir(pathname) {
418
+ return dirname(pathname);
419
+ }
420
+ export function splitDelegationConfigOverrides(raw) {
421
+ if (!raw) {
422
+ return [];
423
+ }
424
+ const entries = [];
425
+ let current = '';
426
+ let bracketDepth = 0;
427
+ let inSingle = false;
428
+ let inDouble = false;
429
+ let escaping = false;
430
+ const flush = () => {
431
+ const trimmed = current.trim();
432
+ if (trimmed) {
433
+ entries.push(trimmed);
434
+ }
435
+ current = '';
436
+ };
437
+ for (const char of raw) {
438
+ if (escaping) {
439
+ current += char;
440
+ escaping = false;
441
+ continue;
442
+ }
443
+ if ((inSingle || inDouble) && char === '\\') {
444
+ current += char;
445
+ escaping = true;
446
+ continue;
447
+ }
448
+ if (!inDouble && char === '\'') {
449
+ inSingle = !inSingle;
450
+ current += char;
451
+ continue;
452
+ }
453
+ if (!inSingle && char === '"') {
454
+ inDouble = !inDouble;
455
+ current += char;
456
+ continue;
457
+ }
458
+ if (!inSingle && !inDouble) {
459
+ if (char === '[') {
460
+ bracketDepth += 1;
461
+ }
462
+ else if (char === ']') {
463
+ bracketDepth = Math.max(0, bracketDepth - 1);
464
+ }
465
+ if (bracketDepth === 0 && (char === ',' || char === ';' || char === '\n')) {
466
+ flush();
467
+ continue;
468
+ }
469
+ }
470
+ current += char;
471
+ }
472
+ flush();
473
+ return entries;
474
+ }
475
+ export function parseDelegationConfigOverride(value, source) {
476
+ const trimmed = value.trim();
477
+ if (!trimmed) {
478
+ return null;
479
+ }
480
+ const parsed = toml.parse(trimmed);
481
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
482
+ return null;
483
+ }
484
+ return normalizeLayer(parsed, source);
485
+ }
@@ -1,28 +1,102 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { logger } from '../../logger.js';
4
+ import { findPackageRoot } from '../utils/packageInfo.js';
5
+ export async function loadRepoConfig(env) {
6
+ const repoConfigPath = join(env.repoRoot, 'codex.orchestrator.json');
7
+ const repoConfig = await readConfig(repoConfigPath);
8
+ if (repoConfig) {
9
+ logger.info(`[codex-config] Loaded user config from ${repoConfigPath}`);
10
+ return normalizeUserConfig(repoConfig, 'repo');
11
+ }
12
+ logger.warn(`[codex-config] Missing codex.orchestrator.json at ${repoConfigPath}`);
13
+ return null;
14
+ }
15
+ export async function loadPackageConfig(env) {
16
+ const repoConfigPath = join(env.repoRoot, 'codex.orchestrator.json');
17
+ const packageRoot = findPackageRoot();
18
+ const packageConfigPath = join(packageRoot, 'codex.orchestrator.json');
19
+ if (packageConfigPath === repoConfigPath) {
20
+ return null;
21
+ }
22
+ const packageConfig = await readConfig(packageConfigPath);
23
+ if (packageConfig) {
24
+ logger.info(`[codex-config] Loaded user config from ${packageConfigPath}`);
25
+ return normalizeUserConfig(packageConfig, 'package');
26
+ }
27
+ logger.warn(`[codex-config] Missing codex.orchestrator.json at ${packageConfigPath}`);
28
+ return null;
29
+ }
4
30
  export async function loadUserConfig(env) {
5
- const configPath = join(env.repoRoot, 'codex.orchestrator.json');
31
+ const repoConfig = await loadRepoConfig(env);
32
+ if (repoConfig) {
33
+ return repoConfig;
34
+ }
35
+ return await loadPackageConfig(env);
36
+ }
37
+ export function findPipeline(config, id) {
38
+ if (!config?.pipelines) {
39
+ return null;
40
+ }
41
+ return config.pipelines.find((pipeline) => pipeline.id === id) ?? null;
42
+ }
43
+ function normalizeUserConfig(config, source) {
44
+ if (!config) {
45
+ return null;
46
+ }
47
+ const stageSets = normalizeStageSets(config.stageSets);
48
+ const pipelines = Array.isArray(config.pipelines)
49
+ ? config.pipelines.map((pipeline) => expandPipelineStages(pipeline, stageSets))
50
+ : config.pipelines;
51
+ return { pipelines, defaultPipeline: config.defaultPipeline, source };
52
+ }
53
+ async function readConfig(configPath) {
6
54
  try {
7
55
  const raw = await readFile(configPath, 'utf8');
8
- const parsed = JSON.parse(raw);
9
- logger.info(`[codex-config] Loaded user config from ${configPath}`);
10
- if (parsed && Array.isArray(parsed.pipelines)) {
11
- return parsed;
12
- }
13
- return parsed ?? null;
56
+ return JSON.parse(raw);
14
57
  }
15
58
  catch (error) {
16
59
  if (error.code === 'ENOENT') {
17
- logger.warn(`[codex-config] Missing codex.orchestrator.json at ${configPath}`);
18
60
  return null;
19
61
  }
20
62
  throw error;
21
63
  }
22
64
  }
23
- export function findPipeline(config, id) {
24
- if (!config?.pipelines) {
25
- return null;
65
+ function normalizeStageSets(stageSets) {
66
+ if (!stageSets) {
67
+ return {};
26
68
  }
27
- return config.pipelines.find((pipeline) => pipeline.id === id) ?? null;
69
+ if (typeof stageSets !== 'object' || Array.isArray(stageSets)) {
70
+ throw new Error('codex.orchestrator.json stageSets must be an object of stage arrays.');
71
+ }
72
+ const normalized = {};
73
+ for (const [key, value] of Object.entries(stageSets)) {
74
+ if (!Array.isArray(value)) {
75
+ throw new Error(`Stage set "${key}" must be an array.`);
76
+ }
77
+ if (value.some((stage) => isStageSetRef(stage))) {
78
+ throw new Error(`Stage set "${key}" cannot include stage-set references.`);
79
+ }
80
+ normalized[key] = value;
81
+ }
82
+ return normalized;
83
+ }
84
+ function expandPipelineStages(pipeline, stageSets) {
85
+ const expanded = [];
86
+ for (const stage of pipeline.stages ?? []) {
87
+ if (isStageSetRef(stage)) {
88
+ const sharedStages = stageSets[stage.ref];
89
+ if (!sharedStages) {
90
+ throw new Error(`Pipeline "${pipeline.id}" references unknown stage set "${stage.ref}".`);
91
+ }
92
+ expanded.push(...sharedStages);
93
+ }
94
+ else {
95
+ expanded.push(stage);
96
+ }
97
+ }
98
+ return { ...pipeline, stages: expanded };
99
+ }
100
+ function isStageSetRef(stage) {
101
+ return stage.kind === 'stage-set';
28
102
  }