@kbediako/codex-orchestrator 0.2.0 → 0.2.1

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 (41) hide show
  1. package/README.md +43 -83
  2. package/dist/bin/codex-orchestrator.js +2 -0
  3. package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +50 -0
  4. package/dist/orchestrator/src/cli/adapters/cloudFailureDiagnostics.js +117 -5
  5. package/dist/orchestrator/src/cli/coStatusAttachCliShell.js +2 -2
  6. package/dist/orchestrator/src/cli/coStatusCliShell.js +28 -6
  7. package/dist/orchestrator/src/cli/codexCliShell.js +48 -1
  8. package/dist/orchestrator/src/cli/codexDefaultsSetup.js +217 -26
  9. package/dist/orchestrator/src/cli/control/controlHostSupervision.js +28 -6
  10. package/dist/orchestrator/src/cli/control/controlRuntime.js +17 -6
  11. package/dist/orchestrator/src/cli/control/controlStatusDashboard.js +6 -1
  12. package/dist/orchestrator/src/cli/control/selectedRunProjection.js +49 -2
  13. package/dist/orchestrator/src/cli/doctor.js +142 -48
  14. package/dist/orchestrator/src/cli/init.js +94 -1
  15. package/dist/orchestrator/src/cli/providerLinearChildLaneRunner.js +64 -1
  16. package/dist/orchestrator/src/cli/providerLinearWorkerRunner.js +1165 -69
  17. package/dist/orchestrator/src/cli/rlm/alignment.js +3 -3
  18. package/dist/orchestrator/src/cli/services/commandRunner.js +31 -0
  19. package/dist/orchestrator/src/cli/utils/cloudPreflight.js +202 -6
  20. package/dist/orchestrator/src/cli/utils/codexFeatures.js +60 -0
  21. package/dist/orchestrator/src/manager.js +74 -4
  22. package/dist/scripts/lib/docs-catalog.js +35 -1
  23. package/docs/README.md +333 -0
  24. package/docs/book/README.md +19 -0
  25. package/docs/book/codex-cli-0124-adoption.md +68 -0
  26. package/docs/book/local-hook-impact.md +73 -0
  27. package/docs/book/operations.md +60 -0
  28. package/docs/book/public-posture.md +34 -0
  29. package/docs/book/setup.md +91 -0
  30. package/docs/book/skills.md +11 -0
  31. package/docs/guides/codex-version-policy.md +104 -0
  32. package/docs/public/downstream-setup.md +25 -18
  33. package/package.json +4 -1
  34. package/plugins/codex-orchestrator/.codex-plugin/plugin.json +1 -1
  35. package/plugins/codex-orchestrator/launcher.mjs +6 -4
  36. package/schemas/manifest.json +17 -0
  37. package/skills/README.md +26 -0
  38. package/skills/collab-subagents-first/SKILL.md +1 -1
  39. package/skills/delegation-usage/DELEGATION_GUIDE.md +12 -7
  40. package/skills/delegation-usage/SKILL.md +13 -8
  41. package/templates/codex/AGENTS.md +12 -10
@@ -6,6 +6,7 @@ import { release as osRelease } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { buildDevtoolsSetupPlan, DEVTOOLS_SKILL_NAME, resolveDevtoolsReadiness } from './utils/devtools.js';
8
8
  import { isManagedCodexCliEnabled, resolveCodexCliBin, resolveCodexCliReadiness } from './utils/codexCli.js';
9
+ import { codexFeatureProbeDisablesMultiAgentV2, codexFeatureProbeRejectsAgentMaxThreads, readCodexFeatureProbe } from './utils/codexFeatures.js';
9
10
  import { resolveCodexHome } from './utils/codexPaths.js';
10
11
  import { resolveOptionalDependency } from './utils/optionalDeps.js';
11
12
  import { buildCloudPreflightAuthProvenance, buildCloudPreflightRequest, runCloudPreflight } from './utils/cloudPreflight.js';
