oh-my-codex 0.20.0 → 0.20.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 (56) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/codex-plugin-layout.test.js +27 -0
  4. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  5. package/dist/cli/__tests__/doctor-context-window-warning.test.js +83 -78
  6. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  7. package/dist/cli/__tests__/doctor-invalid-config.test.js +9 -0
  8. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  9. package/dist/cli/__tests__/doctor-warning-copy.test.js +87 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  11. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +54 -2
  12. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  13. package/dist/cli/__tests__/setup-install-mode.test.js +3 -3
  14. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  15. package/dist/cli/__tests__/setup-refresh.test.js +137 -5
  16. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  17. package/dist/cli/__tests__/setup-scope.test.js +27 -3
  18. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  19. package/dist/cli/__tests__/uninstall.test.js +297 -3
  20. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  21. package/dist/cli/doctor.d.ts +2 -0
  22. package/dist/cli/doctor.d.ts.map +1 -1
  23. package/dist/cli/doctor.js +42 -31
  24. package/dist/cli/doctor.js.map +1 -1
  25. package/dist/cli/setup.js +2 -2
  26. package/dist/cli/setup.js.map +1 -1
  27. package/dist/cli/uninstall.d.ts.map +1 -1
  28. package/dist/cli/uninstall.js +3 -4
  29. package/dist/cli/uninstall.js.map +1 -1
  30. package/dist/config/__tests__/generator-idempotent.test.js +200 -104
  31. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  32. package/dist/config/__tests__/generator-notify.test.js +22 -48
  33. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  34. package/dist/config/generator.d.ts +19 -10
  35. package/dist/config/generator.d.ts.map +1 -1
  36. package/dist/config/generator.js +169 -152
  37. package/dist/config/generator.js.map +1 -1
  38. package/dist/leader/__tests__/contract.test.js +43 -1
  39. package/dist/leader/__tests__/contract.test.js.map +1 -1
  40. package/dist/leader/contract.d.ts.map +1 -1
  41. package/dist/leader/contract.js +23 -5
  42. package/dist/leader/contract.js.map +1 -1
  43. package/dist/scripts/__tests__/codex-native-hook.test.js +1104 -20
  44. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  45. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  46. package/dist/scripts/codex-native-hook.js +139 -19
  47. package/dist/scripts/codex-native-hook.js.map +1 -1
  48. package/dist/utils/__tests__/agents-md.test.js +16 -0
  49. package/dist/utils/__tests__/agents-md.test.js.map +1 -1
  50. package/dist/utils/agents-md.d.ts.map +1 -1
  51. package/dist/utils/agents-md.js +4 -3
  52. package/dist/utils/agents-md.js.map +1 -1
  53. package/package.json +1 -1
  54. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  55. package/src/scripts/__tests__/codex-native-hook.test.ts +1281 -53
  56. package/src/scripts/codex-native-hook.ts +142 -16
@@ -13,7 +13,6 @@ import { readFile, writeFile } from "fs/promises";
13
13
  import { existsSync } from "fs";
14
14
  import { join, resolve } from "path";
15
15
  import TOML from "@iarna/toml";
16
- import { AGENT_DEFINITIONS } from "../agents/definitions.js";
17
16
  import { DEFAULT_FRONTIER_MODEL } from "./models.js";
18
17
  import { DEFAULT_CODEX_HOOK_FEATURE_FLAG, CODEX_HOOK_FEATURE_FLAGS, CODEX_PLUGIN_SCOPED_HOOKS_FEATURE_FLAG, formatCodexHookFeatureFlagLine, normalizeCodexHookFeatureFlag, } from "./codex-feature-flags.js";
19
18
  import { OMX_FIRST_PARTY_MCP_SERVER_NAMES, getOmxFirstPartySetupMcpServers, } from "./omx-first-party-mcp.js";
@@ -34,25 +33,65 @@ const OMX_TOP_LEVEL_KEYS = [
34
33
  "developer_instructions",
35
34
  ];
