oh-my-codex 0.10.2 → 0.10.3

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 (260) hide show
  1. package/README.de.md +4 -4
  2. package/README.es.md +4 -4
  3. package/README.fr.md +4 -4
  4. package/README.it.md +4 -4
  5. package/README.ja.md +4 -4
  6. package/README.ko.md +4 -4
  7. package/README.md +13 -7
  8. package/README.pt.md +4 -4
  9. package/README.ru.md +4 -4
  10. package/README.tr.md +4 -4
  11. package/README.vi.md +4 -4
  12. package/README.zh-TW.md +4 -4
  13. package/README.zh.md +4 -4
  14. package/dist/agents/__tests__/native-config.test.js +37 -33
  15. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  16. package/dist/agents/__tests__/skill-bridge.test.d.ts +2 -0
  17. package/dist/agents/__tests__/skill-bridge.test.d.ts.map +1 -0
  18. package/dist/agents/__tests__/skill-bridge.test.js +71 -0
  19. package/dist/agents/__tests__/skill-bridge.test.js.map +1 -0
  20. package/dist/agents/native-config.d.ts +18 -6
  21. package/dist/agents/native-config.d.ts.map +1 -1
  22. package/dist/agents/native-config.js +109 -92
  23. package/dist/agents/native-config.js.map +1 -1
  24. package/dist/agents/skill-bridge.d.ts +20 -0
  25. package/dist/agents/skill-bridge.d.ts.map +1 -0
  26. package/dist/agents/skill-bridge.js +150 -0
  27. package/dist/agents/skill-bridge.js.map +1 -0
  28. package/dist/autoresearch/__tests__/contracts.test.js +37 -1
  29. package/dist/autoresearch/__tests__/contracts.test.js.map +1 -1
  30. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js +10 -10
  31. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js.map +1 -1
  32. package/dist/autoresearch/__tests__/runtime.test.js +2 -2
  33. package/dist/autoresearch/__tests__/runtime.test.js.map +1 -1
  34. package/dist/autoresearch/contracts.d.ts.map +1 -1
  35. package/dist/autoresearch/contracts.js +17 -10
  36. package/dist/autoresearch/contracts.js.map +1 -1
  37. package/dist/autoresearch/runtime.d.ts.map +1 -1
  38. package/dist/autoresearch/runtime.js +71 -96
  39. package/dist/autoresearch/runtime.js.map +1 -1
  40. package/dist/cli/__tests__/agents-init.test.js +2 -0
  41. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  42. package/dist/cli/__tests__/agents.test.d.ts +2 -0
  43. package/dist/cli/__tests__/agents.test.d.ts.map +1 -0
  44. package/dist/cli/__tests__/agents.test.js +114 -0
  45. package/dist/cli/__tests__/agents.test.js.map +1 -0
  46. package/dist/cli/__tests__/autoresearch-guided.test.js +156 -1
  47. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  48. package/dist/cli/__tests__/autoresearch.test.js +195 -24
  49. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  50. package/dist/cli/__tests__/cleanup.test.d.ts +2 -0
  51. package/dist/cli/__tests__/cleanup.test.d.ts.map +1 -0
  52. package/dist/cli/__tests__/cleanup.test.js +213 -0
  53. package/dist/cli/__tests__/cleanup.test.js.map +1 -0
  54. package/dist/cli/__tests__/error-handling-warnings.test.js +1 -1
  55. package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
  56. package/dist/cli/__tests__/explore.test.js +3 -3
  57. package/dist/cli/__tests__/explore.test.js.map +1 -1
  58. package/dist/cli/__tests__/index.test.js +521 -401
  59. package/dist/cli/__tests__/index.test.js.map +1 -1
  60. package/dist/cli/__tests__/native-assets.test.js +72 -9
  61. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  62. package/dist/cli/__tests__/ralphthon.test.d.ts +2 -0
  63. package/dist/cli/__tests__/ralphthon.test.d.ts.map +1 -0
  64. package/dist/cli/__tests__/ralphthon.test.js +28 -0
  65. package/dist/cli/__tests__/ralphthon.test.js.map +1 -0
  66. package/dist/cli/__tests__/setup-agents-overwrite.test.js +36 -1
  67. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  68. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +35 -5
  69. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
  70. package/dist/cli/__tests__/setup-refresh.test.js +2 -2
  71. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  72. package/dist/cli/__tests__/setup-scope.test.js +131 -161
  73. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  74. package/dist/cli/__tests__/setup-skills-overwrite.test.js +10 -10
  75. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  76. package/dist/cli/__tests__/sparkshell-cli.test.js +28 -2
  77. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  78. package/dist/cli/__tests__/team.test.js +1 -112
  79. package/dist/cli/__tests__/team.test.js.map +1 -1
  80. package/dist/cli/__tests__/uninstall.test.js +7 -20
  81. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  82. package/dist/cli/agents-init.d.ts.map +1 -1
  83. package/dist/cli/agents-init.js +99 -95
  84. package/dist/cli/agents-init.js.map +1 -1
  85. package/dist/cli/agents.d.ts +14 -0
  86. package/dist/cli/agents.d.ts.map +1 -0
  87. package/dist/cli/agents.js +261 -0
  88. package/dist/cli/agents.js.map +1 -0
  89. package/dist/cli/autoresearch-guided.d.ts +8 -0
  90. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  91. package/dist/cli/autoresearch-guided.js +104 -37
  92. package/dist/cli/autoresearch-guided.js.map +1 -1
  93. package/dist/cli/autoresearch-intake.d.ts +60 -0
  94. package/dist/cli/autoresearch-intake.d.ts.map +1 -0
  95. package/dist/cli/autoresearch-intake.js +318 -0
  96. package/dist/cli/autoresearch-intake.js.map +1 -0
  97. package/dist/cli/autoresearch.d.ts +3 -1
  98. package/dist/cli/autoresearch.d.ts.map +1 -1
  99. package/dist/cli/autoresearch.js +64 -10
  100. package/dist/cli/autoresearch.js.map +1 -1
  101. package/dist/cli/cleanup.d.ts +52 -0
  102. package/dist/cli/cleanup.d.ts.map +1 -0
  103. package/dist/cli/cleanup.js +302 -0
  104. package/dist/cli/cleanup.js.map +1 -0
  105. package/dist/cli/doctor.d.ts.map +1 -1
  106. package/dist/cli/doctor.js +9 -37
  107. package/dist/cli/doctor.js.map +1 -1
  108. package/dist/cli/explore.d.ts.map +1 -1
  109. package/dist/cli/explore.js +5 -4
  110. package/dist/cli/explore.js.map +1 -1
  111. package/dist/cli/index.d.ts +5 -7
  112. package/dist/cli/index.d.ts.map +1 -1
  113. package/dist/cli/index.js +610 -427
  114. package/dist/cli/index.js.map +1 -1
  115. package/dist/cli/native-assets.d.ts +15 -1
  116. package/dist/cli/native-assets.d.ts.map +1 -1
  117. package/dist/cli/native-assets.js +134 -32
  118. package/dist/cli/native-assets.js.map +1 -1
  119. package/dist/cli/ralph.d.ts.map +1 -1
  120. package/dist/cli/ralph.js +38 -1
  121. package/dist/cli/ralph.js.map +1 -1
  122. package/dist/cli/ralphthon.d.ts +14 -0
  123. package/dist/cli/ralphthon.d.ts.map +1 -0
  124. package/dist/cli/ralphthon.js +234 -0
  125. package/dist/cli/ralphthon.js.map +1 -0
  126. package/dist/cli/setup.d.ts +1 -4
  127. package/dist/cli/setup.d.ts.map +1 -1
  128. package/dist/cli/setup.js +111 -76
  129. package/dist/cli/setup.js.map +1 -1
  130. package/dist/cli/sparkshell.d.ts +3 -1
  131. package/dist/cli/sparkshell.d.ts.map +1 -1
  132. package/dist/cli/sparkshell.js +35 -16
  133. package/dist/cli/sparkshell.js.map +1 -1
  134. package/dist/cli/team.d.ts.map +1 -1
  135. package/dist/cli/team.js +1 -0
  136. package/dist/cli/team.js.map +1 -1
  137. package/dist/cli/uninstall.d.ts +1 -1
  138. package/dist/cli/uninstall.d.ts.map +1 -1
  139. package/dist/cli/uninstall.js +82 -64
  140. package/dist/cli/uninstall.js.map +1 -1
  141. package/dist/config/__tests__/generator-idempotent.test.js +10 -10
  142. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  143. package/dist/config/__tests__/generator-notify.test.js +15 -0
  144. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  145. package/dist/config/generator.d.ts +0 -1
  146. package/dist/config/generator.d.ts.map +1 -1
  147. package/dist/config/generator.js +53 -42
  148. package/dist/config/generator.js.map +1 -1
  149. package/dist/hooks/__tests__/agents-overlay.test.js +295 -230
  150. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  151. package/dist/hooks/__tests__/deep-interview-contract.test.js +49 -24
  152. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  153. package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.d.ts +2 -0
  154. package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.d.ts.map +1 -0
  155. package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.js +193 -0
  156. package/dist/hooks/__tests__/notify-fallback-watcher-ralphthon.test.js.map +1 -0
  157. package/dist/hooks/agents-overlay.d.ts +1 -1
  158. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  159. package/dist/hooks/agents-overlay.js +109 -106
  160. package/dist/hooks/agents-overlay.js.map +1 -1
  161. package/dist/modes/base.d.ts +1 -1
  162. package/dist/modes/base.d.ts.map +1 -1
  163. package/dist/modes/base.js +1 -1
  164. package/dist/modes/base.js.map +1 -1
  165. package/dist/ralphthon/__tests__/bootstrap.test.d.ts +2 -0
  166. package/dist/ralphthon/__tests__/bootstrap.test.d.ts.map +1 -0
  167. package/dist/ralphthon/__tests__/bootstrap.test.js +23 -0
  168. package/dist/ralphthon/__tests__/bootstrap.test.js.map +1 -0
  169. package/dist/ralphthon/__tests__/orchestrator.test.d.ts +2 -0
  170. package/dist/ralphthon/__tests__/orchestrator.test.d.ts.map +1 -0
  171. package/dist/ralphthon/__tests__/orchestrator.test.js +309 -0
  172. package/dist/ralphthon/__tests__/orchestrator.test.js.map +1 -0
  173. package/dist/ralphthon/__tests__/prd.test.d.ts +2 -0
  174. package/dist/ralphthon/__tests__/prd.test.d.ts.map +1 -0
  175. package/dist/ralphthon/__tests__/prd.test.js +133 -0
  176. package/dist/ralphthon/__tests__/prd.test.js.map +1 -0
  177. package/dist/ralphthon/bootstrap.d.ts +3 -0
  178. package/dist/ralphthon/bootstrap.d.ts.map +1 -0
  179. package/dist/ralphthon/bootstrap.js +84 -0
  180. package/dist/ralphthon/bootstrap.js.map +1 -0
  181. package/dist/ralphthon/orchestrator.d.ts +50 -0
  182. package/dist/ralphthon/orchestrator.d.ts.map +1 -0
  183. package/dist/ralphthon/orchestrator.js +362 -0
  184. package/dist/ralphthon/orchestrator.js.map +1 -0
  185. package/dist/ralphthon/prd.d.ts +191 -0
  186. package/dist/ralphthon/prd.d.ts.map +1 -0
  187. package/dist/ralphthon/prd.js +355 -0
  188. package/dist/ralphthon/prd.js.map +1 -0
  189. package/dist/ralphthon/runtime.d.ts +31 -0
  190. package/dist/ralphthon/runtime.d.ts.map +1 -0
  191. package/dist/ralphthon/runtime.js +104 -0
  192. package/dist/ralphthon/runtime.js.map +1 -0
  193. package/dist/ralphthon/tmux.d.ts +3 -0
  194. package/dist/ralphthon/tmux.d.ts.map +1 -0
  195. package/dist/ralphthon/tmux.js +39 -0
  196. package/dist/ralphthon/tmux.js.map +1 -0
  197. package/dist/subagents/__tests__/tracker.test.d.ts +2 -0
  198. package/dist/subagents/__tests__/tracker.test.d.ts.map +1 -0
  199. package/dist/subagents/__tests__/tracker.test.js +47 -0
  200. package/dist/subagents/__tests__/tracker.test.js.map +1 -0
  201. package/dist/subagents/tracker.d.ts +52 -0
  202. package/dist/subagents/tracker.d.ts.map +1 -0
  203. package/dist/subagents/tracker.js +175 -0
  204. package/dist/subagents/tracker.js.map +1 -0
  205. package/dist/team/__tests__/worker-bootstrap.test.js +189 -163
  206. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  207. package/dist/team/__tests__/worktree.test.js +1 -1
  208. package/dist/team/__tests__/worktree.test.js.map +1 -1
  209. package/dist/team/worker-bootstrap.d.ts +1 -1
  210. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  211. package/dist/team/worker-bootstrap.js +58 -63
  212. package/dist/team/worker-bootstrap.js.map +1 -1
  213. package/dist/team/worktree.js +1 -1
  214. package/dist/team/worktree.js.map +1 -1
  215. package/dist/utils/__tests__/agents-md.test.d.ts +2 -0
  216. package/dist/utils/__tests__/agents-md.test.d.ts.map +1 -0
  217. package/dist/utils/__tests__/agents-md.test.js +32 -0
  218. package/dist/utils/__tests__/agents-md.test.js.map +1 -0
  219. package/dist/utils/__tests__/agents-model-table.test.d.ts +2 -0
  220. package/dist/utils/__tests__/agents-model-table.test.d.ts.map +1 -0
  221. package/dist/utils/__tests__/agents-model-table.test.js +84 -0
  222. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -0
  223. package/dist/utils/__tests__/paths.test.js +78 -83
  224. package/dist/utils/__tests__/paths.test.js.map +1 -1
  225. package/dist/utils/agents-md.d.ts.map +1 -1
  226. package/dist/utils/agents-md.js +10 -0
  227. package/dist/utils/agents-md.js.map +1 -1
  228. package/dist/utils/agents-model-table.d.ts +16 -0
  229. package/dist/utils/agents-model-table.d.ts.map +1 -0
  230. package/dist/utils/agents-model-table.js +83 -0
  231. package/dist/utils/agents-model-table.js.map +1 -0
  232. package/dist/utils/paths.d.ts +6 -6
  233. package/dist/utils/paths.d.ts.map +1 -1
  234. package/dist/utils/paths.js +31 -31
  235. package/dist/utils/paths.js.map +1 -1
  236. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +21 -3
  237. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  238. package/dist/verification/__tests__/native-release-manifest.test.d.ts +2 -0
  239. package/dist/verification/__tests__/native-release-manifest.test.d.ts.map +1 -0
  240. package/dist/verification/__tests__/native-release-manifest.test.js +80 -0
  241. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -0
  242. package/package.json +1 -1
  243. package/prompts/executor.md +15 -0
  244. package/scripts/__tests__/smoke-packed-install.test.mjs +137 -8
  245. package/scripts/eval-adaptive-sort-optimization.py +24 -0
  246. package/scripts/eval-in-action-cat-shellout-demo.js +31 -0
  247. package/scripts/eval-ml-kaggle-model-optimization.py +29 -0
  248. package/scripts/eval-noisy-bayesopt-highdim.py +44 -0
  249. package/scripts/eval-noisy-latent-subspace-discovery.py +44 -0
  250. package/scripts/generate-native-release-manifest.mjs +14 -3
  251. package/scripts/notify-fallback-watcher.js +308 -6
  252. package/scripts/notify-hook.js +20 -0
  253. package/scripts/run-autoresearch-showcase.sh +75 -0
  254. package/scripts/smoke-packed-install.mjs +142 -10
  255. package/skills/deep-interview/SKILL.md +30 -1
  256. package/skills/omx-setup/SKILL.md +2 -2
  257. package/skills/skill/SKILL.md +32 -32
  258. package/skills/team/SKILL.md +6 -0
  259. package/skills/worker/SKILL.md +2 -2
  260. package/templates/AGENTS.md +97 -16
