oh-my-codex 0.18.5 → 0.18.7

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 (297) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +56 -7
  4. package/dist/agents/__tests__/definitions.test.js +11 -0
  5. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  6. package/dist/agents/__tests__/native-config.test.js +14 -5
  7. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  8. package/dist/agents/definitions.d.ts +2 -0
  9. package/dist/agents/definitions.d.ts.map +1 -1
  10. package/dist/agents/definitions.js +4 -1
  11. package/dist/agents/definitions.js.map +1 -1
  12. package/dist/agents/native-config.js +2 -2
  13. package/dist/agents/native-config.js.map +1 -1
  14. package/dist/autopilot/__tests__/fsm.test.d.ts +2 -0
  15. package/dist/autopilot/__tests__/fsm.test.d.ts.map +1 -0
  16. package/dist/autopilot/__tests__/fsm.test.js +75 -0
  17. package/dist/autopilot/__tests__/fsm.test.js.map +1 -0
  18. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +2 -0
  19. package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +1 -0
  20. package/dist/autopilot/__tests__/ralplan-gate.test.js +79 -0
  21. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -0
  22. package/dist/autopilot/deep-interview-gate.d.ts +18 -0
  23. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -0
  24. package/dist/autopilot/deep-interview-gate.js +256 -0
  25. package/dist/autopilot/deep-interview-gate.js.map +1 -0
  26. package/dist/autopilot/fsm.d.ts +13 -0
  27. package/dist/autopilot/fsm.d.ts.map +1 -0
  28. package/dist/autopilot/fsm.js +70 -0
  29. package/dist/autopilot/fsm.js.map +1 -0
  30. package/dist/autopilot/ralplan-gate.d.ts +17 -0
  31. package/dist/autopilot/ralplan-gate.d.ts.map +1 -0
  32. package/dist/autopilot/ralplan-gate.js +61 -0
  33. package/dist/autopilot/ralplan-gate.js.map +1 -0
  34. package/dist/cli/__tests__/index.test.js +24 -4
  35. package/dist/cli/__tests__/index.test.js.map +1 -1
  36. package/dist/cli/__tests__/launch-fallback.test.js +175 -6
  37. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  38. package/dist/cli/__tests__/question.test.js +100 -0
  39. package/dist/cli/__tests__/question.test.js.map +1 -1
  40. package/dist/cli/__tests__/setup-refresh.test.js +18 -0
  41. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  42. package/dist/cli/__tests__/team.test.js +2 -2
  43. package/dist/cli/__tests__/team.test.js.map +1 -1
  44. package/dist/cli/index.d.ts +3 -1
  45. package/dist/cli/index.d.ts.map +1 -1
  46. package/dist/cli/index.js +191 -36
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/question.d.ts.map +1 -1
  49. package/dist/cli/question.js +36 -5
  50. package/dist/cli/question.js.map +1 -1
  51. package/dist/config/__tests__/deep-interview.test.js +7 -6
  52. package/dist/config/__tests__/deep-interview.test.js.map +1 -1
  53. package/dist/config/deep-interview.d.ts.map +1 -1
  54. package/dist/config/deep-interview.js +14 -4
  55. package/dist/config/deep-interview.js.map +1 -1
  56. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +8 -0
  57. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  58. package/dist/hooks/__tests__/deep-interview-contract.test.js +10 -0
  59. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  60. package/dist/hooks/__tests__/keyword-detector.test.js +649 -11
  61. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +63 -0
  63. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  64. package/dist/hooks/__tests__/session.test.js +25 -0
  65. package/dist/hooks/__tests__/session.test.js.map +1 -1
  66. package/dist/hooks/deep-interview-config-instruction.js +1 -1
  67. package/dist/hooks/deep-interview-config-instruction.js.map +1 -1
  68. package/dist/hooks/keyword-detector.d.ts +1 -0
  69. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  70. package/dist/hooks/keyword-detector.js +171 -21
  71. package/dist/hooks/keyword-detector.js.map +1 -1
  72. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  73. package/dist/hooks/keyword-registry.js +1 -0
  74. package/dist/hooks/keyword-registry.js.map +1 -1
  75. package/dist/hooks/session.d.ts +2 -0
  76. package/dist/hooks/session.d.ts.map +1 -1
  77. package/dist/hooks/session.js +13 -5
  78. package/dist/hooks/session.js.map +1 -1
  79. package/dist/hud/__tests__/authority.test.js +35 -0
  80. package/dist/hud/__tests__/authority.test.js.map +1 -1
  81. package/dist/hud/__tests__/index.test.js +320 -3
  82. package/dist/hud/__tests__/index.test.js.map +1 -1
  83. package/dist/hud/__tests__/reconcile.test.js +117 -14
  84. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  85. package/dist/hud/__tests__/render.test.js +117 -8
  86. package/dist/hud/__tests__/render.test.js.map +1 -1
  87. package/dist/hud/__tests__/state.test.js +80 -0
  88. package/dist/hud/__tests__/state.test.js.map +1 -1
  89. package/dist/hud/__tests__/tmux.test.js +134 -1
  90. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  91. package/dist/hud/authority.d.ts.map +1 -1
  92. package/dist/hud/authority.js +13 -2
  93. package/dist/hud/authority.js.map +1 -1
  94. package/dist/hud/colors.d.ts +1 -0
  95. package/dist/hud/colors.d.ts.map +1 -1
  96. package/dist/hud/colors.js +4 -0
  97. package/dist/hud/colors.js.map +1 -1
  98. package/dist/hud/constants.d.ts +3 -2
  99. package/dist/hud/constants.d.ts.map +1 -1
  100. package/dist/hud/constants.js +3 -2
  101. package/dist/hud/constants.js.map +1 -1
  102. package/dist/hud/index.d.ts +20 -1
  103. package/dist/hud/index.d.ts.map +1 -1
  104. package/dist/hud/index.js +99 -18
  105. package/dist/hud/index.js.map +1 -1
  106. package/dist/hud/reconcile.d.ts +2 -1
  107. package/dist/hud/reconcile.d.ts.map +1 -1
  108. package/dist/hud/reconcile.js +6 -3
  109. package/dist/hud/reconcile.js.map +1 -1
  110. package/dist/hud/render.d.ts +1 -0
  111. package/dist/hud/render.d.ts.map +1 -1
  112. package/dist/hud/render.js +69 -17
  113. package/dist/hud/render.js.map +1 -1
  114. package/dist/hud/state.d.ts.map +1 -1
  115. package/dist/hud/state.js +16 -1
  116. package/dist/hud/state.js.map +1 -1
  117. package/dist/hud/tmux.d.ts +2 -0
  118. package/dist/hud/tmux.d.ts.map +1 -1
  119. package/dist/hud/tmux.js +39 -2
  120. package/dist/hud/tmux.js.map +1 -1
  121. package/dist/mcp/__tests__/hermes-bridge.test.js +203 -7
  122. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  123. package/dist/mcp/__tests__/state-server.test.js +13 -1
  124. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  125. package/dist/mcp/hermes-bridge.d.ts +12 -2
  126. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  127. package/dist/mcp/hermes-bridge.js +83 -9
  128. package/dist/mcp/hermes-bridge.js.map +1 -1
  129. package/dist/modes/__tests__/base-autoresearch-contract.test.js +7 -1
  130. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  131. package/dist/pipeline/__tests__/stages.test.js +130 -0
  132. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  133. package/dist/pipeline/orchestrator.js +1 -1
  134. package/dist/pipeline/orchestrator.js.map +1 -1
  135. package/dist/pipeline/stages/ralplan.d.ts +1 -0
  136. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  137. package/dist/pipeline/stages/ralplan.js +14 -5
  138. package/dist/pipeline/stages/ralplan.js.map +1 -1
  139. package/dist/question/__tests__/deep-interview.test.js +160 -2
  140. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  141. package/dist/question/__tests__/policy.test.js +63 -3
  142. package/dist/question/__tests__/policy.test.js.map +1 -1
  143. package/dist/question/__tests__/renderer.test.js +191 -2
  144. package/dist/question/__tests__/renderer.test.js.map +1 -1
  145. package/dist/question/__tests__/state.test.js +94 -3
  146. package/dist/question/__tests__/state.test.js.map +1 -1
  147. package/dist/question/__tests__/ui.test.js +4 -0
  148. package/dist/question/__tests__/ui.test.js.map +1 -1
  149. package/dist/question/autopilot-wait.d.ts +12 -2
  150. package/dist/question/autopilot-wait.d.ts.map +1 -1
  151. package/dist/question/autopilot-wait.js +158 -47
  152. package/dist/question/autopilot-wait.js.map +1 -1
  153. package/dist/question/deep-interview.d.ts.map +1 -1
  154. package/dist/question/deep-interview.js +22 -6
  155. package/dist/question/deep-interview.js.map +1 -1
  156. package/dist/question/policy.d.ts.map +1 -1
  157. package/dist/question/policy.js +2 -5
  158. package/dist/question/policy.js.map +1 -1
  159. package/dist/question/renderer.d.ts +12 -0
  160. package/dist/question/renderer.d.ts.map +1 -1
  161. package/dist/question/renderer.js +87 -3
  162. package/dist/question/renderer.js.map +1 -1
  163. package/dist/question/state.d.ts +8 -1
  164. package/dist/question/state.d.ts.map +1 -1
  165. package/dist/question/state.js +54 -14
  166. package/dist/question/state.js.map +1 -1
  167. package/dist/question/types.d.ts +1 -1
  168. package/dist/question/types.d.ts.map +1 -1
  169. package/dist/question/ui.d.ts +1 -0
  170. package/dist/question/ui.d.ts.map +1 -1
  171. package/dist/question/ui.js +1 -0
  172. package/dist/question/ui.js.map +1 -1
  173. package/dist/ralplan/__tests__/runtime.test.js +191 -0
  174. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  175. package/dist/ralplan/consensus-gate.d.ts +9 -1
  176. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  177. package/dist/ralplan/consensus-gate.js +84 -2
  178. package/dist/ralplan/consensus-gate.js.map +1 -1
  179. package/dist/ralplan/runtime.d.ts +9 -0
  180. package/dist/ralplan/runtime.d.ts.map +1 -1
  181. package/dist/ralplan/runtime.js +32 -11
  182. package/dist/ralplan/runtime.js.map +1 -1
  183. package/dist/scripts/__tests__/codex-native-hook.test.js +1487 -34
  184. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  185. package/dist/scripts/check-version-sync.js +8 -4
  186. package/dist/scripts/check-version-sync.js.map +1 -1
  187. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  188. package/dist/scripts/codex-native-hook.js +356 -38
  189. package/dist/scripts/codex-native-hook.js.map +1 -1
  190. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  191. package/dist/scripts/codex-native-pre-post.js +79 -1
  192. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  193. package/dist/scripts/hook-payload-guard.d.ts +9 -0
  194. package/dist/scripts/hook-payload-guard.d.ts.map +1 -0
  195. package/dist/scripts/hook-payload-guard.js +111 -0
  196. package/dist/scripts/hook-payload-guard.js.map +1 -0
  197. package/dist/scripts/notify-fallback-watcher.js +8 -1
  198. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  199. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts +2 -0
  200. package/dist/scripts/notify-hook/__tests__/payload-guard.test.d.ts.map +1 -0
  201. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js +39 -0
  202. package/dist/scripts/notify-hook/__tests__/payload-guard.test.js.map +1 -0
  203. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/team-worker-stop.js +234 -86
  205. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  206. package/dist/scripts/notify-hook.js +11 -2
  207. package/dist/scripts/notify-hook.js.map +1 -1
  208. package/dist/state/__tests__/operations.test.js +1012 -1
  209. package/dist/state/__tests__/operations.test.js.map +1 -1
  210. package/dist/state/__tests__/skill-active.test.js +59 -1
  211. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  212. package/dist/state/__tests__/workflow-transition.test.js +73 -7
  213. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  214. package/dist/state/operations.d.ts.map +1 -1
  215. package/dist/state/operations.js +102 -0
  216. package/dist/state/operations.js.map +1 -1
  217. package/dist/state/skill-active.d.ts.map +1 -1
  218. package/dist/state/skill-active.js +33 -3
  219. package/dist/state/skill-active.js.map +1 -1
  220. package/dist/state/workflow-transition-reconcile.d.ts +6 -0
  221. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  222. package/dist/state/workflow-transition-reconcile.js +28 -1
  223. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  224. package/dist/state/workflow-transition.d.ts.map +1 -1
  225. package/dist/state/workflow-transition.js +10 -3
  226. package/dist/state/workflow-transition.js.map +1 -1
  227. package/dist/subagents/__tests__/tracker.test.js +139 -0
  228. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  229. package/dist/subagents/tracker.d.ts +3 -0
  230. package/dist/subagents/tracker.d.ts.map +1 -1
  231. package/dist/subagents/tracker.js +41 -4
  232. package/dist/subagents/tracker.js.map +1 -1
  233. package/dist/team/__tests__/coordination-protocol.test.d.ts +2 -0
  234. package/dist/team/__tests__/coordination-protocol.test.d.ts.map +1 -0
  235. package/dist/team/__tests__/coordination-protocol.test.js +173 -0
  236. package/dist/team/__tests__/coordination-protocol.test.js.map +1 -0
  237. package/dist/team/__tests__/runtime.test.js +51 -2
  238. package/dist/team/__tests__/runtime.test.js.map +1 -1
  239. package/dist/team/__tests__/state.test.js +83 -0
  240. package/dist/team/__tests__/state.test.js.map +1 -1
  241. package/dist/team/__tests__/tmux-session.test.js +45 -0
  242. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  243. package/dist/team/__tests__/worker-bootstrap.test.js +84 -0
  244. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  245. package/dist/team/coordination-protocol.d.ts +14 -0
  246. package/dist/team/coordination-protocol.d.ts.map +1 -0
  247. package/dist/team/coordination-protocol.js +244 -0
  248. package/dist/team/coordination-protocol.js.map +1 -0
  249. package/dist/team/runtime.d.ts +1 -0
  250. package/dist/team/runtime.d.ts.map +1 -1
  251. package/dist/team/runtime.js +19 -3
  252. package/dist/team/runtime.js.map +1 -1
  253. package/dist/team/state/tasks.d.ts.map +1 -1
  254. package/dist/team/state/tasks.js +24 -0
  255. package/dist/team/state/tasks.js.map +1 -1
  256. package/dist/team/state/types.d.ts +21 -1
  257. package/dist/team/state/types.d.ts.map +1 -1
  258. package/dist/team/state/types.js.map +1 -1
  259. package/dist/team/state.d.ts +17 -1
  260. package/dist/team/state.d.ts.map +1 -1
  261. package/dist/team/state.js +12 -5
  262. package/dist/team/state.js.map +1 -1
  263. package/dist/team/team-ops.d.ts +1 -1
  264. package/dist/team/team-ops.d.ts.map +1 -1
  265. package/dist/team/team-ops.js.map +1 -1
  266. package/dist/team/tmux-session.d.ts.map +1 -1
  267. package/dist/team/tmux-session.js +19 -1
  268. package/dist/team/tmux-session.js.map +1 -1
  269. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  270. package/dist/team/worker-bootstrap.js +63 -0
  271. package/dist/team/worker-bootstrap.js.map +1 -1
  272. package/dist/utils/__tests__/agents-model-table.test.js +4 -2
  273. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  274. package/dist/utils/agents-model-table.d.ts.map +1 -1
  275. package/dist/utils/agents-model-table.js +3 -0
  276. package/dist/utils/agents-model-table.js.map +1 -1
  277. package/package.json +1 -1
  278. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  279. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +10 -5
  280. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +9 -4
  281. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +12 -0
  282. package/plugins/oh-my-codex/skills/team/SKILL.md +16 -0
  283. package/plugins/oh-my-codex/skills/worker/SKILL.md +14 -0
  284. package/skills/autopilot/SKILL.md +10 -5
  285. package/skills/deep-interview/SKILL.md +9 -4
  286. package/skills/ralplan/SKILL.md +12 -0
  287. package/skills/team/SKILL.md +16 -0
  288. package/skills/worker/SKILL.md +14 -0
  289. package/src/scripts/__tests__/codex-native-hook.test.ts +2202 -523
  290. package/src/scripts/check-version-sync.ts +8 -4
  291. package/src/scripts/codex-native-hook.ts +444 -36
  292. package/src/scripts/codex-native-pre-post.ts +80 -0
  293. package/src/scripts/hook-payload-guard.ts +113 -0
  294. package/src/scripts/notify-fallback-watcher.ts +8 -1
  295. package/src/scripts/notify-hook/__tests__/payload-guard.test.ts +41 -0
  296. package/src/scripts/notify-hook/team-worker-stop.ts +193 -52
  297. package/src/scripts/notify-hook.ts +14 -2