36
35
  export const DEFAULT_SETUP_MODEL = DEFAULT_FRONTIER_MODEL;
37
- export const DEFAULT_SETUP_MODEL_CONTEXT_WINDOW = 250000;
38
- export const DEFAULT_SETUP_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 200000;
39
- export function getModelContextRecommendation(model) {
40
- if (model !== DEFAULT_SETUP_MODEL)
41
- return null;
42
- return {
43
- model,
44
- modelContextWindow: DEFAULT_SETUP_MODEL_CONTEXT_WINDOW,
45
- modelAutoCompactTokenLimit: DEFAULT_SETUP_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
46
- };
47
- }
36
+ const LEGACY_SEEDED_MODEL_CONTEXT_WINDOW = 250000;
37
+ const LEGACY_SEEDED_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 200000;
48
38
  const OMX_SEEDED_BEHAVIORAL_DEFAULTS_START_MARKER = "# oh-my-codex seeded behavioral defaults (uninstall removes unchanged defaults)";
49
39
  const OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER = "# End oh-my-codex seeded behavioral defaults";
50
40
  export const OMX_DEVELOPER_INSTRUCTIONS = "You have oh-my-codex installed. AGENTS.md is the orchestration brain and main control surface. Follow AGENTS.md for skill/keyword routing, $name workflow invocation, and role-specialized subagents; when spawning native subagents, set `agent_type` to an installed role and never omit it for OMX work. Use outcome-first, concise progress updates: state the target result, constraints, validation evidence, and stop condition before adding process detail. Native subagents live in .codex/agents and may handle independent parallel subtasks within one Codex session or team pane. Skills load from .codex/skills, not native-agent TOMLs. Treat installed prompts as narrower execution surfaces under AGENTS.md authority.";
51
41
  export const OMX_PLUGIN_DEVELOPER_INSTRUCTIONS = '<omx version="1">You have oh-my-codex installed through Codex plugin mode. AGENTS.md is the orchestration brain and main control surface. Follow AGENTS.md for skill/keyword routing and $name workflow invocation. When spawning native subagents, set `agent_type` to an installed role and never omit it for OMX work. Registered Codex plugin marketplace surfaces supply OMX workflows and plugin-scoped companion resources when the plugin is installed; native agent roles are installed as setup-owned Codex agent TOML files in plugin mode so agent_type routing works. User-installed skills may still live under ~/.codex/skills. Use outcome-first, concise progress updates: state the target result, constraints, validation evidence, and stop condition before adding process detail.</omx>';
52
42
  const SHARED_MCP_REGISTRY_MARKER = "oh-my-codex (OMX) Shared MCP Registry Sync";
53
43
  const SHARED_MCP_REGISTRY_END_MARKER = "# End oh-my-codex shared MCP registry sync";
54
- const OMX_AGENTS_MAX_THREADS = 6;
55
- const OMX_AGENTS_MAX_DEPTH = 2;
44
+ const LEGACY_MULTI_AGENT_KEYS = [
45
+ "features.multi_agent",
46
+ "agents.max_threads",
47
+ "agents.max_depth",
48
+ ];
49
+ const LEGACY_MULTI_AGENT_VALUES = {
50
+ "features.multi_agent": true,
51
+ "agents.max_threads": 6,
52
+ "agents.max_depth": 2,
53
+ };
54
+ export function analyzeLegacyMultiAgentConfig(configText) {
55
+ let parsed;
56
+ try {
57
+ parsed = TOML.parse(configText);
58
+ }
59
+ catch (error) {
60
+ const reasonCode = /duplicate|redefine|already defined/i.test(String(error))
61
+ ? "toml-duplicate-key"
62
+ : "toml-parse-error";
63
+ return {
64
+ assessments: Object.fromEntries(LEGACY_MULTI_AGENT_KEYS.map((key) => [
65
+ key,
66
+ { key, state: "invalid/duplicate", reasonCode },
67
+ ])),
68
+ };
69
+ }
70
+ return {
71
+ assessments: Object.fromEntries(LEGACY_MULTI_AGENT_KEYS.map((key) => {
72
+ const [table, entry] = key.split(".");
73
+ const value = isRecord(parsed[table]) ? parsed[table][entry] : undefined;
74
+ if (value === undefined) {
75
+ return [key, { key, state: "absent", reasonCode: "key-absent" }];
76
+ }
77
+ if (value === LEGACY_MULTI_AGENT_VALUES[key]) {
78
+ return [
79
+ key,
80
+ {
81
+ key,
82
+ state: "retained-legacy",
83
+ reasonCode: "exact-legacy-value",
84
+ value,
85
+ },
86
+ ];
87
+ }
88
+ return [
89
+ key,
90
+ { key, state: "custom", reasonCode: "custom-value", value },
91
+ ];
92
+ })),
93
+ };
94
+ }
56
95
  const OMX_EXPLORE_ROUTING_DEFAULT = "0";
