oh-my-codex 0.18.11 → 0.18.13

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 (196) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -1
  4. package/dist/autopilot/__tests__/ralplan-gate.test.js +668 -0
  5. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  6. package/dist/autopilot/completion-gate.d.ts +10 -0
  7. package/dist/autopilot/completion-gate.d.ts.map +1 -0
  8. package/dist/autopilot/completion-gate.js +154 -0
  9. package/dist/autopilot/completion-gate.js.map +1 -0
  10. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  11. package/dist/autopilot/ralplan-gate.js +42 -21
  12. package/dist/autopilot/ralplan-gate.js.map +1 -1
  13. package/dist/cli/__tests__/codex-plugin-layout.test.js +46 -3
  14. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  15. package/dist/cli/__tests__/doctor-invalid-config.test.js +35 -0
  16. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  17. package/dist/cli/__tests__/doctor-warning-copy.test.js +317 -0
  18. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  19. package/dist/cli/__tests__/index.test.js +120 -2
  20. package/dist/cli/__tests__/index.test.js.map +1 -1
  21. package/dist/cli/__tests__/launch-fallback.test.js +1 -1
  22. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  23. package/dist/cli/__tests__/resume.test.js +217 -1
  24. package/dist/cli/__tests__/resume.test.js.map +1 -1
  25. package/dist/cli/__tests__/session-scoped-runtime.test.js +101 -0
  26. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  27. package/dist/cli/__tests__/session-search-help.test.js +3 -2
  28. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  29. package/dist/cli/__tests__/session-search.test.js +64 -2
  30. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  31. package/dist/cli/__tests__/setup-agents-overwrite.test.js +289 -1
  32. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  33. package/dist/cli/__tests__/setup-install-mode.test.js +290 -17
  34. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  35. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +74 -0
  36. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-scope.test.js +45 -0
  38. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  39. package/dist/cli/__tests__/state.test.js +93 -0
  40. package/dist/cli/__tests__/state.test.js.map +1 -1
  41. package/dist/cli/__tests__/update.test.js +157 -3
  42. package/dist/cli/__tests__/update.test.js.map +1 -1
  43. package/dist/cli/__tests__/version-sync-contract.test.js +2 -0
  44. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  45. package/dist/cli/doctor.d.ts.map +1 -1
  46. package/dist/cli/doctor.js +90 -12
  47. package/dist/cli/doctor.js.map +1 -1
  48. package/dist/cli/index.d.ts +13 -4
  49. package/dist/cli/index.d.ts.map +1 -1
  50. package/dist/cli/index.js +439 -46
  51. package/dist/cli/index.js.map +1 -1
  52. package/dist/cli/project-runtime-codex-homes.d.ts +6 -0
  53. package/dist/cli/project-runtime-codex-homes.d.ts.map +1 -0
  54. package/dist/cli/project-runtime-codex-homes.js +27 -0
  55. package/dist/cli/project-runtime-codex-homes.js.map +1 -0
  56. package/dist/cli/session-search.d.ts.map +1 -1
  57. package/dist/cli/session-search.js +8 -1
  58. package/dist/cli/session-search.js.map +1 -1
  59. package/dist/cli/setup.d.ts +2 -2
  60. package/dist/cli/setup.d.ts.map +1 -1
  61. package/dist/cli/setup.js +482 -126
  62. package/dist/cli/setup.js.map +1 -1
  63. package/dist/cli/state.d.ts.map +1 -1
  64. package/dist/cli/state.js +79 -8
  65. package/dist/cli/state.js.map +1 -1
  66. package/dist/cli/update.d.ts +1 -0
  67. package/dist/cli/update.d.ts.map +1 -1
  68. package/dist/cli/update.js +42 -10
  69. package/dist/cli/update.js.map +1 -1
  70. package/dist/config/__tests__/codex-hooks.test.js +73 -29
  71. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  72. package/dist/config/codex-hooks.d.ts +14 -0
  73. package/dist/config/codex-hooks.d.ts.map +1 -1
  74. package/dist/config/codex-hooks.js +54 -51
  75. package/dist/config/codex-hooks.js.map +1 -1
  76. package/dist/config/generator.d.ts +1 -1
  77. package/dist/config/generator.d.ts.map +1 -1
  78. package/dist/config/generator.js +1 -1
  79. package/dist/config/generator.js.map +1 -1
  80. package/dist/hooks/__tests__/best-practice-research-skill.test.js +12 -0
  81. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -1
  82. package/dist/hud/__tests__/authority.test.js +45 -12
  83. package/dist/hud/__tests__/authority.test.js.map +1 -1
  84. package/dist/hud/__tests__/reconcile.test.js +95 -0
  85. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  86. package/dist/hud/__tests__/render.test.js +6 -6
  87. package/dist/hud/__tests__/render.test.js.map +1 -1
  88. package/dist/hud/__tests__/tmux.test.js +2 -2
  89. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  90. package/dist/hud/authority.d.ts.map +1 -1
  91. package/dist/hud/authority.js +17 -2
  92. package/dist/hud/authority.js.map +1 -1
  93. package/dist/hud/index.js +1 -4
  94. package/dist/hud/index.js.map +1 -1
  95. package/dist/hud/reconcile.d.ts.map +1 -1
  96. package/dist/hud/reconcile.js +42 -0
  97. package/dist/hud/reconcile.js.map +1 -1
  98. package/dist/hud/render.d.ts.map +1 -1
  99. package/dist/hud/render.js +6 -0
  100. package/dist/hud/render.js.map +1 -1
  101. package/dist/hud/tmux.d.ts.map +1 -1
  102. package/dist/hud/tmux.js +5 -4
  103. package/dist/hud/tmux.js.map +1 -1
  104. package/dist/mcp/__tests__/bootstrap.test.js +31 -1
  105. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  106. package/dist/mcp/bootstrap.d.ts +1 -0
  107. package/dist/mcp/bootstrap.d.ts.map +1 -1
  108. package/dist/mcp/bootstrap.js +32 -0
  109. package/dist/mcp/bootstrap.js.map +1 -1
  110. package/dist/modes/__tests__/base-autopilot-gates.test.d.ts +2 -0
  111. package/dist/modes/__tests__/base-autopilot-gates.test.d.ts.map +1 -0
  112. package/dist/modes/__tests__/base-autopilot-gates.test.js +154 -0
  113. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -0
  114. package/dist/modes/base.d.ts +4 -1
  115. package/dist/modes/base.d.ts.map +1 -1
  116. package/dist/modes/base.js +71 -1
  117. package/dist/modes/base.js.map +1 -1
  118. package/dist/pipeline/__tests__/orchestrator.test.js +144 -3
  119. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  120. package/dist/pipeline/__tests__/stages.test.js +109 -0
  121. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  122. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  123. package/dist/pipeline/orchestrator.js +11 -4
  124. package/dist/pipeline/orchestrator.js.map +1 -1
  125. package/dist/pipeline/stages/code-review.d.ts +2 -0
  126. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  127. package/dist/pipeline/stages/code-review.js +2 -0
  128. package/dist/pipeline/stages/code-review.js.map +1 -1
  129. package/dist/pipeline/stages/ultraqa.d.ts +3 -0
  130. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -1
  131. package/dist/pipeline/stages/ultraqa.js +3 -0
  132. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  133. package/dist/ralplan/__tests__/consensus-gate.test.d.ts +2 -0
  134. package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +1 -0
  135. package/dist/ralplan/__tests__/consensus-gate.test.js +631 -0
  136. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -0
  137. package/dist/ralplan/consensus-gate.d.ts +9 -1
  138. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  139. package/dist/ralplan/consensus-gate.js +287 -65
  140. package/dist/ralplan/consensus-gate.js.map +1 -1
  141. package/dist/scripts/__tests__/codex-native-hook.test.js +481 -0
  142. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  143. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  144. package/dist/scripts/codex-native-hook.js +145 -25
  145. package/dist/scripts/codex-native-hook.js.map +1 -1
  146. package/dist/scripts/codex-native-pre-post.d.ts +1 -0
  147. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  148. package/dist/scripts/codex-native-pre-post.js +130 -0
  149. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  150. package/dist/session-history/__tests__/search.test.js +166 -0
  151. package/dist/session-history/__tests__/search.test.js.map +1 -1
  152. package/dist/session-history/search.d.ts +7 -0
  153. package/dist/session-history/search.d.ts.map +1 -1
  154. package/dist/session-history/search.js +83 -24
  155. package/dist/session-history/search.js.map +1 -1
  156. package/dist/sidecar/__tests__/collector.test.js +60 -0
  157. package/dist/sidecar/__tests__/collector.test.js.map +1 -1
  158. package/dist/sidecar/collector.d.ts.map +1 -1
  159. package/dist/sidecar/collector.js +3 -6
  160. package/dist/sidecar/collector.js.map +1 -1
  161. package/dist/state/__tests__/operations.test.js +622 -0
  162. package/dist/state/__tests__/operations.test.js.map +1 -1
  163. package/dist/state/__tests__/skill-active.test.js +82 -0
  164. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  165. package/dist/state/operations.d.ts.map +1 -1
  166. package/dist/state/operations.js +31 -9
  167. package/dist/state/operations.js.map +1 -1
  168. package/dist/state/skill-active.d.ts.map +1 -1
  169. package/dist/state/skill-active.js +41 -1
  170. package/dist/state/skill-active.js.map +1 -1
  171. package/dist/team/__tests__/runtime.test.js +81 -57
  172. package/dist/team/__tests__/runtime.test.js.map +1 -1
  173. package/dist/team/runtime.js +4 -4
  174. package/dist/team/runtime.js.map +1 -1
  175. package/dist/utils/__tests__/paths.test.js +23 -0
  176. package/dist/utils/__tests__/paths.test.js.map +1 -1
  177. package/dist/utils/__tests__/version.test.js +27 -0
  178. package/dist/utils/__tests__/version.test.js.map +1 -1
  179. package/dist/utils/paths.d.ts.map +1 -1
  180. package/dist/utils/paths.js +4 -2
  181. package/dist/utils/paths.js.map +1 -1
  182. package/dist/utils/version.d.ts.map +1 -1
  183. package/dist/utils/version.js +7 -2
  184. package/dist/utils/version.js.map +1 -1
  185. package/dist/verification/__tests__/ci-rust-gates.test.js +4 -2
  186. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  187. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +71 -3
  188. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  189. package/package.json +1 -1
  190. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  191. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +53 -2
  192. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +6 -1
  193. package/skills/best-practice-research/SKILL.md +6 -1
  194. package/src/scripts/__tests__/codex-native-hook.test.ts +615 -0
  195. package/src/scripts/codex-native-hook.ts +162 -32
  196. package/src/scripts/codex-native-pre-post.ts +137 -0