@@ -1223,6 +1223,85 @@ function hasActionableBashHardFailure(normalized: NormalizedPostToolUsePayload):
1223
1223
  return containsHardFailure(`${normalized.stderrText}\n${normalized.stdoutText}`);
1224
1224
  }
1225
1225
 
1226
+ function isShellEnvAssignment(token: string): boolean {
1227
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
1228
+ }
1229
+
1230
+ function shellCommandBasename(token: string): string {
1231
+ const normalized = token.replace(/\\/g, "/");
1232
+ return (normalized.split("/").pop() || normalized).toLowerCase();
1233
+ }
1234
+
1235
+ function skipEnvWrapper(tokens: string[]): number {
1236
+ let index = 0;
1237
+ while (isShellEnvAssignment(tokens[index] || "")) index += 1;
1238
+ if (!isEnvExecutableToken(tokens[index] || "")) return index;
1239
+
1240
+ index += 1;
1241
+ while (index < tokens.length) {
1242
+ const token = tokens[index] || "";
1243
+ if (token === "--") {
1244
+ index += 1;
1245
+ break;
1246
+ }
1247
+ if (isShellEnvAssignment(token)) {
1248
+ index += 1;
1249
+ continue;
1250
+ }
1251
+ if (envOptionConsumesNextValue(token)) {
1252
+ index += 2;
1253
+ continue;
1254
+ }
1255
+ if (token === "-i" || token === "--ignore-environment" || token.startsWith("-u") && token.length > 2) {
1256
+ index += 1;
1257
+ continue;
1258
+ }
1259
+ break;
1260
+ }
1261
+ return index;
1262
+ }
1263
+
1264
+ function skipGhGlobalOptions(tokens: string[], startIndex: number): number {
1265
+ let index = startIndex;
1266
+ const optionsWithValue = new Set(["--repo", "-R", "--hostname", "--config-dir"]);
1267
+ while (index < tokens.length) {
1268
+ const token = tokens[index] || "";
1269
+ if (optionsWithValue.has(token)) {
1270
+ index += 2;
1271
+ continue;
1272
+ }
1273
+ if (/^(?:--repo|--hostname|--config-dir)=/.test(token)) {
1274
+ index += 1;
1275
+ continue;
1276
+ }
1277
+ if (token.startsWith("-") && token !== "-") {
1278
+ index += 1;
1279
+ continue;
1280
+ }
1281
+ break;
1282
+ }
1283
+ return index;
1284
+ }
1285
+
1286
+ function isReviewableGhPrChecksTokens(tokens: string[]): boolean {
1287
+ const commandIndex = skipEnvWrapper(tokens);
1288
+ const basename = shellCommandBasename(tokens[commandIndex] || "");
1289
+ if (basename !== "gh" && basename !== "gh.exe") return false;
1290
+ const subcommandIndex = skipGhGlobalOptions(tokens, commandIndex + 1);
1291
+ return tokens[subcommandIndex] === "pr" && tokens[subcommandIndex + 1] === "checks";
1292
+ }
1293
+
1294
+ function isReviewableNonZeroBashCommand(command: string): boolean {
1295
+ const boundaryTokens = tokenizeShellCommandWithBoundaries(removeHereDocBodies(command));
1296
+ if (!boundaryTokens) return false;
1297
+ for (let commandStart = 0; commandStart < boundaryTokens.length; commandStart = nextCommandStart(boundaryTokens, commandStart)) {
1298
+ const commandEnd = nextCommandStart(boundaryTokens, commandStart);
1299
+ const commandTokens = boundaryTokens.slice(commandStart, commandEnd).map((candidate) => candidate.value);
1300
+ if (isReviewableGhPrChecksTokens(commandTokens)) return true;
1301
+ }
1302
+ return false;
1303
+ }
1304
+
1226
1305
  export function buildNativePostToolUseOutput(
1227
1306
  payload: CodexHookPayload,
1228
1307
  ): Record<string, unknown> | null {
@@ -1264,6 +1343,7 @@ export function buildNativePostToolUseOutput(
1264
1343
  && normalized.exitCode !== 0
1265
1344
  && combined.length > 0
1266
1345
  && !containsHardFailure(combined)
1346
+ && isReviewableNonZeroBashCommand(normalized.normalizedCommand)
1267
1347
  ) {
1268
1348
  return {
1269
1349
  decision: "block",
@@ -0,0 +1,113 @@
1
+ export const MAX_NOTIFY_ARGV_JSON_BYTES = 64 * 1024;
2
+ export const MAX_NATIVE_STDIN_JSON_BYTES = 1024 * 1024;
3
+ export const RAW_JSON_FIELD_SCAN_BYTES = 64 * 1024;
4
+
5
+ export const CODEX_HOOK_EVENT_NAMES = [
6
+ "SessionStart",
7
+ "PreToolUse",
8
+ "PostToolUse",
9
+ "UserPromptSubmit",
10
+ "PreCompact",
11
+ "PostCompact",
12
+ "Stop",
13
+ ] as const;
14
+
15
+ export type RawCodexHookEventName = typeof CODEX_HOOK_EVENT_NAMES[number];
16
+
17
+ export function utf8ByteLength(value: string): number {
18
+ return Buffer.byteLength(value, "utf-8");
19
+ }
20
+
21
+ function skipJsonWhitespace(raw: string, index: number): number {
22
+ while (index < raw.length && /\s/.test(raw[index] ?? "")) index += 1;
23
+ return index;
24
+ }
25
+
26
+ function readJsonStringLiteral(raw: string, quoteIndex: number): { value: string; endIndex: number } | null {
27
+ if (raw[quoteIndex] !== '"') return null;
28
+ let value = "";
29
+ for (let index = quoteIndex + 1; index < raw.length; index += 1) {
30
+ const char = raw[index];
31
+ if (char === '"') return { value, endIndex: index + 1 };
32
+ if (char !== "\\") {
33
+ value += char;
34
+ continue;
35
+ }
36
+
37
+ index += 1;
38
+ if (index >= raw.length) return null;
39
+ const escaped = raw[index];
40
+ switch (escaped) {
41
+ case '"':
42
+ case "\\":
43
+ case "/":
44
+ value += escaped;
45
+ break;
46
+ case "b":
47
+ value += "\b";
48
+ break;
49
+ case "f":
50
+ value += "\f";
51
+ break;
52
+ case "n":
53
+ value += "\n";
54
+ break;
55
+ case "r":
56
+ value += "\r";
57
+ break;
58
+ case "t":
59
+ value += "\t";
60
+ break;
61
+ case "u": {
62
+ const hex = raw.slice(index + 1, index + 5);
63
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null;
64
+ value += String.fromCharCode(Number.parseInt(hex, 16));
65
+ index += 4;
66
+ break;
67
+ }
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ export function extractRawJsonStringField(rawInput: string, fieldNames: readonly string[]): string | null {
76
+ const raw = rawInput.slice(0, RAW_JSON_FIELD_SCAN_BYTES);
77
+ const wanted = new Set(fieldNames);
78
+ let depth = 0;
79
+ let index = 0;
80
+
81
+ while (index < raw.length) {
82
+ const char = raw[index];
83
+ if (char === '"') {
84
+ const key = readJsonStringLiteral(raw, index);
85
+ if (!key) return null;
86
+ index = key.endIndex;
87
+ const afterKey = skipJsonWhitespace(raw, index);
88
+ if (depth === 1 && raw[afterKey] === ":" && wanted.has(key.value)) {
89
+ const valueStart = skipJsonWhitespace(raw, afterKey + 1);
90
+ const value = readJsonStringLiteral(raw, valueStart);
91
+ return value?.value ?? null;
92
+ }
93
+ continue;
94
+ }
95
+ if (char === "{") depth += 1;
96
+ else if (char === "}") depth = Math.max(0, depth - 1);
97
+ index += 1;
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ export function extractRawCodexHookEventName(rawInput: string): RawCodexHookEventName | null {
104
+ const raw = extractRawJsonStringField(rawInput, [
105
+ "hook_event_name",
106
+ "hookEventName",
107
+ "event",
108
+ "name",
109
+ ]);
110
+ return CODEX_HOOK_EVENT_NAMES.includes(raw as RawCodexHookEventName)
111
+ ? raw as RawCodexHookEventName
112
+ : null;
113
+ }
@@ -1963,10 +1963,15 @@ function shutdown(signal: string): void {
1963
1963
  }
1964
1964
 
1965
1965
  async function main(): Promise<void> {
1966
+ if (process.env.NODE_ENV === 'test' && process.env.OMX_NOTIFY_FALLBACK_TEST_FATAL === '1') {
1967
+ throw new Error('test fatal notify fallback failure');
1968
+ }
1966
1969
  await mkdir(logsDir, { recursive: true }).catch(() => {});
1967
1970
  await mkdir(stateDir, { recursive: true }).catch(() => {});
1968
1971
  if (!existsSync(notifyScript)) {
1972
+ const reason = `notify script missing: ${notifyScript}`;
1969
1973
  await eventLog({ type: 'watcher_error', reason: 'notify_script_missing', notify_script: notifyScript });
1974
+ process.stderr.write(`notify-fallback-watcher: ${reason}\n`);
1970
1975
  process.exit(1);
1971
1976
  }
1972
1977
 
@@ -2006,10 +2011,12 @@ async function main(): Promise<void> {
2006
2011
 
2007
2012
  main().catch(async (err) => {
2008
2013
  await mkdir(dirname(logPath), { recursive: true }).catch(() => {});
2014
+ const message = err instanceof Error ? err.message : safeString(err);
2009
2015
  await eventLog({
2010
2016
  type: 'watcher_error',
2011
2017
  reason: 'fatal',
2012
- error: err instanceof Error ? err.message : safeString(err),
2018
+ error: message,
2013
2019
  });
2020
+ process.stderr.write(`notify-fallback-watcher: fatal: ${message || 'unknown error'}\n`);
2014
2021
  process.exit(1);
2015
2022
  });
@@ -0,0 +1,41 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { describe, it } from 'node:test';
8
+ import { MAX_NOTIFY_ARGV_JSON_BYTES } from '../../hook-payload-guard.js';
9
+
10
+ function notifyHookScriptPath(): string {
11
+ return join(process.cwd(), 'dist', 'scripts', 'notify-hook.js');
12
+ }
13
+
14
+ describe('notify-hook raw payload guard', () => {
15
+ it('ignores oversized argv JSON before parsing or writing hook state', async () => {
16
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-notify-hook-oversized-'));
17
+ try {
18
+ await mkdir(join(cwd, '.omx'), { recursive: true });
19
+ await writeFile(join(cwd, '.omx', 'setup-scope.json'), '{}', 'utf-8');
20
+ const payload = JSON.stringify({
21
+ cwd,
22
+ type: 'agent-turn-complete',
23
+ session_id: 'sess-notify-oversized',
24
+ turn_id: 'turn-notify-oversized',
25
+ input_messages: ['hello'],
26
+ last_assistant_message: 'x'.repeat(MAX_NOTIFY_ARGV_JSON_BYTES + 1),
27
+ });
28
+
29
+ execFileSync(process.execPath, [notifyHookScriptPath(), payload], {
30
+ cwd,
31
+ stdio: ['ignore', 'pipe', 'pipe'],
32
+ env: process.env,
33
+ });
34
+
35
+ assert.equal(existsSync(join(cwd, '.omx', 'logs')), false);
36
+ assert.equal(existsSync(join(cwd, '.omx', 'state')), false);
37
+ } finally {
38
+ await rm(cwd, { recursive: true, force: true });
39
+ }
40
+ });
41
+ });
@@ -6,11 +6,12 @@
6
6
  * It must not depend on idle/heartbeat freshness or inferred progress stalls.
7
7
  */
8
8
 
9
- import { appendFile, mkdir, rename, writeFile } from 'fs/promises';
10
- import { dirname, join } from 'path';
9
+ import { existsSync } from 'fs';
10
+ import { appendFile, mkdir, rename, rm, stat, writeFile } from 'fs/promises';
11
+ import { dirname, join, relative } from 'path';
11
12
  import { DEFAULT_MARKER, paneHasActiveTask } from '../tmux-hook-engine.js';
12
13
  import { appendTeamDeliveryLog } from '../../team/delivery-log.js';
13
- import { safeString, asNumber } from './utils.js';
14
+ import { safeString, asNumber, isTerminalPhase } from './utils.js';
14
15
  import { readJsonIfExists } from './state-io.js';
15
16
  import { logTmuxHookEvent } from './log.js';
16
17
  import { evaluatePaneInjectionReadiness, queuePaneInput, sendPaneInput } from './team-tmux-guard.js';
@@ -21,6 +22,110 @@ const STOP_NUDGE_COOLDOWN_MS = 30_000;
21
22
  const SOURCE_TYPE = 'worker_stop';
22
23
  const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
23
24
  const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
25
+ const TEAM_SHUTDOWN_NO_INJECTION_REASON = 'team_state_gone_or_shutdown';
26
+ const TEAM_LOCK_HELD_REASON = 'suppressed_team_lock_held';
27
+
28
+ async function teamStateAllowsWorkerStopNudge(stateDir, teamName) {
29
+ const teamDir = join(stateDir, 'team', teamName);
30
+ if (!existsSync(teamDir)) return false;
31
+ if (existsSync(join(teamDir, 'shutdown.json'))) return false;
32
+
33
+ const phase = await readJsonIfExists(join(teamDir, 'phase.json'), null);
34
+ const currentPhase = safeString(phase?.current_phase || phase?.phase || '').trim();
35
+ if (currentPhase && isTerminalPhase(currentPhase)) return false;
36
+
37
+ return true;
38
+ }
39
+
40
+ async function acquireTeamStopNudgeLock(teamDir, nowMs, cooldownMs) {
41
+ const lockDir = join(teamDir, 'worker-stop-nudge.lock');
42
+ const staleAfterMs = Math.max(cooldownMs, 30_000);
43
+
44
+ for (let attempt = 0; attempt < 2; attempt += 1) {
45
+ try {
46
+ await mkdir(lockDir);
47
+ await writeFile(join(lockDir, 'owner.json'), JSON.stringify({
48
+ pid: process.pid,
49
+ acquired_at_ms: nowMs,
50
+ acquired_at: new Date(nowMs).toISOString(),
51
+ }, null, 2)).catch(() => {});
52
+ return { acquired: true, lockDir };
53
+ } catch (error) {
54
+ if (error?.code === 'ENOENT') return { acquired: false, reason: TEAM_SHUTDOWN_NO_INJECTION_REASON };
55
+ if (error?.code !== 'EEXIST') return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
56
+
57
+ try {
58
+ const lockStat = await stat(lockDir);
59
+ if ((nowMs - lockStat.mtimeMs) >= staleAfterMs) {
60
+ await rm(lockDir, { recursive: true, force: true });
61
+ continue;
62
+ }
63
+ } catch {
64
+ continue;
65
+ }
66
+ return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
67
+ }
68
+ }
69
+
70
+ return { acquired: false, reason: TEAM_LOCK_HELD_REASON };
71
+ }
72
+
73
+ async function releaseTeamStopNudgeLock(lockDir) {
74
+ if (!lockDir) return;
75
+ await rm(lockDir, { recursive: true, force: true }).catch(() => {});
76
+ }
77
+
78
+ async function ensureDirectoryUnderExistingTeam(teamDir, targetDir) {
79
+ if (!existsSync(teamDir)) return false;
80
+ const rel = relative(teamDir, targetDir);
81
+ if (!rel) return true;
82
+ if (rel.startsWith('..')) throw new Error('state_path_outside_team_dir');
83
+
84
+ let current = teamDir;
85
+ for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
86
+ if (!existsSync(teamDir)) return false;
87
+ current = join(current, segment);
88
+ try {
89
+ await mkdir(current);
90
+ } catch (error) {
91
+ if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
92
+ if (error?.code !== 'EEXIST') throw error;
93
+ const currentStat = await stat(current);
94
+ if (!currentStat.isDirectory()) throw error;
95
+ }
96
+ }
97
+ return existsSync(teamDir);
98
+ }
99
+
100
+ async function writeStopNudgeStateIfTeamExists(teamDir, statePath, state) {
101
+ const stateDir = dirname(statePath);
102
+ const ready = await ensureDirectoryUnderExistingTeam(teamDir, stateDir);
103
+ if (!ready) return false;
104
+ try {
105
+ const tmpPath = `${statePath}.tmp.${process.pid}`;
106
+ await writeFile(tmpPath, JSON.stringify(state, null, 2));
107
+ await rename(tmpPath, statePath);
108
+ } catch (error) {
109
+ if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
110
+ throw error;
111
+ }
112
+ return existsSync(teamDir);
113
+ }
114
+
115
+ async function appendWorkerStopEventIfTeamExists(stateDir, teamName, event) {
116
+ const teamDir = join(stateDir, 'team', teamName);
117
+ const eventsDir = join(teamDir, 'events');
118
+ const ready = await ensureDirectoryUnderExistingTeam(teamDir, eventsDir);
119
+ if (!ready) return false;
120
+ const eventsPath = join(eventsDir, 'events.ndjson');
121
+ try {
122
+ await appendFile(eventsPath, JSON.stringify(event) + '\n');
123
+ } catch (error) {
124
+ if (error?.code === 'ENOENT' && !existsSync(teamDir)) return false;
125
+ throw error;
126
+ }
127
+ return existsSync(teamDir);
128
+ }
24
129
 
25
130
  function resolveWorkerStopCooldownMs() {
26
131
  const raw = safeString(process.env.OMX_TEAM_WORKER_STOP_COOLDOWN_MS || '');
@@ -42,20 +147,6 @@ async function resolveCanonicalLeaderPaneId(leaderPaneId) {
42
147
  return normalizedLeaderPaneId;
43
148
  }
44
149
 
45
- async function appendWorkerStopEvent(stateDir, teamName, event) {
46
- const eventsDir = join(stateDir, 'team', teamName, 'events');
47
- const eventsPath = join(eventsDir, 'events.ndjson');
48
- await mkdir(eventsDir, { recursive: true }).catch(() => {});
49
- await appendFile(eventsPath, JSON.stringify(event) + '\n').catch(() => {});
50
- }
51
-
52
- async function writeStopNudgeState(statePath, state) {
53
- await mkdir(dirname(statePath), { recursive: true }).catch(() => {});
54
- const tmpPath = `${statePath}.tmp.${process.pid}`;
55
- await writeFile(tmpPath, JSON.stringify(state, null, 2));
56
- await rename(tmpPath, statePath);
57
- }
58
-
59
150
  async function recordDeferred({
60
151
  stateDir,
61
152
  logsDir,
@@ -69,23 +160,26 @@ async function recordDeferred({
69
160
  paneCurrentCommand = '',
70
161
  }) {
71
162
  const nowIso = nextState.last_notified_at;
72
- await writeStopNudgeState(statePath, {
163
+ const teamDir = join(stateDir, 'team', teamName);
164
+ const wroteState = await writeStopNudgeStateIfTeamExists(teamDir, statePath, {
73
165
  ...nextState,
74
166
  delivery: 'deferred',
75
167
  reason,
76
168
  pane_current_command: paneCurrentCommand || null,
77
169
  }).catch(() => {});
78
- await appendWorkerStopEvent(stateDir, teamName, {
79
- event_id: `worker-stop-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
80
- team: teamName,
81
- type: 'worker_stop_leader_nudge',
82
- worker: workerName,
83
- to_worker: 'leader-fixed',
84
- delivery: 'deferred',
85
- reason,
86
- created_at: nowIso,
87
- source_type: SOURCE_TYPE,
88
- });
170
+ if (wroteState) {
171
+ await appendWorkerStopEventIfTeamExists(stateDir, teamName, {
172
+ event_id: `worker-stop-deferred-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
173
+ team: teamName,
174
+ type: 'worker_stop_leader_nudge',
175
+ worker: workerName,
176
+ to_worker: 'leader-fixed',
177
+ delivery: 'deferred',
178
+ reason,
179
+ created_at: nowIso,
180
+ source_type: SOURCE_TYPE,
181
+ }).catch(() => {});
182
+ }
89
183
  await logTmuxHookEvent(logsDir, {
90
184
  timestamp: nowIso,
91
185
  type: 'leader_notification_deferred',
@@ -119,30 +213,58 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
119
213
  const { teamName, workerName } = workerContext || {};
120
214
  if (!teamName || !workerName || !stateDir) return { ok: false, result: 'unresolved' };
121
215
 
122
- const workerDir = join(stateDir, 'team', teamName, 'workers', workerName);
216
+ const teamDir = join(stateDir, 'team', teamName);
217
+ const workerDir = join(teamDir, 'workers', workerName);
123
218
  const statePath = join(workerDir, 'worker-stop-nudge.json');
219
+ const teamStatePath = join(teamDir, 'worker-stop-nudge.json');
124
220
  const nowMs = Date.now();
125
221
  const nowIso = new Date(nowMs).toISOString();
222
+ const cooldownMs = resolveWorkerStopCooldownMs();
223
+ const nextState = {
224
+ last_notified_at_ms: nowMs,
225
+ last_notified_at: nowIso,
226
+ team: teamName,
227
+ worker: workerName,
228
+ source_type: SOURCE_TYPE,
229
+ };
230
+ let tmuxSession = '';
231
+ let leaderPaneId = '';
232
+
233
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
234
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
235
+ }
236
+
237
+ const lock = await acquireTeamStopNudgeLock(teamDir, nowMs, cooldownMs);
238
+ if (!lock.acquired) {
239
+ return { ok: true, result: lock.reason || TEAM_LOCK_HELD_REASON };
240
+ }
241
+
242
+ try {
243
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
244
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
245
+ }
246
+
247
+ const teamExisting = (await readJsonIfExists(teamStatePath, null)) || {};
248
+ const teamLastNotifiedMs = asNumber(teamExisting.last_notified_at_ms) ?? 0;
249
+ if ((nowMs - teamLastNotifiedMs) < cooldownMs) {
250
+ return { ok: true, result: 'suppressed_team_cooldown' };
251
+ }
252
+
126
253
  const existing = (await readJsonIfExists(statePath, null)) || {};
127
254
  const lastNotifiedMs = asNumber(existing.last_notified_at_ms) ?? 0;
128
- const cooldownMs = resolveWorkerStopCooldownMs();
129
255
  if ((nowMs - lastNotifiedMs) < cooldownMs) {
130
256
  return { ok: true, result: 'suppressed_cooldown' };
131
257
  }
132
258
 
133
259
  const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
134
260
  if (!teamInfo) return { ok: false, result: 'unresolved' };
135
- const { tmuxSession, leaderPaneId } = teamInfo;
261
+ ({ tmuxSession, leaderPaneId } = teamInfo);
136
262
  const tmuxTarget = await resolveCanonicalLeaderPaneId(leaderPaneId);
137
- const nextState = {
138
- last_notified_at_ms: nowMs,
139
- last_notified_at: nowIso,
140
- team: teamName,
141
- worker: workerName,
142
- source_type: SOURCE_TYPE,
143
- };
144
263
 
145
264
  if (!tmuxTarget) {
265
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
266
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
267
+ }
146
268
  await recordDeferred({
147
269
  stateDir,
148
270
  logsDir,
@@ -164,6 +286,9 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
164
286
  requireIdle: false,
165
287
  });
166
288
  if (!paneGuard.ok) {
289
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
290
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
291
+ }
167
292
  await recordDeferred({
168
293
  stateDir,
169
294
  logsDir,
@@ -179,12 +304,15 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
179
304
  return { ok: true, result: 'deferred' };
180
305
  }
181
306
 
307
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
308
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
309
+ }
310
+
182
311
  const prompt =
183
312
  `[OMX] ${workerName} native Stop allowed. `
184
313
  + `Run \`omx team status ${teamName}\`, read worker messages/results, then assign next task, reconcile completion, or shut down. `
185
314
  + DEFAULT_MARKER;
186
315
 
187
- try {
188
316
  const leaderHasActiveTask = paneHasActiveTask(paneGuard.paneCapture);
189
317
  let deliveryMode = 'sent';
190
318
  if (leaderHasActiveTask) {
@@ -204,22 +332,28 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
204
332
  if (!sendResult.ok) throw new Error(sendResult.error || sendResult.reason || 'send_failed');
205
333
  }
206
334
 
207
- await writeStopNudgeState(statePath, {
335
+ const deliveryState = {
208
336
  ...nextState,
209
337
  delivery: deliveryMode,
210
338
  leader_pane_id: leaderPaneId || null,
211
339
  tmux_target: tmuxTarget,
212
- });
213
- await appendWorkerStopEvent(stateDir, teamName, {
214
- event_id: `worker-stop-nudge-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
215
- team: teamName,
216
- type: 'worker_stop_leader_nudge',
217
- worker: workerName,
218
- to_worker: 'leader-fixed',
219
- delivery: deliveryMode,
220
- created_at: nowIso,
221
- source_type: SOURCE_TYPE,
222
- });
340
+ };
341
+ const wroteWorkerState = await writeStopNudgeStateIfTeamExists(teamDir, statePath, deliveryState);
342
+ const wroteTeamState = wroteWorkerState
343
+ ? await writeStopNudgeStateIfTeamExists(teamDir, teamStatePath, deliveryState)
344
+ : false;
345
+ if (wroteTeamState) {
346
+ await appendWorkerStopEventIfTeamExists(stateDir, teamName, {
347
+ event_id: `worker-stop-nudge-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
348
+ team: teamName,
349
+ type: 'worker_stop_leader_nudge',
350
+ worker: workerName,
351
+ to_worker: 'leader-fixed',
352
+ delivery: deliveryMode,
353
+ created_at: nowIso,
354
+ source_type: SOURCE_TYPE,
355
+ }).catch(() => {});
356
+ }
223
357
  await logTmuxHookEvent(logsDir, {
224
358
  timestamp: nowIso,
225
359
  type: 'worker_stop_leader_nudge',
@@ -241,6 +375,11 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
241
375
  }).catch(() => {});
242
376
  return { ok: true, result: deliveryMode };
243
377
  } catch (err) {
378
+ if (!(await teamStateAllowsWorkerStopNudge(stateDir, teamName))) {
379
+ return { ok: true, result: TEAM_SHUTDOWN_NO_INJECTION_REASON };
380
+ }
381
+ // Worker Stop is already allowed before this helper runs; nudge failures are
382
+ // surfaced as deferred operational evidence instead of re-blocking Stop.
244
383
  await recordDeferred({
245
384
  stateDir,
246
385
  logsDir,
@@ -253,5 +392,7 @@ export async function maybeNudgeLeaderForAllowedWorkerStop({
253
392
  leaderPaneId,
254
393
  });
255
394
  return { ok: true, result: 'deferred' };
395
+ } finally {
396
+ await releaseTeamStopNudgeLock(lock.lockDir);
256
397
  }
257
398
  }
@@ -68,6 +68,11 @@ import {
68
68
  } from './notify-hook/team-worker.js';
69
69
  import { DEFAULT_MARKER } from './tmux-hook-engine.js';
70
70
  import { sameFilePath } from '../utils/paths.js';
71
+ import {
72
+ MAX_NOTIFY_ARGV_JSON_BYTES,
73
+ extractRawJsonStringField,
74
+ utf8ByteLength,
75
+ } from './hook-payload-guard.js';
71
76
 
72
77
  const RALPH_ACTIVE_PROGRESS_PHASES = new Set([
73
78
  'start',
@@ -285,6 +290,9 @@ async function main() {
285
290
  if (!rawPayload || rawPayload.startsWith('-')) {
286
291
  process.exit(0);
287
292
  }
293
+ if (utf8ByteLength(rawPayload) > MAX_NOTIFY_ARGV_JSON_BYTES) {
294
+ process.exit(0);
295
+ }
288
296
 
289
297
  let payload;
290
298
  try {
@@ -909,8 +917,12 @@ async function logFatalNotifyHookError(err: unknown): Promise<void> {
909
917
  try {
910
918
  const rawPayload = process.argv[process.argv.length - 1];
911
919
  if (rawPayload && !rawPayload.startsWith('-')) {
912
- const payload = JSON.parse(rawPayload) as Record<string, unknown>;
913
- cwd = safeString(payload.cwd || payload['cwd'] || cwd) || cwd;
920
+ if (utf8ByteLength(rawPayload) <= MAX_NOTIFY_ARGV_JSON_BYTES) {
921
+ const payload = JSON.parse(rawPayload) as Record<string, unknown>;
922
+ cwd = safeString(payload.cwd || payload['cwd'] || cwd) || cwd;
923
+ } else {
924
+ cwd = extractRawJsonStringField(rawPayload, ['cwd']) || cwd;
925
+ }
914
926
  }
915
927
  } catch {
916
928
  // Keep notification hook failures silent in Codex TUI surfaces.