57
96
  const OMX_EXPLORE_CMD_ENV = "USE_OMX_EXPLORE_CMD";
58
97
  const DEFAULT_LAUNCHER_MCP_STARTUP_TIMEOUT_SEC = 15;
@@ -343,71 +382,117 @@ function getOmxTopLevelLines(pkgRoot, existingConfig = "", modelOverride, notify
343
382
  `developer_instructions = "${escapeTomlString(OMX_DEVELOPER_INSTRUCTIONS)}"`,
344
383
  ];
345
384
  const existingModel = rootValues.get("model");
346
- const existingContextWindow = rootValues.get("model_context_window");
347
- const existingAutoCompact = rootValues.get("model_auto_compact_token_limit");
348
385
  const selectedModel = modelOverride ?? unwrapTomlString(existingModel) ?? DEFAULT_SETUP_MODEL;
349
386
  if (modelOverride || !existingModel) {
350
387
  lines.push(`model = "${selectedModel}"`);
351
388
  }
352
- if (selectedModel === DEFAULT_SETUP_MODEL) {
353
- const seededBehavioralDefaults = [];
354
- if (!existingContextWindow) {
355
- seededBehavioralDefaults.push(`model_context_window = ${DEFAULT_SETUP_MODEL_CONTEXT_WINDOW}`);
356
- }
357
- if (!existingAutoCompact) {
358
- seededBehavioralDefaults.push(`model_auto_compact_token_limit = ${DEFAULT_SETUP_MODEL_AUTO_COMPACT_TOKEN_LIMIT}`);
359
- }
360
- if (seededBehavioralDefaults.length > 0) {
361
- lines.push(OMX_SEEDED_BEHAVIORAL_DEFAULTS_START_MARKER);
362
- lines.push(...seededBehavioralDefaults);
363
- lines.push(OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER);
364
- }
365
- }
366
389
  return lines;
367
390
  }
