oh-my-codex 0.13.2 → 0.14.0

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 (296) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/autoresearch/__tests__/skill-validation.test.d.ts +2 -0
  4. package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +1 -0
  5. package/dist/autoresearch/__tests__/skill-validation.test.js +91 -0
  6. package/dist/autoresearch/__tests__/skill-validation.test.js.map +1 -0
  7. package/dist/autoresearch/skill-validation.d.ts +13 -0
  8. package/dist/autoresearch/skill-validation.d.ts.map +1 -0
  9. package/dist/autoresearch/skill-validation.js +165 -0
  10. package/dist/autoresearch/skill-validation.js.map +1 -0
  11. package/dist/catalog/__tests__/schema.test.js +6 -0
  12. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  13. package/dist/cli/__tests__/autoresearch-guided.test.js +236 -273
  14. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  15. package/dist/cli/__tests__/autoresearch.test.js +64 -653
  16. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  17. package/dist/cli/__tests__/index.test.js +7 -0
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/nested-help-routing.test.js +2 -1
  20. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  21. package/dist/cli/__tests__/question.test.d.ts +2 -0
  22. package/dist/cli/__tests__/question.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/question.test.js +113 -0
  24. package/dist/cli/__tests__/question.test.js.map +1 -0
  25. package/dist/cli/__tests__/session-search-help.test.js +1 -1
  26. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  27. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -0
  28. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  29. package/dist/cli/autoresearch-guided.d.ts +24 -7
  30. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  31. package/dist/cli/autoresearch-guided.js +189 -130
  32. package/dist/cli/autoresearch-guided.js.map +1 -1
  33. package/dist/cli/autoresearch.d.ts +3 -2
  34. package/dist/cli/autoresearch.d.ts.map +1 -1
  35. package/dist/cli/autoresearch.js +29 -305
  36. package/dist/cli/autoresearch.js.map +1 -1
  37. package/dist/cli/doctor.d.ts.map +1 -1
  38. package/dist/cli/doctor.js +43 -0
  39. package/dist/cli/doctor.js.map +1 -1
  40. package/dist/cli/index.d.ts +1 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +8 -1
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/question.d.ts +3 -0
  45. package/dist/cli/question.d.ts.map +1 -0
  46. package/dist/cli/question.js +182 -0
  47. package/dist/cli/question.js.map +1 -0
  48. package/dist/hooks/__tests__/analyze-routing-contract.test.js +22 -13
  49. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  50. package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
  51. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  52. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +2 -2
  53. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -1
  54. package/dist/hooks/__tests__/deep-interview-contract.test.js +22 -5
  55. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  56. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +2 -2
  57. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  58. package/dist/hooks/__tests__/keyword-detector.test.js +308 -17
  59. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  60. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +570 -2
  61. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +717 -16
  63. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  64. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +25 -0
  65. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  66. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +894 -1
  67. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  68. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +34 -0
  69. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  70. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +132 -0
  71. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  72. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +22 -4
  73. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  74. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +4 -2
  75. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  76. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +1 -0
  77. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
  78. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +4 -1
  79. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  80. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +28 -0
  81. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  82. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +5 -4
  83. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +1 -1
  84. package/dist/hooks/__tests__/prompt-team-routing.test.js +2 -2
  85. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
  86. package/dist/hooks/__tests__/triage-config.test.d.ts +2 -0
  87. package/dist/hooks/__tests__/triage-config.test.d.ts.map +1 -0
  88. package/dist/hooks/__tests__/triage-config.test.js +211 -0
  89. package/dist/hooks/__tests__/triage-config.test.js.map +1 -0
  90. package/dist/hooks/__tests__/triage-heuristic.test.d.ts +2 -0
  91. package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +1 -0
  92. package/dist/hooks/__tests__/triage-heuristic.test.js +230 -0
  93. package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -0
  94. package/dist/hooks/__tests__/triage-state.test.d.ts +2 -0
  95. package/dist/hooks/__tests__/triage-state.test.d.ts.map +1 -0
  96. package/dist/hooks/__tests__/triage-state.test.js +426 -0
  97. package/dist/hooks/__tests__/triage-state.test.js.map +1 -0
  98. package/dist/hooks/keyword-detector.d.ts +26 -7
  99. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  100. package/dist/hooks/keyword-detector.js +97 -26
  101. package/dist/hooks/keyword-detector.js.map +1 -1
  102. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  103. package/dist/hooks/keyword-registry.js +16 -9
  104. package/dist/hooks/keyword-registry.js.map +1 -1
  105. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  106. package/dist/hooks/prompt-guidance-contract.js +28 -1
  107. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  108. package/dist/hooks/triage-config.d.ts +33 -0
  109. package/dist/hooks/triage-config.d.ts.map +1 -0
  110. package/dist/hooks/triage-config.js +87 -0
  111. package/dist/hooks/triage-config.js.map +1 -0
  112. package/dist/hooks/triage-heuristic.d.ts +20 -0
  113. package/dist/hooks/triage-heuristic.d.ts.map +1 -0
  114. package/dist/hooks/triage-heuristic.js +210 -0
  115. package/dist/hooks/triage-heuristic.js.map +1 -0
  116. package/dist/hooks/triage-state.d.ts +63 -0
  117. package/dist/hooks/triage-state.d.ts.map +1 -0
  118. package/dist/hooks/triage-state.js +138 -0
  119. package/dist/hooks/triage-state.js.map +1 -0
  120. package/dist/hud/__tests__/reconcile.test.js +20 -0
  121. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  122. package/dist/hud/reconcile.d.ts +1 -0
  123. package/dist/hud/reconcile.d.ts.map +1 -1
  124. package/dist/hud/reconcile.js +2 -1
  125. package/dist/hud/reconcile.js.map +1 -1
  126. package/dist/mcp/__tests__/state-server.test.js +1 -0
  127. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  128. package/dist/mcp/state-server.d.ts +8 -0
  129. package/dist/mcp/state-server.d.ts.map +1 -1
  130. package/dist/mcp/state-server.js +4 -0
  131. package/dist/mcp/state-server.js.map +1 -1
  132. package/dist/modes/__tests__/base-ralph-contract.test.js +15 -0
  133. package/dist/modes/__tests__/base-ralph-contract.test.js.map +1 -1
  134. package/dist/modes/base.d.ts +1 -0
  135. package/dist/modes/base.d.ts.map +1 -1
  136. package/dist/modes/base.js +22 -6
  137. package/dist/modes/base.js.map +1 -1
  138. package/dist/notifications/__tests__/index.test.js +78 -0
  139. package/dist/notifications/__tests__/index.test.js.map +1 -1
  140. package/dist/notifications/index.d.ts.map +1 -1
  141. package/dist/notifications/index.js +39 -22
  142. package/dist/notifications/index.js.map +1 -1
  143. package/dist/openclaw/index.d.ts +5 -3
  144. package/dist/openclaw/index.d.ts.map +1 -1
  145. package/dist/openclaw/index.js +5 -3
  146. package/dist/openclaw/index.js.map +1 -1
  147. package/dist/question/__tests__/client.test.d.ts +2 -0
  148. package/dist/question/__tests__/client.test.d.ts.map +1 -0
  149. package/dist/question/__tests__/client.test.js +70 -0
  150. package/dist/question/__tests__/client.test.js.map +1 -0
  151. package/dist/question/__tests__/deep-interview.test.d.ts +2 -0
  152. package/dist/question/__tests__/deep-interview.test.d.ts.map +1 -0
  153. package/dist/question/__tests__/deep-interview.test.js +108 -0
  154. package/dist/question/__tests__/deep-interview.test.js.map +1 -0
  155. package/dist/question/__tests__/policy.test.d.ts +2 -0
  156. package/dist/question/__tests__/policy.test.d.ts.map +1 -0
  157. package/dist/question/__tests__/policy.test.js +107 -0
  158. package/dist/question/__tests__/policy.test.js.map +1 -0
  159. package/dist/question/__tests__/renderer.test.d.ts +2 -0
  160. package/dist/question/__tests__/renderer.test.d.ts.map +1 -0
  161. package/dist/question/__tests__/renderer.test.js +88 -0
  162. package/dist/question/__tests__/renderer.test.js.map +1 -0
  163. package/dist/question/__tests__/state.test.d.ts +2 -0
  164. package/dist/question/__tests__/state.test.d.ts.map +1 -0
  165. package/dist/question/__tests__/state.test.js +55 -0
  166. package/dist/question/__tests__/state.test.js.map +1 -0
  167. package/dist/question/__tests__/types.test.d.ts +2 -0
  168. package/dist/question/__tests__/types.test.d.ts.map +1 -0
  169. package/dist/question/__tests__/types.test.js +44 -0
  170. package/dist/question/__tests__/types.test.js.map +1 -0
  171. package/dist/question/__tests__/ui.test.d.ts +2 -0
  172. package/dist/question/__tests__/ui.test.d.ts.map +1 -0
  173. package/dist/question/__tests__/ui.test.js +169 -0
  174. package/dist/question/__tests__/ui.test.js.map +1 -0
  175. package/dist/question/client.d.ts +54 -0
  176. package/dist/question/client.d.ts.map +1 -0
  177. package/dist/question/client.js +77 -0
  178. package/dist/question/client.js.map +1 -0
  179. package/dist/question/deep-interview.d.ts +27 -0
  180. package/dist/question/deep-interview.d.ts.map +1 -0
  181. package/dist/question/deep-interview.js +101 -0
  182. package/dist/question/deep-interview.js.map +1 -0
  183. package/dist/question/policy.d.ts +18 -0
  184. package/dist/question/policy.d.ts.map +1 -0
  185. package/dist/question/policy.js +77 -0
  186. package/dist/question/policy.js.map +1 -0
  187. package/dist/question/renderer.d.ts +18 -0
  188. package/dist/question/renderer.d.ts.map +1 -0
  189. package/dist/question/renderer.js +128 -0
  190. package/dist/question/renderer.js.map +1 -0
  191. package/dist/question/state.d.ts +19 -0
  192. package/dist/question/state.d.ts.map +1 -0
  193. package/dist/question/state.js +108 -0
  194. package/dist/question/state.js.map +1 -0
  195. package/dist/question/types.d.ts +66 -0
  196. package/dist/question/types.d.ts.map +1 -0
  197. package/dist/question/types.js +82 -0
  198. package/dist/question/types.js.map +1 -0
  199. package/dist/question/ui.d.ts +38 -0
  200. package/dist/question/ui.d.ts.map +1 -0
  201. package/dist/question/ui.js +321 -0
  202. package/dist/question/ui.js.map +1 -0
  203. package/dist/ralph/contract.d.ts +1 -1
  204. package/dist/ralph/contract.d.ts.map +1 -1
  205. package/dist/ralph/contract.js +4 -1
  206. package/dist/ralph/contract.js.map +1 -1
  207. package/dist/ralplan/runtime.js +1 -1
  208. package/dist/ralplan/runtime.js.map +1 -1
  209. package/dist/runtime/__tests__/run-loop.test.d.ts +2 -0
  210. package/dist/runtime/__tests__/run-loop.test.d.ts.map +1 -0
  211. package/dist/runtime/__tests__/run-loop.test.js +35 -0
  212. package/dist/runtime/__tests__/run-loop.test.js.map +1 -0
  213. package/dist/runtime/__tests__/run-outcome.test.d.ts +2 -0
  214. package/dist/runtime/__tests__/run-outcome.test.d.ts.map +1 -0
  215. package/dist/runtime/__tests__/run-outcome.test.js +64 -0
  216. package/dist/runtime/__tests__/run-outcome.test.js.map +1 -0
  217. package/dist/runtime/run-loop.d.ts +41 -0
  218. package/dist/runtime/run-loop.d.ts.map +1 -0
  219. package/dist/runtime/run-loop.js +46 -0
  220. package/dist/runtime/run-loop.js.map +1 -0
  221. package/dist/runtime/run-outcome.d.ts +28 -0
  222. package/dist/runtime/run-outcome.d.ts.map +1 -0
  223. package/dist/runtime/run-outcome.js +136 -0
  224. package/dist/runtime/run-outcome.js.map +1 -0
  225. package/dist/runtime/run-state.d.ts +36 -0
  226. package/dist/runtime/run-state.d.ts.map +1 -0
  227. package/dist/runtime/run-state.js +110 -0
  228. package/dist/runtime/run-state.js.map +1 -0
  229. package/dist/scripts/__tests__/codex-native-hook.test.js +1128 -85
  230. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  231. package/dist/scripts/codex-native-hook.d.ts +2 -0
  232. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  233. package/dist/scripts/codex-native-hook.js +199 -11
  234. package/dist/scripts/codex-native-hook.js.map +1 -1
  235. package/dist/scripts/notify-fallback-watcher.js +81 -2
  236. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  237. package/dist/scripts/notify-hook/auto-nudge.d.ts +27 -0
  238. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  239. package/dist/scripts/notify-hook/auto-nudge.js +83 -20
  240. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  241. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  242. package/dist/scripts/notify-hook/managed-tmux.js +64 -38
  243. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  244. package/dist/scripts/notify-hook/ralph-session-resume.js +1 -1
  245. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  246. package/dist/scripts/notify-hook.js +15 -5
  247. package/dist/scripts/notify-hook.js.map +1 -1
  248. package/dist/scripts/sync-prompt-guidance-fragments.js +5 -0
  249. package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
  250. package/dist/state/__tests__/operations-ralph-phase.test.js +21 -0
  251. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  252. package/dist/state/__tests__/workflow-transition.test.js +11 -0
  253. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  254. package/dist/state/operations.d.ts.map +1 -1
  255. package/dist/state/operations.js +15 -0
  256. package/dist/state/operations.js.map +1 -1
  257. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  258. package/dist/state/workflow-transition-reconcile.js +14 -1
  259. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  260. package/dist/state/workflow-transition.d.ts.map +1 -1
  261. package/dist/state/workflow-transition.js +3 -1
  262. package/dist/state/workflow-transition.js.map +1 -1
  263. package/dist/team/__tests__/followup-planner.test.js +15 -0
  264. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  265. package/dist/team/__tests__/role-router.test.js +41 -0
  266. package/dist/team/__tests__/role-router.test.js.map +1 -1
  267. package/dist/team/followup-planner.d.ts.map +1 -1
  268. package/dist/team/followup-planner.js +31 -9
  269. package/dist/team/followup-planner.js.map +1 -1
  270. package/dist/team/role-router.d.ts.map +1 -1
  271. package/dist/team/role-router.js +73 -0
  272. package/dist/team/role-router.js.map +1 -1
  273. package/package.json +3 -2
  274. package/prompts/dependency-expert.md +3 -0
  275. package/prompts/executor.md +5 -0
  276. package/prompts/explore.md +2 -0
  277. package/prompts/planner.md +5 -0
  278. package/prompts/product-analyst.md +8 -8
  279. package/prompts/researcher.md +78 -30
  280. package/prompts/verifier.md +4 -0
  281. package/skills/autoresearch/SKILL.md +68 -0
  282. package/skills/deep-interview/SKILL.md +10 -9
  283. package/skills/help/SKILL.md +3 -1
  284. package/skills/ralplan/SKILL.md +1 -0
  285. package/skills/team/SKILL.md +1 -0
  286. package/skills/ultrawork/SKILL.md +1 -0
  287. package/src/scripts/__tests__/codex-native-hook.test.ts +1495 -188
  288. package/src/scripts/codex-native-hook.ts +235 -19
  289. package/src/scripts/notify-fallback-watcher.ts +92 -2
  290. package/src/scripts/notify-hook/auto-nudge.ts +89 -20
  291. package/src/scripts/notify-hook/managed-tmux.ts +70 -31
  292. package/src/scripts/notify-hook/ralph-session-resume.ts +1 -1
  293. package/src/scripts/notify-hook.ts +23 -5
  294. package/src/scripts/sync-prompt-guidance-fragments.ts +4 -0
  295. package/templates/AGENTS.md +48 -37
  296. package/templates/catalog-manifest.json +7 -0