@@ -1,640 +1,760 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- import { normalizeCodexLaunchArgs, buildTmuxShellCommand, buildTmuxPaneCommand, buildWindowsPromptCommand, buildTmuxSessionName, resolveCliInvocation, commandOwnsLocalHelp, resolveCodexLaunchPolicy, classifyCodexExecFailure, resolveSignalExitCode, parseTmuxPaneSnapshot, findHudWatchPaneIds, buildHudPaneCleanupTargets, readTopLevelTomlString, upsertTopLevelTomlString, collectInheritableTeamWorkerArgs, resolveTeamWorkerLaunchArgsEnv, injectModelInstructionsBypassArgs, resolveWorkerSparkModel, resolveSetupScopeArg, resolveSetupSkillTargetArg, readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexHomeForLaunch, buildDetachedSessionBootstrapSteps, buildDetachedSessionFinalizeSteps, buildDetachedSessionRollbackSteps, resolveNotifyTempContract, buildNotifyTempStartupMessages, } from '../index.js';
7
- import { HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
8
- import { DEFAULT_FRONTIER_MODEL, getTeamLowComplexityModel } from '../../config/models.js';
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { normalizeCodexLaunchArgs, buildTmuxShellCommand, buildTmuxPaneCommand, buildWindowsPromptCommand, buildTmuxSessionName, resolveCliInvocation, commandOwnsLocalHelp, resolveCodexLaunchPolicy, classifyCodexExecFailure, resolveSignalExitCode, parseTmuxPaneSnapshot, findHudWatchPaneIds, buildHudPaneCleanupTargets, readTopLevelTomlString, upsertTopLevelTomlString, collectInheritableTeamWorkerArgs, resolveTeamWorkerLaunchArgsEnv, injectModelInstructionsBypassArgs, resolveWorkerSparkModel, resolveSetupScopeArg, readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexHomeForLaunch, buildDetachedSessionBootstrapSteps, buildDetachedSessionFinalizeSteps, buildDetachedSessionRollbackSteps, resolveNotifyTempContract, buildNotifyTempStartupMessages, } from "../index.js";
7
+ import { HUD_TMUX_HEIGHT_LINES } from "../../hud/constants.js";
8
+ import { DEFAULT_FRONTIER_MODEL, getTeamLowComplexityModel, } from "../../config/models.js";
9
9
  function expectedLowComplexityModel(codexHomeOverride) {
10
10
  return getTeamLowComplexityModel(codexHomeOverride);
11
11
  }
12
- describe('normalizeCodexLaunchArgs', () => {
13
- it('maps --madmax to codex bypass flag', () => {
14
- assert.deepEqual(normalizeCodexLaunchArgs(['--madmax']), ['--dangerously-bypass-approvals-and-sandbox']);
12
+ describe("normalizeCodexLaunchArgs", () => {
13
+ it("maps --madmax to codex bypass flag", () => {
14
+ assert.deepEqual(normalizeCodexLaunchArgs(["--madmax"]), [
15
+ "--dangerously-bypass-approvals-and-sandbox",
16
+ ]);
15
17
  });
16
- it('does not forward --madmax and preserves other args', () => {
17
- assert.deepEqual(normalizeCodexLaunchArgs(['--model', 'gpt-5', '--madmax', '--yolo']), ['--model', 'gpt-5', '--yolo', '--dangerously-bypass-approvals-and-sandbox']);
18
+ it("does not forward --madmax and preserves other args", () => {
19
+ assert.deepEqual(normalizeCodexLaunchArgs(["--model", "gpt-5", "--madmax", "--yolo"]), [
20
+ "--model",
21
+ "gpt-5",
22
+ "--yolo",
23
+ "--dangerously-bypass-approvals-and-sandbox",
24
+ ]);
18
25
  });
19
- it('avoids duplicate bypass flags when both are present', () => {
26
+ it("avoids duplicate bypass flags when both are present", () => {
20
27
  assert.deepEqual(normalizeCodexLaunchArgs([
21
- '--dangerously-bypass-approvals-and-sandbox',
22
- '--madmax',
23
- ]), ['--dangerously-bypass-approvals-and-sandbox']);
28
+ "--dangerously-bypass-approvals-and-sandbox",
29
+ "--madmax",
30
+ ]), ["--dangerously-bypass-approvals-and-sandbox"]);
24
31
  });
25
- it('deduplicates repeated bypass-related flags', () => {
32
+ it("deduplicates repeated bypass-related flags", () => {
26
33
  assert.deepEqual(normalizeCodexLaunchArgs([
27
- '--madmax',
28
- '--dangerously-bypass-approvals-and-sandbox',
29
- '--madmax',
30
- '--dangerously-bypass-approvals-and-sandbox',
31
- ]), ['--dangerously-bypass-approvals-and-sandbox']);
32
- });
33
- it('leaves unrelated args unchanged', () => {
34
- assert.deepEqual(normalizeCodexLaunchArgs(['--model', 'gpt-5', '--yolo']), ['--model', 'gpt-5', '--yolo']);
35
- });
36
- it('maps --high to reasoning override', () => {
37
- assert.deepEqual(normalizeCodexLaunchArgs(['--high']), ['-c', 'model_reasoning_effort="high"']);
34
+ "--madmax",
35
+ "--dangerously-bypass-approvals-and-sandbox",
36
+ "--madmax",
37
+ "--dangerously-bypass-approvals-and-sandbox",
38
+ ]), ["--dangerously-bypass-approvals-and-sandbox"]);
39
+ });
40
+ it("leaves unrelated args unchanged", () => {
41
+ assert.deepEqual(normalizeCodexLaunchArgs(["--model", "gpt-5", "--yolo"]), [
42
+ "--model",
43
+ "gpt-5",
44
+ "--yolo",
45
+ ]);
38
46
  });
39
- it('maps --xhigh to reasoning override', () => {
40
- assert.deepEqual(normalizeCodexLaunchArgs(['--xhigh']), ['-c', 'model_reasoning_effort="xhigh"']);
47
+ it("maps --high to reasoning override", () => {
48
+ assert.deepEqual(normalizeCodexLaunchArgs(["--high"]), [
49
+ "-c",
50
+ 'model_reasoning_effort="high"',
51
+ ]);
41
52
  });
42
- it('uses the last reasoning shorthand when both are present', () => {
43
- assert.deepEqual(normalizeCodexLaunchArgs(['--high', '--xhigh']), ['-c', 'model_reasoning_effort="xhigh"']);
53
+ it("maps --xhigh to reasoning override", () => {
54
+ assert.deepEqual(normalizeCodexLaunchArgs(["--xhigh"]), [
55
+ "-c",
56
+ 'model_reasoning_effort="xhigh"',
57
+ ]);
44
58
  });
45
- it('maps --xhigh --madmax to codex-native flags only', () => {
46
- assert.deepEqual(normalizeCodexLaunchArgs(['--xhigh', '--madmax']), ['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="xhigh"']);
59
+ it("uses the last reasoning shorthand when both are present", () => {
60
+ assert.deepEqual(normalizeCodexLaunchArgs(["--high", "--xhigh"]), [
61
+ "-c",
62
+ 'model_reasoning_effort="xhigh"',
63
+ ]);
47
64
  });
48
- it('--spark is stripped from leader args (model goes to workers only)', () => {
49
- assert.deepEqual(normalizeCodexLaunchArgs(['--spark', '--yolo']), ['--yolo']);
65
+ it("maps --xhigh --madmax to codex-native flags only", () => {
66
+ assert.deepEqual(normalizeCodexLaunchArgs(["--xhigh", "--madmax"]), [
67
+ "--dangerously-bypass-approvals-and-sandbox",
68
+ "-c",
69
+ 'model_reasoning_effort="xhigh"',
70
+ ]);
50
71
  });
51
- it('--spark alone produces no leader args', () => {
52
- assert.deepEqual(normalizeCodexLaunchArgs(['--spark']), []);
72
+ it("--spark is stripped from leader args (model goes to workers only)", () => {
73
+ assert.deepEqual(normalizeCodexLaunchArgs(["--spark", "--yolo"]), [
74
+ "--yolo",
75
+ ]);
53
76
  });
54
- it('--madmax-spark adds bypass flag to leader args and is otherwise consumed', () => {
55
- assert.deepEqual(normalizeCodexLaunchArgs(['--madmax-spark']), ['--dangerously-bypass-approvals-and-sandbox']);
77
+ it("--spark alone produces no leader args", () => {
78
+ assert.deepEqual(normalizeCodexLaunchArgs(["--spark"]), []);
56
79
  });
57
- it('--madmax-spark deduplicates bypass when --madmax also present', () => {
58
- assert.deepEqual(normalizeCodexLaunchArgs(['--madmax', '--madmax-spark']), ['--dangerously-bypass-approvals-and-sandbox']);
80
+ it("--madmax-spark adds bypass flag to leader args and is otherwise consumed", () => {
81
+ assert.deepEqual(normalizeCodexLaunchArgs(["--madmax-spark"]), [
82
+ "--dangerously-bypass-approvals-and-sandbox",
83
+ ]);
59
84
  });
60
- it('--madmax-spark does not inject spark model into leader args', () => {
61
- const args = normalizeCodexLaunchArgs(['--madmax-spark']);
62
- assert.ok(!args.includes('--model'), 'leader args must not contain --model from --madmax-spark');
63
- assert.ok(!args.some(a => a.includes('spark')), 'leader args must not reference spark model');
85
+ it("--madmax-spark deduplicates bypass when --madmax also present", () => {
86
+ assert.deepEqual(normalizeCodexLaunchArgs(["--madmax", "--madmax-spark"]), [
87
+ "--dangerously-bypass-approvals-and-sandbox",
88
+ ]);
64
89
  });
65
- it('strips detached worktree flag from leader codex args', () => {
66
- assert.deepEqual(normalizeCodexLaunchArgs(['--worktree', '--yolo']), ['--yolo']);
90
+ it("--madmax-spark does not inject spark model into leader args", () => {
91
+ const args = normalizeCodexLaunchArgs(["--madmax-spark"]);
92
+ assert.ok(!args.includes("--model"), "leader args must not contain --model from --madmax-spark");
93
+ assert.ok(!args.some((a) => a.includes("spark")), "leader args must not reference spark model");
67
94
  });
68
- it('strips named worktree flag from leader codex args', () => {
69
- assert.deepEqual(normalizeCodexLaunchArgs(['--worktree=feature/demo', '--model', 'gpt-5']), ['--model', 'gpt-5']);
95
+ it("strips detached worktree flag from leader codex args", () => {
96
+ assert.deepEqual(normalizeCodexLaunchArgs(["--worktree", "--yolo"]), [
97
+ "--yolo",
98
+ ]);
70
99
  });
71
- it('does not forward notify-temp flags/selectors to leader codex args', () => {
72
- const parsed = resolveNotifyTempContract(['--notify-temp', '--discord', '--custom', 'openclaw:ops', '--custom=my-hook', '--model', 'gpt-5'], {});
73
- assert.deepEqual(normalizeCodexLaunchArgs(parsed.passthroughArgs), ['--model', 'gpt-5']);
100
+ it("strips named worktree flag from leader codex args", () => {
101
+ assert.deepEqual(normalizeCodexLaunchArgs(["--worktree=feature/demo", "--model", "gpt-5"]), ["--model", "gpt-5"]);
102
+ });
103
+ it("does not forward notify-temp flags/selectors to leader codex args", () => {
104
+ const parsed = resolveNotifyTempContract([
105
+ "--notify-temp",
106
+ "--discord",
107
+ "--custom",
108
+ "openclaw:ops",
109
+ "--custom=my-hook",
110
+ "--model",
111
+ "gpt-5",
112
+ ], {});
113
+ assert.deepEqual(normalizeCodexLaunchArgs(parsed.passthroughArgs), [
114
+ "--model",
115
+ "gpt-5",
116
+ ]);
74
117
  });
75
118
  });
76
- describe('resolveNotifyTempContract', () => {
77
- it('activates from --notify-temp with no providers', () => {
78
- const parsed = resolveNotifyTempContract(['--notify-temp', '--model', 'gpt-5'], {});
119
+ describe("resolveNotifyTempContract", () => {
120
+ it("activates from --notify-temp with no providers", () => {
121
+ const parsed = resolveNotifyTempContract(["--notify-temp", "--model", "gpt-5"], {});
79
122
  assert.equal(parsed.contract.active, true);
80
- assert.equal(parsed.contract.source, 'cli');
123
+ assert.equal(parsed.contract.source, "cli");
81
124
  assert.deepEqual(parsed.contract.canonicalSelectors, []);
82
- assert.deepEqual(parsed.passthroughArgs, ['--model', 'gpt-5']);
125
+ assert.deepEqual(parsed.passthroughArgs, ["--model", "gpt-5"]);
83
126
  });
84
- it('auto-activates when provider selectors are present', () => {
85
- const parsed = resolveNotifyTempContract(['--discord', '--slack'], {});
127
+ it("auto-activates when provider selectors are present", () => {
128
+ const parsed = resolveNotifyTempContract(["--discord", "--slack"], {});
86
129
  assert.equal(parsed.contract.active, true);
87
- assert.equal(parsed.contract.source, 'providers');
88
- assert.deepEqual(parsed.contract.canonicalSelectors, ['discord', 'slack']);
89
- assert.equal(parsed.contract.warnings.some((line) => line.includes('imply temp mode')), true);
90
- });
91
- it('supports repeated --custom forms and canonicalizes selectors', () => {
92
- const parsed = resolveNotifyTempContract(['--custom', 'OpenClaw:Ops', '--custom=my-hook', '--custom=', '--custom'], {});
93
- assert.deepEqual(parsed.contract.canonicalSelectors, ['openclaw:ops', 'custom:my-hook']);
130
+ assert.equal(parsed.contract.source, "providers");
131
+ assert.deepEqual(parsed.contract.canonicalSelectors, ["discord", "slack"]);
132
+ assert.equal(parsed.contract.warnings.some((line) => line.includes("imply temp mode")), true);
133
+ });
134
+ it("supports repeated --custom forms and canonicalizes selectors", () => {
135
+ const parsed = resolveNotifyTempContract(["--custom", "OpenClaw:Ops", "--custom=my-hook", "--custom=", "--custom"], {});
136
+ assert.deepEqual(parsed.contract.canonicalSelectors, [
137
+ "openclaw:ops",
138
+ "custom:my-hook",
139
+ ]);
94
140
  assert.equal(parsed.contract.warnings.length >= 1, true);
95
141
  });
96
- it('activates from OMX_NOTIFY_TEMP=1 env parity', () => {
97
- const parsed = resolveNotifyTempContract(['--model', 'gpt-5'], { OMX_NOTIFY_TEMP: '1' });
142
+ it("activates from OMX_NOTIFY_TEMP=1 env parity", () => {
143
+ const parsed = resolveNotifyTempContract(["--model", "gpt-5"], {
144
+ OMX_NOTIFY_TEMP: "1",
145
+ });
98
146
  assert.equal(parsed.contract.active, true);
99
- assert.equal(parsed.contract.source, 'env');
100
- assert.deepEqual(parsed.passthroughArgs, ['--model', 'gpt-5']);
147
+ assert.equal(parsed.contract.source, "env");
148
+ assert.deepEqual(parsed.passthroughArgs, ["--model", "gpt-5"]);
101
149
  });
102
150
  });
103
- describe('buildNotifyTempStartupMessages', () => {
104
- it('always emits summary when temp mode is active', () => {
151
+ describe("buildNotifyTempStartupMessages", () => {
152
+ it("always emits summary when temp mode is active", () => {
105
153
  const result = buildNotifyTempStartupMessages({
106
154
  active: true,
107
- selectors: ['discord'],
108
- canonicalSelectors: ['discord'],
155
+ selectors: ["discord"],
156
+ canonicalSelectors: ["discord"],
109
157
  warnings: [],
110
- source: 'cli',
158
+ source: "cli",
111
159
  }, true);
112
160
  assert.deepEqual(result.infoLines, [
113
- 'notify temp: active | providers=discord | persistent-routing=bypassed',
161
+ "notify temp: active | providers=discord | persistent-routing=bypassed",
114
162
  ]);
115
163
  assert.deepEqual(result.warningLines, []);
116
164
  });
117
- it('emits no-valid-provider warning when no provider is configured', () => {
165
+ it("emits no-valid-provider warning when no provider is configured", () => {
118
166
  const result = buildNotifyTempStartupMessages({
119
167
  active: true,
120
168
  selectors: [],
121
169
  canonicalSelectors: [],
122
- warnings: ['notify temp: provider selectors imply temp mode (auto-activated)'],
123
- source: 'providers',
170
+ warnings: [
171
+ "notify temp: provider selectors imply temp mode (auto-activated)",
172
+ ],
173
+ source: "providers",
124
174
  }, false);
125
- assert.equal(result.warningLines.includes('notify temp: no valid providers resolved; notifications skipped'), true);
175
+ assert.equal(result.warningLines.includes("notify temp: no valid providers resolved; notifications skipped"), true);
126
176
  });
127
177
  });
128
- describe('resolveWorkerSparkModel', () => {
129
- it('returns spark model string when --spark is present', () => {
130
- assert.equal(resolveWorkerSparkModel(['--spark', '--yolo']), expectedLowComplexityModel());
178
+ describe("resolveWorkerSparkModel", () => {
179
+ it("returns spark model string when --spark is present", () => {
180
+ assert.equal(resolveWorkerSparkModel(["--spark", "--yolo"]), expectedLowComplexityModel());
131
181
  });
132
- it('returns spark model string when --madmax-spark is present', () => {
133
- assert.equal(resolveWorkerSparkModel(['--madmax-spark']), expectedLowComplexityModel());
182
+ it("returns spark model string when --madmax-spark is present", () => {
183
+ assert.equal(resolveWorkerSparkModel(["--madmax-spark"]), expectedLowComplexityModel());
134
184
  });
135
- it('returns undefined when neither spark flag is present', () => {
136
- assert.equal(resolveWorkerSparkModel(['--madmax', '--yolo', '--model', 'gpt-5']), undefined);
185
+ it("returns undefined when neither spark flag is present", () => {
186
+ assert.equal(resolveWorkerSparkModel(["--madmax", "--yolo", "--model", "gpt-5"]), undefined);
137
187
  });
138
- it('returns undefined for empty args', () => {
188
+ it("returns undefined for empty args", () => {
139
189
  assert.equal(resolveWorkerSparkModel([]), undefined);
140
190
  });
141
- it('reads low-complexity team model from config when codexHomeOverride is provided', async () => {
142
- const codexHome = await mkdtemp(join(tmpdir(), 'omx-codex-home-'));
191
+ it("reads low-complexity team model from config when codexHomeOverride is provided", async () => {
192
+ const codexHome = await mkdtemp(join(tmpdir(), "omx-codex-home-"));
143
193
  try {
144
- await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({ models: { team_low_complexity: 'gpt-4.1-mini' } }));
145
- assert.equal(resolveWorkerSparkModel(['--spark'], codexHome), 'gpt-4.1-mini');
194
+ await writeFile(join(codexHome, ".omx-config.json"), JSON.stringify({ models: { team_low_complexity: "gpt-4.1-mini" } }));
195
+ assert.equal(resolveWorkerSparkModel(["--spark"], codexHome), "gpt-4.1-mini");
146
196
  }
147
197
  finally {
148
198
  await rm(codexHome, { recursive: true, force: true });
149
199
  }
150
200
  });
151
201
  });
152
- describe('resolveTeamWorkerLaunchArgsEnv (spark)', () => {
153
- it('injects spark model as worker default when no explicit env model', () => {
202
+ describe("resolveTeamWorkerLaunchArgsEnv (spark)", () => {
203
+ it("injects spark model as worker default when no explicit env model", () => {
154
204
  assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, [], true, expectedLowComplexityModel()), `--model ${expectedLowComplexityModel()}`);
155
205
  });
156
- it('explicit env model overrides spark default', () => {
157
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--model gpt-5', [], true, expectedLowComplexityModel()), '--model gpt-5');
206
+ it("explicit env model overrides spark default", () => {
207
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--model gpt-5", [], true, expectedLowComplexityModel()), "--model gpt-5");
158
208
  });
159
- it('inherited leader model overrides spark default', () => {
160
- assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, ['--model', 'gpt-4.1'], true, expectedLowComplexityModel()), '--model gpt-4.1');
209
+ it("inherited leader model overrides spark default", () => {
210
+ assert.equal(resolveTeamWorkerLaunchArgsEnv(undefined, ["--model", "gpt-4.1"], true, expectedLowComplexityModel()), "--model gpt-4.1");
161
211
  });
162
212
  });
163
- describe('commandOwnsLocalHelp', () => {
164
- it('returns true for nested commands that render their own help output', () => {
165
- for (const command of ['agents-init', 'ask', 'autoresearch', 'deepinit', 'hooks', 'hud', 'ralph', 'resume', 'session', 'sparkshell', 'team', 'tmux-hook']) {
213
+ describe("commandOwnsLocalHelp", () => {
214
+ it("returns true for nested commands that render their own help output", () => {
215
+ for (const command of [
216
+ "agents-init",
217
+ "ask",
218
+ "autoresearch",
219
+ "deepinit",
220
+ "hooks",
221
+ "hud",
222
+ "ralph",
223
+ "ralphthon",
224
+ "resume",
225
+ "session",
226
+ "sparkshell",
227
+ "team",
228
+ "tmux-hook",
229
+ ]) {
166
230
  assert.equal(commandOwnsLocalHelp(command), true, `expected ${command} to own local help`);
167
231
  }
168
232
  });
169
- it('returns false for top-level help-only commands', () => {
170
- for (const command of ['help', 'launch', 'version']) {
233
+ it("returns false for top-level help-only commands", () => {
234
+ for (const command of ["help", "launch", "version"]) {
171
235
  assert.equal(commandOwnsLocalHelp(command), false, `expected ${command} to use top-level help`);
172
236
  }
173
237
  });
174
238
  });
175
- describe('resolveCliInvocation', () => {
176
- it('resolves explore to explore command', () => {
177
- assert.deepEqual(resolveCliInvocation(['explore', '--prompt', 'find', 'auth']), {
178
- command: 'explore',
239
+ describe("resolveCliInvocation", () => {
240
+ it("resolves explore to explore command", () => {
241
+ assert.deepEqual(resolveCliInvocation(["explore", "--prompt", "find", "auth"]), {
242
+ command: "explore",
179
243
  launchArgs: [],
180
244
  });
181
245
  });
182
- it('resolves ask to ask command', () => {
183
- assert.deepEqual(resolveCliInvocation(['ask', 'claude', 'hello']), {
184
- command: 'ask',
246
+ it("resolves ask to ask command", () => {
247
+ assert.deepEqual(resolveCliInvocation(["ask", "claude", "hello"]), {
248
+ command: "ask",
185
249
  launchArgs: [],
186
250
  });
187
251
  });
188
- it('resolves autoresearch to autoresearch command', () => {
189
- assert.deepEqual(resolveCliInvocation(['autoresearch', 'missions/demo']), {
190
- command: 'autoresearch',
252
+ it("resolves autoresearch to autoresearch command", () => {
253
+ assert.deepEqual(resolveCliInvocation(["autoresearch", "missions/demo"]), {
254
+ command: "autoresearch",
191
255
  launchArgs: [],
192
256
  });
193
257
  });
194
- it('resolves session to session command', () => {
195
- assert.deepEqual(resolveCliInvocation(['session', 'search', 'startup evidence']), {
196
- command: 'session',
258
+ it("resolves ralphthon to ralphthon command", () => {
259
+ assert.deepEqual(resolveCliInvocation(["ralphthon", "--resume"]), {
260
+ command: "ralphthon",
197
261
  launchArgs: [],
198
262
  });
199
263
  });
200
- it('resolves resume to resume command and forwards trailing args', () => {
201
- assert.deepEqual(resolveCliInvocation(['resume', '--last']), {
202
- command: 'resume',
203
- launchArgs: ['--last'],
264
+ it("resolves session to session command", () => {
265
+ assert.deepEqual(resolveCliInvocation(["session", "search", "startup evidence"]), {
266
+ command: "session",
267
+ launchArgs: [],
204
268
  });
205
269
  });
206
- it('resolves resume session id and prompt as forwarded args', () => {
207
- assert.deepEqual(resolveCliInvocation(['resume', 'session-123', 'continue here']), {
208
- command: 'resume',
209
- launchArgs: ['session-123', 'continue here'],
270
+ it("resolves resume to resume command and forwards trailing args", () => {
271
+ assert.deepEqual(resolveCliInvocation(["resume", "--last"]), {
272
+ command: "resume",
273
+ launchArgs: ["--last"],
210
274
  });
211
275
  });
212
- it('resolves exec to non-interactive launch passthrough and forwards trailing args', () => {
213
- assert.deepEqual(resolveCliInvocation(['exec', '--model', 'gpt-5', 'say hi']), {
214
- command: 'exec',
215
- launchArgs: ['--model', 'gpt-5', 'say hi'],
276
+ it("resolves resume session id and prompt as forwarded args", () => {
277
+ assert.deepEqual(resolveCliInvocation(["resume", "session-123", "continue here"]), {
278
+ command: "resume",
279
+ launchArgs: ["session-123", "continue here"],
216
280
  });
217
281
  });
218
- it('resolves hooks to hooks command', () => {
219
- assert.deepEqual(resolveCliInvocation(['hooks']), {
220
- command: 'hooks',
221
- launchArgs: [],
282
+ it("resolves exec to non-interactive launch passthrough and forwards trailing args", () => {
283
+ assert.deepEqual(resolveCliInvocation(["exec", "--model", "gpt-5", "say hi"]), {
284
+ command: "exec",
285
+ launchArgs: ["--model", "gpt-5", "say hi"],
222
286
  });
223
287
  });
224
- it('resolves agents-init to agents-init command', () => {
225
- assert.deepEqual(resolveCliInvocation(['agents-init', '.']), {
226
- command: 'agents-init',
288
+ it("resolves hooks to hooks command", () => {
289
+ assert.deepEqual(resolveCliInvocation(["hooks"]), {
290
+ command: "hooks",
227
291
  launchArgs: [],
228
292
  });
229
293
  });
230
- it('resolves deepinit to deepinit alias command', () => {
231
- assert.deepEqual(resolveCliInvocation(['deepinit', 'src']), {
232
- command: 'deepinit',
294
+ it("resolves agents-init to agents-init command", () => {
295
+ assert.deepEqual(resolveCliInvocation(["agents-init", "."]), {
296
+ command: "agents-init",
233
297
  launchArgs: [],
234
298
  });
235
299
  });
236
- it('resolves --help to the help command instead of launch', () => {
237
- assert.deepEqual(resolveCliInvocation(['--help']), {
238
- command: 'help',
300
+ it("resolves deepinit to deepinit alias command", () => {
301
+ assert.deepEqual(resolveCliInvocation(["deepinit", "src"]), {
302
+ command: "deepinit",
239
303
  launchArgs: [],
240
304
  });
241
305
  });
242
- it('resolves --version to the version command instead of launch', () => {
243
- assert.deepEqual(resolveCliInvocation(['--version']), {
244
- command: 'version',
306
+ it("resolves --help to the help command instead of launch", () => {
307
+ assert.deepEqual(resolveCliInvocation(["--help"]), {
308
+ command: "help",
245
309
  launchArgs: [],
246
310
  });
247
311
  });
248
- it('resolves -v to the version command instead of launch', () => {
249
- assert.deepEqual(resolveCliInvocation(['-v']), {
250
- command: 'version',
312
+ it("resolves --version to the version command instead of launch", () => {
313
+ assert.deepEqual(resolveCliInvocation(["--version"]), {
314
+ command: "version",
251
315
  launchArgs: [],
252
316
  });
253
317
  });
254
- it('keeps unknown long flags as launch passthrough args', () => {
255
- assert.deepEqual(resolveCliInvocation(['--model', 'gpt-5']), {
256
- command: 'launch',
257
- launchArgs: ['--model', 'gpt-5'],
318
+ it("resolves -v to the version command instead of launch", () => {
319
+ assert.deepEqual(resolveCliInvocation(["-v"]), {
320
+ command: "version",
321
+ launchArgs: [],
258
322
  });
259
323
  });
260
- });
261
- describe('resolveSetupScopeArg', () => {
262
- it('returns undefined when scope is omitted', () => {
263
- assert.equal(resolveSetupScopeArg(['--dry-run']), undefined);
264
- });
265
- it('parses --scope <value> form', () => {
266
- assert.equal(resolveSetupScopeArg(['--dry-run', '--scope', 'project']), 'project');
267
- });
268
- it('parses --scope=<value> form', () => {
269
- assert.equal(resolveSetupScopeArg(['--scope=project']), 'project');
270
- });
271
- it('throws on invalid scope value', () => {
272
- assert.throws(() => resolveSetupScopeArg(['--scope', 'workspace']), /Invalid setup scope: workspace/);
273
- });
274
- it('throws when --scope value is missing', () => {
275
- assert.throws(() => resolveSetupScopeArg(['--scope']), /Missing setup scope value after --scope/);
324
+ it("keeps unknown long flags as launch passthrough args", () => {
325
+ assert.deepEqual(resolveCliInvocation(["--model", "gpt-5"]), {
326
+ command: "launch",
327
+ launchArgs: ["--model", "gpt-5"],
328
+ });
276
329
  });
277
330
  });
278
- describe('resolveSetupSkillTargetArg', () => {
279
- it('returns undefined when skill target is omitted', () => {
280
- assert.equal(resolveSetupSkillTargetArg(['--dry-run']), undefined);
331
+ describe("resolveSetupScopeArg", () => {
332
+ it("returns undefined when scope is omitted", () => {
333
+ assert.equal(resolveSetupScopeArg(["--dry-run"]), undefined);
281
334
  });
282
- it('parses --skill-target <value> form', () => {
283
- assert.equal(resolveSetupSkillTargetArg(['--skill-target', 'agents']), 'agents');
335
+ it("parses --scope <value> form", () => {
336
+ assert.equal(resolveSetupScopeArg(["--dry-run", "--scope", "project"]), "project");
284
337
  });
285
- it('parses --skill-target=<value> form', () => {
286
- assert.equal(resolveSetupSkillTargetArg(['--skill-target=codex-home']), 'codex-home');
338
+ it("parses --scope=<value> form", () => {
339
+ assert.equal(resolveSetupScopeArg(["--scope=project"]), "project");
287
340
  });
288
- it('throws on invalid skill target value', () => {
289
- assert.throws(() => resolveSetupSkillTargetArg(['--skill-target', 'workspace']), /Invalid setup skill target: workspace/);
341
+ it("throws on invalid scope value", () => {
342
+ assert.throws(() => resolveSetupScopeArg(["--scope", "workspace"]), /Invalid setup scope: workspace/);
290
343
  });
291
- it('throws when --skill-target value is missing', () => {
292
- assert.throws(() => resolveSetupSkillTargetArg(['--skill-target']), /Missing setup skill target value after --skill-target/);
344
+ it("throws when --scope value is missing", () => {
345
+ assert.throws(() => resolveSetupScopeArg(["--scope"]), /Missing setup scope value after --scope/);
293
346
  });
294
347
  });
295
- describe('project launch scope helpers', () => {
296
- it('reads persisted setup scope when valid', async () => {
297
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
348
+ describe("project launch scope helpers", () => {
349
+ it("reads persisted setup scope when valid", async () => {
350
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
298
351
  try {
299
- await mkdir(join(wd, '.omx'), { recursive: true });
300
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
301
- assert.equal(readPersistedSetupScope(wd), 'project');
352
+ await mkdir(join(wd, ".omx"), { recursive: true });
353
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
354
+ assert.equal(readPersistedSetupScope(wd), "project");
302
355
  }
303
356
  finally {
304
357
  await rm(wd, { recursive: true, force: true });
305
358
  }
306
359
  });
307
- it('reads persisted setup preferences when skill target is present', async () => {
308
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
360
+ it("reads persisted setup preferences when skill target is present", async () => {
361
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
309
362
  try {
310
- await mkdir(join(wd, '.omx'), { recursive: true });
311
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'user', skillTarget: 'agents' }));
363
+ await mkdir(join(wd, ".omx"), { recursive: true });
364
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "user" }));
312
365
  assert.deepEqual(readPersistedSetupPreferences(wd), {
313
- scope: 'user',
314
- skillTarget: 'agents',
366
+ scope: "user",
315
367
  });
316
368
  }
317
369
  finally {
318
370
  await rm(wd, { recursive: true, force: true });
319
371
  }
320
372
  });
321
- it('ignores malformed persisted setup scope', async () => {
322
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
373
+ it("ignores malformed persisted setup scope", async () => {
374
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
323
375
  try {
324
- await mkdir(join(wd, '.omx'), { recursive: true });
325
- await writeFile(join(wd, '.omx', 'setup-scope.json'), '{not-json');
376
+ await mkdir(join(wd, ".omx"), { recursive: true });
377
+ await writeFile(join(wd, ".omx", "setup-scope.json"), "{not-json");
326
378
  assert.equal(readPersistedSetupScope(wd), undefined);
327
379
  }
328
380
  finally {
329
381
  await rm(wd, { recursive: true, force: true });
330
382
  }
331
383
  });
332
- it('uses project CODEX_HOME when persisted scope is project', async () => {
333
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
384
+ it("uses project CODEX_HOME when persisted scope is project", async () => {
385
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
334
386
  try {
335
- await mkdir(join(wd, '.omx'), { recursive: true });
336
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
337
- assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, '.codex'));
387
+ await mkdir(join(wd, ".omx"), { recursive: true });
388
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
389
+ assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, ".codex"));
338
390
  }
339
391
  finally {
340
392
  await rm(wd, { recursive: true, force: true });
341
393
  }
342
394
  });
343
- it('keeps explicit CODEX_HOME override from env', async () => {
344
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
395
+ it("keeps explicit CODEX_HOME override from env", async () => {
396
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
345
397
  try {
346
- await mkdir(join(wd, '.omx'), { recursive: true });
347
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project' }));
348
- assert.equal(resolveCodexHomeForLaunch(wd, { CODEX_HOME: '/tmp/explicit-codex-home' }), '/tmp/explicit-codex-home');
398
+ await mkdir(join(wd, ".omx"), { recursive: true });
399
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project" }));
400
+ assert.equal(resolveCodexHomeForLaunch(wd, {
401
+ CODEX_HOME: "/tmp/explicit-codex-home",
402
+ }), "/tmp/explicit-codex-home");
349
403
  }
350
404
  finally {
351
405
  await rm(wd, { recursive: true, force: true });
352
406
  }
353
407
  });
354
408
  it('migrates legacy "project-local" persisted scope to "project"', async () => {
355
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
409
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
356
410
  try {
357
- await mkdir(join(wd, '.omx'), { recursive: true });
358
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project-local' }));
359
- assert.equal(readPersistedSetupScope(wd), 'project');
411
+ await mkdir(join(wd, ".omx"), { recursive: true });
412
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project-local" }));
413
+ assert.equal(readPersistedSetupScope(wd), "project");
360
414
  }
361
415
  finally {
362
416
  await rm(wd, { recursive: true, force: true });
363
417
  }
364
418
  });
365
419
  it('resolves CODEX_HOME for legacy "project-local" persisted scope', async () => {
366
- const wd = await mkdtemp(join(tmpdir(), 'omx-launch-scope-'));
420
+ const wd = await mkdtemp(join(tmpdir(), "omx-launch-scope-"));
367
421
  try {
368
- await mkdir(join(wd, '.omx'), { recursive: true });
369
- await writeFile(join(wd, '.omx', 'setup-scope.json'), JSON.stringify({ scope: 'project-local' }));
370
- assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, '.codex'));
422
+ await mkdir(join(wd, ".omx"), { recursive: true });
423
+ await writeFile(join(wd, ".omx", "setup-scope.json"), JSON.stringify({ scope: "project-local" }));
424
+ assert.equal(resolveCodexHomeForLaunch(wd, {}), join(wd, ".codex"));
371
425
  }
372
426
  finally {
373
427
  await rm(wd, { recursive: true, force: true });
374
428
  }
375
429
  });
376
430
  });
377
- describe('resolveCodexLaunchPolicy', () => {
378
- it('uses detached tmux on macOS when outside tmux and tmux is available', () => {
379
- assert.equal(resolveCodexLaunchPolicy({}, 'darwin', true), 'detached-tmux');
431
+ describe("resolveCodexLaunchPolicy", () => {
432
+ it("uses detached tmux on macOS when outside tmux and tmux is available", () => {
433
+ assert.equal(resolveCodexLaunchPolicy({}, "darwin", true), "detached-tmux");
380
434
  });
381
- it('uses tmux-aware launch path when already inside tmux', () => {
382
- assert.equal(resolveCodexLaunchPolicy({ TMUX: '/tmp/tmux-1000/default,123,0' }, 'darwin', true), 'inside-tmux');
435
+ it("uses tmux-aware launch path when already inside tmux", () => {
436
+ assert.equal(resolveCodexLaunchPolicy({ TMUX: "/tmp/tmux-1000/default,123,0" }, "darwin", true), "inside-tmux");
383
437
  });
384
- it('uses detached tmux on non-macOS hosts when outside tmux and tmux is available', () => {
385
- assert.equal(resolveCodexLaunchPolicy({}, 'linux', true), 'detached-tmux');
438
+ it("uses detached tmux on non-macOS hosts when outside tmux and tmux is available", () => {
439
+ assert.equal(resolveCodexLaunchPolicy({}, "linux", true), "detached-tmux");
386
440
  });
387
- it('launches directly when tmux is unavailable outside tmux', () => {
388
- assert.equal(resolveCodexLaunchPolicy({}, 'linux', false), 'direct');
441
+ it("launches directly when tmux is unavailable outside tmux", () => {
442
+ assert.equal(resolveCodexLaunchPolicy({}, "linux", false), "direct");
389
443
  });
390
444
  });
391
- describe('classifyCodexExecFailure', () => {
392
- it('classifies child process exit status as codex exit', () => {
393
- const err = Object.assign(new Error('codex exited 9'), { status: 9 });
445
+ describe("classifyCodexExecFailure", () => {
446
+ it("classifies child process exit status as codex exit", () => {
447
+ const err = Object.assign(new Error("codex exited 9"), { status: 9 });
394
448
  const classified = classifyCodexExecFailure(err);
395
- assert.equal(classified.kind, 'exit');
449
+ assert.equal(classified.kind, "exit");
396
450
  assert.equal(classified.exitCode, 9);
397
451
  });
398
- it('classifies signal termination as codex exit and maps to signal-based exit code', () => {
399
- const err = Object.assign(new Error('terminated'), { status: null, signal: 'SIGTERM' });
452
+ it("classifies signal termination as codex exit and maps to signal-based exit code", () => {
453
+ const err = Object.assign(new Error("terminated"), {
454
+ status: null,
455
+ signal: "SIGTERM",
456
+ });
400
457
  const classified = classifyCodexExecFailure(err);
401
- assert.equal(classified.kind, 'exit');
402
- assert.equal(classified.signal, 'SIGTERM');
403
- assert.equal(classified.exitCode, resolveSignalExitCode('SIGTERM'));
458
+ assert.equal(classified.kind, "exit");
459
+ assert.equal(classified.signal, "SIGTERM");
460
+ assert.equal(classified.exitCode, resolveSignalExitCode("SIGTERM"));
404
461
  });
405
- it('classifies ENOENT as launch error', () => {
406
- const err = Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' });
462
+ it("classifies ENOENT as launch error", () => {
463
+ const err = Object.assign(new Error("spawn codex ENOENT"), {
464
+ code: "ENOENT",
465
+ });
407
466
  const classified = classifyCodexExecFailure(err);
408
- assert.equal(classified.kind, 'launch-error');
409
- assert.equal(classified.code, 'ENOENT');
467
+ assert.equal(classified.kind, "launch-error");
468
+ assert.equal(classified.code, "ENOENT");
410
469
  });
411
470
  });
412
- describe('tmux HUD pane helpers', () => {
413
- it('findHudWatchPaneIds detects stale HUD watch panes and excludes current pane', () => {
471
+ describe("tmux HUD pane helpers", () => {
472
+ it("findHudWatchPaneIds detects stale HUD watch panes and excludes current pane", () => {
414
473
  const panes = parseTmuxPaneSnapshot([
415
- '%1\tzsh\tzsh',
416
- '%2\tnode\tnode /tmp/bin/omx.js hud --watch',
417
- '%3\tnode\tnode /tmp/bin/omx.js hud --watch',
418
- '%4\tcodex\tcodex --model gpt-5',
419
- ].join('\n'));
420
- assert.deepEqual(findHudWatchPaneIds(panes, '%2'), ['%3']);
474
+ "%1\tzsh\tzsh",
475
+ "%2\tnode\tnode /tmp/bin/omx.js hud --watch",
476
+ "%3\tnode\tnode /tmp/bin/omx.js hud --watch",
477
+ "%4\tcodex\tcodex --model gpt-5",
478
+ ].join("\n"));
479
+ assert.deepEqual(findHudWatchPaneIds(panes, "%2"), ["%3"]);
421
480
  });
422
- it('buildHudPaneCleanupTargets de-dupes pane ids and includes created pane', () => {
423
- assert.deepEqual(buildHudPaneCleanupTargets(['%3', '%3', 'invalid'], '%4'), ['%3', '%4']);
481
+ it("buildHudPaneCleanupTargets de-dupes pane ids and includes created pane", () => {
482
+ assert.deepEqual(buildHudPaneCleanupTargets(["%3", "%3", "invalid"], "%4"), ["%3", "%4"]);
424
483
  });
425
- it('buildHudPaneCleanupTargets excludes leader pane from existing ids', () => {
484
+ it("buildHudPaneCleanupTargets excludes leader pane from existing ids", () => {
426
485
  // %5 is the leader pane — it must not be included even if findHudWatchPaneIds let it through.
427
- assert.deepEqual(buildHudPaneCleanupTargets(['%3', '%5'], '%4', '%5'), ['%3', '%4']);
486
+ assert.deepEqual(buildHudPaneCleanupTargets(["%3", "%5"], "%4", "%5"), [
487
+ "%3",
488
+ "%4",
489
+ ]);
428
490
  });
429
- it('buildHudPaneCleanupTargets excludes leader pane even when it matches the created HUD pane id', () => {
491
+ it("buildHudPaneCleanupTargets excludes leader pane even when it matches the created HUD pane id", () => {
430
492
  // Defensive edge case: if createHudWatchPane somehow returned the leader pane id, guard protects it.
431
- assert.deepEqual(buildHudPaneCleanupTargets(['%3'], '%5', '%5'), ['%3']);
493
+ assert.deepEqual(buildHudPaneCleanupTargets(["%3"], "%5", "%5"), ["%3"]);
432
494
  });
433
- it('buildHudPaneCleanupTargets is a no-op guard when leaderPaneId is absent', () => {
434
- assert.deepEqual(buildHudPaneCleanupTargets(['%3'], '%4'), ['%3', '%4']);
495
+ it("buildHudPaneCleanupTargets is a no-op guard when leaderPaneId is absent", () => {
496
+ assert.deepEqual(buildHudPaneCleanupTargets(["%3"], "%4"), ["%3", "%4"]);
435
497
  });
436
498
  });
437
- describe('detached tmux new-session sequencing', () => {
438
- it('buildDetachedSessionBootstrapSteps uses shared HUD height and split-capture ordering', () => {
439
- const steps = buildDetachedSessionBootstrapSteps('omx-demo', '/tmp/project', "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", '--model gpt-5', '/tmp/codex-home', '{"active":true}');
440
- assert.deepEqual(steps.map((step) => step.name), ['new-session', 'split-and-capture-hud-pane']);
499
+ describe("detached tmux new-session sequencing", () => {
500
+ it("buildDetachedSessionBootstrapSteps uses shared HUD height and split-capture ordering", () => {
501
+ const steps = buildDetachedSessionBootstrapSteps("omx-demo", "/tmp/project", "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", "--model gpt-5", "/tmp/codex-home", '{"active":true}');
502
+ assert.deepEqual(steps.map((step) => step.name), ["new-session", "split-and-capture-hud-pane"]);
441
503
  assert.equal(steps[1]?.args[3], String(HUD_TMUX_HEIGHT_LINES));
442
- assert.equal(steps[1]?.args[6], 'omx-demo');
443
- assert.equal(steps[1]?.args.includes('-P'), true);
444
- assert.equal(steps[1]?.args.includes('#{pane_id}'), true);
445
- assert.equal(steps[0]?.args.includes('-e'), true);
504
+ assert.equal(steps[1]?.args[6], "omx-demo");
505
+ assert.equal(steps[1]?.args.includes("-P"), true);
506
+ assert.equal(steps[1]?.args.includes("#{pane_id}"), true);
507
+ assert.equal(steps[0]?.args.includes("-e"), true);
446
508
  assert.equal(steps[0]?.args.includes('OMX_NOTIFY_TEMP_CONTRACT={\"active\":true}'), true);
447
509
  });
448
- it('buildDetachedSessionBootstrapSteps forwards temp contract env to detached tmux session', () => {
449
- const steps = buildDetachedSessionBootstrapSteps('omx-demo', '/tmp/project', "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", null, undefined, '{"active":true,"canonicalSelectors":["discord"]}');
450
- const newSession = steps.find((step) => step.name === 'new-session');
510
+ it("buildDetachedSessionBootstrapSteps forwards temp contract env to detached tmux session", () => {
511
+ const steps = buildDetachedSessionBootstrapSteps("omx-demo", "/tmp/project", "'codex' '--model' 'gpt-5'", "'node' '/tmp/omx.js' 'hud' '--watch'", null, undefined, '{"active":true,"canonicalSelectors":["discord"]}');
512
+ const newSession = steps.find((step) => step.name === "new-session");
451
513
  assert.ok(newSession);
452
- assert.equal(newSession.args.includes('-e')
453
- && newSession.args.some((arg) => arg.startsWith('OMX_NOTIFY_TEMP_CONTRACT=')), true);
454
- });
455
- it('buildDetachedSessionBootstrapSteps starts native Windows detached sessions with powershell', () => {
456
- const hudCmd = buildWindowsPromptCommand('node', ['omx.js', 'hud', '--watch']);
457
- const steps = buildDetachedSessionBootstrapSteps('omx-demo', 'C:/project', "'codex' '--dangerously-bypass-approvals-and-sandbox'", hudCmd, '--model gpt-5', 'C:/codex-home', null, true);
458
- assert.equal(steps[0]?.name, 'new-session');
459
- assert.equal(steps[0]?.args.at(-1), 'powershell.exe');
460
- assert.equal(steps[1]?.name, 'split-and-capture-hud-pane');
514
+ assert.equal(newSession.args.includes("-e") &&
515
+ newSession.args.some((arg) => arg.startsWith("OMX_NOTIFY_TEMP_CONTRACT=")), true);
516
+ });
517
+ it("buildDetachedSessionBootstrapSteps starts native Windows detached sessions with powershell", () => {
518
+ const hudCmd = buildWindowsPromptCommand("node", [
519
+ "omx.js",
520
+ "hud",
521
+ "--watch",
522
+ ]);
523
+ const steps = buildDetachedSessionBootstrapSteps("omx-demo", "C:/project", "'codex' '--dangerously-bypass-approvals-and-sandbox'", hudCmd, "--model gpt-5", "C:/codex-home", null, true);
524
+ assert.equal(steps[0]?.name, "new-session");
525
+ assert.equal(steps[0]?.args.at(-1), "powershell.exe");
526
+ assert.equal(steps[1]?.name, "split-and-capture-hud-pane");
461
527
  assert.equal(steps[1]?.args.at(-1), hudCmd);
462
528
  });
463
- it('buildDetachedSessionFinalizeSteps keeps schedule after split-capture and before attach', () => {
464
- const steps = buildDetachedSessionFinalizeSteps('omx-demo', '%12', '3', true);
529
+ it("buildDetachedSessionFinalizeSteps keeps schedule after split-capture and before attach", () => {
530
+ const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
465
531
  const names = steps.map((step) => step.name);
466
- const attachedIndex = names.indexOf('register-client-attached-reconcile');
467
- const scheduleIndex = names.indexOf('schedule-delayed-resize');
468
- const attachIndex = names.indexOf('attach-session');
532
+ const attachedIndex = names.indexOf("register-client-attached-reconcile");
533
+ const scheduleIndex = names.indexOf("schedule-delayed-resize");
534
+ const attachIndex = names.indexOf("attach-session");
469
535
  assert.equal(attachedIndex >= 0, true);
470
536
  assert.equal(scheduleIndex > attachedIndex, true);
471
537
  assert.equal(scheduleIndex >= 0, true);
472
538
  assert.equal(attachIndex > scheduleIndex, true);
473
- assert.equal(names.includes('register-resize-hook'), true);
474
- assert.equal(names.includes('reconcile-hud-resize'), true);
475
- });
476
- it('buildDetachedSessionFinalizeSteps uses quiet best-effort tmux resize commands', () => {
477
- const steps = buildDetachedSessionFinalizeSteps('omx-demo', '%12', '3', false);
478
- const registerHook = steps.find((step) => step.name === 'register-resize-hook');
479
- const schedule = steps.find((step) => step.name === 'schedule-delayed-resize');
480
- const reconcile = steps.find((step) => step.name === 'reconcile-hud-resize');
481
- assert.match(registerHook?.args[4] ?? '', />\/dev\/null 2>&1 \|\| true/);
482
- assert.match(registerHook?.args[4] ?? '', new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
483
- assert.match(schedule?.args[2] ?? '', />\/dev\/null 2>&1 \|\| true/);
484
- assert.match(schedule?.args[2] ?? '', new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
485
- assert.match((reconcile?.args ?? []).join(' '), />\/dev\/null 2>&1 \|\| true/);
486
- assert.match((reconcile?.args ?? []).join(' '), new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
487
- });
488
- it('buildDetachedSessionFinalizeSteps skips detached resize hooks on native Windows', () => {
489
- const steps = buildDetachedSessionFinalizeSteps('omx-demo', '%12', '3', true, true);
490
- assert.deepEqual(steps.map((step) => step.name), ['set-mouse', 'attach-session']);
491
- });
492
- it('buildDetachedSessionFinalizeSteps never appends server-global terminal-overrides', () => {
493
- const steps = buildDetachedSessionFinalizeSteps('omx-demo', '%12', '3', true);
494
- assert.equal(steps.some((step) => step.name === 'set-wsl-xt'), false);
495
- assert.equal(steps.some((step) => step.args.includes('terminal-overrides')), false);
496
- });
497
- it('buildDetachedSessionRollbackSteps unregisters hooks before killing session', () => {
498
- const steps = buildDetachedSessionRollbackSteps('omx-demo', 'omx-demo:0', 'omx_resize_launch_demo_0_12', 'omx_attached_launch_demo_0_12');
499
- assert.deepEqual(steps.map((step) => step.name), ['unregister-client-attached-reconcile', 'unregister-resize-hook', 'kill-session']);
500
- assert.equal(steps[0]?.args[0], 'set-hook');
501
- assert.equal(steps[0]?.args[1], '-u');
502
- assert.equal(steps[0]?.args[2], '-t');
503
- assert.equal(steps[0]?.args[3], 'omx-demo:0');
504
- assert.match(steps[0]?.args[4] ?? '', /^client-attached\[\d+\]$/);
505
- assert.match(steps[1]?.args[4] ?? '', /^client-resized\[\d+\]$/);
506
- assert.deepEqual(steps[2]?.args, ['kill-session', '-t', 'omx-demo']);
507
- });
508
- it('buildDetachedSessionRollbackSteps only kills session when no hook metadata exists', () => {
509
- const steps = buildDetachedSessionRollbackSteps('omx-demo', null, null, null);
510
- assert.deepEqual(steps.map((step) => step.name), ['kill-session']);
539
+ assert.equal(names.includes("register-resize-hook"), true);
540
+ assert.equal(names.includes("reconcile-hud-resize"), true);
541
+ });
542
+ it("buildDetachedSessionFinalizeSteps uses quiet best-effort tmux resize commands", () => {
543
+ const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", false);
544
+ const registerHook = steps.find((step) => step.name === "register-resize-hook");
545
+ const schedule = steps.find((step) => step.name === "schedule-delayed-resize");
546
+ const reconcile = steps.find((step) => step.name === "reconcile-hud-resize");
547
+ assert.match(registerHook?.args[4] ?? "", />\/dev\/null 2>&1 \|\| true/);
548
+ assert.match(registerHook?.args[4] ?? "", new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
549
+ assert.match(schedule?.args[2] ?? "", />\/dev\/null 2>&1 \|\| true/);
550
+ assert.match(schedule?.args[2] ?? "", new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
551
+ assert.match((reconcile?.args ?? []).join(" "), />\/dev\/null 2>&1 \|\| true/);
552
+ assert.match((reconcile?.args ?? []).join(" "), new RegExp(`-y ${HUD_TMUX_HEIGHT_LINES}\\b`));
553
+ });
554
+ it("buildDetachedSessionFinalizeSteps skips detached resize hooks on native Windows", () => {
555
+ const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true, true);
556
+ assert.deepEqual(steps.map((step) => step.name), ["set-mouse", "attach-session"]);
557
+ });
558
+ it("buildDetachedSessionFinalizeSteps never appends server-global terminal-overrides", () => {
559
+ const steps = buildDetachedSessionFinalizeSteps("omx-demo", "%12", "3", true);
560
+ assert.equal(steps.some((step) => step.name === "set-wsl-xt"), false);
561
+ assert.equal(steps.some((step) => step.args.includes("terminal-overrides")), false);
562
+ });
563
+ it("buildDetachedSessionRollbackSteps unregisters hooks before killing session", () => {
564
+ const steps = buildDetachedSessionRollbackSteps("omx-demo", "omx-demo:0", "omx_resize_launch_demo_0_12", "omx_attached_launch_demo_0_12");
565
+ assert.deepEqual(steps.map((step) => step.name), [
566
+ "unregister-client-attached-reconcile",
567
+ "unregister-resize-hook",
568
+ "kill-session",
569
+ ]);
570
+ assert.equal(steps[0]?.args[0], "set-hook");
571
+ assert.equal(steps[0]?.args[1], "-u");
572
+ assert.equal(steps[0]?.args[2], "-t");
573
+ assert.equal(steps[0]?.args[3], "omx-demo:0");
574
+ assert.match(steps[0]?.args[4] ?? "", /^client-attached\[\d+\]$/);
575
+ assert.match(steps[1]?.args[4] ?? "", /^client-resized\[\d+\]$/);
576
+ assert.deepEqual(steps[2]?.args, ["kill-session", "-t", "omx-demo"]);
577
+ });
578
+ it("buildDetachedSessionRollbackSteps only kills session when no hook metadata exists", () => {
579
+ const steps = buildDetachedSessionRollbackSteps("omx-demo", null, null, null);
580
+ assert.deepEqual(steps.map((step) => step.name), ["kill-session"]);
511
581
  });
512
582
  });
513
- describe('buildTmuxShellCommand', () => {
514
- it('preserves quoted config values for tmux shell-command execution', () => {
515
- assert.equal(buildTmuxShellCommand('codex', ['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="xhigh"']), `'codex' '--dangerously-bypass-approvals-and-sandbox' '-c' 'model_reasoning_effort="xhigh"'`);
583
+ describe("buildTmuxShellCommand", () => {
584
+ it("preserves quoted config values for tmux shell-command execution", () => {
585
+ assert.equal(buildTmuxShellCommand("codex", [
586
+ "--dangerously-bypass-approvals-and-sandbox",
587
+ "-c",
588
+ 'model_reasoning_effort="xhigh"',
589
+ ]), `'codex' '--dangerously-bypass-approvals-and-sandbox' '-c' 'model_reasoning_effort="xhigh"'`);
516
590
  });
517
591
  });
518
- describe('buildTmuxPaneCommand', () => {
519
- it('wraps command with zsh profile sourcing for zsh shell', () => {
520
- const result = buildTmuxPaneCommand('codex', ['--model', 'gpt-5'], '/usr/bin/zsh');
521
- assert.ok(result.startsWith("'/usr/bin/zsh' -lc "), 'should start with zsh login shell');
522
- assert.ok(result.includes('source ~/.zshrc'), 'should source .zshrc');
523
- assert.ok(result.includes('exec '), 'should exec the command');
524
- });
525
- it('wraps command with bash profile sourcing for bash shell', () => {
526
- const result = buildTmuxPaneCommand('codex', [], '/bin/bash');
527
- assert.ok(result.startsWith("'/bin/bash' -lc "), 'should start with bash login shell');
528
- assert.ok(result.includes('source ~/.bashrc'), 'should source .bashrc');
529
- assert.ok(result.includes('exec '), 'should exec the command');
530
- });
531
- it('skips rc sourcing for unknown shells but still uses login flag', () => {
532
- const result = buildTmuxPaneCommand('codex', [], '/bin/fish');
533
- assert.ok(result.startsWith("'/bin/fish' -lc "), 'should start with fish login shell');
534
- assert.ok(!result.includes('source'), 'should not source any rc file');
535
- assert.ok(result.includes('exec '), 'should exec the command');
536
- });
537
- it('falls back to /bin/sh when shell path is empty', () => {
538
- const result = buildTmuxPaneCommand('codex', [], '');
539
- assert.ok(result.startsWith("'/bin/sh' -lc "), 'should fall back to /bin/sh');
592
+ describe("buildTmuxPaneCommand", () => {
593
+ it("wraps command with zsh profile sourcing for zsh shell", () => {
594
+ const result = buildTmuxPaneCommand("codex", ["--model", "gpt-5"], "/usr/bin/zsh");
595
+ assert.ok(result.startsWith("'/usr/bin/zsh' -lc "), "should start with zsh login shell");
596
+ assert.ok(result.includes("source ~/.zshrc"), "should source .zshrc");
597
+ assert.ok(result.includes("exec "), "should exec the command");
598
+ });
599
+ it("wraps command with bash profile sourcing for bash shell", () => {
600
+ const result = buildTmuxPaneCommand("codex", [], "/bin/bash");
601
+ assert.ok(result.startsWith("'/bin/bash' -lc "), "should start with bash login shell");
602
+ assert.ok(result.includes("source ~/.bashrc"), "should source .bashrc");
603
+ assert.ok(result.includes("exec "), "should exec the command");
604
+ });
605
+ it("skips rc sourcing for unknown shells but still uses login flag", () => {
606
+ const result = buildTmuxPaneCommand("codex", [], "/bin/fish");
607
+ assert.ok(result.startsWith("'/bin/fish' -lc "), "should start with fish login shell");
608
+ assert.ok(!result.includes("source"), "should not source any rc file");
609
+ assert.ok(result.includes("exec "), "should exec the command");
610
+ });
611
+ it("falls back to /bin/sh when shell path is empty", () => {
612
+ const result = buildTmuxPaneCommand("codex", [], "");
613
+ assert.ok(result.startsWith("'/bin/sh' -lc "), "should fall back to /bin/sh");
540
614
  });
541
615
  });
542
- describe('buildWindowsPromptCommand', () => {
543
- it('encodes detached Windows commands for safe PowerShell prompt injection', () => {
544
- const result = buildWindowsPromptCommand('codex', ['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="high"', "it's"]);
545
- const prefix = 'powershell.exe -NoLogo -NoExit -EncodedCommand ';
616
+ describe("buildWindowsPromptCommand", () => {
617
+ it("encodes detached Windows commands for safe PowerShell prompt injection", () => {
618
+ const result = buildWindowsPromptCommand("codex", [
619
+ "--dangerously-bypass-approvals-and-sandbox",
620
+ "-c",
621
+ 'model_reasoning_effort="high"',
622
+ "it's",
623
+ ]);
624
+ const prefix = "powershell.exe -NoLogo -NoExit -EncodedCommand ";
546
625
  assert.ok(result.startsWith(prefix));
547
626
  const payload = result.slice(prefix.length);
548
- const decoded = Buffer.from(payload, 'base64').toString('utf16le');
627
+ const decoded = Buffer.from(payload, "base64").toString("utf16le");
549
628
  assert.equal(decoded, "$ErrorActionPreference = 'Stop'; & { & 'codex' '--dangerously-bypass-approvals-and-sandbox' '-c' 'model_reasoning_effort=\"high\"' 'it''s' }");
550
629
  });
551
630
  });
552
- describe('buildTmuxSessionName', () => {
553
- it('uses detached fallback quietly outside git repos', () => {
554
- const name = buildTmuxSessionName('/tmp/My Repo', 'omx-1770992424158-abc123');
555
- assert.equal(name, 'omx-my-repo-detached-1770992424158-abc123');
631
+ describe("buildTmuxSessionName", () => {
632
+ it("uses detached fallback quietly outside git repos", () => {
633
+ const name = buildTmuxSessionName("/tmp/My Repo", "omx-1770992424158-abc123");
634
+ assert.equal(name, "omx-my-repo-detached-1770992424158-abc123");
556
635
  });
557
- it('sanitizes invalid characters', () => {
558
- const name = buildTmuxSessionName('/tmp/@#$', 'omx-+++');
636
+ it("sanitizes invalid characters", () => {
637
+ const name = buildTmuxSessionName("/tmp/@#$", "omx-+++");
559
638
  assert.match(name, /^omx-(unknown|[a-z0-9-]+)-[a-z0-9-]+-(unknown|[a-z0-9-]+)$/);
560
- assert.equal(name.includes('_'), false);
561
- assert.equal(name.includes(' '), false);
639
+ assert.equal(name.includes("_"), false);
640
+ assert.equal(name.includes(" "), false);
562
641
  });
563
- it('includes repo name when cwd is inside .omx-worktrees', () => {
564
- const name = buildTmuxSessionName('/home/user/my-repo.omx-worktrees/launch-feature-x', 'omx-123-abc');
642
+ it("includes repo name when cwd is inside .omx-worktrees", () => {
643
+ const name = buildTmuxSessionName("/home/user/my-repo.omx-worktrees/launch-feature-x", "omx-123-abc");
565
644
  assert.match(name, /^omx-my-repo-launch-feature-x-/);
566
645
  });
567
- it('includes repo name for detached worktree paths', () => {
568
- const name = buildTmuxSessionName('/projects/cool-project.omx-worktrees/launch-detached', 'omx-456-def');
646
+ it("includes repo name for detached worktree paths", () => {
647
+ const name = buildTmuxSessionName("/projects/cool-project.omx-worktrees/launch-detached", "omx-456-def");
569
648
  assert.match(name, /^omx-cool-project-launch-detached-/);
570
649
  });
650
+ it("includes repo name when cwd is inside .omx/worktrees", () => {
651
+ const name = buildTmuxSessionName("/home/user/my-repo/.omx/worktrees/autoresearch-demo", "omx-789-ghi");
652
+ assert.match(name, /^omx-my-repo-autoresearch-demo-/);
653
+ });
571
654
  });
572
- describe('team worker launch arg inheritance helpers', () => {
573
- it('collectInheritableTeamWorkerArgs extracts bypass, reasoning, and model overrides', () => {
574
- assert.deepEqual(collectInheritableTeamWorkerArgs(['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="xhigh"', '--model', 'gpt-5']), ['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="xhigh"', '--model', 'gpt-5']);
655
+ describe("team worker launch arg inheritance helpers", () => {
656
+ it("collectInheritableTeamWorkerArgs extracts bypass, reasoning, and model overrides", () => {
657
+ assert.deepEqual(collectInheritableTeamWorkerArgs([
658
+ "--dangerously-bypass-approvals-and-sandbox",
659
+ "-c",
660
+ 'model_reasoning_effort="xhigh"',
661
+ "--model",
662
+ "gpt-5",
663
+ ]), [
664
+ "--dangerously-bypass-approvals-and-sandbox",
665
+ "-c",
666
+ 'model_reasoning_effort="xhigh"',
667
+ "--model",
668
+ "gpt-5",
669
+ ]);
575
670
  });
576
- it('collectInheritableTeamWorkerArgs supports --model=<value> syntax', () => {
577
- assert.deepEqual(collectInheritableTeamWorkerArgs(['--model=gpt-5.3-codex']), ['--model', 'gpt-5.3-codex']);
671
+ it("collectInheritableTeamWorkerArgs supports --model=<value> syntax", () => {
672
+ assert.deepEqual(collectInheritableTeamWorkerArgs(["--model=gpt-5.3-codex"]), ["--model", "gpt-5.3-codex"]);
578
673
  });
579
- it('resolveTeamWorkerLaunchArgsEnv merges and normalizes with de-dupe + last reasoning/model wins', () => {
580
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="high" --model old-a --no-alt-screen --model=old-b', ['-c', 'model_reasoning_effort="xhigh"', '--dangerously-bypass-approvals-and-sandbox', '--model', 'gpt-5'], true), '--no-alt-screen --dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="xhigh" --model old-b');
674
+ it("resolveTeamWorkerLaunchArgsEnv merges and normalizes with de-dupe + last reasoning/model wins", () => {
675
+ assert.equal(resolveTeamWorkerLaunchArgsEnv('--dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="high" --model old-a --no-alt-screen --model=old-b', [
676
+ "-c",
677
+ 'model_reasoning_effort="xhigh"',
678
+ "--dangerously-bypass-approvals-and-sandbox",
679
+ "--model",
680
+ "gpt-5",
681
+ ], true), '--no-alt-screen --dangerously-bypass-approvals-and-sandbox -c model_reasoning_effort="xhigh" --model old-b');
581
682
  });
582
- it('resolveTeamWorkerLaunchArgsEnv can opt out of leader inheritance', () => {
583
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--no-alt-screen', ['--dangerously-bypass-approvals-and-sandbox', '-c', 'model_reasoning_effort="xhigh"'], false), '--no-alt-screen');
683
+ it("resolveTeamWorkerLaunchArgsEnv can opt out of leader inheritance", () => {
684
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", [
685
+ "--dangerously-bypass-approvals-and-sandbox",
686
+ "-c",
687
+ 'model_reasoning_effort="xhigh"',
688
+ ], false), "--no-alt-screen");
584
689
  });
585
- it('resolveTeamWorkerLaunchArgsEnv uses inherited model when env model is absent', () => {
586
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--no-alt-screen', ['--model=gpt-5.3-codex'], true), '--no-alt-screen --model gpt-5.3-codex');
690
+ it("resolveTeamWorkerLaunchArgsEnv uses inherited model when env model is absent", () => {
691
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--model=gpt-5.3-codex"], true), "--no-alt-screen --model gpt-5.3-codex");
587
692
  });
588
- it('resolveTeamWorkerLaunchArgsEnv uses frontier default model when env and inherited models are absent', () => {
589
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--no-alt-screen', ['--dangerously-bypass-approvals-and-sandbox'], true, DEFAULT_FRONTIER_MODEL), `--no-alt-screen --dangerously-bypass-approvals-and-sandbox --model ${DEFAULT_FRONTIER_MODEL}`);
693
+ it("resolveTeamWorkerLaunchArgsEnv uses frontier default model when env and inherited models are absent", () => {
694
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--dangerously-bypass-approvals-and-sandbox"], true, DEFAULT_FRONTIER_MODEL), `--no-alt-screen --dangerously-bypass-approvals-and-sandbox --model ${DEFAULT_FRONTIER_MODEL}`);
590
695
  });
591
- it('resolveTeamWorkerLaunchArgsEnv keeps exactly one final model with precedence env > inherited > default', () => {
592
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--model env-model --model=env-model-final', ['--model', 'inherited-model'], true, 'fallback-model'), '--model env-model-final');
696
+ it("resolveTeamWorkerLaunchArgsEnv keeps exactly one final model with precedence env > inherited > default", () => {
697
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--model env-model --model=env-model-final", ["--model", "inherited-model"], true, "fallback-model"), "--model env-model-final");
593
698
  });
594
- it('resolveTeamWorkerLaunchArgsEnv prefers inherited model over default when env model is absent', () => {
595
- assert.equal(resolveTeamWorkerLaunchArgsEnv('--no-alt-screen', ['--model', 'inherited-model'], true, 'fallback-model'), '--no-alt-screen --model inherited-model');
699
+ it("resolveTeamWorkerLaunchArgsEnv prefers inherited model over default when env model is absent", () => {
700
+ assert.equal(resolveTeamWorkerLaunchArgsEnv("--no-alt-screen", ["--model", "inherited-model"], true, "fallback-model"), "--no-alt-screen --model inherited-model");
596
701
  });
597
702
  });
598
- describe('readTopLevelTomlString', () => {
599
- it('reads a top-level string value', () => {
600
- const value = readTopLevelTomlString('model_reasoning_effort = "high"\n[mcp_servers.test]\nmodel_reasoning_effort = "low"\n', 'model_reasoning_effort');
601
- assert.equal(value, 'high');
703
+ describe("readTopLevelTomlString", () => {
704
+ it("reads a top-level string value", () => {
705
+ const value = readTopLevelTomlString('model_reasoning_effort = "high"\n[mcp_servers.test]\nmodel_reasoning_effort = "low"\n', "model_reasoning_effort");
706
+ assert.equal(value, "high");
602
707
  });
603
- it('ignores table-local values', () => {
604
- const value = readTopLevelTomlString('[mcp_servers.test]\nmodel_reasoning_effort = "xhigh"\n', 'model_reasoning_effort');
708
+ it("ignores table-local values", () => {
709
+ const value = readTopLevelTomlString('[mcp_servers.test]\nmodel_reasoning_effort = "xhigh"\n', "model_reasoning_effort");
605
710
  assert.equal(value, null);
606
711
  });
607
712
  });
608
- describe('injectModelInstructionsBypassArgs', () => {
609
- it('appends model_instructions_file override by default', () => {
610
- const args = injectModelInstructionsBypassArgs('/tmp/my-project', ['--model', 'gpt-5'], {});
611
- assert.deepEqual(args, ['--model', 'gpt-5', '-c', 'model_instructions_file="/tmp/my-project/AGENTS.md"']);
713
+ describe("injectModelInstructionsBypassArgs", () => {
714
+ it("appends model_instructions_file override by default", () => {
715
+ const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], {});
716
+ assert.deepEqual(args, [
717
+ "--model",
718
+ "gpt-5",
719
+ "-c",
720
+ 'model_instructions_file="/tmp/my-project/AGENTS.md"',
721
+ ]);
612
722
  });
613
- it('does not append when bypass is disabled via env', () => {
614
- const args = injectModelInstructionsBypassArgs('/tmp/my-project', ['--model', 'gpt-5'], { OMX_BYPASS_DEFAULT_SYSTEM_PROMPT: '0' });
615
- assert.deepEqual(args, ['--model', 'gpt-5']);
723
+ it("does not append when bypass is disabled via env", () => {
724
+ const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], { OMX_BYPASS_DEFAULT_SYSTEM_PROMPT: "0" });
725
+ assert.deepEqual(args, ["--model", "gpt-5"]);
616
726
  });
617
- it('does not append when model_instructions_file is already set', () => {
618
- const args = injectModelInstructionsBypassArgs('/tmp/my-project', ['-c', 'model_instructions_file="/tmp/custom.md"'], {});
619
- assert.deepEqual(args, ['-c', 'model_instructions_file="/tmp/custom.md"']);
727
+ it("does not append when model_instructions_file is already set", () => {
728
+ const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["-c", 'model_instructions_file="/tmp/custom.md"'], {});
729
+ assert.deepEqual(args, ["-c", 'model_instructions_file="/tmp/custom.md"']);
620
730
  });
621
- it('respects OMX_MODEL_INSTRUCTIONS_FILE env override', () => {
622
- const args = injectModelInstructionsBypassArgs('/tmp/my-project', [], { OMX_MODEL_INSTRUCTIONS_FILE: '/tmp/alt instructions.md' });
623
- assert.deepEqual(args, ['-c', 'model_instructions_file="/tmp/alt instructions.md"']);
731
+ it("respects OMX_MODEL_INSTRUCTIONS_FILE env override", () => {
732
+ const args = injectModelInstructionsBypassArgs("/tmp/my-project", [], {
733
+ OMX_MODEL_INSTRUCTIONS_FILE: "/tmp/alt instructions.md",
734
+ });
735
+ assert.deepEqual(args, [
736
+ "-c",
737
+ 'model_instructions_file="/tmp/alt instructions.md"',
738
+ ]);
624
739
  });
625
- it('uses session-scoped default model_instructions_file when provided', () => {
626
- const args = injectModelInstructionsBypassArgs('/tmp/my-project', ['--model', 'gpt-5'], {}, '/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md');
627
- assert.deepEqual(args, ['--model', 'gpt-5', '-c', 'model_instructions_file="/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md"']);
740
+ it("uses session-scoped default model_instructions_file when provided", () => {
741
+ const args = injectModelInstructionsBypassArgs("/tmp/my-project", ["--model", "gpt-5"], {}, "/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md");
742
+ assert.deepEqual(args, [
743
+ "--model",
744
+ "gpt-5",
745
+ "-c",
746
+ 'model_instructions_file="/tmp/my-project/.omx/state/sessions/session-1/AGENTS.md"',
747
+ ]);
628
748
  });
629
749
  });
630
- describe('upsertTopLevelTomlString', () => {
631
- it('replaces an existing top-level key', () => {
632
- const updated = upsertTopLevelTomlString('model_reasoning_effort = "low"\n[tui]\nstatus_line = []\n', 'model_reasoning_effort', 'high');
750
+ describe("upsertTopLevelTomlString", () => {
751
+ it("replaces an existing top-level key", () => {
752
+ const updated = upsertTopLevelTomlString('model_reasoning_effort = "low"\n[tui]\nstatus_line = []\n', "model_reasoning_effort", "high");
633
753
  assert.match(updated, /^model_reasoning_effort = "high"$/m);
634
754
  assert.doesNotMatch(updated, /^model_reasoning_effort = "low"$/m);
635
755
  });
636
- it('inserts before the first table when key is missing', () => {
637
- const updated = upsertTopLevelTomlString('[tui]\nstatus_line = []\n', 'model_reasoning_effort', 'xhigh');
756
+ it("inserts before the first table when key is missing", () => {
757
+ const updated = upsertTopLevelTomlString("[tui]\nstatus_line = []\n", "model_reasoning_effort", "xhigh");
638
758
  assert.equal(updated, 'model_reasoning_effort = "xhigh"\n[tui]\nstatus_line = []\n');
639
759
  });
640
760
  });