368
- function isUnchangedOmxSeededBehavioralDefaultsBlock(lines) {
369
- const relevant = lines.filter((line) => {
370
- const trimmed = line.trim();
371
- return trimmed.length > 0 && !trimmed.startsWith("#");
372
- });
373
- if (relevant.length !== 2)
374
- return false;
375
- const parsed = parseRootKeyValues(relevant.join("\n"));
376
- return (parsed.size === 2 &&
377
- parsed.get("model_context_window") ===
378
- String(DEFAULT_SETUP_MODEL_CONTEXT_WINDOW) &&
379
- parsed.get("model_auto_compact_token_limit") ===
380
- String(DEFAULT_SETUP_MODEL_AUTO_COMPACT_TOKEN_LIMIT));
391
+ function sourceLines(config) {
392
+ const lines = [];
393
+ let start = 0;
394
+ for (let index = 0; index < config.length; index += 1) {
395
+ if (config[index] !== "\n")
396
+ continue;
397
+ lines.push({
398
+ content: config.slice(start, index - (index > start && config[index - 1] === "\r" ? 1 : 0)),
399
+ start,
400
+ end: index + 1,
401
+ });
402
+ start = index + 1;
403
+ }
404
+ if (start < config.length)
405
+ lines.push({ content: config.slice(start), start, end: config.length });
406
+ return lines;
381
407
  }
382
- export function stripOmxSeededBehavioralDefaults(config) {
383
- const lines = config.split(/\r?\n/);
384
- const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
385
- const boundary = firstTable >= 0 ? firstTable : lines.length;
386
- const result = [];
387
- for (let index = 0; index < lines.length; index += 1) {
388
- const trimmed = lines[index].trim();
389
- if (index < boundary &&
390
- trimmed === OMX_SEEDED_BEHAVIORAL_DEFAULTS_START_MARKER) {
391
- const endIndex = lines.findIndex((line, candidateIndex) => candidateIndex > index &&
392
- candidateIndex < boundary &&
393
- line.trim() === OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER);
394
- if (endIndex < 0) {
395
- continue;
396
- }
397
- const blockLines = lines.slice(index + 1, endIndex);
398
- if (!isUnchangedOmxSeededBehavioralDefaultsBlock(blockLines)) {
399
- result.push(...blockLines);
408
+ function analyzeOmxSeededBehavioralDefaults(config) {
409
+ const lines = sourceLines(config);
410
+ const firstTable = lines.findIndex((line) => TOML_TABLE_HEADER_PATTERN.test(line.content));
411
+ const boundary = firstTable < 0 ? lines.length : firstTable;
412
+ const starts = lines.map((line, index) => ({ line, index }))
413
+ .filter(({ line }) => line.content === OMX_SEEDED_BEHAVIORAL_DEFAULTS_START_MARKER);
414
+ const ends = lines.map((line, index) => ({ line, index }))
415
+ .filter(({ line }) => line.content === OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER);
416
+ if (starts.length === 0 && ends.length === 0)
417
+ return { state: "absent", spans: [] };
418
+ if (starts.length !== 1 || ends.length !== 1 || starts[0].index >= ends[0].index) {
419
+ return { state: "malformed-or-ambiguous", spans: [] };
420
+ }
421
+ const rootText = lines.slice(0, boundary).map((line) => line.content).join("\n");
422
+ let entryStart = 0;
423
+ const rootEntries = splitRootLevelEntries(rootText).entries.map((entry) => {
424
+ const indexed = { entry, start: entryStart, end: entryStart + entry.lines.length };
425
+ entryStart = indexed.end;
426
+ return indexed;
427
+ });
428
+ const isStandaloneRootMarker = (index) => index < boundary && rootEntries.some(({ entry, start, end }) => !entry.key && start === index && end === index + 1);
429
+ const start = starts[0];
430
+ const end = ends[0];
431
+ if (!isStandaloneRootMarker(start.index) || !isStandaloneRootMarker(end.index)) {
432
+ return { state: "malformed-or-ambiguous", spans: [] };
433
+ }
434
+ const block = lines.slice(start.index, end.index + 1);
435
+ const exact = (body) => block.length === body.length + 2 &&
436
+ block[0].content === OMX_SEEDED_BEHAVIORAL_DEFAULTS_START_MARKER &&
437
+ block.at(-1)?.content === OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER &&
438
+ body.every((line, index) => block[index + 1].content === line);
439
+ const blockSpan = [{ start: start.line.start, end: end.line.end }];
440
+ const markerSpans = [
441
+ { start: start.line.start, end: start.line.end },
442
+ { start: end.line.start, end: end.line.end },
443
+ ];
444
+ if (exact([
445
+ `model_context_window = ${LEGACY_SEEDED_MODEL_CONTEXT_WINDOW}`,
446
+ `model_auto_compact_token_limit = ${LEGACY_SEEDED_MODEL_AUTO_COMPACT_TOKEN_LIMIT}`,
447
+ ])) {
448
+ const hasOutsideDuplicate = rootEntries.some(({ entry, start: entryStart, end: entryEnd }) => (entryEnd <= start.index || entryStart > end.index) &&
449
+ (entry.key === "model_context_window" || entry.key === "model_auto_compact_token_limit"));
450
+ return hasOutsideDuplicate
451
+ ? { state: "malformed-or-ambiguous", spans: [] }
452
+ : { state: "exact-pair", spans: blockSpan };
453
+ }
454
+ const singletonState = (singletonKey, siblingKey) => {
455
+ let singletonDuplicates = 0;
456
+ let siblingCount = 0;
457
+ let invalidSibling = false;
458
+ for (const { entry, start: entryStart, end: entryEnd } of rootEntries) {
459
+ const outside = entryEnd <= start.index || entryStart > end.index;
460
+ if (outside && entry.key === singletonKey)
461
+ singletonDuplicates += 1;
462
+ if (outside && entry.key === siblingKey) {
463
+ siblingCount += 1;
464
+ invalidSibling ||= !parseStandaloneToml(entry.lines.join("\n"));
400
465
  }
401
- index = endIndex;
402
- continue;
403
- }
404
- if (index < boundary &&
405
- trimmed === OMX_SEEDED_BEHAVIORAL_DEFAULTS_END_MARKER) {
406
- continue;
407
466
  }
408
- result.push(lines[index]);
409
- }
410
- return result.join("\n");
467
+ if (singletonDuplicates > 0 || siblingCount > 1 || invalidSibling)
468
+ return "malformed-or-ambiguous";
469
+ return siblingCount === 1 ? "exact" : "bounded-nonremovable";
470
+ };
471
+ if (exact([`model_context_window = ${LEGACY_SEEDED_MODEL_CONTEXT_WINDOW}`])) {
472
+ const state = singletonState("model_context_window", "model_auto_compact_token_limit");
473
+ return state === "exact"
474
+ ? { state: "exact-context-singleton", spans: blockSpan }
475
+ : state === "bounded-nonremovable"
476
+ ? { state, spans: markerSpans }
477
+ : { state, spans: [] };
478
+ }
479
+ if (exact([`model_auto_compact_token_limit = ${LEGACY_SEEDED_MODEL_AUTO_COMPACT_TOKEN_LIMIT}`])) {
480
+ const state = singletonState("model_auto_compact_token_limit", "model_context_window");
481
+ return state === "exact"
482
+ ? { state: "exact-auto-singleton", spans: blockSpan }
483
+ : state === "bounded-nonremovable"
484
+ ? { state, spans: markerSpans }
485
+ : { state, spans: [] };
486
+ }
487
+ return { state: "bounded-nonremovable", spans: markerSpans };
488
+ }
489
+ export function hasExactOmxSeededBehavioralDefaultsPair(config) {
490
+ return analyzeOmxSeededBehavioralDefaults(config).state === "exact-pair";
491
+ }
492
+ export function stripOmxSeededBehavioralDefaults(config) {
493
+ return [...analyzeOmxSeededBehavioralDefaults(config).spans]
494
+ .sort((left, right) => right.start - left.start)
495
+ .reduce((result, span) => result.slice(0, span.start) + result.slice(span.end), config);
411
496
  }
412
497
  function stripRootLevelKeys(config, keys) {
413
498
  const { entries, remainder } = splitRootLevelEntries(config);
@@ -511,7 +596,6 @@ function upsertFeatureFlags(config, codexHookFeatureFlag = DEFAULT_CODEX_HOOK_FE
511
596
  const base = config.trimEnd();
512
597
  const featureBlock = [
513
598
  "[features]",
514
- "multi_agent = true",
515
599
  "child_agents_md = true",
516
600
  hookFeatureFlagLine,
517
601
  "goals = true",
@@ -529,31 +613,19 @@ function upsertFeatureFlags(config, codexHookFeatureFlag = DEFAULT_CODEX_HOOK_FE
529
613
  break;
530
614
  }
531
615
  }
532
- // Remove deprecated 'collab' key (superseded by multi_agent) and
533
- // the misspelled singular 'goal' flag written by unreleased PR builds.
616
+ // Remove deprecated collab and unreleased singular goal flags.
534
617
  for (let i = sectionEnd - 1; i > featuresStart; i--) {
535
618
  if (/^\s*(?:collab|goal)\s*=/.test(lines[i])) {
536
619
  lines.splice(i, 1);
537
620
  sectionEnd -= 1;
538
621
  }
539
622
  }
540
- let multiAgentIdx = -1;
541
623
  let childAgentsIdx = -1;
542
624
  for (let i = featuresStart + 1; i < sectionEnd; i++) {
543
- if (/^\s*multi_agent\s*=/.test(lines[i])) {
544
- multiAgentIdx = i;
545
- }
546
- else if (/^\s*child_agents_md\s*=/.test(lines[i])) {
625
+ if (/^\s*child_agents_md\s*=/.test(lines[i])) {
547
626
  childAgentsIdx = i;
548
627
  }
549
628
  }
550
- if (multiAgentIdx >= 0) {
551
- lines[multiAgentIdx] = "multi_agent = true";
552
- }
553
- else {
554
- lines.splice(sectionEnd, 0, "multi_agent = true");
555
- sectionEnd += 1;
556
- }
557
629
  if (childAgentsIdx >= 0) {
558
630
  lines[childAgentsIdx] = "child_agents_md = true";
559
631
  }
@@ -1015,52 +1087,12 @@ function upsertEnvSettings(config) {
1015
1087
  }
1016
1088
  return lines.join("\n");
1017
1089
  }
1018
- function upsertAgentsSettings(config) {
1019
- const lines = config.split(/\r?\n/);
1020
- const agentsStart = lines.findIndex((line) => /^\s*\[agents\]\s*$/.test(line));
1021
- if (agentsStart < 0) {
1022
- const base = config.trimEnd();
1023
- const agentsBlock = [
1024
- "[agents]",
1025
- `max_threads = ${OMX_AGENTS_MAX_THREADS}`,
1026
- `max_depth = ${OMX_AGENTS_MAX_DEPTH}`,
1027
- "",
1028
- ].join("\n");
1029
- if (base.length === 0)
1030
- return agentsBlock;
1031
- return `${base}\n\n${agentsBlock}`;
1032
- }
1033
- let sectionEnd = lines.length;
1034
- for (let i = agentsStart + 1; i < lines.length; i++) {
1035
- if (/^\s*\[\[?[^\]]+\]?\]\s*$/.test(lines[i])) {
1036
- sectionEnd = i;
1037
- break;
1038
- }
1039
- }
1040
- let maxThreadsIdx = -1;
1041
- let maxDepthIdx = -1;
1042
- for (let i = agentsStart + 1; i < sectionEnd; i++) {
1043
- if (/^\s*max_threads\s*=/.test(lines[i])) {
1044
- maxThreadsIdx = i;
1045
- }
1046
- else if (/^\s*max_depth\s*=/.test(lines[i])) {
1047
- maxDepthIdx = i;
1048
- }
1049
- }
1050
- if (maxThreadsIdx < 0) {
1051
- lines.splice(sectionEnd, 0, `max_threads = ${OMX_AGENTS_MAX_THREADS}`);
1052
- sectionEnd += 1;
1053
- }
1054
- if (maxDepthIdx < 0) {
1055
- lines.splice(sectionEnd, 0, `max_depth = ${OMX_AGENTS_MAX_DEPTH}`);
1056
- }
1057
- return lines.join("\n");
1058
- }
1059
1090
  /**
1060
1091
  * Remove OMX-owned feature flags from the [features] section.
1092
+ * If `preserveMultiAgent` is set, retain multi_agent unchanged.
1061
1093
  * If the section becomes empty after removal, remove the section header too.
1062
1094
  */
1063
- export function stripOmxFeatureFlags(config) {
1095
+ export function stripOmxFeatureFlags(config, options = {}) {
1064
1096
  const lines = config.split(/\r?\n/);
1065
1097
  const featuresStart = lines.findIndex((line) => /^\s*\[features\]\s*$/.test(line));
1066
1098
  if (featuresStart < 0)
@@ -1073,7 +1105,7 @@ export function stripOmxFeatureFlags(config) {
1073
1105
  }
1074
1106
  }
1075
1107
  const omxFlags = [
1076
- "multi_agent",
1108
+ ...(options.preserveMultiAgent ? [] : ["multi_agent"]),
1077
1109
  "child_agents_md",
1078
1110
  "hooks",
1079
1111
  "codex_hooks",
@@ -1139,10 +1171,6 @@ export function stripOmxEnvSettings(config) {
1139
1171
  // ---------------------------------------------------------------------------
1140
1172
  // Orphaned OMX table sections (no marker block)
1141
1173
  // ---------------------------------------------------------------------------
1142
- /**
1143
- * Check whether a TOML table name belongs to a legacy OMX-managed agent entry.
1144
- * Handles both `agents.name` and `agents."name"` forms.
1145
- */
1146
1174
  function isOmxFirstPartyMcpSection(tableName) {
1147
1175
  const match = tableName.match(/^mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))$/);
1148
1176
  const name = match?.[1] ?? match?.[2];
@@ -1150,20 +1178,10 @@ function isOmxFirstPartyMcpSection(tableName) {
1150
1178
  (OMX_FIRST_PARTY_MCP_SERVER_NAMES.includes(name) ||
1151
1179
  name === "omx_team_run"));
1152
1180
  }
1153
- function isLegacyOmxAgentSection(tableName) {
1154
- const m = tableName.match(/^agents\.(?:"([^"]+)"|(\w[\w-]*))$/);
1155
- if (!m)
1156
- return false;
1157
- const name = m[1] || m[2] || "";
1158
- return Object.prototype.hasOwnProperty.call(AGENT_DEFINITIONS, name);
1159
- }
1160
1181
  /**
1161
- * Strip OMX-owned table sections that exist outside the marker block.
1182
+ * Strip first-party OMX MCP table sections that exist outside the marker block.
1162
1183
  * This covers legacy configs that were written before markers were added,
1163
1184
  * or configs where the marker was accidentally removed.
1164
- *
1165
- * Targets: exact first-party [mcp_servers.<name>] entries, retired
1166
- * [mcp_servers.omx_team_run], and legacy [agents.<name>] entries.
1167
1185
  */
1168
1186
  function stripOrphanedOmxSections(config) {
1169
1187
  const lines = config.split(/\r?\n/);
@@ -1177,8 +1195,7 @@ function stripOrphanedOmxSections(config) {
1177
1195
  // Note: [tui] is NOT stripped here because it could be user-owned.
1178
1196
  // The marker-based stripExistingOmxBlocks already handles [tui]
1179
1197
  // when it lives inside the OMX marker block.
1180
- const isOmxSection = isOmxFirstPartyMcpSection(tableName) ||
1181
- isLegacyOmxAgentSection(tableName);
1198
+ const isOmxSection = isOmxFirstPartyMcpSection(tableName);
1182
1199
  if (isOmxSection) {
1183
1200
  // Remove preceding OMX comment lines and blank lines
1184
1201
  while (result.length > 0) {
@@ -1726,13 +1743,13 @@ function getOmxTablesBlock(pkgRoot, includeTui = true, statusLinePreset = DEFAUL
1726
1743
  *
1727
1744
  * Layout:
1728
1745
  * 1. OMX top-level keys (notify, model_reasoning_effort, developer_instructions)
1729
- * 2. [features] with multi_agent + child_agents_md + hooks + goals
1746
+ * 2. [features] with child_agents_md + hooks + goals
1730
1747
  * 3. [shell_environment_policy.set] with defaulted deprecated explore-routing opt-out
1731
1748
  * 4. … user sections …
1732
1749
  * 5. OMX [table] sections (mcp_servers, tui)
1733
1750
  */
1734
1751
  export function buildMergedConfig(existingConfig, pkgRoot, options = {}) {
1735
- let existing = existingConfig;
1752
+ let existing = stripOmxSeededBehavioralDefaults(existingConfig);
1736
1753
  const preservedFirstPartyMcpSections = options.preserveExistingFirstPartyMcp === true &&
1737
1754
  options.includeFirstPartyMcp !== true
1738
1755
  ? extractFirstPartyOmxMcpSections(existing)
@@ -1755,11 +1772,11 @@ export function buildMergedConfig(existingConfig, pkgRoot, options = {}) {
1755
1772
  !isOmxManagedNotifyCommand(getRootTomlArray(existing, "notify"), pkgRoot)
1756
1773
  ? getRootTomlArray(existing, "notify")
1757
1774
  : null;
1758
- existing = stripOmxTopLevelKeys(existing);
1775
+ existing = stripOmxTopLevelKeys(existing).trimStart();
1759
1776
  if (userNotifyToPreserve) {
1760
1777
  existing = `${`notify = ${formatTomlStringArray(userNotifyToPreserve)}`}\n${existing.trimStart()}`;
1761
1778
  }
1762
- existing = stripOrphanedManagedNotify(existing, pkgRoot);
1779
+ existing = stripOrphanedManagedNotify(existing, pkgRoot).trimStart();
1763
1780
  const managedTrustState = buildManagedCodexHookTrustStateForConfig(options.codexHooksFile, pkgRoot, {
1764
1781
  codexHomeDir: options.codexHomeDir,
1765
1782
  platform: options.hookCommandPlatform,
@@ -1777,7 +1794,6 @@ export function buildMergedConfig(existingConfig, pkgRoot, options = {}) {
1777
1794
  }
1778
1795
  existing = upsertFeatureFlags(existing, options.codexHookFeatureFlag);
1779
1796
  existing = upsertEnvSettings(existing);
1780
- existing = upsertAgentsSettings(existing);
1781
1797
  const tuiUpsert = includeTui
1782
1798
  ? upsertTuiStatusLine(existing, statusLinePreset, {
1783
1799
  forceStatusLinePreset: options.forceStatusLinePreset,
@@ -1792,11 +1808,12 @@ export function buildMergedConfig(existingConfig, pkgRoot, options = {}) {
1792
1808
  platform: options.hookCommandPlatform,
1793
1809
  }, options.includeFirstPartyMcp === true, hookTrustStrip.preservedConflictKeys);
1794
1810
  const sharedRegistryBlock = getSharedMcpRegistryBlock(options.sharedMcpServers ?? [], options.sharedMcpRegistrySource, existing);
1795
- let body = existing.trimEnd();
1811
+ let body = existing.trim();
1796
1812
  if (sharedRegistryBlock) {
1797
1813
  body = body ? `${body}\n\n${sharedRegistryBlock}` : sharedRegistryBlock;
1798
1814
  }
1799
- return addDefaultLauncherMcpStartupTimeouts(topLines.join("\n") + "\n\n" + body + "\n" + tablesBlock);
1815
+ const bodySeparator = body.length > 0 && !/^\s*\[/.test(body) ? "\n" : "\n\n";
1816
+ return addDefaultLauncherMcpStartupTimeouts(topLines.join("\n") + bodySeparator + body + "\n" + tablesBlock);
1800
1817
  }
1801
1818
  /**
1802
1819
  * Detect and repair upgrade-era managed config incompatibilities in config.toml.