package/dist/cli/index.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * Multi-agent orchestration for OpenAI Codex CLI
4
4
  */
5
5
  import { execFileSync, spawn } from "child_process";
6
- import { basename, delimiter, dirname, join, posix, win32 } from "path";
7
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
6
+ import { basename, dirname, join, posix, resolve, win32 } from "path";
7
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "fs";
8
8
  import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, symlink, writeFile } from "fs/promises";
9
9
  import { constants as osConstants, homedir } from "os";
10
10
  import { createHash } from "crypto";
@@ -41,6 +41,7 @@ import { MADMAX_FLAG, CODEX_BYPASS_FLAG, HIGH_REASONING_FLAG, XHIGH_REASONING_FL
41
41
  import { getBaseStateDir, getStateDir, listModeStateFilesWithScopePreference, } from "../mcp/state-paths.js";
42
42
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
43
43
  import { readPersistedSetupPreferences, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
44
+ import { discoverProjectRuntimeCodexHomes } from "./project-runtime-codex-homes.js";
44
45
  import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
45
46
  export { readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
46
47
  import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, getSkillActiveStatePathsForStateDir, listActiveSkills, readSkillActiveState, syncCanonicalSkillStateForMode, } from "../state/skill-active.js";
@@ -107,10 +108,10 @@ Usage:
107
108
  omx auth Manage Codex OAuth auth slots (add|list|use)
108
109
  omx question OMX-owned blocking question UI entrypoint for agent-invoked user questions
109
110
  omx adapt Scaffold OMX-owned adapter foundations for persistent external targets
110
- omx resume Resume a previous interactive Codex session
111
+ omx resume Resume Codex sessions (supports --project and --codex-home <path>)
111
112
  omx explore DEPRECATED compatibility command; use normal repo inspection or omx sparkshell
112
113
  omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
113
- omx session Search prior local session transcripts and history artifacts
114
+ omx session Search prior local session transcripts (--codex-home <path> escape hatch)
114
115
  omx agents-init [path]
115
116
  Bootstrap lightweight AGENTS.md files for a repo/subtree
116
117
  omx agents Manage Codex native agent TOML files
@@ -563,14 +564,124 @@ const PROJECT_LAUNCH_PERSISTED_RUNTIME_ENTRY_NAMES = new Set([
563
564
  // log the contents.
564
565
  "auth.json",
565
566
  ]);
567
+ const PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES = new Set([
568
+ "sessions",
569
+ "history.jsonl",
570
+ "session_index.jsonl",
571
+ ]);
566
572
  // Mirroring these files into the runtime CODEX_HOME would cause Codex to load
567
573
  // them as user-scope config alongside the canonical project-scope copies under
568
574
  // <cwd>/.codex, duplicating every native hook and asking the user to re-trust
569
575
  // hooks on every launch. See GH issue #2470.
570
576
  const PROJECT_LAUNCH_RUNTIME_SKIPPED_ENTRY_NAMES = new Set(["hooks.json"]);
577
+ function shouldMirrorProjectLaunchRuntimeEntry(entryName, includeHistoryArtifacts) {
578
+ if (PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES.has(entryName))
579
+ return true;
580
+ if (isCodexSqliteArtifact(entryName))
581
+ return includeHistoryArtifacts;
582
+ return true;
583
+ }
571
584
  function shouldPersistProjectLaunchRuntimeEntry(entryName) {
572
585
  return PROJECT_LAUNCH_PERSISTED_RUNTIME_ENTRY_NAMES.has(entryName);
573
586
  }
587
+ function uniqueJsonlLines(contents) {
588
+ const seen = new Set();
589
+ const lines = [];
590
+ for (const line of contents.split(/\r?\n/)) {
591
+ if (line === "" || seen.has(line))
592
+ continue;
593
+ seen.add(line);
594
+ lines.push(line);
595
+ }
596
+ return lines;
597
+ }
598
+ async function persistProjectLaunchRuntimeJsonlArtifact(source, destination) {
599
+ const existing = existsSync(destination) ? await readFile(destination, "utf-8").catch(() => "") : "";
600
+ const sourceContents = await readFile(source, "utf-8");
601
+ const separator = existing === "" || existing.endsWith("\n") || sourceContents === "" ? "" : "\n";
602
+ const lines = uniqueJsonlLines(`${existing}${separator}${sourceContents}`);
603
+ await writeFile(destination, lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf-8");
604
+ }
605
+ async function persistProjectLaunchRuntimeHistoryArtifacts(runtimeCodexHome, projectCodexHome) {
606
+ if (!runtimeCodexHome || !projectCodexHome)
607
+ return;
608
+ if (!existsSync(runtimeCodexHome))
609
+ return;
610
+ await mkdir(projectCodexHome, { recursive: true });
611
+ for (const entryName of PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES) {
612
+ const source = join(runtimeCodexHome, entryName);
613
+ if (!existsSync(source))
614
+ continue;
615
+ const sourceStat = await lstat(source);
616
+ if (sourceStat.isSymbolicLink())
617
+ continue;
618
+ const destination = join(projectCodexHome, entryName);
619
+ if (sourceStat.isDirectory()) {
620
+ await cp(source, destination, { recursive: true, force: true, verbatimSymlinks: true });
621
+ continue;
622
+ }
623
+ if (entryName === "history.jsonl" || entryName === "session_index.jsonl") {
624
+ await persistProjectLaunchRuntimeJsonlArtifact(source, destination);
625
+ continue;
626
+ }
627
+ if (sourceStat.isFile()) {
628
+ await copyFile(source, destination);
629
+ }
630
+ }
631
+ }
632
+ async function ensureProjectLaunchRuntimeHistoryLinks(runtimeCodexHome, projectCodexHome) {
633
+ await mkdir(projectCodexHome, { recursive: true });
634
+ for (const entryName of PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES) {
635
+ const runtimeEntry = join(runtimeCodexHome, entryName);
636
+ if (existsSync(runtimeEntry))
637
+ continue;
638
+ const projectEntry = join(projectCodexHome, entryName);
639
+ if (entryName === "sessions") {
640
+ await mkdir(projectEntry, { recursive: true });
641
+ }
642
+ else if (!existsSync(projectEntry)) {
643
+ await writeFile(projectEntry, "");
644
+ }
645
+ await linkOrCopyCodexHomeEntry(projectEntry, runtimeEntry);
646
+ }
647
+ }
648
+ async function materializeProjectLaunchRuntimeHistoryEntries(runtimeCodexHome, sourceCodexHome) {
649
+ for (const entryName of PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES) {
650
+ const source = join(sourceCodexHome, entryName);
651
+ if (!existsSync(source))
652
+ continue;
653
+ const destination = join(runtimeCodexHome, entryName);
654
+ await rm(destination, { recursive: true, force: true });
655
+ const sourceStat = await lstat(source);
656
+ if (sourceStat.isDirectory()) {
657
+ await cp(source, destination, { recursive: true, force: true, dereference: true });
658
+ continue;
659
+ }
660
+ await copyFile(source, destination);
661
+ }
662
+ }
663
+ async function mergeProjectLaunchRuntimeHistoryEntries(runtimeCodexHome, sourceCodexHome) {
664
+ for (const entryName of PROJECT_LAUNCH_DURABLE_HISTORY_ENTRY_NAMES) {
665
+ const source = join(sourceCodexHome, entryName);
666
+ if (!existsSync(source))
667
+ continue;
668
+ const destination = join(runtimeCodexHome, entryName);
669
+ const sourceStat = await lstat(source);
670
+ if (sourceStat.isDirectory()) {
671
+ await mkdir(destination, { recursive: true });
672
+ await cp(source, destination, { recursive: true, force: true, dereference: true });
673
+ continue;
674
+ }
675
+ if (existsSync(destination)) {
676
+ const existing = await readFile(destination, "utf-8").catch(() => "");
677
+ const addition = await readFile(source, "utf-8");
678
+ const separator = existing === "" || existing.endsWith("\n") || addition === "" ? "" : "\n";
679
+ await writeFile(destination, `${existing}${separator}${addition}`, "utf-8");
680
+ continue;
681
+ }
682
+ await copyFile(source, destination);
683
+ }
684
+ }
574
685
  export async function persistProjectLaunchRuntimeAuthState(runtimeCodexHome, projectCodexHome) {
575
686
  if (!runtimeCodexHome || !projectCodexHome)
576
687
  return;
@@ -587,10 +698,12 @@ export async function prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, pr
587
698
  const runtimeCodexHome = runtimeCodexHomePath(cwd, sessionId);
588
699
  await rm(runtimeCodexHome, { recursive: true, force: true });
589
700
  await mkdir(runtimeCodexHome, { recursive: true });
590
- if (!existsSync(projectCodexHome))
701
+ if (!existsSync(projectCodexHome)) {
702
+ await ensureProjectLaunchRuntimeHistoryLinks(runtimeCodexHome, projectCodexHome);
591
703
  return runtimeCodexHome;
704
+ }
592
705
  for (const entry of await readdir(projectCodexHome, { withFileTypes: true })) {
593
- if (isCodexSqliteArtifact(entry.name) && !options.includeHistoryArtifacts)
706
+ if (!shouldMirrorProjectLaunchRuntimeEntry(entry.name, options.includeHistoryArtifacts === true))
594
707
  continue;
595
708
  if (PROJECT_LAUNCH_RUNTIME_SKIPPED_ENTRY_NAMES.has(entry.name))
596
709
  continue;
@@ -608,6 +721,13 @@ export async function prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, pr
608
721
  }
609
722
  await linkOrCopyCodexHomeEntry(source, destination);
610
723
  }
724
+ await ensureProjectLaunchRuntimeHistoryLinks(runtimeCodexHome, projectCodexHome);
725
+ if (options.includeHistoryArtifacts === true && (options.extraHistoryCodexHomes?.length ?? 0) > 0) {
726
+ await materializeProjectLaunchRuntimeHistoryEntries(runtimeCodexHome, projectCodexHome);
727
+ for (const extraCodexHome of options.extraHistoryCodexHomes ?? []) {
728
+ await mergeProjectLaunchRuntimeHistoryEntries(runtimeCodexHome, extraCodexHome);
729
+ }
730
+ }
611
731
  return runtimeCodexHome;
612
732
  }
613
733
  function resolveProjectSqliteHomeForLaunch(projectCodexHome, env) {
@@ -619,7 +739,7 @@ function resolveProjectSqliteHomeForLaunch(projectCodexHome, env) {
619
739
  export async function prepareCodexHomeForLaunch(cwd, sessionId, env = process.env, options = {}) {
620
740
  const projectLocalCodexHomeForCleanup = resolveProjectLocalCodexHomeForLaunch(cwd, env);
621
741
  if (projectLocalCodexHomeForCleanup) {
622
- const runtimeCodexHome = await prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, { includeHistoryArtifacts: options.includeHistoryArtifacts });
742
+ const runtimeCodexHome = await prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, { includeHistoryArtifacts: options.includeHistoryArtifacts, extraHistoryCodexHomes: options.extraHistoryCodexHomes });
623
743
  return {
624
744
  codexHomeOverride: runtimeCodexHome,
625
745
  sqliteHomeOverride: resolveProjectSqliteHomeForLaunch(projectLocalCodexHomeForCleanup, env),
@@ -632,6 +752,81 @@ export async function prepareCodexHomeForLaunch(cwd, sessionId, env = process.en
632
752
  projectLocalCodexHomeForCleanup,
633
753
  };
634
754
  }
755
+ export function parseResumeCodexHomeSelection(args) {
756
+ const nextArgs = [];
757
+ let explicitCodexHome;
758
+ let projectOnly = false;
759
+ for (let index = 0; index < args.length; index += 1) {
760
+ const arg = args[index];
761
+ if (arg === "--codex-home") {
762
+ const value = args[index + 1];
763
+ if (!value || value.startsWith("-")) {
764
+ throw new Error("Missing value after --codex-home.");
765
+ }
766
+ explicitCodexHome = value;
767
+ index += 1;
768
+ continue;
769
+ }
770
+ if (arg.startsWith("--codex-home=")) {
771
+ explicitCodexHome = arg.slice("--codex-home=".length);
772
+ if (explicitCodexHome.trim() === "") {
773
+ throw new Error("Missing value after --codex-home.");
774
+ }
775
+ continue;
776
+ }
777
+ if (arg === "--project") {
778
+ projectOnly = true;
779
+ continue;
780
+ }
781
+ nextArgs.push(arg);
782
+ }
783
+ return {
784
+ args: nextArgs,
785
+ explicitCodexHome,
786
+ projectOnly,
787
+ };
788
+ }
789
+ async function prepareResumeCodexHomeForLaunch(cwd, sessionId, args, env = process.env) {
790
+ const selection = parseResumeCodexHomeSelection(args);
791
+ if (selection.explicitCodexHome) {
792
+ return {
793
+ args: selection.args,
794
+ prepared: {
795
+ codexHomeOverride: resolve(selection.explicitCodexHome),
796
+ },
797
+ };
798
+ }
799
+ const projectHomes = await discoverProjectRuntimeCodexHomes(cwd);
800
+ if (selection.projectOnly) {
801
+ if (projectHomes.length === 0) {
802
+ const emptyRuntimeCodexHome = runtimeCodexHomePath(cwd, sessionId);
803
+ await rm(emptyRuntimeCodexHome, { recursive: true, force: true });
804
+ await mkdir(join(emptyRuntimeCodexHome, "sessions"), { recursive: true });
805
+ return {
806
+ args: selection.args,
807
+ prepared: {
808
+ codexHomeOverride: emptyRuntimeCodexHome,
809
+ runtimeCodexHomeForCleanup: emptyRuntimeCodexHome,
810
+ },
811
+ };
812
+ }
813
+ const runtimeCodexHome = await prepareRuntimeCodexHomeForProjectLaunch(cwd, sessionId, projectHomes[0].path, {
814
+ includeHistoryArtifacts: true,
815
+ extraHistoryCodexHomes: projectHomes.slice(1).map((home) => home.path),
816
+ });
817
+ return {
818
+ args: selection.args,
819
+ prepared: {
820
+ codexHomeOverride: runtimeCodexHome,
821
+ },
822
+ };
823
+ }
824
+ const prepared = await prepareCodexHomeForLaunch(cwd, sessionId, env, {
825
+ includeHistoryArtifacts: true,
826
+ extraHistoryCodexHomes: projectHomes.map((home) => home.path),
827
+ });
828
+ return { args: selection.args, prepared };
829
+ }
635
830
  export async function persistProjectLaunchRuntimeProjectTrustState(runtimeCodexHome, projectCodexHome) {
636
831
  if (!runtimeCodexHome || !projectCodexHome)
637
832
  return;
@@ -654,6 +849,7 @@ export async function cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projec
654
849
  if (!runtimeCodexHomeForCleanup)
655
850
  return;
656
851
  await persistProjectLaunchRuntimeAuthState(runtimeCodexHomeForCleanup, projectCodexHomeForPersistence);
852
+ await persistProjectLaunchRuntimeHistoryArtifacts(runtimeCodexHomeForCleanup, projectCodexHomeForPersistence);
657
853
  await persistProjectLaunchRuntimeProjectTrustState(runtimeCodexHomeForCleanup, projectCodexHomeForPersistence);
658
854
  await rm(runtimeCodexHomeForCleanup, { recursive: true, force: true });
659
855
  }
@@ -774,7 +970,10 @@ function tmuxPaneBelongsToSession(paneId, sessionName) {
774
970
  }
775
971
  }
776
972
  function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
777
- return `if-shell -F '#{==:#{session_attached},0}' 'clear-history -t ${leaderPaneId}'`;
973
+ // The leader pane can be gone by the time the hook fires (e.g. crashed
974
+ // leader with a lingering session); suppress errors so tmux does not queue
975
+ // "(null):0: can't find pane" for the next attaching client.
976
+ return `if-shell -F '#{==:#{session_attached},0}' 'run-shell -b "tmux clear-history -t ${leaderPaneId} >/dev/null 2>&1 || true"'`;
778
977
  }
779
978
  function buildDetachedHistoryPruneHookSlot(sessionName, leaderPaneId) {
780
979
  const key = `${sessionName}:${leaderPaneId}:omx-history-prune`;
@@ -992,8 +1191,11 @@ function runCodexBlocking(cwd, launchArgs, codexEnv) {
992
1191
  }
993
1192
  }
994
1193
  }
995
- export function omxRuntimeCommandShimPath(cwd) {
996
- return join(omxRoot(cwd), "runtime", "bin", "omx");
1194
+ export function omxRuntimeCommandShimFileName(platform = process.platform) {
1195
+ return platform === "win32" ? "omx.cmd" : "omx";
1196
+ }
1197
+ export function omxRuntimeCommandShimPath(cwd, platform = process.platform) {
1198
+ return join(omxRoot(cwd), "runtime", "bin", omxRuntimeCommandShimFileName(platform));
997
1199
  }
998
1200
  function ensureRuntimeShimDirectory(path) {
999
1201
  if (existsSync(path)) {
@@ -1008,15 +1210,22 @@ function ensureRuntimeShimDirectory(path) {
1008
1210
  }
1009
1211
  mkdirSync(path, { mode: 0o700 });
1010
1212
  }
1011
- function buildOmxRuntimeCommandShim(nodePath, omxBin) {
1213
+ function buildOmxRuntimeCommandShim(nodePath, omxBin, platform = process.platform) {
1214
+ if (platform === "win32") {
1215
+ return [
1216
+ "@echo off",
1217
+ `"${nodePath}" "${omxBin}" %*`,
1218
+ "",
1219
+ ].join("\r\n");
1220
+ }
1012
1221
  return [
1013
1222
  "#!/bin/sh",
1014
1223
  `exec ${quoteShellArg(nodePath)} ${quoteShellArg(omxBin)} "$@"`,
1015
1224
  "",
1016
1225
  ].join("\n");
1017
1226
  }
1018
- export function ensureOmxRuntimeCommandShim(cwd, omxBin, nodePath = process.execPath) {
1019
- const shimPath = omxRuntimeCommandShimPath(cwd);
1227
+ export function ensureOmxRuntimeCommandShim(cwd, omxBin, nodePath = process.execPath, platform = process.platform) {
1228
+ const shimPath = omxRuntimeCommandShimPath(cwd, platform);
1020
1229
  const shimDir = dirname(shimPath);
1021
1230
  const rootDir = omxRoot(cwd);
1022
1231
  const runtimeDir = dirname(shimDir);
@@ -1032,24 +1241,53 @@ export function ensureOmxRuntimeCommandShim(cwd, omxBin, nodePath = process.exec
1032
1241
  rmSync(shimPath, { force: true });
1033
1242
  }
1034
1243
  }
1035
- writeFileSync(shimPath, buildOmxRuntimeCommandShim(nodePath, omxBin), {
1244
+ writeFileSync(shimPath, buildOmxRuntimeCommandShim(nodePath, omxBin, platform), {
1036
1245
  encoding: "utf-8",
1037
1246
  mode: 0o700,
1038
1247
  });
1039
- chmodSync(shimPath, 0o700);
1248
+ if (platform !== "win32") {
1249
+ chmodSync(shimPath, 0o700);
1250
+ }
1040
1251
  return shimDir;
1041
1252
  }
1042
- export function prependOmxRuntimeCommandShimToEnv(cwd, env, omxBin, nodePath = process.execPath) {
1043
- const shimDir = ensureOmxRuntimeCommandShim(cwd, omxBin, nodePath);
1044
- const currentPath = typeof env.PATH === "string" ? env.PATH : "";
1045
- return {
1046
- ...env,
1047
- PATH: currentPath ? `${shimDir}${delimiter}${currentPath}` : shimDir,
1048
- OMX_ENTRY_PATH: omxBin,
1049
- OMX_STARTUP_CWD: typeof env.OMX_STARTUP_CWD === "string" && env.OMX_STARTUP_CWD.trim()
1050
- ? env.OMX_STARTUP_CWD
1051
- : cwd,
1052
- };
1253
+ export function prependOmxRuntimeCommandShimToEnv(cwd, env, omxBin, nodePath = process.execPath, platform = process.platform) {
1254
+ const shimDir = ensureOmxRuntimeCommandShim(cwd, omxBin, nodePath, platform);
1255
+ const pathDelimiter = platform === "win32" ? win32.delimiter : posix.delimiter;
1256
+ const result = { ...env };
1257
+ if (platform === "win32") {
1258
+ // Windows env var names are case-insensitive; the inherited key is usually
1259
+ // `Path`, not `PATH`. Find every case variant, preserve the existing value,
1260
+ // prepend the shim directory, and collapse to a single key so the child does
1261
+ // not see an empty `PATH` shadowing the real `Path` (which drops System32,
1262
+ // WindowsPowerShell, etc.).
1263
+ const pathVariants = Object.keys(result).filter((key) => key.toLowerCase() === "path");
1264
+ let pathKey = "Path";
1265
+ let currentPath = "";
1266
+ for (const variant of pathVariants) {
1267
+ const value = result[variant];
1268
+ if (typeof value === "string" && value.length > 0) {
1269
+ pathKey = variant;
1270
+ currentPath = value;
1271
+ break;
1272
+ }
1273
+ }
1274
+ for (const variant of pathVariants) {
1275
+ delete result[variant];
1276
+ }
1277
+ result[pathKey] = currentPath
1278
+ ? `${shimDir}${pathDelimiter}${currentPath}`
1279
+ : shimDir;
1280
+ }
1281
+ else {
1282
+ const currentPath = typeof result.PATH === "string" ? result.PATH : "";
1283
+ result.PATH = currentPath ? `${shimDir}${pathDelimiter}${currentPath}` : shimDir;
1284
+ }
1285
+ result.OMX_ENTRY_PATH = omxBin;
1286
+ result.OMX_STARTUP_CWD =
1287
+ typeof result.OMX_STARTUP_CWD === "string" && result.OMX_STARTUP_CWD.trim()
1288
+ ? result.OMX_STARTUP_CWD
1289
+ : cwd;
1290
+ return result;
1053
1291
  }
1054
1292
  export function buildHudPaneCleanupTargets(existingPaneIds, createdPaneId, leaderPaneId) {
1055
1293
  const targets = new Set(existingPaneIds.filter((id) => id.startsWith("%")));
@@ -1656,7 +1894,31 @@ async function showStatus() {
1656
1894
  const { readFile } = await import("fs/promises");
1657
1895
  const cwd = process.cwd();
1658
1896
  try {
1659
- const refs = await listModeStateFilesWithScopePreference(cwd);
1897
+ let refs = await listModeStateFilesWithScopePreference(cwd);
1898
+ // Reconcile with hook-visible run-dir state when the worktree-scoped state
1899
+ // list reports no active workflow mode (parity with `omx cancel`). This
1900
+ // surfaces detached/madmax sessions whose state lives under the run dir.
1901
+ const hasActiveWorkflowMode = async (candidate) => {
1902
+ for (const ref of candidate) {
1903
+ const mode = basename(ref.path).replace("-state.json", "");
1904
+ if (mode === SKILL_ACTIVE_STATE_MODE)
1905
+ continue;
1906
+ try {
1907
+ const parsed = JSON.parse(await readFile(ref.path, "utf-8"));
1908
+ if (parsed.active === true)
1909
+ return true;
1910
+ }
1911
+ catch {
1912
+ continue;
1913
+ }
1914
+ }
1915
+ return false;
1916
+ };
1917
+ if (!(await hasActiveWorkflowMode(refs))) {
1918
+ const runDirRefs = await listHookVisibleRunDirStateRefs(cwd);
1919
+ if (await hasActiveWorkflowMode(runDirRefs))
1920
+ refs = runDirRefs;
1921
+ }
1660
1922
  const states = refs.map((ref) => ref.path);
1661
1923
  const ultragoalState = await readUltragoalState(cwd).catch(() => null);
1662
1924
  if (states.length === 0) {
@@ -1825,7 +2087,7 @@ export async function launchWithHud(args) {
1825
2087
  const { launchPolicy, effectiveExplicitLaunchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
1826
2088
  const enableNotifyFallbackAuthority = launchPolicy === "direct";
1827
2089
  const workerSparkModel = resolveWorkerSparkModel(notifyTempResult.passthroughArgs, persistentCodexHomeForLaunch);
1828
- const normalizedArgs = normalizeCodexLaunchArgs(notifyTempResult.passthroughArgs);
2090
+ let normalizedArgs = normalizeCodexLaunchArgs(notifyTempResult.passthroughArgs);
1829
2091
  let cwd = launchCwd;
1830
2092
  let worktreeDirty = false;
1831
2093
  let ensuredLaunchWorktree;
@@ -1883,7 +2145,13 @@ export async function launchWithHud(args) {
1883
2145
  catch {
1884
2146
  // Non-fatal: repair failure must not block launch
1885
2147
  }
1886
- const preparedCodexHome = await prepareCodexHomeForLaunch(launchCwd, sessionId, process.env, {
2148
+ const resumePrepared = normalizedArgs[0] === "resume"
2149
+ ? await prepareResumeCodexHomeForLaunch(launchCwd, sessionId, normalizedArgs, process.env)
2150
+ : null;
2151
+ if (resumePrepared) {
2152
+ normalizedArgs = resumePrepared.args;
2153
+ }
2154
+ const preparedCodexHome = resumePrepared?.prepared ?? await prepareCodexHomeForLaunch(launchCwd, sessionId, process.env, {
1887
2155
  includeHistoryArtifacts: normalizedArgs[0] === "resume",
1888
2156
  });
1889
2157
  const codexHomeOverride = preparedCodexHome.codexHomeOverride;
@@ -4321,28 +4589,144 @@ async function flushHookDerivedWatcherOnce(cwd) {
4321
4589
  },
4322
4590
  });
4323
4591
  }
4592
+ // Canonicalize a path for comparing a registry `source_cwd` against the current
4593
+ // working directory. `process.cwd()` resolves symlinks (e.g. macOS `/var` ->
4594
+ // `/private/var`), so registry values must be canonicalized the same way or the
4595
+ // run-dir fallback never matches. Falls back to `resolve` when the path is
4596
+ // missing (realpathSync requires an existing target).
4597
+ function canonicalizePathForRunDirMatch(p) {
4598
+ try {
4599
+ return realpathSync(resolve(p));
4600
+ }
4601
+ catch {
4602
+ return resolve(p);
4603
+ }
4604
+ }
4605
+ async function listHookVisibleRunDirStateRefs(cwd) {
4606
+ const runsRoot = resolveMadmaxRunsRoot(process.env);
4607
+ const registryPath = join(runsRoot, "registry.jsonl");
4608
+ const runDirs = new Set();
4609
+ const canonicalCwd = canonicalizePathForRunDirMatch(cwd);
4610
+ const canonicalRunsRoot = resolve(runsRoot);
4611
+ const addRecord = (raw) => {
4612
+ if (!raw || typeof raw !== "object")
4613
+ return;
4614
+ const record = raw;
4615
+ const sourceCwd = typeof record.source_cwd === "string" ? record.source_cwd.trim() : "";
4616
+ const runDir = typeof record.run_dir === "string"
4617
+ ? record.run_dir.trim()
4618
+ : typeof record.cwd === "string"
4619
+ ? record.cwd.trim()
4620
+ : "";
4621
+ if (!sourceCwd || !runDir)
4622
+ return;
4623
+ try {
4624
+ if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd)
4625
+ return;
4626
+ const resolvedRunDir = resolve(runDir);
4627
+ if (resolvedRunDir !== canonicalRunsRoot
4628
+ && !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`)) {
4629
+ return;
4630
+ }
4631
+ runDirs.add(resolvedRunDir);
4632
+ }
4633
+ catch {
4634
+ return;
4635
+ }
4636
+ };
4637
+ try {
4638
+ const rawRegistry = await readFile(registryPath, "utf-8");
4639
+ for (const line of rawRegistry.split(/\r?\n/)) {
4640
+ const trimmed = line.trim();
4641
+ if (!trimmed)
4642
+ continue;
4643
+ try {
4644
+ addRecord(JSON.parse(trimmed));
4645
+ }
4646
+ catch {
4647
+ continue;
4648
+ }
4649
+ }
4650
+ }
4651
+ catch { }
4652
+ try {
4653
+ const activeDir = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR);
4654
+ const files = await readdir(activeDir).catch(() => []);
4655
+ for (const file of files) {
4656
+ if (!file.endsWith(".json"))
4657
+ continue;
4658
+ try {
4659
+ addRecord(JSON.parse(await readFile(join(activeDir, file), "utf-8")));
4660
+ }
4661
+ catch {
4662
+ continue;
4663
+ }
4664
+ }
4665
+ }
4666
+ catch { }
4667
+ const refs = [];
4668
+ const seenPaths = new Set();
4669
+ for (const runDir of runDirs) {
4670
+ const stateDir = join(runDir, ".omx", "state");
4671
+ let sessionId;
4672
+ try {
4673
+ const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
4674
+ if (typeof session.session_id === "string" && session.session_id.trim()) {
4675
+ sessionId = session.session_id.trim();
4676
+ }
4677
+ }
4678
+ catch { }
4679
+ const candidateDirs = sessionId ? [join(stateDir, "sessions", sessionId), stateDir] : [stateDir];
4680
+ for (const dir of candidateDirs) {
4681
+ const files = await readdir(dir).catch(() => []);
4682
+ for (const file of files) {
4683
+ if (!file.endsWith("-state.json") || file === "session.json")
4684
+ continue;
4685
+ const path = join(dir, file);
4686
+ if (seenPaths.has(path))
4687
+ continue;
4688
+ seenPaths.add(path);
4689
+ refs.push({
4690
+ mode: file.slice(0, -"-state.json".length),
4691
+ path,
4692
+ scope: dir === stateDir ? "root" : "session",
4693
+ });
4694
+ }
4695
+ }
4696
+ }
4697
+ return refs.sort((a, b) => a.mode.localeCompare(b.mode));
4698
+ }
4324
4699
  async function cancelModes() {
4325
4700
  const { writeFile, readFile } = await import("fs/promises");
4326
4701
  const cwd = process.cwd();
4327
4702
  const nowIso = new Date().toISOString();
4328
4703
  try {
4329
- const refs = await listModeStateFilesWithScopePreference(cwd);
4330
- const states = new Map();
4331
- for (const ref of refs) {
4332
- const content = await readFile(ref.path, "utf-8");
4333
- let parsedState;
4334
- try {
4335
- parsedState = JSON.parse(content);
4336
- }
4337
- catch (err) {
4338
- logCliOperationFailure(err);
4339
- continue;
4704
+ const loadStates = async (refs) => {
4705
+ const loaded = new Map();
4706
+ for (const ref of refs) {
4707
+ const content = await readFile(ref.path, "utf-8");
4708
+ let parsedState;
4709
+ try {
4710
+ parsedState = JSON.parse(content);
4711
+ }
4712
+ catch (err) {
4713
+ logCliOperationFailure(err);
4714
+ continue;
4715
+ }
4716
+ loaded.set(ref.mode, {
4717
+ path: ref.path,
4718
+ scope: ref.scope,
4719
+ state: parsedState,
4720
+ });
4340
4721
  }
4341
- states.set(ref.mode, {
4342
- path: ref.path,
4343
- scope: ref.scope,
4344
- state: parsedState,
4345
- });
4722
+ return loaded;
4723
+ };
4724
+ let states = await loadStates(await listModeStateFilesWithScopePreference(cwd));
4725
+ const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
4726
+ if (!hasActiveWorkflowMode(states)) {
4727
+ const runDirStates = await loadStates(await listHookVisibleRunDirStateRefs(cwd));
4728
+ if (hasActiveWorkflowMode(runDirStates))
4729
+ states = runDirStates;
4346
4730
  }
4347
4731
  const changed = new Set();
4348
4732
  const reported = new Set();
@@ -4361,8 +4745,17 @@ async function cancelModes() {
4361
4745
  entry.state.current_phase = phase;
4362
4746
  entry.state.completed_at = nowIso;
4363
4747
  entry.state.last_turn_at = nowIso;
4748
+ if (mode === SKILL_ACTIVE_STATE_MODE) {
4749
+ entry.state.phase = phase;
4750
+ const activeSkills = Array.isArray(entry.state.active_skills)
4751
+ ? entry.state.active_skills
4752
+ : [];
4753
+ entry.state.active_skills = activeSkills.map((skill) => (skill && typeof skill === "object"
4754
+ ? { ...skill, active: false, phase }
4755
+ : skill));
4756
+ }
4364
4757
  changed.add(mode);
4365
- if (reportIfWasActive && wasActive)
4758
+ if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
4366
4759
  reported.add(mode);
4367
4760
  };
4368
4761
  const ralphLinksUltrawork = (state) => state.linked_ultrawork === true || state.linked_mode === "ultrawork";