@@ -1,11 +1,39 @@
1
1
  import { afterEach, describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
4
5
  import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
  import { buildTmuxSessionName } from '../../cli/index.js';
7
- import { resolveManagedSessionContext, verifyManagedPaneTarget } from '../../scripts/notify-hook/managed-tmux.js';
8
+ import { resolveManagedPaneFromAnchor, resolveManagedSessionContext, resolveManagedSessionPane, verifyManagedPaneTarget, } from '../../scripts/notify-hook/managed-tmux.js';
8
9
  import { writeSessionStart } from '../session.js';
10
+ function readLinuxStartTicks(pid) {
11
+ try {
12
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
13
+ const commandEnd = stat.lastIndexOf(')');
14
+ if (commandEnd === -1)
15
+ return null;
16
+ const remainder = stat.slice(commandEnd + 1).trim();
17
+ const fields = remainder.split(/\s+/);
18
+ if (fields.length <= 19)
19
+ return null;
20
+ const startTicks = Number(fields[19]);
21
+ return Number.isFinite(startTicks) ? startTicks : null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function readLinuxCmdline(pid) {
28
+ try {
29
+ const raw = readFileSync(`/proc/${pid}/cmdline`);
30
+ const text = raw.toString('utf-8').replace(/\0+/g, ' ').trim();
31
+ return text.length > 0 ? text : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
9
37
  describe('notify-hook managed tmux windows fallback', () => {
10
38
  async function withFakeTmux(cwd, script, run) {
11
39
  const fakeBinDir = join(cwd, 'fake-bin');
@@ -160,5 +188,870 @@ exit 1
160
188
  await rm(cwd, { recursive: true, force: true });
161
189
  }
162
190
  });
191
+ it('keeps the verified anchor pane instead of rebinding to the active codex pane in the session', async () => {
192
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-anchor-pane-'));
193
+ const originalPath = process.env.PATH;
194
+ try {
195
+ const stateDir = join(cwd, '.omx', 'state');
196
+ const fakeBinDir = join(cwd, 'fake-bin');
197
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
198
+ const sessionId = 'omx-anchor-pane';
199
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
200
+ await mkdir(stateDir, { recursive: true });
201
+ await mkdir(fakeBinDir, { recursive: true });
202
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
203
+ session_id: sessionId,
204
+ started_at: new Date().toISOString(),
205
+ cwd,
206
+ pid: process.pid,
207
+ platform: process.platform,
208
+ pid_start_ticks: readLinuxStartTicks(process.pid),
209
+ pid_cmdline: readLinuxCmdline(process.pid),
210
+ }, null, 2));
211
+ const fakeTmux = `#!/usr/bin/env bash
212
+ set -eu
213
+ cmd="$1"
214
+ shift || true
215
+ if [[ "$cmd" == "display-message" ]]; then
216
+ target=""
217
+ format=""
218
+ while (($#)); do
219
+ case "$1" in
220
+ -p) shift ;;
221
+ -t) target="$2"; shift 2 ;;
222
+ *) format="$1"; shift ;;
223
+ esac
224
+ done
225
+ if [[ -z "$target" && "$format" == "#S" ]]; then
226
+ echo "${managedSessionName}"
227
+ exit 0
228
+ fi
229
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
230
+ echo "codex"
231
+ exit 0
232
+ fi
233
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
234
+ echo "codex"
235
+ exit 0
236
+ fi
237
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
238
+ echo "${managedSessionName}"
239
+ exit 0
240
+ fi
241
+ echo "unsupported display target: $target / $format" >&2
242
+ exit 1
243
+ fi
244
+ if [[ "$cmd" == "list-panes" ]]; then
245
+ target=""
246
+ while (($#)); do
247
+ case "$1" in
248
+ -s) shift ;;
249
+ -t) target="$2"; shift 2 ;;
250
+ -F) shift 2 ;;
251
+ *) shift ;;
252
+ esac
253
+ done
254
+ if [[ "$target" == "${managedSessionName}" ]]; then
255
+ printf "%%42\\t0\\tcodex\\tcodex\\n%%55\\t1\\tcodex\\tcodex\\n"
256
+ exit 0
257
+ fi
258
+ echo "unexpected list-panes target: $target" >&2
259
+ exit 1
260
+ fi
261
+ echo "unsupported cmd: $cmd" >&2
262
+ exit 1
263
+ `;
264
+ await writeFile(fakeTmuxPath, fakeTmux);
265
+ await chmod(fakeTmuxPath, 0o755);
266
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
267
+ process.env.TMUX = '1';
268
+ delete process.env.TMUX_PANE;
269
+ process.env.OMX_TEAM_WORKER = '';
270
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
271
+ assert.equal(paneId, '%42');
272
+ }
273
+ finally {
274
+ process.env.PATH = originalPath;
275
+ await rm(cwd, { recursive: true, force: true });
276
+ }
277
+ });
278
+ it('rebinds a node shell anchor to the live codex pane in the managed session', async () => {
279
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-node-shell-anchor-'));
280
+ const originalPath = process.env.PATH;
281
+ try {
282
+ const stateDir = join(cwd, '.omx', 'state');
283
+ const fakeBinDir = join(cwd, 'fake-bin');
284
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
285
+ const sessionId = 'omx-node-shell-anchor';
286
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
287
+ await mkdir(stateDir, { recursive: true });
288
+ await mkdir(fakeBinDir, { recursive: true });
289
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
290
+ session_id: sessionId,
291
+ started_at: new Date().toISOString(),
292
+ cwd,
293
+ pid: process.pid,
294
+ platform: process.platform,
295
+ pid_start_ticks: readLinuxStartTicks(process.pid),
296
+ pid_cmdline: readLinuxCmdline(process.pid),
297
+ }, null, 2));
298
+ const fakeTmux = `#!/usr/bin/env bash
299
+ set -eu
300
+ cmd="$1"
301
+ shift || true
302
+ if [[ "$cmd" == "display-message" ]]; then
303
+ target=""
304
+ format=""
305
+ while (($#)); do
306
+ case "$1" in
307
+ -p) shift ;;
308
+ -t) target="$2"; shift 2 ;;
309
+ *) format="$1"; shift ;;
310
+ esac
311
+ done
312
+ if [[ -z "$target" && "$format" == "#S" ]]; then
313
+ echo "${managedSessionName}"
314
+ exit 0
315
+ fi
316
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
317
+ echo "node"
318
+ exit 0
319
+ fi
320
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
321
+ echo "bash"
322
+ exit 0
323
+ fi
324
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
325
+ echo "${managedSessionName}"
326
+ exit 0
327
+ fi
328
+ echo "unsupported display target: $target / $format" >&2
329
+ exit 1
330
+ fi
331
+ if [[ "$cmd" == "list-panes" ]]; then
332
+ target=""
333
+ while (($#)); do
334
+ case "$1" in
335
+ -s) shift ;;
336
+ -t) target="$2"; shift 2 ;;
337
+ -F) shift 2 ;;
338
+ *) shift ;;
339
+ esac
340
+ done
341
+ if [[ "$target" == "${managedSessionName}" ]]; then
342
+ printf "%%42\\t0\\tnode\\tbash\\n%%55\\t1\\tcodex\\tcodex\\n"
343
+ exit 0
344
+ fi
345
+ echo "unexpected list-panes target: $target" >&2
346
+ exit 1
347
+ fi
348
+ echo "unsupported cmd: $cmd" >&2
349
+ exit 1
350
+ `;
351
+ await writeFile(fakeTmuxPath, fakeTmux);
352
+ await chmod(fakeTmuxPath, 0o755);
353
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
354
+ process.env.TMUX = '1';
355
+ delete process.env.TMUX_PANE;
356
+ process.env.OMX_TEAM_WORKER = '';
357
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
358
+ assert.equal(paneId, '%55');
359
+ }
360
+ finally {
361
+ process.env.PATH = originalPath;
362
+ await rm(cwd, { recursive: true, force: true });
363
+ }
364
+ });
365
+ it('fails closed for anchorless managed-session recovery when only a wrapper-launched node pane exists', async () => {
366
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-wrapper-node-session-pane-'));
367
+ const originalPath = process.env.PATH;
368
+ try {
369
+ const stateDir = join(cwd, '.omx', 'state');
370
+ const fakeBinDir = join(cwd, 'fake-bin');
371
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
372
+ const sessionId = 'omx-wrapper-node-session-pane';
373
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
374
+ await mkdir(stateDir, { recursive: true });
375
+ await mkdir(fakeBinDir, { recursive: true });
376
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
377
+ session_id: sessionId,
378
+ started_at: new Date().toISOString(),
379
+ cwd,
380
+ pid: process.pid,
381
+ platform: process.platform,
382
+ pid_start_ticks: readLinuxStartTicks(process.pid),
383
+ pid_cmdline: readLinuxCmdline(process.pid),
384
+ }, null, 2));
385
+ const fakeTmux = `#!/usr/bin/env bash
386
+ set -eu
387
+ cmd="$1"
388
+ shift || true
389
+ if [[ "$cmd" == "display-message" ]]; then
390
+ target=""
391
+ format=""
392
+ while (($#)); do
393
+ case "$1" in
394
+ -p) shift ;;
395
+ -t) target="$2"; shift 2 ;;
396
+ *) format="$1"; shift ;;
397
+ esac
398
+ done
399
+ if [[ -z "$target" && "$format" == "#S" ]]; then
400
+ echo "${managedSessionName}"
401
+ exit 0
402
+ fi
403
+ echo "unsupported display target: $target / $format" >&2
404
+ exit 1
405
+ fi
406
+ if [[ "$cmd" == "list-panes" ]]; then
407
+ target=""
408
+ while (($#)); do
409
+ case "$1" in
410
+ -s) shift ;;
411
+ -t) target="$2"; shift 2 ;;
412
+ -F) shift 2 ;;
413
+ *) shift ;;
414
+ esac
415
+ done
416
+ if [[ "$target" == "${managedSessionName}" ]]; then
417
+ printf "%%42\\t1\\tnode\\tbash\\n"
418
+ exit 0
419
+ fi
420
+ echo "unexpected list-panes target: $target" >&2
421
+ exit 1
422
+ fi
423
+ echo "unsupported cmd: $cmd" >&2
424
+ exit 1
425
+ `;
426
+ await writeFile(fakeTmuxPath, fakeTmux);
427
+ await chmod(fakeTmuxPath, 0o755);
428
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
429
+ process.env.TMUX = '1';
430
+ delete process.env.TMUX_PANE;
431
+ process.env.OMX_TEAM_WORKER = '';
432
+ const paneId = await resolveManagedSessionPane(cwd, { session_id: sessionId });
433
+ assert.equal(paneId, '');
434
+ }
435
+ finally {
436
+ process.env.PATH = originalPath;
437
+ await rm(cwd, { recursive: true, force: true });
438
+ }
439
+ });
440
+ it('keeps a wrapper-launched node anchor when detached anchor fallback has no stricter codex candidate', async () => {
441
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-wrapper-node-anchor-'));
442
+ const originalPath = process.env.PATH;
443
+ try {
444
+ const stateDir = join(cwd, '.omx', 'state');
445
+ const fakeBinDir = join(cwd, 'fake-bin');
446
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
447
+ const sessionId = 'omx-wrapper-node-anchor';
448
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
449
+ await mkdir(stateDir, { recursive: true });
450
+ await mkdir(fakeBinDir, { recursive: true });
451
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
452
+ session_id: sessionId,
453
+ started_at: new Date().toISOString(),
454
+ cwd,
455
+ pid: process.pid,
456
+ platform: process.platform,
457
+ pid_start_ticks: readLinuxStartTicks(process.pid),
458
+ pid_cmdline: readLinuxCmdline(process.pid),
459
+ }, null, 2));
460
+ const fakeTmux = `#!/usr/bin/env bash
461
+ set -eu
462
+ cmd="$1"
463
+ shift || true
464
+ if [[ "$cmd" == "display-message" ]]; then
465
+ target=""
466
+ format=""
467
+ while (($#)); do
468
+ case "$1" in
469
+ -p) shift ;;
470
+ -t) target="$2"; shift 2 ;;
471
+ *) format="$1"; shift ;;
472
+ esac
473
+ done
474
+ if [[ -z "$target" && "$format" == "#S" ]]; then
475
+ echo "${managedSessionName}"
476
+ exit 0
477
+ fi
478
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
479
+ echo "node"
480
+ exit 0
481
+ fi
482
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
483
+ echo "bash"
484
+ exit 0
485
+ fi
486
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
487
+ echo "${managedSessionName}"
488
+ exit 0
489
+ fi
490
+ echo "unsupported display target: $target / $format" >&2
491
+ exit 1
492
+ fi
493
+ if [[ "$cmd" == "list-panes" ]]; then
494
+ target=""
495
+ while (($#)); do
496
+ case "$1" in
497
+ -s) shift ;;
498
+ -t) target="$2"; shift 2 ;;
499
+ -F) shift 2 ;;
500
+ *) shift ;;
501
+ esac
502
+ done
503
+ if [[ "$target" == "${managedSessionName}" ]]; then
504
+ printf "%%42\\t1\\tnode\\tbash\\n"
505
+ exit 0
506
+ fi
507
+ echo "unexpected list-panes target: $target" >&2
508
+ exit 1
509
+ fi
510
+ echo "unsupported cmd: $cmd" >&2
511
+ exit 1
512
+ `;
513
+ await writeFile(fakeTmuxPath, fakeTmux);
514
+ await chmod(fakeTmuxPath, 0o755);
515
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
516
+ process.env.TMUX = '1';
517
+ delete process.env.TMUX_PANE;
518
+ process.env.OMX_TEAM_WORKER = '';
519
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
520
+ assert.equal(paneId, '%42');
521
+ }
522
+ finally {
523
+ process.env.PATH = originalPath;
524
+ await rm(cwd, { recursive: true, force: true });
525
+ }
526
+ });
527
+ it('fails closed for a shell-degraded codex anchor when only a detached wrapper fallback exists', async () => {
528
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-degraded-codex-wrapper-only-'));
529
+ const originalPath = process.env.PATH;
530
+ try {
531
+ const stateDir = join(cwd, '.omx', 'state');
532
+ const fakeBinDir = join(cwd, 'fake-bin');
533
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
534
+ const sessionId = 'omx-degraded-codex-wrapper-only';
535
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
536
+ await mkdir(stateDir, { recursive: true });
537
+ await mkdir(fakeBinDir, { recursive: true });
538
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
539
+ session_id: sessionId,
540
+ started_at: new Date().toISOString(),
541
+ cwd,
542
+ pid: process.pid,
543
+ platform: process.platform,
544
+ pid_start_ticks: readLinuxStartTicks(process.pid),
545
+ pid_cmdline: readLinuxCmdline(process.pid),
546
+ }, null, 2));
547
+ const fakeTmux = `#!/usr/bin/env bash
548
+ set -eu
549
+ cmd="$1"
550
+ shift || true
551
+ if [[ "$cmd" == "display-message" ]]; then
552
+ target=""
553
+ format=""
554
+ while (($#)); do
555
+ case "$1" in
556
+ -p) shift ;;
557
+ -t) target="$2"; shift 2 ;;
558
+ *) format="$1"; shift ;;
559
+ esac
560
+ done
561
+ if [[ -z "$target" && "$format" == "#S" ]]; then
562
+ echo "${managedSessionName}"
563
+ exit 0
564
+ fi
565
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
566
+ echo "bash"
567
+ exit 0
568
+ fi
569
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
570
+ echo "codex --model gpt-5"
571
+ exit 0
572
+ fi
573
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
574
+ echo "${managedSessionName}"
575
+ exit 0
576
+ fi
577
+ echo "unsupported display target: $target / $format" >&2
578
+ exit 1
579
+ fi
580
+ if [[ "$cmd" == "list-panes" ]]; then
581
+ target=""
582
+ while (($#)); do
583
+ case "$1" in
584
+ -s) shift ;;
585
+ -t) target="$2"; shift 2 ;;
586
+ -F) shift 2 ;;
587
+ *) shift ;;
588
+ esac
589
+ done
590
+ if [[ "$target" == "${managedSessionName}" ]]; then
591
+ printf "%%42\\t0\\tbash\\tcodex --model gpt-5\\n%%55\\t1\\tnode\\tbash\\n"
592
+ exit 0
593
+ fi
594
+ echo "unexpected list-panes target: $target" >&2
595
+ exit 1
596
+ fi
597
+ echo "unsupported cmd: $cmd" >&2
598
+ exit 1
599
+ `;
600
+ await writeFile(fakeTmuxPath, fakeTmux);
601
+ await chmod(fakeTmuxPath, 0o755);
602
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
603
+ process.env.TMUX = '1';
604
+ delete process.env.TMUX_PANE;
605
+ process.env.OMX_TEAM_WORKER = '';
606
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
607
+ assert.equal(paneId, '');
608
+ }
609
+ finally {
610
+ process.env.PATH = originalPath;
611
+ await rm(cwd, { recursive: true, force: true });
612
+ }
613
+ });
614
+ it('rebinds a shell-degraded codex anchor to the live codex pane in the managed session', async () => {
615
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-degraded-codex-anchor-'));
616
+ const originalPath = process.env.PATH;
617
+ try {
618
+ const stateDir = join(cwd, '.omx', 'state');
619
+ const fakeBinDir = join(cwd, 'fake-bin');
620
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
621
+ const sessionId = 'omx-degraded-codex-anchor';
622
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
623
+ await mkdir(stateDir, { recursive: true });
624
+ await mkdir(fakeBinDir, { recursive: true });
625
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
626
+ session_id: sessionId,
627
+ started_at: new Date().toISOString(),
628
+ cwd,
629
+ pid: process.pid,
630
+ platform: process.platform,
631
+ pid_start_ticks: readLinuxStartTicks(process.pid),
632
+ pid_cmdline: readLinuxCmdline(process.pid),
633
+ }, null, 2));
634
+ const fakeTmux = `#!/usr/bin/env bash
635
+ set -eu
636
+ cmd="$1"
637
+ shift || true
638
+ if [[ "$cmd" == "display-message" ]]; then
639
+ target=""
640
+ format=""
641
+ while (($#)); do
642
+ case "$1" in
643
+ -p) shift ;;
644
+ -t) target="$2"; shift 2 ;;
645
+ *) format="$1"; shift ;;
646
+ esac
647
+ done
648
+ if [[ -z "$target" && "$format" == "#S" ]]; then
649
+ echo "${managedSessionName}"
650
+ exit 0
651
+ fi
652
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
653
+ echo "bash"
654
+ exit 0
655
+ fi
656
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
657
+ echo "codex --model gpt-5"
658
+ exit 0
659
+ fi
660
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
661
+ echo "${managedSessionName}"
662
+ exit 0
663
+ fi
664
+ echo "unsupported display target: $target / $format" >&2
665
+ exit 1
666
+ fi
667
+ if [[ "$cmd" == "list-panes" ]]; then
668
+ target=""
669
+ while (($#)); do
670
+ case "$1" in
671
+ -s) shift ;;
672
+ -t) target="$2"; shift 2 ;;
673
+ -F) shift 2 ;;
674
+ *) shift ;;
675
+ esac
676
+ done
677
+ if [[ "$target" == "${managedSessionName}" ]]; then
678
+ printf "%%42\\t0\\tbash\\tcodex --model gpt-5\\n%%55\\t1\\tcodex\\tcodex\\n"
679
+ exit 0
680
+ fi
681
+ echo "unexpected list-panes target: $target" >&2
682
+ exit 1
683
+ fi
684
+ echo "unsupported cmd: $cmd" >&2
685
+ exit 1
686
+ `;
687
+ await writeFile(fakeTmuxPath, fakeTmux);
688
+ await chmod(fakeTmuxPath, 0o755);
689
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
690
+ process.env.TMUX = '1';
691
+ delete process.env.TMUX_PANE;
692
+ process.env.OMX_TEAM_WORKER = '';
693
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
694
+ assert.equal(paneId, '%55');
695
+ }
696
+ finally {
697
+ process.env.PATH = originalPath;
698
+ await rm(cwd, { recursive: true, force: true });
699
+ }
700
+ });
701
+ it('ignores an active shell-degraded codex pane when selecting the live managed replacement', async () => {
702
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-active-degraded-codex-anchor-'));
703
+ const originalPath = process.env.PATH;
704
+ try {
705
+ const stateDir = join(cwd, '.omx', 'state');
706
+ const fakeBinDir = join(cwd, 'fake-bin');
707
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
708
+ const sessionId = 'omx-active-degraded-codex-anchor';
709
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
710
+ await mkdir(stateDir, { recursive: true });
711
+ await mkdir(fakeBinDir, { recursive: true });
712
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
713
+ session_id: sessionId,
714
+ started_at: new Date().toISOString(),
715
+ cwd,
716
+ pid: process.pid,
717
+ platform: process.platform,
718
+ pid_start_ticks: readLinuxStartTicks(process.pid),
719
+ pid_cmdline: readLinuxCmdline(process.pid),
720
+ }, null, 2));
721
+ const fakeTmux = `#!/usr/bin/env bash
722
+ set -eu
723
+ cmd="$1"
724
+ shift || true
725
+ if [[ "$cmd" == "display-message" ]]; then
726
+ target=""
727
+ format=""
728
+ while (($#)); do
729
+ case "$1" in
730
+ -p) shift ;;
731
+ -t) target="$2"; shift 2 ;;
732
+ *) format="$1"; shift ;;
733
+ esac
734
+ done
735
+ if [[ -z "$target" && "$format" == "#S" ]]; then
736
+ echo "${managedSessionName}"
737
+ exit 0
738
+ fi
739
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
740
+ echo "zsh"
741
+ exit 0
742
+ fi
743
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
744
+ echo "codex --model gpt-5"
745
+ exit 0
746
+ fi
747
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
748
+ echo "${managedSessionName}"
749
+ exit 0
750
+ fi
751
+ echo "unsupported display target: $target / $format" >&2
752
+ exit 1
753
+ fi
754
+ if [[ "$cmd" == "list-panes" ]]; then
755
+ target=""
756
+ while (($#)); do
757
+ case "$1" in
758
+ -s) shift ;;
759
+ -t) target="$2"; shift 2 ;;
760
+ -F) shift 2 ;;
761
+ *) shift ;;
762
+ esac
763
+ done
764
+ if [[ "$target" == "${managedSessionName}" ]]; then
765
+ printf "%%42\\t1\\tzsh\\tcodex --model gpt-5\\n%%55\\t0\\tcodex\\tcodex\\n"
766
+ exit 0
767
+ fi
768
+ echo "unexpected list-panes target: $target" >&2
769
+ exit 1
770
+ fi
771
+ echo "unsupported cmd: $cmd" >&2
772
+ exit 1
773
+ `;
774
+ await writeFile(fakeTmuxPath, fakeTmux);
775
+ await chmod(fakeTmuxPath, 0o755);
776
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
777
+ process.env.TMUX = '1';
778
+ delete process.env.TMUX_PANE;
779
+ process.env.OMX_TEAM_WORKER = '';
780
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
781
+ assert.equal(paneId, '%55');
782
+ }
783
+ finally {
784
+ process.env.PATH = originalPath;
785
+ await rm(cwd, { recursive: true, force: true });
786
+ }
787
+ });
788
+ it('rebinds a degraded anchor using the verified session name when a follow-up #S lookup would fail', async () => {
789
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-degraded-anchor-session-reuse-'));
790
+ const originalPath = process.env.PATH;
791
+ try {
792
+ const stateDir = join(cwd, '.omx', 'state');
793
+ const fakeBinDir = join(cwd, 'fake-bin');
794
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
795
+ const sessionId = 'omx-degraded-anchor-session-reuse';
796
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
797
+ const sessionLookupCountPath = join(cwd, 'session-lookup-count');
798
+ await mkdir(stateDir, { recursive: true });
799
+ await mkdir(fakeBinDir, { recursive: true });
800
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
801
+ session_id: sessionId,
802
+ started_at: new Date().toISOString(),
803
+ cwd,
804
+ pid: process.pid,
805
+ platform: process.platform,
806
+ pid_start_ticks: readLinuxStartTicks(process.pid),
807
+ pid_cmdline: readLinuxCmdline(process.pid),
808
+ }, null, 2));
809
+ const fakeTmux = `#!/usr/bin/env bash
810
+ set -eu
811
+ cmd="$1"
812
+ shift || true
813
+ if [[ "$cmd" == "display-message" ]]; then
814
+ target=""
815
+ format=""
816
+ while (($#)); do
817
+ case "$1" in
818
+ -p) shift ;;
819
+ -t) target="$2"; shift 2 ;;
820
+ *) format="$1"; shift ;;
821
+ esac
822
+ done
823
+ if [[ -z "$target" && "$format" == "#S" ]]; then
824
+ echo "${managedSessionName}"
825
+ exit 0
826
+ fi
827
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
828
+ echo "bash"
829
+ exit 0
830
+ fi
831
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
832
+ echo "codex --model gpt-5"
833
+ exit 0
834
+ fi
835
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
836
+ count=0
837
+ if [[ -f "${sessionLookupCountPath}" ]]; then
838
+ count="$(cat "${sessionLookupCountPath}")"
839
+ fi
840
+ count=$((count + 1))
841
+ printf '%s' "$count" > "${sessionLookupCountPath}"
842
+ if [[ "$count" -gt 1 ]]; then
843
+ echo "session lookup should not repeat" >&2
844
+ exit 1
845
+ fi
846
+ echo "${managedSessionName}"
847
+ exit 0
848
+ fi
849
+ echo "unsupported display target: $target / $format" >&2
850
+ exit 1
851
+ fi
852
+ if [[ "$cmd" == "list-panes" ]]; then
853
+ target=""
854
+ while (($#)); do
855
+ case "$1" in
856
+ -s) shift ;;
857
+ -t) target="$2"; shift 2 ;;
858
+ -F) shift 2 ;;
859
+ *) shift ;;
860
+ esac
861
+ done
862
+ if [[ "$target" == "${managedSessionName}" ]]; then
863
+ printf "%%42\\t1\\tbash\\tcodex --model gpt-5\\n%%55\\t0\\tcodex\\tcodex\\n"
864
+ exit 0
865
+ fi
866
+ echo "unexpected list-panes target: $target" >&2
867
+ exit 1
868
+ fi
869
+ echo "unsupported cmd: $cmd" >&2
870
+ exit 1
871
+ `;
872
+ await writeFile(fakeTmuxPath, fakeTmux);
873
+ await chmod(fakeTmuxPath, 0o755);
874
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
875
+ process.env.TMUX = '1';
876
+ delete process.env.TMUX_PANE;
877
+ process.env.OMX_TEAM_WORKER = '';
878
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
879
+ assert.equal(paneId, '%55');
880
+ }
881
+ finally {
882
+ process.env.PATH = originalPath;
883
+ await rm(cwd, { recursive: true, force: true });
884
+ }
885
+ });
886
+ it('fails closed when a degraded anchor has no live codex sibling in the managed session', async () => {
887
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-degraded-anchor-no-live-sibling-'));
888
+ const originalPath = process.env.PATH;
889
+ try {
890
+ const stateDir = join(cwd, '.omx', 'state');
891
+ const fakeBinDir = join(cwd, 'fake-bin');
892
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
893
+ const sessionId = 'omx-degraded-anchor-no-live-sibling';
894
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
895
+ await mkdir(stateDir, { recursive: true });
896
+ await mkdir(fakeBinDir, { recursive: true });
897
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
898
+ session_id: sessionId,
899
+ started_at: new Date().toISOString(),
900
+ cwd,
901
+ pid: process.pid,
902
+ platform: process.platform,
903
+ pid_start_ticks: readLinuxStartTicks(process.pid),
904
+ pid_cmdline: readLinuxCmdline(process.pid),
905
+ }, null, 2));
906
+ const fakeTmux = `#!/usr/bin/env bash
907
+ set -eu
908
+ cmd="$1"
909
+ shift || true
910
+ if [[ "$cmd" == "display-message" ]]; then
911
+ target=""
912
+ format=""
913
+ while (($#)); do
914
+ case "$1" in
915
+ -p) shift ;;
916
+ -t) target="$2"; shift 2 ;;
917
+ *) format="$1"; shift ;;
918
+ esac
919
+ done
920
+ if [[ -z "$target" && "$format" == "#S" ]]; then
921
+ echo "${managedSessionName}"
922
+ exit 0
923
+ fi
924
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "%42" ]]; then
925
+ echo "bash"
926
+ exit 0
927
+ fi
928
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "%42" ]]; then
929
+ echo "codex --model gpt-5"
930
+ exit 0
931
+ fi
932
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
933
+ echo "${managedSessionName}"
934
+ exit 0
935
+ fi
936
+ echo "unsupported display target: $target / $format" >&2
937
+ exit 1
938
+ fi
939
+ if [[ "$cmd" == "list-panes" ]]; then
940
+ target=""
941
+ while (($#)); do
942
+ case "$1" in
943
+ -s) shift ;;
944
+ -t) target="$2"; shift 2 ;;
945
+ -F) shift 2 ;;
946
+ *) shift ;;
947
+ esac
948
+ done
949
+ if [[ "$target" == "${managedSessionName}" ]]; then
950
+ printf "%%42\\t1\\tbash\\tcodex --model gpt-5\\n%%55\\t0\\tbash\\tbash\\n"
951
+ exit 0
952
+ fi
953
+ echo "unexpected list-panes target: $target" >&2
954
+ exit 1
955
+ fi
956
+ echo "unsupported cmd: $cmd" >&2
957
+ exit 1
958
+ `;
959
+ await writeFile(fakeTmuxPath, fakeTmux);
960
+ await chmod(fakeTmuxPath, 0o755);
961
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
962
+ process.env.TMUX = '1';
963
+ delete process.env.TMUX_PANE;
964
+ process.env.OMX_TEAM_WORKER = '';
965
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
966
+ assert.equal(paneId, '');
967
+ }
968
+ finally {
969
+ process.env.PATH = originalPath;
970
+ await rm(cwd, { recursive: true, force: true });
971
+ }
972
+ });
973
+ it('keeps a verified live anchor when command-state lookup fails and another codex pane is active', async () => {
974
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-managed-anchor-lookup-failure-'));
975
+ const originalPath = process.env.PATH;
976
+ try {
977
+ const stateDir = join(cwd, '.omx', 'state');
978
+ const fakeBinDir = join(cwd, 'fake-bin');
979
+ const fakeTmuxPath = join(fakeBinDir, 'tmux');
980
+ const sessionId = 'omx-anchor-lookup-failure';
981
+ const managedSessionName = buildTmuxSessionName(cwd, sessionId);
982
+ await mkdir(stateDir, { recursive: true });
983
+ await mkdir(fakeBinDir, { recursive: true });
984
+ await writeFile(join(stateDir, 'session.json'), JSON.stringify({
985
+ session_id: sessionId,
986
+ started_at: new Date().toISOString(),
987
+ cwd,
988
+ pid: process.pid,
989
+ platform: process.platform,
990
+ pid_start_ticks: readLinuxStartTicks(process.pid),
991
+ pid_cmdline: readLinuxCmdline(process.pid),
992
+ }, null, 2));
993
+ const fakeTmux = `#!/usr/bin/env bash
994
+ set -eu
995
+ cmd="$1"
996
+ shift || true
997
+ if [[ "$cmd" == "display-message" ]]; then
998
+ target=""
999
+ format=""
1000
+ while (($#)); do
1001
+ case "$1" in
1002
+ -p) shift ;;
1003
+ -t) target="$2"; shift 2 ;;
1004
+ *) format="$1"; shift ;;
1005
+ esac
1006
+ done
1007
+ if [[ -z "$target" && "$format" == "#S" ]]; then
1008
+ echo "${managedSessionName}"
1009
+ exit 0
1010
+ fi
1011
+ if [[ "$format" == "#S" && "$target" == "%42" ]]; then
1012
+ echo "${managedSessionName}"
1013
+ exit 0
1014
+ fi
1015
+ if [[ "$target" == "%42" && ( "$format" == "#{pane_current_command}" || "$format" == "#{pane_start_command}" ) ]]; then
1016
+ echo "transient lookup failure" >&2
1017
+ exit 1
1018
+ fi
1019
+ echo "unsupported display target: $target / $format" >&2
1020
+ exit 1
1021
+ fi
1022
+ if [[ "$cmd" == "list-panes" ]]; then
1023
+ target=""
1024
+ while (($#)); do
1025
+ case "$1" in
1026
+ -s) shift ;;
1027
+ -t) target="$2"; shift 2 ;;
1028
+ -F) shift 2 ;;
1029
+ *) shift ;;
1030
+ esac
1031
+ done
1032
+ if [[ "$target" == "${managedSessionName}" ]]; then
1033
+ printf "%%42\\t0\\tcodex\\tcodex\\n%%55\\t1\\tcodex\\tcodex\\n"
1034
+ exit 0
1035
+ fi
1036
+ echo "unexpected list-panes target: $target" >&2
1037
+ exit 1
1038
+ fi
1039
+ echo "unsupported cmd: $cmd" >&2
1040
+ exit 1
1041
+ `;
1042
+ await writeFile(fakeTmuxPath, fakeTmux);
1043
+ await chmod(fakeTmuxPath, 0o755);
1044
+ process.env.PATH = `${fakeBinDir}:${originalPath || ''}`;
1045
+ process.env.TMUX = '1';
1046
+ delete process.env.TMUX_PANE;
1047
+ process.env.OMX_TEAM_WORKER = '';
1048
+ const paneId = await resolveManagedPaneFromAnchor('%42', cwd, { session_id: sessionId }, { allowTeamWorker: false });
1049
+ assert.equal(paneId, '%42');
1050
+ }
1051
+ finally {
1052
+ process.env.PATH = originalPath;
1053
+ await rm(cwd, { recursive: true, force: true });
1054
+ }
1055
+ });
163
1056
  });
164
1057
  //# sourceMappingURL=notify-hook-managed-tmux.test.js.map