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
@@ -7,7 +7,7 @@ import { join } from 'node:path';
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { initTeamState, enqueueDispatchRequest, readDispatchRequest } from '../../team/state.js';
10
- import { buildWindowsMsysBackgroundHelperBootstrapScript } from '../../cli/index.js';
10
+ import { buildTmuxSessionName, buildWindowsMsysBackgroundHelperBootstrapScript } from '../../cli/index.js';
11
11
  import { writeSessionStart } from '../session.js';
12
12
  const DEFAULT_AUTO_NUDGE_RESPONSE = 'continue with the current task only if it is already authorized';
13
13
  async function appendLine(path, line) {
@@ -236,6 +236,101 @@ fi
236
236
  exit 0
237
237
  `;
238
238
  }
239
+ function buildManagedRalphTmux(tmuxLogPath, options) {
240
+ const { cwd, managedSessionName, anchorPane, livePane, codexPanes, missingAnchor = false } = options;
241
+ const panes = (codexPanes && codexPanes.length > 0)
242
+ ? codexPanes
243
+ : [{ paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' }];
244
+ const listPaneOutput = panes
245
+ .map((pane) => {
246
+ const paneId = pane.paneId;
247
+ const active = pane.active ? '1' : '0';
248
+ const currentCommand = pane.currentCommand || 'codex';
249
+ const startCommand = pane.startCommand || 'codex';
250
+ return `${paneId}\t${active}\t${currentCommand}\t${startCommand}`;
251
+ })
252
+ .join('\n');
253
+ const paneCommandBranches = panes
254
+ .map((pane) => {
255
+ const currentCommand = (pane.currentCommand || 'codex').replace(/"/g, '\\"');
256
+ const startCommand = (pane.startCommand || 'codex').replace(/"/g, '\\"');
257
+ return ` if [[ "$format" == "#{pane_current_command}" && "$target" == "${pane.paneId}" ]]; then
258
+ echo "${currentCommand}"
259
+ exit 0
260
+ fi
261
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "${pane.paneId}" ]]; then
262
+ echo "${startCommand}"
263
+ exit 0
264
+ fi`;
265
+ })
266
+ .join('\n');
267
+ return `#!/usr/bin/env bash
268
+ set -eu
269
+ echo "$@" >> "${tmuxLogPath}"
270
+ cmd="$1"
271
+ shift || true
272
+ if [[ "$cmd" == "display-message" ]]; then
273
+ target=""
274
+ format=""
275
+ while [[ "$#" -gt 0 ]]; do
276
+ case "$1" in
277
+ -p) shift ;;
278
+ -t) target="$2"; shift 2 ;;
279
+ *) format="$1"; shift ;;
280
+ esac
281
+ done
282
+ if [[ "$target" == "${anchorPane}" && "${missingAnchor ? '1' : '0'}" == "1" ]]; then
283
+ echo "pane missing" >&2
284
+ exit 1
285
+ fi
286
+ if [[ "$format" == "#{pane_in_mode}" ]]; then
287
+ echo "0"
288
+ exit 0
289
+ fi
290
+ if [[ "$format" == "#{pane_id}" ]]; then
291
+ echo "$target"
292
+ exit 0
293
+ fi
294
+ if [[ "$format" == "#{pane_current_path}" ]]; then
295
+ echo "${cwd}"
296
+ exit 0
297
+ fi
298
+ ${paneCommandBranches}
299
+ if [[ "$format" == "#S" ]]; then
300
+ if [[ "$target" == "${anchorPane}" || "$target" == "${livePane}" ]]; then
301
+ echo "${managedSessionName}"
302
+ exit 0
303
+ fi
304
+ echo "unknown target" >&2
305
+ exit 1
306
+ fi
307
+ exit 0
308
+ fi
309
+ if [[ "$cmd" == "list-panes" ]]; then
310
+ target=""
311
+ while [[ "$#" -gt 0 ]]; do
312
+ case "$1" in
313
+ -F) shift 2 ;;
314
+ -t) shift; target="$1" ;;
315
+ esac
316
+ shift || true
317
+ done
318
+ if [[ "$target" == "${managedSessionName}" ]]; then
319
+ printf '%s\n' "${listPaneOutput}"
320
+ exit 0
321
+ fi
322
+ echo "can't find session" >&2
323
+ exit 1
324
+ fi
325
+ if [[ "$cmd" == "capture-pane" ]]; then
326
+ exit 0
327
+ fi
328
+ if [[ "$cmd" == "send-keys" ]]; then
329
+ exit 0
330
+ fi
331
+ exit 0
332
+ `;
333
+ }
239
334
  function buildCleanNotifyEnv(overrides = {}) {
240
335
  return {
241
336
  ...process.env,
@@ -1941,8 +2036,391 @@ exit 0
1941
2036
  await rm(wd, { recursive: true, force: true });
1942
2037
  }
1943
2038
  });
2039
+ it('rebinds a stale-but-present session-scoped Ralph shell pane to the live pane before continue steer', async () => {
2040
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-rebind-stale-anchor-'));
2041
+ const fakeBinDir = join(wd, 'fake-bin');
2042
+ const stateDir = join(wd, '.omx', 'state');
2043
+ const tmuxLogPath = join(wd, 'tmux.log');
2044
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2045
+ const sessionId = 'sess-ralph-rebind';
2046
+ const sessionStateDir = join(stateDir, 'sessions', sessionId);
2047
+ const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2048
+ const anchorPane = '%99';
2049
+ const livePane = '%42';
2050
+ try {
2051
+ await mkdir(sessionStateDir, { recursive: true });
2052
+ await mkdir(fakeBinDir, { recursive: true });
2053
+ await writeSessionStart(wd, sessionId);
2054
+ const managedSessionName = buildTmuxSessionName(wd, sessionId);
2055
+ await writeFile(join(fakeBinDir, 'tmux'), buildManagedRalphTmux(tmuxLogPath, {
2056
+ cwd: wd,
2057
+ managedSessionName,
2058
+ anchorPane,
2059
+ livePane,
2060
+ codexPanes: [
2061
+ { paneId: anchorPane, active: false, currentCommand: 'sh', startCommand: 'bash' },
2062
+ { paneId: '%41', active: false, currentCommand: 'codex', startCommand: 'codex' },
2063
+ { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2064
+ ],
2065
+ }));
2066
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2067
+ await writeFile(ralphStatePath, JSON.stringify({
2068
+ active: true,
2069
+ current_phase: 'executing',
2070
+ tmux_pane_id: anchorPane,
2071
+ }, null, 2));
2072
+ await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2073
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2074
+ }, null, 2));
2075
+ await writeFile(watcherStatePath, JSON.stringify({
2076
+ ralph_continue_steer: {
2077
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2078
+ },
2079
+ }, null, 2));
2080
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2081
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2082
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2083
+ encoding: 'utf-8',
2084
+ env: buildCleanNotifyEnv({
2085
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2086
+ }),
2087
+ });
2088
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2089
+ const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2090
+ assert.equal(persistedRalph.tmux_pane_id, livePane);
2091
+ assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2092
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2093
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2094
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2095
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2096
+ assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2097
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2098
+ }
2099
+ finally {
2100
+ await rm(wd, { recursive: true, force: true });
2101
+ }
2102
+ });
2103
+ it('preserves newer Ralph state fields when a pane rebound happens after the state file advances', async () => {
2104
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-rebind-state-merge-'));
2105
+ const fakeBinDir = join(wd, 'fake-bin');
2106
+ const stateDir = join(wd, '.omx', 'state');
2107
+ const tmuxLogPath = join(wd, 'tmux.log');
2108
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2109
+ const sessionId = 'sess-ralph-rebind-merge';
2110
+ const sessionStateDir = join(stateDir, 'sessions', sessionId);
2111
+ const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2112
+ const anchorPane = '%99';
2113
+ const livePane = '%42';
2114
+ try {
2115
+ await mkdir(sessionStateDir, { recursive: true });
2116
+ await mkdir(fakeBinDir, { recursive: true });
2117
+ await writeSessionStart(wd, sessionId);
2118
+ const managedSessionName = buildTmuxSessionName(wd, sessionId);
2119
+ const fakeTmux = `#!/usr/bin/env bash
2120
+ set -eu
2121
+ echo "$@" >> "${tmuxLogPath}"
2122
+ cmd="$1"
2123
+ shift || true
2124
+ if [[ "$cmd" == "display-message" ]]; then
2125
+ target=""
2126
+ format=""
2127
+ while [[ "$#" -gt 0 ]]; do
2128
+ case "$1" in
2129
+ -p) shift ;;
2130
+ -t) target="$2"; shift 2 ;;
2131
+ *) format="$1"; shift ;;
2132
+ esac
2133
+ done
2134
+ if [[ "$format" == "#{pane_in_mode}" ]]; then
2135
+ echo "0"
2136
+ exit 0
2137
+ fi
2138
+ if [[ "$format" == "#{pane_id}" ]]; then
2139
+ echo "$target"
2140
+ exit 0
2141
+ fi
2142
+ if [[ "$format" == "#{pane_current_path}" ]]; then
2143
+ echo "${wd}"
2144
+ exit 0
2145
+ fi
2146
+ if [[ "$format" == "#{pane_current_command}" && "$target" == "${anchorPane}" ]]; then
2147
+ echo "sh"
2148
+ exit 0
2149
+ fi
2150
+ if [[ "$format" == "#{pane_start_command}" && "$target" == "${anchorPane}" ]]; then
2151
+ echo "bash"
2152
+ exit 0
2153
+ fi
2154
+ if [[ "$format" == "#S" && "$target" == "${anchorPane}" ]]; then
2155
+ echo "${managedSessionName}"
2156
+ exit 0
2157
+ fi
2158
+ exit 0
2159
+ fi
2160
+ if [[ "$cmd" == "list-panes" ]]; then
2161
+ target=""
2162
+ while [[ "$#" -gt 0 ]]; do
2163
+ case "$1" in
2164
+ -F) shift 2 ;;
2165
+ -t) shift; target="$1" ;;
2166
+ esac
2167
+ shift || true
2168
+ done
2169
+ if [[ "$target" == "${managedSessionName}" ]]; then
2170
+ cat > "${ralphStatePath}" <<'JSON'
2171
+ {
2172
+ "active": true,
2173
+ "current_phase": "reviewing",
2174
+ "iteration": 11,
2175
+ "owner_codex_session_id": "codex-updated-owner",
2176
+ "tmux_pane_id": "%99"
2177
+ }
2178
+ JSON
2179
+ printf "%%99\t0\tsh\tbash\n%%42\t1\tcodex\tcodex\n"
2180
+ exit 0
2181
+ fi
2182
+ echo "can't find session" >&2
2183
+ exit 1
2184
+ fi
2185
+ if [[ "$cmd" == "capture-pane" ]]; then
2186
+ exit 0
2187
+ fi
2188
+ if [[ "$cmd" == "send-keys" ]]; then
2189
+ exit 0
2190
+ fi
2191
+ exit 0
2192
+ `;
2193
+ await writeFile(join(fakeBinDir, 'tmux'), fakeTmux);
2194
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2195
+ await writeFile(ralphStatePath, JSON.stringify({
2196
+ active: true,
2197
+ current_phase: 'executing',
2198
+ iteration: 1,
2199
+ owner_codex_session_id: 'codex-stale-owner',
2200
+ tmux_pane_id: anchorPane,
2201
+ }, null, 2));
2202
+ await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2203
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2204
+ }, null, 2));
2205
+ await writeFile(watcherStatePath, JSON.stringify({
2206
+ ralph_continue_steer: {
2207
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2208
+ },
2209
+ }, null, 2));
2210
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2211
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2212
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2213
+ encoding: 'utf-8',
2214
+ env: buildCleanNotifyEnv({
2215
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2216
+ }),
2217
+ });
2218
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2219
+ const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2220
+ assert.equal(persistedRalph.tmux_pane_id, livePane);
2221
+ assert.equal(persistedRalph.current_phase, 'reviewing');
2222
+ assert.equal(persistedRalph.iteration, 11);
2223
+ assert.equal(persistedRalph.owner_codex_session_id, 'codex-updated-owner');
2224
+ assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2225
+ }
2226
+ finally {
2227
+ await rm(wd, { recursive: true, force: true });
2228
+ }
2229
+ });
2230
+ it('keeps the verified Ralph anchor pane when another codex pane is focused in the same managed session', async () => {
2231
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-keep-anchor-pane-'));
2232
+ const fakeBinDir = join(wd, 'fake-bin');
2233
+ const stateDir = join(wd, '.omx', 'state');
2234
+ const tmuxLogPath = join(wd, 'tmux.log');
2235
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2236
+ const sessionId = 'sess-ralph-keep-anchor';
2237
+ const sessionStateDir = join(stateDir, 'sessions', sessionId);
2238
+ const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2239
+ const anchorPane = '%99';
2240
+ const livePane = '%42';
2241
+ try {
2242
+ await mkdir(sessionStateDir, { recursive: true });
2243
+ await mkdir(fakeBinDir, { recursive: true });
2244
+ await writeSessionStart(wd, sessionId);
2245
+ const managedSessionName = buildTmuxSessionName(wd, sessionId);
2246
+ await writeFile(join(fakeBinDir, 'tmux'), buildManagedRalphTmux(tmuxLogPath, {
2247
+ cwd: wd,
2248
+ managedSessionName,
2249
+ anchorPane,
2250
+ livePane,
2251
+ codexPanes: [
2252
+ { paneId: anchorPane, active: false, currentCommand: 'codex', startCommand: 'codex' },
2253
+ { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2254
+ ],
2255
+ }));
2256
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2257
+ await writeFile(ralphStatePath, JSON.stringify({
2258
+ active: true,
2259
+ current_phase: 'executing',
2260
+ tmux_pane_id: anchorPane,
2261
+ }, null, 2));
2262
+ await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2263
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2264
+ }, null, 2));
2265
+ await writeFile(watcherStatePath, JSON.stringify({
2266
+ ralph_continue_steer: {
2267
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2268
+ },
2269
+ }, null, 2));
2270
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2271
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2272
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2273
+ encoding: 'utf-8',
2274
+ env: buildCleanNotifyEnv({
2275
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2276
+ }),
2277
+ });
2278
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2279
+ const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2280
+ assert.equal(persistedRalph.tmux_pane_id, anchorPane);
2281
+ assert.equal(typeof persistedRalph.tmux_pane_set_at, 'undefined');
2282
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2283
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2284
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, anchorPane);
2285
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2286
+ assert.match(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2287
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2288
+ }
2289
+ finally {
2290
+ await rm(wd, { recursive: true, force: true });
2291
+ }
2292
+ });
2293
+ it('rebinds a shell-degraded codex anchor to the live pane before continue steer', async () => {
2294
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-rebind-degraded-codex-anchor-'));
2295
+ const fakeBinDir = join(wd, 'fake-bin');
2296
+ const stateDir = join(wd, '.omx', 'state');
2297
+ const tmuxLogPath = join(wd, 'tmux.log');
2298
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2299
+ const sessionId = 'sess-ralph-degraded-codex-anchor';
2300
+ const sessionStateDir = join(stateDir, 'sessions', sessionId);
2301
+ const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2302
+ const anchorPane = '%99';
2303
+ const livePane = '%42';
2304
+ try {
2305
+ await mkdir(sessionStateDir, { recursive: true });
2306
+ await mkdir(fakeBinDir, { recursive: true });
2307
+ await writeSessionStart(wd, sessionId);
2308
+ const managedSessionName = buildTmuxSessionName(wd, sessionId);
2309
+ await writeFile(join(fakeBinDir, 'tmux'), buildManagedRalphTmux(tmuxLogPath, {
2310
+ cwd: wd,
2311
+ managedSessionName,
2312
+ anchorPane,
2313
+ livePane,
2314
+ codexPanes: [
2315
+ { paneId: anchorPane, active: true, currentCommand: 'bash', startCommand: 'codex --model gpt-5' },
2316
+ { paneId: livePane, active: false, currentCommand: 'codex', startCommand: 'codex' },
2317
+ ],
2318
+ }));
2319
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2320
+ await writeFile(ralphStatePath, JSON.stringify({
2321
+ active: true,
2322
+ current_phase: 'executing',
2323
+ tmux_pane_id: anchorPane,
2324
+ }, null, 2));
2325
+ await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2326
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2327
+ }, null, 2));
2328
+ await writeFile(watcherStatePath, JSON.stringify({
2329
+ ralph_continue_steer: {
2330
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2331
+ },
2332
+ }, null, 2));
2333
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2334
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2335
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2336
+ encoding: 'utf-8',
2337
+ env: buildCleanNotifyEnv({
2338
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2339
+ }),
2340
+ });
2341
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2342
+ const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2343
+ assert.equal(persistedRalph.tmux_pane_id, livePane);
2344
+ assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2345
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2346
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2347
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2348
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2349
+ assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2350
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2351
+ }
2352
+ finally {
2353
+ await rm(wd, { recursive: true, force: true });
2354
+ }
2355
+ });
2356
+ it('falls back to the current managed session pane when the stored Ralph pane anchor is dead', async () => {
2357
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-rebind-dead-anchor-'));
2358
+ const fakeBinDir = join(wd, 'fake-bin');
2359
+ const stateDir = join(wd, '.omx', 'state');
2360
+ const tmuxLogPath = join(wd, 'tmux.log');
2361
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2362
+ const sessionId = 'sess-ralph-dead-anchor';
2363
+ const sessionStateDir = join(stateDir, 'sessions', sessionId);
2364
+ const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2365
+ const anchorPane = '%99';
2366
+ const livePane = '%42';
2367
+ try {
2368
+ await mkdir(sessionStateDir, { recursive: true });
2369
+ await mkdir(fakeBinDir, { recursive: true });
2370
+ await writeSessionStart(wd, sessionId);
2371
+ const managedSessionName = buildTmuxSessionName(wd, sessionId);
2372
+ await writeFile(join(fakeBinDir, 'tmux'), buildManagedRalphTmux(tmuxLogPath, {
2373
+ cwd: wd,
2374
+ managedSessionName,
2375
+ anchorPane,
2376
+ livePane,
2377
+ codexPanes: [
2378
+ { paneId: '%41', active: false, currentCommand: 'codex', startCommand: 'codex' },
2379
+ { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2380
+ ],
2381
+ missingAnchor: true,
2382
+ }));
2383
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2384
+ await writeFile(ralphStatePath, JSON.stringify({
2385
+ active: true,
2386
+ current_phase: 'executing',
2387
+ tmux_pane_id: anchorPane,
2388
+ }, null, 2));
2389
+ await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2390
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2391
+ }, null, 2));
2392
+ await writeFile(watcherStatePath, JSON.stringify({
2393
+ ralph_continue_steer: {
2394
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2395
+ },
2396
+ }, null, 2));
2397
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2398
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2399
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2400
+ encoding: 'utf-8',
2401
+ env: buildCleanNotifyEnv({
2402
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2403
+ }),
2404
+ });
2405
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2406
+ const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2407
+ assert.equal(persistedRalph.tmux_pane_id, livePane);
2408
+ assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2409
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2410
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2411
+ assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2412
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2413
+ assert.match(tmuxLog, /display-message -p -t %99 #S/);
2414
+ assert.match(tmuxLog, /list-panes -s -t .*sess-ralph-dead-anchor/);
2415
+ assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2416
+ assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/);
2417
+ }
2418
+ finally {
2419
+ await rm(wd, { recursive: true, force: true });
2420
+ }
2421
+ });
1944
2422
  it('sends the first Ralph continue steer immediately when persisted steer state is empty', async () => {
1945
- const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-first-steer-'));
2423
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-startup-cooldown-'));
1946
2424
  const fakeBinDir = join(wd, 'fake-bin');
1947
2425
  const stateDir = join(wd, '.omx', 'state');
1948
2426
  const tmuxLogPath = join(wd, 'tmux.log');
@@ -2029,6 +2507,51 @@ exit 0
2029
2507
  await rm(wd, { recursive: true, force: true });
2030
2508
  }
2031
2509
  });
2510
+ it('treats blocked_on_user as terminal so Ralph continue steer stays off', async () => {
2511
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-blocked-on-user-'));
2512
+ const fakeBinDir = join(wd, 'fake-bin');
2513
+ const tmuxLogPath = join(wd, 'tmux.log');
2514
+ const stateDir = join(wd, '.omx', 'state');
2515
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2516
+ try {
2517
+ await mkdir(stateDir, { recursive: true });
2518
+ await mkdir(fakeBinDir, { recursive: true });
2519
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
2520
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2521
+ await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2522
+ active: false,
2523
+ current_phase: 'blocked_on_user',
2524
+ completed_at: new Date().toISOString(),
2525
+ tmux_pane_id: '%42',
2526
+ }, null, 2));
2527
+ await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2528
+ last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2529
+ }, null, 2));
2530
+ await writeFile(watcherStatePath, JSON.stringify({
2531
+ ralph_continue_steer: {
2532
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2533
+ },
2534
+ }, null, 2));
2535
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2536
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2537
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2538
+ encoding: 'utf-8',
2539
+ env: buildCleanNotifyEnv({
2540
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2541
+ }),
2542
+ });
2543
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2544
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2545
+ const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/g) || [];
2546
+ assert.equal(sends.length, 0, 'blocked_on_user should suppress Ralph continue steer');
2547
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2548
+ assert.equal(watcherState.ralph_continue_steer?.active, false);
2549
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'terminal');
2550
+ }
2551
+ finally {
2552
+ await rm(wd, { recursive: true, force: true });
2553
+ }
2554
+ });
2032
2555
  it('stops Ralph continue steer immediately once Ralph state is terminal or cleared', async () => {
2033
2556
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-terminal-'));
2034
2557
  const fakeBinDir = join(wd, 'fake-bin');
@@ -2133,6 +2656,51 @@ exit 0
2133
2656
  await rm(wd, { recursive: true, force: true });
2134
2657
  }
2135
2658
  });
2659
+ it('treats an explicit blocked_on_user run_outcome as terminal for Ralph continue steer', async () => {
2660
+ const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-blocked-on-user-'));
2661
+ const fakeBinDir = join(wd, 'fake-bin');
2662
+ const tmuxLogPath = join(wd, 'tmux.log');
2663
+ const stateDir = join(wd, '.omx', 'state');
2664
+ const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2665
+ try {
2666
+ await mkdir(stateDir, { recursive: true });
2667
+ await mkdir(fakeBinDir, { recursive: true });
2668
+ await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
2669
+ await chmod(join(fakeBinDir, 'tmux'), 0o755);
2670
+ await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2671
+ active: true,
2672
+ current_phase: 'executing',
2673
+ run_outcome: 'blocked_on_user',
2674
+ tmux_pane_id: '%42',
2675
+ }, null, 2));
2676
+ await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2677
+ last_progress_at: new Date(Date.now() - 5 * 60_000).toISOString(),
2678
+ }, null, 2));
2679
+ await writeFile(watcherStatePath, JSON.stringify({
2680
+ ralph_continue_steer: {
2681
+ last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2682
+ },
2683
+ }, null, 2));
2684
+ const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
2685
+ const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
2686
+ const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2687
+ encoding: 'utf-8',
2688
+ env: buildCleanNotifyEnv({
2689
+ PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
2690
+ }),
2691
+ });
2692
+ assert.equal(run.status, 0, run.stderr || run.stdout);
2693
+ const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2694
+ const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/g) || [];
2695
+ assert.equal(sends.length, 0, 'blocked_on_user should suppress Ralph continue steer');
2696
+ const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2697
+ assert.equal(watcherState.ralph_continue_steer?.active, false);
2698
+ assert.equal(watcherState.ralph_continue_steer?.last_reason, 'terminal');
2699
+ }
2700
+ finally {
2701
+ await rm(wd, { recursive: true, force: true });
2702
+ }
2703
+ });
2136
2704
  it('globally debounces Ralph continue steer across concurrent watcher instances', async () => {
2137
2705
  const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-ralph-global-debounce-'));
2138
2706
  const fakeBinDir = join(wd, 'fake-bin');