@@ -13,7 +14,7 @@ import { classifyDelegationTransport, formatDelegateServerProcessSummary, inspec
13
14
  import { sanitizeProviderOverrideEnv } from './utils/providerOverrideEnv.js';
14
15
  import { hasLinearApiCredentials, hasLinearSourceBinding, resolveLinearSourceSetup } from './control/linearDispatchSource.js';
15
16
  import { normalizeDispatchSourceProvider } from './control/trackerDispatchPilot.js';
16
- import { BASELINE_AGENTS, BASELINE_MODEL, BASELINE_REVIEW_MODEL, BASELINE_REASONING_MINIMUM } from './codexDefaultsSetup.js';
17
+ import { BASELINE_AGENTS, BASELINE_MODEL, BASELINE_REVIEW_MODEL, BASELINE_REASONING_MINIMUM, formatModelDefaultExpectation, isLocalModelOptIn } from './codexDefaultsSetup.js';
17
18
  import { CommandPlanner } from './adapters/CommandPlanner.js';
18
19
  import { PipelineResolver } from './services/pipelineResolver.js';
19
20
  import { isRepoConfigRequired } from './config/repoConfigPolicy.js';
@@ -29,6 +30,7 @@ const OPTIONAL_DEPENDENCIES = [
29
30
  { name: 'cheerio', install: 'npm install --save-dev cheerio' }
30
31
  ];
31
32
  const PROVIDER_ROOT_RELATIVE_PATH = '.codex/providers';
33
+ const CODEX_DEBUG_MODELS_JSON_ENV = 'CODEX_ORCHESTRATOR_DEBUG_MODELS_JSON';
32
34
  export function runDoctor(cwd = process.cwd()) {
33
35
  const dependencies = OPTIONAL_DEPENDENCIES.map((entry) => {
34
36
  const resolved = resolveOptionalDependency(entry.name, cwd);
@@ -85,11 +87,12 @@ export function runDoctor(cwd = process.cwd()) {
85
87
  if (readiness.config.status !== 'ok') {
86
88
  missing.push(`${DEVTOOLS_SKILL_NAME}-config`);
87
89
  }
88
- const codexDefaults = inspectCodexDefaultsAdvisory(process.env);
89
90
  const codexBin = resolveCodexCliBin(process.env);
90
91
  const managedOptIn = isManagedCodexCliEnabled(process.env);
91
92
  const managedCodex = resolveCodexCliReadiness(process.env);
92
- const features = readCodexFeatureFlags(codexBin);
93
+ const featureProbe = readCodexFeatureProbe(codexBin, process.env);
94
+ const features = featureProbe.flags;
95
+ const codexDefaults = inspectCodexDefaultsAdvisory(process.env, codexBin, featureProbe);
93
96
  const collabFeatureKey = features === null
94
97
  ? null
95
98
  : Object.prototype.hasOwnProperty.call(features, 'multi_agent')
@@ -473,7 +476,15 @@ export function formatDoctorSummary(result) {
473
476
  lines.push(` - model: ${result.codex_defaults.checks.model.status} (actual: ${result.codex_defaults.checks.model.actual ?? '<unset>'}, expected: ${result.codex_defaults.checks.model.expected})`);
474
477
  lines.push(` - review_model: ${result.codex_defaults.checks.review_model.status} (actual: ${result.codex_defaults.checks.review_model.actual ?? '<unset>'}, expected: ${result.codex_defaults.checks.review_model.expected})`);
475
478
  lines.push(` - model_reasoning_effort: ${result.codex_defaults.checks.model_reasoning_effort.status} (actual: ${result.codex_defaults.checks.model_reasoning_effort.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.model_reasoning_effort.expected_minimum})`);
476
- lines.push(` - agents.max_threads: ${result.codex_defaults.checks.max_threads.status} (actual: ${result.codex_defaults.checks.max_threads.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_threads.expected_minimum})`);
479
+ if (result.codex_defaults.checks.max_threads.status === 'skipped') {
480
+ lines.push(` - agents.max_threads: skipped (actual: ${result.codex_defaults.checks.max_threads.actual ?? '<unset>'}; ${result.codex_defaults.checks.max_threads.detail ?? 'omitted by policy'})`);
481
+ }
482
+ else if (result.codex_defaults.checks.max_threads.detail) {
483
+ lines.push(` - agents.max_threads: ${result.codex_defaults.checks.max_threads.status} (actual: ${result.codex_defaults.checks.max_threads.actual ?? '<unset>'}; ${result.codex_defaults.checks.max_threads.detail})`);
484
+ }
485
+ else {
486
+ lines.push(` - agents.max_threads: ${result.codex_defaults.checks.max_threads.status} (actual: ${result.codex_defaults.checks.max_threads.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_threads.expected_minimum})`);
487
+ }
477
488
  lines.push(` - agents.max_depth: ${result.codex_defaults.checks.max_depth.status} (actual: ${result.codex_defaults.checks.max_depth.actual ?? '<unset>'}, expected >= ${result.codex_defaults.checks.max_depth.expected_minimum} when set; <unset> accepted)`);
478
489
  if (result.codex_defaults.legacy_max_spawn_depth?.present) {
479
490
  lines.push(` - legacy agents.max_spawn_depth: ${result.codex_defaults.legacy_max_spawn_depth.status} (actual: ${result.codex_defaults.legacy_max_spawn_depth.actual ?? '<unset>'}; ${result.codex_defaults.legacy_max_spawn_depth.detail})`);
@@ -661,11 +672,15 @@ function inspectProviderReadiness(repoRoot, env = process.env) {
661
672
  ]
662
673
  };
663
674
  }
664
- function inspectCodexDefaultsAdvisory(env = process.env) {
675
+ function inspectCodexDefaultsAdvisory(env = process.env, codexBin = resolveCodexCliBin(env), featureProbe = readCodexFeatureProbe(codexBin, env)) {
665
676
  const configPath = join(resolveCodexHome(env), 'config.toml');
666
677
  const checks = {
667
- model: { status: 'advisory', expected: BASELINE_MODEL, actual: null },
668
- review_model: { status: 'advisory', expected: BASELINE_REVIEW_MODEL, actual: null },
678
+ model: { status: 'advisory', expected: formatModelDefaultExpectation(BASELINE_MODEL), actual: null },
679
+ review_model: {
680
+ status: 'advisory',
681
+ expected: formatModelDefaultExpectation(BASELINE_REVIEW_MODEL),
682
+ actual: null
683
+ },
669
684
  model_reasoning_effort: { status: 'advisory', expected_minimum: BASELINE_REASONING_MINIMUM, actual: null },
670
685
  max_threads: { status: 'advisory', expected_minimum: BASELINE_AGENTS.max_threads, actual: null },
671
686
  max_depth: { status: 'advisory', expected_minimum: BASELINE_AGENTS.max_depth, actual: null }
@@ -673,10 +688,21 @@ function inspectCodexDefaultsAdvisory(env = process.env) {
673
688
  let legacyMaxSpawnDepth = null;
674
689
  const guidance = [
675
690
  'Run `codex-orchestrator codex defaults --yes` to apply additive baseline defaults.',
676
- 'Additive policy: unrelated config keys are preserved; existing role files stay untouched unless `--force` is set.',
691
+ 'Additive policy: unrelated config keys are preserved; existing role files stay untouched unless `--force` is set or they exactly match a prior CO-managed model baseline.',
692
+ 'When `features.multi_agent_v2=true`, omit `agents.max_threads`; Codex CLI 0.125+ rejects that key.',
677
693
  'Current CO baseline no longer seeds or expects `agents.max_spawn_depth`; keep it only as a legacy local override when an older parser/runtime still honors it.',
678
694
  'Leaving `agents.max_depth` unset remains accepted when local parser/runtime constraints require it.'
679
695
  ];
696
+ const featureProbeDisablesMultiAgentV2 = featureProbe ? codexFeatureProbeDisablesMultiAgentV2(featureProbe) : false;
697
+ const featureProbeIndicatesMultiAgentV2 = !featureProbeDisablesMultiAgentV2
698
+ && (featureProbe?.flags?.multi_agent_v2 === true
699
+ || (featureProbe ? codexFeatureProbeRejectsAgentMaxThreads(featureProbe) : false));
700
+ if (featureProbeIndicatesMultiAgentV2) {
701
+ checks.max_threads.status = 'skipped';
702
+ checks.max_threads.actual = null;
703
+ checks.max_threads.detail =
704
+ 'features.multi_agent_v2=true; omit agents.max_threads because Codex CLI 0.125+ rejects it';
705
+ }
680
706
  if (!existsSync(configPath)) {
681
707
  return {
682
708
  status: 'advisory',
@@ -710,23 +736,55 @@ function inspectCodexDefaultsAdvisory(env = process.env) {
710
736
  };
711
737
  }
712
738
  const model = normalizeOptionalString(readStringValue(parsed.model));
713
- checks.model.actual = model;
714
- checks.model.status = model === BASELINE_MODEL ? 'ok' : 'advisory';
715
739
  const reviewModel = normalizeOptionalString(readStringValue(parsed.review_model));
740
+ const localModelCandidates = new Set([model, reviewModel].filter(isLocalModelOptIn));
741
+ const modelAccess = localModelCandidates.size === 0
742
+ ? {
743
+ status: 'unavailable',
744
+ models: new Set(),
745
+ detail: 'model access was not checked because no local ChatGPT-auth model is configured'
746
+ }
747
+ : inspectCodexModelAccess(codexBin, env);
748
+ const verifiedLocalModels = new Set([...localModelCandidates].filter((candidate) => modelAccess.status === 'ok' && modelAccess.models.has(candidate)));
749
+ const unverifiedLocalModels = [...localModelCandidates].filter((candidate) => !verifiedLocalModels.has(candidate));
750
+ for (const candidate of unverifiedLocalModels) {
751
+ guidance.push(`Configured local ChatGPT-auth model ${candidate} is not verified by \`codex debug models\`; rerun local access smoke or use the portable ${BASELINE_MODEL} fallback for this surface.`);
752
+ }
753
+ checks.model.actual = model;
754
+ checks.model.status =
755
+ model === BASELINE_MODEL || (isLocalModelOptIn(model) && verifiedLocalModels.has(model))
756
+ ? 'ok'
757
+ : 'advisory';
716
758
  checks.review_model.actual = reviewModel;
717
- checks.review_model.status = reviewModel === BASELINE_REVIEW_MODEL ? 'ok' : 'advisory';
759
+ checks.review_model.status =
760
+ reviewModel === BASELINE_REVIEW_MODEL || (isLocalModelOptIn(reviewModel) && verifiedLocalModels.has(reviewModel))
761
+ ? 'ok'
762
+ : 'advisory';
718
763
  const reasoning = normalizeOptionalString(readStringValue(parsed.model_reasoning_effort));
719
764
  checks.model_reasoning_effort.actual = reasoning;
720
765
  checks.model_reasoning_effort.status = isReasoningAtLeastMinimum(reasoning, BASELINE_REASONING_MINIMUM)
721
766
  ? 'ok'
722
767
  : 'advisory';
723
768
  const agents = isRecord(parsed.agents) ? parsed.agents : {};
769
+ const multiAgentV2Enabled = featureProbeIndicatesMultiAgentV2
770
+ || (!featureProbeDisablesMultiAgentV2
771
+ && readBooleanValue(readRecordValue(parsed, 'features')?.multi_agent_v2) === true);
724
772
  const maxThreads = readNumberValue(agents.max_threads);
725
773
  const maxDepth = readNumberValue(agents.max_depth);
726
774
  const maxSpawnDepth = readNumberValue(agents.max_spawn_depth);
775
+ const hasMaxThreads = Object.prototype.hasOwnProperty.call(agents, 'max_threads');
727
776
  checks.max_threads.actual = maxThreads;
728
- checks.max_threads.status =
729
- typeof maxThreads === 'number' && maxThreads >= BASELINE_AGENTS.max_threads ? 'ok' : 'advisory';
777
+ if (multiAgentV2Enabled) {
778
+ checks.max_threads.status = hasMaxThreads ? 'advisory' : 'skipped';
779
+ checks.max_threads.detail =
780
+ hasMaxThreads
781
+ ? 'features.multi_agent_v2=true; remove agents.max_threads because Codex CLI 0.125+ rejects it'
782
+ : 'features.multi_agent_v2=true; omit agents.max_threads because Codex CLI 0.125+ rejects it';
783
+ }
784
+ else {
785
+ checks.max_threads.status =
786
+ typeof maxThreads === 'number' && maxThreads >= BASELINE_AGENTS.max_threads ? 'ok' : 'advisory';
787
+ }
730
788
  checks.max_depth.actual = maxDepth;
731
789
  checks.max_depth.status =
732
790
  maxDepth === null || (typeof maxDepth === 'number' && maxDepth >= BASELINE_AGENTS.max_depth) ? 'ok' : 'advisory';
@@ -741,7 +799,8 @@ function inspectCodexDefaultsAdvisory(env = process.env) {
741
799
  : `older parser/runtime may still treat this as a hard cap below the CO baseline depth; raise it to >= ${BASELINE_AGENTS.max_depth} or remove it`
742
800
  };
743
801
  }
744
- const allChecksOk = Object.values(checks).every((check) => check.status === 'ok')
802
+ const allChecksOk = Object.values(checks).every((check) => check.status === 'ok' || check.status === 'skipped')
803
+ && unverifiedLocalModels.length === 0
745
804
  && legacyMaxSpawnDepth?.status !== 'advisory';
746
805
  return {
747
806
  status: allChecksOk ? 'ok' : 'advisory',
@@ -751,6 +810,69 @@ function inspectCodexDefaultsAdvisory(env = process.env) {
751
810
  guidance
752
811
  };
753
812
  }
813
+ function inspectCodexModelAccess(codexBin, env) {
814
+ const overrideJson = env[CODEX_DEBUG_MODELS_JSON_ENV];
815
+ if (typeof overrideJson === 'string' && overrideJson.trim().length > 0) {
816
+ return parseCodexDebugModels(overrideJson, `${CODEX_DEBUG_MODELS_JSON_ENV} override`);
817
+ }
818
+ const result = spawnSync(codexBin, ['debug', 'models'], {
819
+ encoding: 'utf8',
820
+ env,
821
+ timeout: 5000,
822
+ maxBuffer: 5 * 1024 * 1024
823
+ });
824
+ if (result.error) {
825
+ return {
826
+ status: 'unavailable',
827
+ models: new Set(),
828
+ detail: result.error.message
829
+ };
830
+ }
831
+ if (result.status !== 0) {
832
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
833
+ return {
834
+ status: 'unavailable',
835
+ models: new Set(),
836
+ detail: stderr || `codex debug models exited ${result.status ?? 'without a status'}`
837
+ };
838
+ }
839
+ return parseCodexDebugModels(result.stdout, '`codex debug models`');
840
+ }
841
+ function parseCodexDebugModels(source, detailPrefix) {
842
+ if (typeof source !== 'string' || source.trim().length === 0) {
843
+ return {
844
+ status: 'unavailable',
845
+ models: new Set(),
846
+ detail: `${detailPrefix} produced no model catalog output`
847
+ };
848
+ }
849
+ try {
850
+ const parsed = JSON.parse(source);
851
+ const models = new Set();
852
+ const entries = isRecord(parsed) && Array.isArray(parsed.models) ? parsed.models : [];
853
+ for (const entry of entries) {
854
+ if (!isRecord(entry)) {
855
+ continue;
856
+ }
857
+ const slug = readStringValue(entry.slug);
858
+ if (slug) {
859
+ models.add(slug);
860
+ }
861
+ }
862
+ return {
863
+ status: models.size > 0 ? 'ok' : 'unavailable',
864
+ models,
865
+ detail: `${detailPrefix} reported ${models.size} model(s)`
866
+ };
867
+ }
868
+ catch (error) {
869
+ return {
870
+ status: 'unavailable',
871
+ models: new Set(),
872
+ detail: error instanceof Error ? error.message : String(error)
873
+ };
874
+ }
875
+ }
754
876
  function getTomlParser() {
755
877
  if (tomlParser) {
756
878
  return tomlParser;
@@ -1077,6 +1199,12 @@ function buildCloudPreflightGuidance(issues) {
1077
1199
  case 'missing_environment':
1078
1200
  guidance.push('Set CODEX_CLOUD_ENV_ID or provide target metadata.cloudEnvId.');
1079
1201
  break;
1202
+ case 'environment_not_found':
1203
+ guidance.push('Set CODEX_CLOUD_ENV_ID to a Codex Cloud environment visible to the active account; run `codex cloud` to list available environments.');
1204
+ break;
1205
+ case 'environment_unavailable':
1206
+ guidance.push('Verify the active Codex account/profile can read CODEX_CLOUD_ENV_ID before running required cloud canaries.');
1207
+ break;
1080
1208
  case 'branch_missing':
1081
1209
  guidance.push('Push the branch to origin or set CODEX_CLOUD_BRANCH to an existing remote branch.');
1082
1210
  break;
@@ -1095,40 +1223,6 @@ function buildCloudPreflightGuidance(issues) {
1095
1223
  }
1096
1224
  return [...new Set(guidance)];
1097
1225
  }
1098
- function readCodexFeatureFlags(codexBin) {
1099
- const result = spawnSync(codexBin, ['features', 'list'], {
1100
- encoding: 'utf8',
1101
- stdio: ['ignore', 'pipe', 'pipe'],
1102
- timeout: 5000
1103
- });
1104
- if (result.error || result.status !== 0) {
1105
- return null;
1106
- }
1107
- const stdout = String(result.stdout ?? '');
1108
- const flags = {};
1109
- for (const line of stdout.split(/\r?\n/u)) {
1110
- const trimmed = line.trim();
1111
- if (!trimmed) {
1112
- continue;
1113
- }
1114
- const tokens = trimmed.split(/\s+/u);
1115
- if (tokens.length < 2) {
1116
- continue;
1117
- }
1118
- const name = tokens[0] ?? '';
1119
- const enabledToken = tokens[tokens.length - 1] ?? '';
1120
- if (!name) {
1121
- continue;
1122
- }
1123
- if (enabledToken === 'true') {
1124
- flags[name] = true;
1125
- }
1126
- else if (enabledToken === 'false') {
1127
- flags[name] = false;
1128
- }
1129
- }
1130
- return flags;
1131
- }
1132
1226
  function canRunCommand(command, args) {
1133
1227
  const result = spawnSync(command, args, {
1134
1228
  encoding: 'utf8',
@@ -1,12 +1,21 @@
1
- import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
1
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
3
4
  import { dirname, join, relative } from 'node:path';
5
+ import process from 'node:process';
6
+ import { resolveCodexHome } from './utils/codexPaths.js';
7
+ import { resolveCodexCliBin } from './utils/codexCli.js';
8
+ import { codexFeatureProbeDisablesMultiAgentV2, codexFeatureProbeRejectsAgentMaxThreads, readCodexFeatureProbe } from './utils/codexFeatures.js';
4
9
  import { findPackageRoot } from './utils/packageInfo.js';
5
10
  const CODEX_TEMPLATE = 'codex';
6
11
  const CODEX_PIPELINE_CONFIG = 'codex.orchestrator.json';
12
+ const CODEX_CONFIG_TEMPLATE = join('.codex', 'config.toml');
13
+ const require = createRequire(import.meta.url);
14
+ let tomlLibrary;
7
15
  export async function initCodexTemplates(options) {
8
16
  const root = findPackageRoot();
9
17
  const templateRoot = join(root, 'templates', options.template);
18
+ const env = options.env ?? process.env;
10
19
  const written = [];
11
20
  const skipped = [];
12
21
  await assertDirectory(templateRoot);
@@ -16,6 +25,10 @@ export async function initCodexTemplates(options) {
16
25
  skipped
17
26
  });
18
27
  if (options.template === CODEX_TEMPLATE) {
28
+ const configPath = join(options.cwd, CODEX_CONFIG_TEMPLATE);
29
+ if (written.includes(configPath) && await isMultiAgentV2Enabled(env)) {
30
+ await omitAgentMaxThreads(configPath);
31
+ }
19
32
  await copyTemplateFile(join(root, CODEX_PIPELINE_CONFIG), join(options.cwd, CODEX_PIPELINE_CONFIG), {
20
33
  force: options.force,
21
34
  written,
@@ -24,6 +37,86 @@ export async function initCodexTemplates(options) {
24
37
  }
25
38
  return { written, skipped, templateRoot };
26
39
  }
40
+ async function isMultiAgentV2Enabled(env) {
41
+ const featureProbe = readCodexFeatureProbe(resolveCodexCliBin(env), env);
42
+ if (featureProbe.flags?.multi_agent_v2 === true) {
43
+ return true;
44
+ }
45
+ if (codexFeatureProbeDisablesMultiAgentV2(featureProbe)) {
46
+ return false;
47
+ }
48
+ if (codexFeatureProbeRejectsAgentMaxThreads(featureProbe)) {
49
+ return true;
50
+ }
51
+ const configPath = join(resolveCodexHome(env), 'config.toml');
52
+ if (!existsSync(configPath)) {
53
+ return false;
54
+ }
55
+ let raw;
56
+ try {
57
+ raw = await readFile(configPath, 'utf8');
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ let parsed;
63
+ try {
64
+ parsed = getTomlLibrary().parse(raw);
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ if (!isRecord(parsed) || !isRecord(parsed.features)) {
70
+ return false;
71
+ }
72
+ return parsed.features.multi_agent_v2 === true;
73
+ }
74
+ async function omitAgentMaxThreads(configPath) {
75
+ const raw = await readFile(configPath, 'utf8');
76
+ const { removed, text } = removeAgentMaxThreadsFromToml(raw);
77
+ if (!removed) {
78
+ return;
79
+ }
80
+ await writeFile(configPath, text, 'utf8');
81
+ }
82
+ function removeAgentMaxThreadsFromToml(raw) {
83
+ const lines = raw.split(/(?<=\n)/u);
84
+ let inAgentsTable = false;
85
+ let removed = false;
86
+ const kept = [];
87
+ for (const line of lines) {
88
+ const withoutEol = line.replace(/\r?\n$/u, '');
89
+ const trimmed = withoutEol.trim();
90
+ if (/^\[[^\]]+\]\s*(?:#.*)?$/u.test(trimmed)) {
91
+ inAgentsTable = /^\[agents\]\s*(?:#.*)?$/u.test(trimmed);
92
+ }
93
+ if (inAgentsTable && /^[ \t]*max_threads\s*=/u.test(withoutEol)) {
94
+ removed = true;
95
+ continue;
96
+ }
97
+ kept.push(line);
98
+ }
99
+ return { removed, text: kept.join('') };
100
+ }
101
+ function getTomlLibrary() {
102
+ if (tomlLibrary) {
103
+ return tomlLibrary;
104
+ }
105
+ if (tomlLibrary === null) {
106
+ throw new Error('Failed to load @iarna/toml.');
107
+ }
108
+ try {
109
+ tomlLibrary = require('@iarna/toml');
110
+ return tomlLibrary;
111
+ }
112
+ catch (error) {
113
+ tomlLibrary = null;
114
+ throw error;
115
+ }
116
+ }
117
+ function isRecord(value) {
118
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
119
+ }
27
120
  async function assertDirectory(path) {
28
121
  const info = await stat(path).catch(() => null);
29
122
  if (!info || !info.isDirectory()) {
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { logger } from '../logger.js';
10
10
  import { acquireLockWithRetry } from '../persistence/lockFile.js';
11
11
  import { writeAtomicFile } from '../utils/atomicWrite.js';
12
- import { buildEmptyProviderLinearWorkerTokenUsage, defaultExecRunner, parseProviderLinearWorkerJsonl } from './providerLinearWorkerRunner.js';
12
+ import { buildEmptyProviderLinearWorkerTokenUsage, defaultExecRunner, parseProviderLinearWorkerJsonl, PROVIDER_LINEAR_CHILD_LANE_DIAGNOSTICS_FILENAME } from './providerLinearWorkerRunner.js';
13
13
  import { createRuntimeCodexCommandContext, formatRuntimeSelectionSummary, parseRuntimeMode, resolveRuntimeCodexCommand } from './runtime/index.js';
14
14
  import { buildRunMemoryPromptLines, selectRunMemoryForRole } from './run/runMemoryController.js';
15
15
  import { resolveProviderLinearChildLaneScopeContract } from './providerLinearChildLanePhaseContract.js';
@@ -22,6 +22,7 @@ const PROVIDER_LINEAR_CHILD_LANE_SCOPE_DRIFT_POLL_INTERVAL_MS = 250;
22
22
  const PROVIDER_LINEAR_CHILD_LANE_SESSION_LOG_DISCOVERY_WINDOW_MS = 10 * 60 * 1000;
23
23
  const PROVIDER_LINEAR_CHILD_LANE_SESSION_LOG_HEADER_BYTES = 256 * 1024;
24
24
  const PROVIDER_LINEAR_CHILD_LANE_SESSION_LOG_MTIME_SKEW_MS = 1000;
25
+ const PROVIDER_LINEAR_CHILD_LANE_RUNNER_ENTRYPOINT = 'providerLinearChildLaneRunner';
25
26
  let tomlParser;
26
27
  const PROVIDER_LINEAR_CHILD_LANE_TRUSTED_PROJECT_CONFIG_LOCK_RETRY = {
27
28
  maxAttempts: 50,
@@ -1501,6 +1502,32 @@ function resolveProviderLinearChildLaneUnauthorizedCommitMessage(input) {
1501
1502
  async function writeChildLaneProof(runDir, proof) {
1502
1503
  await writeFile(join(runDir, PROVIDER_LINEAR_CHILD_LANE_PROOF_FILENAME), `${JSON.stringify(proof, null, 2)}\n`, 'utf8');
1503
1504
  }
1505
+ async function writeProviderLinearChildLaneDiagnostics(context, patch) {
1506
+ const diagnosticsPath = join(context.runDir, PROVIDER_LINEAR_CHILD_LANE_DIAGNOSTICS_FILENAME);
1507
+ try {
1508
+ let existing = {};
1509
+ try {
1510
+ const parsed = JSON.parse(await readFile(diagnosticsPath, 'utf8'));
1511
+ existing = isRecord(parsed) ? parsed : {};
1512
+ }
1513
+ catch {
1514
+ existing = {};
1515
+ }
1516
+ await writeAtomicFile(diagnosticsPath, `${JSON.stringify({
1517
+ ...existing,
1518
+ issue_id: context.issueId,
1519
+ issue_identifier: context.issueIdentifier,
1520
+ task_id: context.taskId,
1521
+ run_id: context.runId,
1522
+ parent_run_id: context.parentRunId,
1523
+ stream: context.stream,
1524
+ ...patch
1525
+ }, null, 2)}\n`, { ensureDir: true, encoding: 'utf8' });
1526
+ }
1527
+ catch (error) {
1528
+ logger.warn(`[provider-linear-child-lane-diagnostics] failed to persist ${basename(diagnosticsPath)} at ${diagnosticsPath}: ${error instanceof Error ? error.message : String(error)}`);
1529
+ }
1530
+ }
1504
1531
  function buildFailedChildLaneProof(input) {
1505
1532
  return {
1506
1533
  issue_id: input.context.issueId,
@@ -1543,12 +1570,35 @@ export async function runProviderLinearChildLane(env = process.env, dependencyOv
1543
1570
  ...dependencyOverrides
1544
1571
  };
1545
1572
  const context = await loadProviderLinearChildLaneContext(env);
1573
+ const runnerStartedAt = deps.now();
1574
+ await writeProviderLinearChildLaneDiagnostics(context, {
1575
+ provider_linear_child_lane_runner_entrypoint: PROVIDER_LINEAR_CHILD_LANE_RUNNER_ENTRYPOINT,
1576
+ provider_linear_child_lane_runner_pid: process.pid,
1577
+ provider_linear_child_lane_runner_started_at: runnerStartedAt,
1578
+ provider_linear_child_lane_runtime_event: 'runner_started',
1579
+ provider_linear_child_lane_runtime_event_at: runnerStartedAt
1580
+ });
1546
1581
  const { laneWorkspacePath, laneBranch } = await prepareLaneWorkspace(context);
1547
1582
  const startingHeadSha = await readProviderLinearChildLaneHeadSha(laneWorkspacePath).catch(() => null);
1548
1583
  const startingReflogEntryCount = await countProviderLinearChildLaneReflogEntries(laneWorkspacePath).catch(() => 0);
1549
1584
  try {
1550
1585
  const runtimeContext = await resolveChildLaneRuntimeContext(env, laneWorkspacePath, context.runId);
1551
1586
  logger.info(`[provider-linear-child-lane-runtime] ${formatRuntimeSelectionSummary(runtimeContext.runtime)}`);
1587
+ await writeProviderLinearChildLaneDiagnostics(context, {
1588
+ provider_linear_child_lane_runtime_requested_mode: runtimeContext.runtime.requested_mode,
1589
+ provider_linear_child_lane_runtime_selected_mode: runtimeContext.runtime.selected_mode,
1590
+ provider_linear_child_lane_runtime_source: runtimeContext.runtime.source,
1591
+ provider_linear_child_lane_runtime_provider: runtimeContext.runtime.provider,
1592
+ provider_linear_child_lane_runtime_session_id: runtimeContext.runtime.runtime_session_id,
1593
+ provider_linear_child_lane_runtime_fallback_occurred: runtimeContext.runtime.fallback.occurred,
1594
+ provider_linear_child_lane_runtime_fallback_code: runtimeContext.runtime.fallback.code,
1595
+ provider_linear_child_lane_runtime_fallback_reason: runtimeContext.runtime.fallback.reason,
1596
+ provider_linear_child_lane_runtime_fallback_from_mode: runtimeContext.runtime.fallback.from_mode,
1597
+ provider_linear_child_lane_runtime_fallback_to_mode: runtimeContext.runtime.fallback.to_mode,
1598
+ provider_linear_child_lane_runtime_fallback_checked_at: runtimeContext.runtime.fallback.checked_at,
1599
+ provider_linear_child_lane_runtime_event: 'runtime_selected',
1600
+ provider_linear_child_lane_runtime_event_at: deps.now()
1601
+ });
1552
1602
  const childEnv = { ...process.env, ...env, ...runtimeContext.env };
1553
1603
  childEnv.CODEX_NON_INTERACTIVE = '1';
1554
1604
  childEnv.CODEX_NO_INTERACTIVE = '1';
@@ -1594,6 +1644,14 @@ export async function runProviderLinearChildLane(env = process.env, dependencyOv
1594
1644
  else {
1595
1645
  if (startupRace.sessionLogPath) {
1596
1646
  logger.info(`[provider-linear-child-lane-runtime] appserver startup observed via session log ${basename(startupRace.sessionLogPath)}`);
1647
+ const startupObservedAt = deps.now();
1648
+ await writeProviderLinearChildLaneDiagnostics(context, {
1649
+ provider_linear_child_lane_runtime_event: 'appserver_startup_observed',
1650
+ provider_linear_child_lane_runtime_event_at: startupObservedAt,
1651
+ provider_linear_child_lane_appserver_startup_observed: true,
1652
+ provider_linear_child_lane_appserver_startup_observed_at: startupObservedAt,
1653
+ provider_linear_child_lane_appserver_session_log: basename(startupRace.sessionLogPath)
1654
+ });
1597
1655
  }
1598
1656
  const execOrDriftRace = startupRace.sessionLogPath
1599
1657
  ? await Promise.race([
@@ -1659,6 +1717,11 @@ export async function runProviderLinearChildLane(env = process.env, dependencyOv
1659
1717
  if (!execResult) {
1660
1718
  throw new Error('provider-linear-child-lane completed without an exec result');
1661
1719
  }
1720
+ await writeProviderLinearChildLaneDiagnostics(context, {
1721
+ provider_linear_child_lane_runtime_event: 'codex_exec_completed',
1722
+ provider_linear_child_lane_runtime_event_at: deps.now(),
1723
+ provider_linear_child_lane_exec_exit_code: execResult.exitCode
1724
+ });
1662
1725
  const parsed = parseProviderLinearWorkerJsonl(execResult.stdout);
1663
1726
  const session = deriveLatestTurnSessionId({
1664
1727
  threadId: parsed.threadId,