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
@@ -4,6 +4,7 @@ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promis
4
4
  import { extname, join, relative, resolve } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
7
+ import { redactAuthSecrets } from "../auth/redact.js";
7
8
  import {
8
9
  SKILL_ACTIVE_STATE_FILE,
9
10
  extractSessionIdFromInitializedStatePath,
@@ -14,6 +15,7 @@ import {
14
15
  type SkillActiveStateLike,
15
16
  } from "../state/skill-active.js";
16
17
  import {
18
+ isTrustedSubagentThread,
17
19
  readSubagentSessionSummary,
18
20
  readSubagentTrackingState,
19
21
  recordSubagentTurnForSession,
@@ -108,6 +110,10 @@ import {
108
110
  isFinalHandoffDocumentRefreshCandidate,
109
111
  } from "../document-refresh/enforcer.js";
110
112
  import { buildExecFollowupStopOutput } from "../exec/followup.js";
113
+ import {
114
+ MAX_NATIVE_STDIN_JSON_BYTES,
115
+ extractRawCodexHookEventName,
116
+ } from "./hook-payload-guard.js";
111
117
 
112
118
  type CodexHookEventName =
113
119
  | "SessionStart"
@@ -142,6 +148,7 @@ const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
142
148
  const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
143
149
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
144
150
  const ORDINARY_STOP_NO_PROGRESS_MAX_MESSAGE_LENGTH = 240;
151
+ const OMX_OWNER_SESSION_ID_PATTERN = /^omx-[A-Za-z0-9_-]{1,60}$/;
145
152
  const STABLE_FINAL_RECOMMENDATION_PATTERNS = [
146
153
  /^\s*(?:launch|release|ship)-?ready\s*:\s*(?:yes|no)\b[^\n\r]*/im,
147
154
  /^\s*ready to release\s*:\s*(?:yes|no)\b[^\n\r]*/im,
@@ -172,6 +179,16 @@ function safeObject(value: unknown): Record<string, unknown> {
172
179
  return value && typeof value === "object" ? value as Record<string, unknown> : {};
173
180
  }
174
181
 
182
+ function resolveHudReconcileSessionId(
183
+ currentSessionState: SessionState | null,
184
+ canonicalSessionId: string | null,
185
+ sessionIdForState: string | null,
186
+ ): string | undefined {
187
+ const ownerOmxSessionId = safeString(currentSessionState?.owner_omx_session_id).trim();
188
+ if (OMX_OWNER_SESSION_ID_PATTERN.test(ownerOmxSessionId)) return ownerOmxSessionId;
189
+ return canonicalSessionId || sessionIdForState || undefined;
190
+ }
191
+
175
192
  function safeContextSnippet(value: unknown, maxLength = 300): string {
176
193
  const text = safeString(value).replace(/\s+/g, " ").trim();
177
194
  if (text.length <= maxLength) return text;
@@ -248,18 +265,25 @@ async function recordNativeSubagentSessionStart(
248
265
  metadata: NativeSubagentSessionStartMetadata,
249
266
  transcriptPath: string,
250
267
  ): Promise<void> {
268
+ const parentThreadId = metadata.parentThreadId.trim();
269
+ const childThreadId = childSessionId.trim();
251
270
  const trackingSessionIds = [...new Set([
252
271
  canonicalSessionId.trim(),
253
- metadata.parentThreadId.trim(),
272
+ parentThreadId,
254
273
  ].filter(Boolean))];
255
274
  for (const sessionId of trackingSessionIds) {
275
+ if (parentThreadId && parentThreadId !== childThreadId) {
276
+ await recordSubagentTurnForSession(cwd, {
277
+ sessionId,
278
+ threadId: parentThreadId,
279
+ kind: 'leader',
280
+ }).catch(() => {});
281
+ }
256
282
  await recordSubagentTurnForSession(cwd, {
257
283
  sessionId,
258
- threadId: metadata.parentThreadId,
259
- }).catch(() => {});
260
- await recordSubagentTurnForSession(cwd, {
261
- sessionId,
262
- threadId: childSessionId,
284
+ threadId: childThreadId,
285
+ kind: 'subagent',
286
+ ...(parentThreadId && parentThreadId !== childThreadId ? { leaderThreadId: parentThreadId } : {}),
263
287
  mode: metadata.agentRole,
264
288
  }).catch(() => {});
265
289
  }
@@ -301,17 +325,54 @@ async function isNativeSubagentHook(
301
325
  canonicalSessionId: string,
302
326
  nativeSessionId: string,
303
327
  threadId: string,
328
+ canonicalLeaderNativeSessionId = "",
304
329
  ): Promise<boolean> {
305
- const candidateIds = [nativeSessionId, threadId]
330
+ const nativeId = nativeSessionId.trim();
331
+ const promptThreadId = threadId.trim();
332
+ const candidateIds = [nativeId, promptThreadId]
306
333
  .map((value) => value.trim())
307
334
  .filter(Boolean);
308
335
  if (candidateIds.length === 0) return false;
309
336
 
310
337
  const sessionId = canonicalSessionId.trim();
311
- if (sessionId) {
312
- const summary = await readSubagentSessionSummary(cwd, sessionId).catch(() => null);
313
- if (summary && candidateIds.some((id) => summary.allSubagentThreadIds.includes(id))) {
314
- return true;
338
+ const currentLeaderNativeSessionId = canonicalLeaderNativeSessionId.trim();
339
+ const summary = sessionId
340
+ ? await readSubagentSessionSummary(cwd, sessionId).catch(() => null)
341
+ : null;
342
+ const currentLeaderIds = new Set([
343
+ currentLeaderNativeSessionId,
344
+ summary?.leaderThreadId?.trim(),
345
+ ].filter(Boolean));
346
+ if (
347
+ summary
348
+ && candidateIds.some((id) => !currentLeaderIds.has(id) && summary.allSubagentThreadIds.includes(id))
349
+ ) {
350
+ return true;
351
+ }
352
+ // Native UserPromptSubmit can carry a per-turn thread_id that differs from
353
+ // the long-lived native session id. Treat the current canonical native
354
+ // session as the leader before consulting stale/global tracker state.
355
+ if (
356
+ sessionId
357
+ && currentLeaderNativeSessionId
358
+ && (
359
+ nativeId === currentLeaderNativeSessionId
360
+ || (!nativeId && promptThreadId === currentLeaderNativeSessionId)
361
+ )
362
+ ) {
363
+ return false;
364
+ }
365
+
366
+ if (summary) {
367
+ const leaderThreadId = summary.leaderThreadId?.trim();
368
+ if (
369
+ leaderThreadId
370
+ && (
371
+ nativeId === leaderThreadId
372
+ || (!nativeId && promptThreadId === leaderThreadId)
373
+ )
374
+ ) {
375
+ return false;
315
376
  }
316
377
  }
317
378
 
@@ -326,7 +387,7 @@ async function isNativeSubagentHook(
326
387
  if (!trackingState) return false;
327
388
 
328
389
  return Object.values(trackingState.sessions).some((session) => (
329
- candidateIds.some((id) => session.threads[id]?.kind === "subagent")
390
+ candidateIds.some((id) => isTrustedSubagentThread(session, id))
330
391
  ));
331
392
  }
332
393
 
@@ -1702,15 +1763,23 @@ function buildSkillStateCliInstruction(mode: string, statePath: string): string
1702
1763
  return `skill: ${mode} activated and initial state initialized at ${statePath}; use CLI-first state updates via \`omx state write/read/clear --input '<json>' --json\`; use omx_state MCP only when explicit MCP compatibility is enabled.`;
1703
1764
  }
1704
1765
 
1705
- function buildAutopilotPromptActivationNote(skillState?: SkillActiveState | null): string | null {
1766
+ function buildAutopilotPromptActivationNote(
1767
+ skillState?: SkillActiveState | null,
1768
+ options: { markedQuestionAnswer?: boolean } = {},
1769
+ ): string | null {
1706
1770
  if (skillState?.initialized_mode !== "autopilot") return null;
1707
1771
  return [
1708
1772
  "Autopilot protocol: the durable default chain is $deep-interview -> $ralplan -> $ultragoal (+ $team if needed) -> $code-review -> $ultraqa (deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa).",
1709
1773
  "Start/resume at current_phase=deep-interview unless the task is clear and bounded; if deep-interview is intentionally skipped, persist and state an explicit deep_interview_gate.skip_reason before moving to ralplan.",
1774
+ "Deep-interview is a structured question chain, not a one-question gate: after an omx question answer, re-score ambiguity against the active threshold, treat max_rounds as a cap, and crystallize once ambiguity is at or below threshold and readiness gates pass.",
1775
+ options.markedQuestionAnswer
1776
+ ? "This turn is a marked omx question answer. Treat ordinary selected option/freeform answer text as interview input, then re-score. Do not close merely because the first question was answered; if ambiguity is at or below threshold and readiness gates pass, write interview_complete evidence and hand off. Ask another deep-interview follow-up only when a readiness gate remains unresolved and the answer would materially change execution."
1777
+ : null,
1778
+ "Do not advance from deep-interview to ralplan merely because the first question was answered; persist explicit interview_complete evidence before setting current_phase=ralplan, and do advance when threshold plus readiness gates are satisfied.",
1710
1779
  "The ralplan phase is not complete until Planner output has been reviewed sequentially by Architect and then Critic; do not hand off to Ultragoal or implementation until the ralplan state/artifact records both ralplan_architect_review and ralplan_critic_review with approval or an explicit blocker.",
1711
1780
  "Do not silently fall back to ordinary $plan/ralplan-only handling; keep autopilot-state.json, skill-active-state.json, HUD/statusline, and Codex goal-mode handoff guidance visible while the workflow is active.",
1712
1781
  "When Codex goal tools are available, call get_goal/create_goal only from the active thread handoff and treat the active goal as the completion contract until code-review and ultraqa are clean.",
1713
- ].join(" ");
1782
+ ].filter(Boolean).join(" ");
1714
1783
  }
1715
1784
 
1716
1785
  function buildAdditionalContextMessage(
@@ -1730,6 +1799,8 @@ function buildAdditionalContextMessage(
1730
1799
  ? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
1731
1800
  : null;
1732
1801
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1802
+ const markedQuestionAnswer = /^\s*\[omx question answered\]/i.test(prompt);
1803
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer });
1733
1804
  return [
1734
1805
  `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}".`,
1735
1806
  promptPriorityMessage,
@@ -1738,12 +1809,36 @@ function buildAdditionalContextMessage(
1738
1809
  : null,
1739
1810
  deepInterviewPromptActivationNote,
1740
1811
  deepInterviewConfigPromptActivationNote,
1812
+ autopilotPromptActivationNote,
1741
1813
  "Follow AGENTS.md routing and preserve workflow transition and planning-safety rules.",
1742
1814
  ].filter(Boolean).join(" ");
1743
1815
  }
1744
1816
  const detectedKeywordMessage = matches.length > 1
1745
1817
  ? `OMX native UserPromptSubmit detected workflow keywords ${matches.map((entry) => `"${entry.keyword}" -> ${entry.skill}`).join(", ")}.`
1746
1818
  : `OMX native UserPromptSubmit detected workflow keyword "${match.keyword}" -> ${match.skill}.`;
1819
+ const continuedSkill = safeString(skillState?.skill).trim();
1820
+ if (
1821
+ continuedSkill
1822
+ && continuedSkill !== match.skill
1823
+ && /^\s*\[omx question answered\]/i.test(prompt)
1824
+ ) {
1825
+ const deepInterviewPromptActivationNote = skillState?.initialized_mode === "deep-interview"
1826
+ ? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
1827
+ : null;
1828
+ const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1829
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true });
1830
+ return [
1831
+ `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}"; workflow-like tokens inside the marked omx question answer are treated as answer text, not a new workflow activation.`,
1832
+ promptPriorityMessage,
1833
+ skillState?.initialized_mode && skillState.initialized_state_path
1834
+ ? buildSkillStateCliInstruction(skillState.initialized_mode, skillState.initialized_state_path)
1835
+ : null,
1836
+ deepInterviewPromptActivationNote,
1837
+ deepInterviewConfigPromptActivationNote,
1838
+ autopilotPromptActivationNote,
1839
+ "Follow AGENTS.md routing and preserve workflow transition and planning-safety rules.",
1840
+ ].filter(Boolean).join(" ");
1841
+ }
1747
1842
  const activeSkills = Array.isArray(skillState?.active_skills)
1748
1843
  ? skillState.active_skills.map((entry) => entry.skill)
1749
1844
  : [];
@@ -2400,14 +2495,34 @@ const DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES = [
2400
2495
  ".omx/state",
2401
2496
  ] as const;
2402
2497
 
2403
- const DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES = new Set([
2498
+ const RALPLAN_ALLOWED_WRITE_PREFIXES = [
2499
+ ".omx/context",
2500
+ ".omx/plans",
2501
+ ".omx/specs",
2502
+ ".omx/state",
2503
+ ] as const;
2504
+
2505
+ const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
2404
2506
  "Write",
2405
2507
  "Edit",
2406
2508
  "MultiEdit",
2509
+ "NotebookEdit",
2407
2510
  "apply_patch",
2408
2511
  "ApplyPatch",
2409
2512
  ]);
2410
2513
 
2514
+ const DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES = PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES;
2515
+
2516
+ const RALPLAN_EXECUTION_HANDOFF_SKILLS = new Set([
2517
+ "autopilot",
2518
+ "autoresearch",
2519
+ "ralph",
2520
+ "team",
2521
+ "ultragoal",
2522
+ "ultrawork",
2523
+ "ultraqa",
2524
+ ]);
2525
+
2411
2526
  function isActiveDeepInterviewPhase(state: Record<string, unknown> | null): boolean {
2412
2527
  if (!state || state.active !== true) return false;
2413
2528
  const mode = safeString(state.mode).trim();
@@ -2417,7 +2532,39 @@ function isActiveDeepInterviewPhase(state: Record<string, unknown> | null): bool
2417
2532
  return true;
2418
2533
  }
2419
2534
 
2420
- function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boolean {
2535
+ function isActiveRalplanPhase(state: Record<string, unknown> | null): boolean {
2536
+ if (!state || state.active !== true) return false;
2537
+ const mode = safeString(state.mode).trim();
2538
+ if (mode && mode !== "ralplan") return false;
2539
+ const phase = safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase();
2540
+ if (phase && (TERMINAL_MODE_PHASES.has(phase) || phase === "completing")) return false;
2541
+ return true;
2542
+ }
2543
+
2544
+ function isActiveAutopilotRalplanPhase(state: Record<string, unknown> | null): boolean {
2545
+ if (!state || state.active !== true) return false;
2546
+ const mode = safeString(state.mode).trim();
2547
+ if (mode && mode !== "autopilot") return false;
2548
+ const phase = safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase();
2549
+ return phase === "ralplan";
2550
+ }
2551
+
2552
+ function hasExplicitExecutionHandoffSkill(
2553
+ state: SkillActiveStateLike | null,
2554
+ sessionId: string,
2555
+ threadId: string,
2556
+ ): boolean {
2557
+ return listActiveSkills(state ?? {}).some((entry) => (
2558
+ RALPLAN_EXECUTION_HANDOFF_SKILLS.has(entry.skill)
2559
+ && matchesSkillStopContext(entry, state ?? {}, sessionId, threadId)
2560
+ ));
2561
+ }
2562
+
2563
+ function isAllowedPlanningArtifactPath(
2564
+ cwd: string,
2565
+ rawPath: string,
2566
+ allowedPrefixes: readonly string[],
2567
+ ): boolean {
2421
2568
  const trimmed = rawPath.trim().replace(/^['"]|['"]$/g, "");
2422
2569
  if (!trimmed || trimmed.includes("\0")) return false;
2423
2570
  let relativePath: string;
@@ -2428,11 +2575,19 @@ function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boole
2428
2575
  return false;
2429
2576
  }
2430
2577
  if (!relativePath || relativePath.startsWith("..") || relativePath.startsWith("/")) return false;
2431
- return DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES.some((prefix) => (
2578
+ return allowedPrefixes.some((prefix) => (
2432
2579
  relativePath === prefix || relativePath.startsWith(`${prefix}/`)
2433
2580
  ));
2434
2581
  }
2435
2582
 
2583
+ function isAllowedDeepInterviewArtifactPath(cwd: string, rawPath: string): boolean {
2584
+ return isAllowedPlanningArtifactPath(cwd, rawPath, DEEP_INTERVIEW_ALLOWED_WRITE_PREFIXES);
2585
+ }
2586
+
2587
+ function isAllowedRalplanArtifactPath(cwd: string, rawPath: string): boolean {
2588
+ return isAllowedPlanningArtifactPath(cwd, rawPath, RALPLAN_ALLOWED_WRITE_PREFIXES);
2589
+ }
2590
+
2436
2591
  function readPreToolUseCommand(payload: CodexHookPayload): string {
2437
2592
  const toolInput = safeObject(payload.tool_input);
2438
2593
  return safeString(toolInput.command).trim();
@@ -2502,6 +2657,86 @@ async function readActiveDeepInterviewStateForPreToolUse(
2502
2657
  return hasActiveDeepInterviewSkill ? modeState : null;
2503
2658
  }
2504
2659
 
2660
+ async function readActiveRalplanStateForPreToolUse(
2661
+ cwd: string,
2662
+ stateDir: string,
2663
+ sessionId: string,
2664
+ threadId: string,
2665
+ ): Promise<Record<string, unknown> | null> {
2666
+ const modeState = sessionId
2667
+ ? await readStopSessionPinnedState("ralplan-state.json", cwd, sessionId, stateDir)
2668
+ : await readJsonIfExists(join(stateDir, "ralplan-state.json"));
2669
+ const canonicalState = sessionId
2670
+ ? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
2671
+ : await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
2672
+ if (isActiveRalplanPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
2673
+ if (hasExplicitExecutionHandoffSkill(canonicalState, sessionId, threadId)) return null;
2674
+ if (!canonicalState) return modeState;
2675
+ const hasActiveRalplanSkill = listActiveSkills(canonicalState).some((entry) => (
2676
+ entry.skill === "ralplan"
2677
+ && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
2678
+ ));
2679
+ if (hasActiveRalplanSkill) return modeState;
2680
+ }
2681
+
2682
+ const autopilotState = sessionId
2683
+ ? await readStopSessionPinnedState("autopilot-state.json", cwd, sessionId, stateDir)
2684
+ : await readJsonIfExists(join(stateDir, "autopilot-state.json"));
2685
+ if (!isActiveAutopilotRalplanPhase(autopilotState) || !autopilotState) return null;
2686
+ if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId)) return null;
2687
+ const terminalAutopilotRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, "autopilot");
2688
+ if (terminalAutopilotRunState) return null;
2689
+ if (!canonicalState) return autopilotState;
2690
+ const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
2691
+ entry.skill === "autopilot"
2692
+ && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
2693
+ ));
2694
+ return hasActiveAutopilotSkill ? autopilotState : null;
2695
+ }
2696
+
2697
+ function isAllowedRalplanBashWrite(cwd: string, command: string): boolean {
2698
+ if (!commandHasDeepInterviewWriteIntent(command)) return true;
2699
+ if (/\bomx\s+(?:state\s+(?:write|read|clear)|question)\b/.test(command)) return true;
2700
+ const targets = extractDeepInterviewCommandWriteTargets(command);
2701
+ return targets.length > 0 && targets.every((target) => isAllowedRalplanArtifactPath(cwd, target));
2702
+ }
2703
+
2704
+ async function buildRalplanPreToolUseBoundaryOutput(
2705
+ payload: CodexHookPayload,
2706
+ cwd: string,
2707
+ stateDir: string,
2708
+ ): Promise<Record<string, unknown> | null> {
2709
+ const sessionId = readPayloadSessionId(payload);
2710
+ const threadId = readPayloadThreadId(payload);
2711
+ const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
2712
+ if (!activeState) return null;
2713
+
2714
+ const toolName = safeString(payload.tool_name).trim();
2715
+ const command = readPreToolUseCommand(payload);
2716
+ const pathCandidates = readPreToolUsePathCandidates(payload);
2717
+ let blocked = false;
2718
+
2719
+ if (toolName === "Bash") {
2720
+ blocked = !isAllowedRalplanBashWrite(cwd, command);
2721
+ } else if (PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES.has(toolName)) {
2722
+ blocked = pathCandidates.length === 0
2723
+ || !pathCandidates.every((candidate) => isAllowedRalplanArtifactPath(cwd, candidate));
2724
+ }
2725
+
2726
+ if (!blocked) return null;
2727
+
2728
+ const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
2729
+ return {
2730
+ decision: "block",
2731
+ reason: `Ralplan is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated.`,
2732
+ hookSpecificOutput: {
2733
+ hookEventName: "PreToolUse",
2734
+ additionalContext:
2735
+ "Ralplan is consensus-planning mode. Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, or required `.omx/state/` files. Do not edit implementation files or run implementation-focused writes from ralplan. To execute, first process an explicit handoff such as `$ultragoal`, `$team`, or `$ralph`, which must emit terminal ralplan state before implementation begins.",
2736
+ },
2737
+ };
2738
+ }
2739
+
2505
2740
  async function buildDeepInterviewPreToolUseBoundaryOutput(
2506
2741
  payload: CodexHookPayload,
2507
2742
  cwd: string,
@@ -2827,15 +3062,15 @@ function buildRalplanContinuationStatus(
2827
3062
  }
2828
3063
 
2829
3064
  const completeHint = blocker.planningComplete
2830
- ? " The planning artifacts are present; if consensus is approved, emit the final complete/approved handoff instead of stopping here."
3065
+ ? " The planning artifacts are present; if consensus is approved, emit terminal ralplan complete/approved handoff state and stop planning. Implementation must wait for an explicit $ultragoal, $team, or $ralph handoff."
2831
3066
  : "";
2832
3067
 
2833
3068
  return {
2834
3069
  reason:
2835
- `Status: continue_from_artifact — ralplan is still active (phase: ${phase}) and has not emitted a terminal complete/paused/waiting status. Continue from the current ralplan artifact, resolve any review ambiguity conservatively or ask the user if needed, and proceed to the next planning/review step before stopping.${artifact}${completeHint}`,
3070
+ `Status: continue_from_artifact — ralplan is still active (phase: ${phase}) and has not emitted a terminal complete/paused/waiting status. Continue from the current ralplan artifact, resolve any review ambiguity conservatively or ask the user if needed, and proceed to the next planning/review step before stopping; do not begin implementation from ralplan.${artifact}${completeHint}`,
2836
3071
  stopReasonSuffix: "continue_artifact",
2837
3072
  systemMessage:
2838
- `OMX ralplan status: continue_from_artifact at phase ${phase}; continue from the current ralplan artifact and finish by stating whether ralplan is complete, paused for review, waiting for input, or still continuing.`,
3073
+ `OMX ralplan status: continue_from_artifact at phase ${phase}; continue from the current ralplan artifact and finish by stating whether ralplan is complete, paused for review, waiting for input, or still continuing; do not begin implementation from ralplan.`,
2839
3074
  };
2840
3075
  }
2841
3076
 
@@ -3732,6 +3967,14 @@ export async function dispatchCodexNativeHook(
3732
3967
  ): Promise<NativeHookDispatchResult> {
3733
3968
  const hookEventName = readHookEventName(payload);
3734
3969
  const cwd = options.cwd ?? (safeString(payload.cwd).trim() || process.cwd());
3970
+ if (hookEventName === "Stop" && !hasNativeStopRuntimeSurface(cwd)) {
3971
+ return {
3972
+ hookEventName,
3973
+ omxEventName: mapCodexHookEventToOmxEvent(hookEventName),
3974
+ skillState: null,
3975
+ outputJson: null,
3976
+ };
3977
+ }
3735
3978
  // Native hooks must use the same authoritative runtime state root as HUD/MCP
3736
3979
  // when boxed/team roots are active; do not bypass it with cwd/.omx/state.
3737
3980
  const stateDir = getBaseStateDir(cwd);
@@ -3814,7 +4057,13 @@ export async function dispatchCodexNativeHook(
3814
4057
  const sessionIdForState = canonicalSessionId || nativeSessionId;
3815
4058
  let outputJson: Record<string, unknown> | null = null;
3816
4059
  const isSubagentPromptSubmit = hookEventName === "UserPromptSubmit"
3817
- ? await isNativeSubagentHook(cwd, canonicalSessionId, nativeSessionId, threadId)
4060
+ ? await isNativeSubagentHook(
4061
+ cwd,
4062
+ canonicalSessionId,
4063
+ nativeSessionId,
4064
+ threadId,
4065
+ safeString(currentSessionState?.native_session_id).trim(),
4066
+ )
3818
4067
  : false;
3819
4068
  const isSubagentStop = hookEventName === "Stop"
3820
4069
  ? (await Promise.all(
@@ -3822,7 +4071,15 @@ export async function dispatchCodexNativeHook(
3822
4071
  canonicalSessionId,
3823
4072
  safeString(currentSessionState?.session_id).trim(),
3824
4073
  ].filter(Boolean))]
3825
- .map((candidateSessionId) => isNativeSubagentHook(cwd, candidateSessionId, nativeSessionId, threadId)),
4074
+ .map((candidateSessionId) => isNativeSubagentHook(
4075
+ cwd,
4076
+ candidateSessionId,
4077
+ nativeSessionId,
4078
+ threadId,
4079
+ candidateSessionId === safeString(currentSessionState?.session_id).trim()
4080
+ ? safeString(currentSessionState?.native_session_id).trim()
4081
+ : "",
4082
+ )),
3826
4083
  )).some(Boolean)
3827
4084
  : false;
3828
4085
  const suppressNoisySubagentLifecycleDispatch =
@@ -3926,7 +4183,12 @@ export async function dispatchCodexNativeHook(
3926
4183
  && await isConfirmedTeamWorkerPromptSubmitPane(cwd).catch(() => false);
3927
4184
  if (!skipHudReconcileForTeamWorkerPane) {
3928
4185
  const reconcileHudForPromptSubmitFn = options.reconcileHudForPromptSubmitFn ?? reconcileHudForPromptSubmit;
3929
- await reconcileHudForPromptSubmitFn(cwd, { sessionId: canonicalSessionId || sessionIdForState || undefined }).catch(() => {});
4186
+ const hudSessionId = resolveHudReconcileSessionId(
4187
+ currentSessionState,
4188
+ canonicalSessionId,
4189
+ sessionIdForState,
4190
+ );
4191
+ await reconcileHudForPromptSubmitFn(cwd, { sessionId: hudSessionId }).catch(() => {});
3930
4192
  }
3931
4193
  }
3932
4194
 
@@ -3987,6 +4249,7 @@ export async function dispatchCodexNativeHook(
3987
4249
  }
3988
4250
  } else if (hookEventName === "PreToolUse") {
3989
4251
  outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir)
4252
+ ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir)
3990
4253
  ?? buildNativePreToolUseOutput(payload);
3991
4254
  } else if (hookEventName === "PostToolUse") {
3992
4255
  if (detectMcpTransportFailure(payload)) {
@@ -4008,9 +4271,31 @@ export async function dispatchCodexNativeHook(
4008
4271
  };
4009
4272
  }
4010
4273
 
4274
+ function hasNativeStopRuntimeSurface(cwd: string): boolean {
4275
+ if (existsSync(join(cwd, ".omx"))) return true;
4276
+ if (findGitLayout(cwd)) return true;
4277
+ const omxRoot = safeString(process.env.OMX_ROOT).trim();
4278
+ if (omxRoot && existsSync(join(omxRoot, ".omx"))) return true;
4279
+ const stateRoot = safeString(process.env.OMX_STATE_ROOT).trim();
4280
+ if (stateRoot && existsSync(stateRoot)) return true;
4281
+ return [
4282
+ process.env.OMX_SESSION_ID,
4283
+ process.env.OMX_TEAM_INTERNAL_WORKER,
4284
+ process.env.OMX_TEAM_WORKER,
4285
+ process.env.OMX_TEAM_STATE_ROOT,
4286
+ process.env.OMX_TEAM_LEADER_CWD,
4287
+ process.env.OMX_NOTIFY_HOOK_TRUSTED_MANAGED_CWD,
4288
+ process.env.OMX_TMUX_HUD_OWNER,
4289
+ process.env.OMX_TMUX_HUD_LEADER_PANE,
4290
+ ].some((value) => safeString(value).trim() !== "");
4291
+ }
4292
+
4011
4293
  interface NativeHookCliReadResult {
4012
4294
  payload: CodexHookPayload;
4013
4295
  parseError: Error | null;
4296
+ rawInput: string;
4297
+ oversized: boolean;
4298
+ rawHookEventName: CodexHookEventName | null;
4014
4299
  }
4015
4300
 
4016
4301
  export function isCodexNativeHookMainModule(
@@ -4023,36 +4308,156 @@ export function isCodexNativeHookMainModule(
4023
4308
 
4024
4309
  async function readStdinJson(): Promise<NativeHookCliReadResult> {
4025
4310
  const chunks: Buffer[] = [];
4311
+ let totalBytes = 0;
4312
+ let oversized = false;
4026
4313
  for await (const chunk of process.stdin) {
4027
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
4314
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
4315
+ totalBytes += buffer.byteLength;
4316
+ if (totalBytes > MAX_NATIVE_STDIN_JSON_BYTES) {
4317
+ const remaining = Math.max(0, MAX_NATIVE_STDIN_JSON_BYTES - (totalBytes - buffer.byteLength));
4318
+ if (remaining > 0) chunks.push(Buffer.from(buffer.subarray(0, remaining)));
4319
+ oversized = true;
4320
+ process.stdin.destroy();
4321
+ break;
4322
+ }
4323
+ chunks.push(buffer);
4028
4324
  }
4029
4325
  const raw = Buffer.concat(chunks).toString("utf-8").trim();
4326
+ const rawHookEventName = extractRawCodexHookEventName(raw);
4327
+ if (oversized) {
4328
+ return {
4329
+ payload: {},
4330
+ parseError: null,
4331
+ rawInput: raw,
4332
+ oversized: true,
4333
+ rawHookEventName,
4334
+ };
4335
+ }
4030
4336
  if (!raw) {
4031
- return { payload: {}, parseError: null };
4337
+ return { payload: {}, parseError: null, rawInput: raw, oversized: false, rawHookEventName };
4032
4338
  }
4033
4339
 
4034
4340
  try {
4035
4341
  return {
4036
4342
  payload: safeObject(JSON.parse(raw)),
4037
4343
  parseError: null,
4344
+ rawInput: raw,
4345
+ oversized: false,
4346
+ rawHookEventName,
4038
4347
  };
4039
4348
  } catch (error) {
4040
4349
  return {
4041
4350
  payload: {},
4042
4351
  parseError: error instanceof Error ? error : new Error(String(error)),
4352
+ rawInput: raw,
4353
+ oversized: false,
4354
+ rawHookEventName,
4043
4355
  };
4044
4356
  }
4045
4357
  }
4046
4358
 
4359
+ function inferHookEventNameFromMalformedInput(raw: string): CodexHookEventName | null {
4360
+ const match = raw.match(/(?:\"|['"])?hook[_-]?event[_-]?name(?:\"|['"])?\s*:\s*(?:\"|['"])?(SessionStart|PreToolUse|PostToolUse|UserPromptSubmit|PreCompact|PostCompact|Stop)\b/i);
4361
+ const value = match?.[1];
4362
+ if (!value) return null;
4363
+ return readHookEventName({ hook_event_name: value });
4364
+ }
4365
+
4366
+ function buildMalformedStdinHookOutput(parseError: Error, rawInput: string): Record<string, unknown> {
4367
+ const reason =
4368
+ "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.";
4369
+ const systemMessage =
4370
+ `${reason} stdin JSON parsing failed inside codex-native-hook: ${parseError.message}.`;
4371
+ if (inferHookEventNameFromMalformedInput(rawInput) === "Stop") {
4372
+ return {
4373
+ decision: "block",
4374
+ reason,
4375
+ stopReason: "native_hook_stdin_parse_error",
4376
+ systemMessage,
4377
+ };
4378
+ }
4379
+ return {
4380
+ continue: false,
4381
+ stopReason: "native_hook_stdin_parse_error",
4382
+ systemMessage,
4383
+ };
4384
+ }
4385
+
4386
+ async function buildOversizedStopActiveWorkflowOutput(cwd: string): Promise<Record<string, unknown> | null> {
4387
+ const currentSession = await readUsableSessionState(cwd);
4388
+ const currentSessionId = safeString(currentSession?.session_id).trim()
4389
+ || safeString(process.env.OMX_SESSION_ID || process.env.CODEX_SESSION_ID).trim();
4390
+ if (!currentSessionId) return null;
4391
+
4392
+ if (await readCanonicalTerminalRunStateForStop(cwd, currentSessionId, "autopilot")) return null;
4393
+
4394
+ const autopilotState = await readModeStateForActiveDecision("autopilot", currentSessionId, cwd);
4395
+ if (!autopilotState || !shouldContinueRun(autopilotState)) return null;
4396
+
4397
+ const phase = formatPhase(autopilotState.current_phase);
4398
+ const reason =
4399
+ `OMX native Stop received oversized stdin before parsing while the current session has active OMX autopilot state (phase: ${phase}); continue once with a compact response or reduce hook payload size so normal Stop gates can run.`;
4400
+ return {
4401
+ decision: "block",
4402
+ reason,
4403
+ stopReason: "native_stop_stdin_oversized_active_workflow",
4404
+ systemMessage:
4405
+ "OMX native Stop rejected oversized stdin before parsing; active current-session workflow state is present, so Stop is blocked instead of silently allowing termination.",
4406
+ };
4407
+ }
4408
+
4409
+ async function buildOversizedStdinHookOutput(
4410
+ rawHookEventName: CodexHookEventName | null,
4411
+ cwd: string,
4412
+ ): Promise<Record<string, unknown>> {
4413
+ if (rawHookEventName === "Stop") {
4414
+ return await buildOversizedStopActiveWorkflowOutput(cwd) ?? {};
4415
+ }
4416
+ const systemMessage =
4417
+ `OMX native hook rejected oversized stdin JSON before parsing; maxBytes=${MAX_NATIVE_STDIN_JSON_BYTES}.`;
4418
+ return {
4419
+ continue: false,
4420
+ stopReason: "native_hook_stdin_oversized",
4421
+ systemMessage,
4422
+ };
4423
+ }
4424
+
4047
4425
  function writeNativeHookJsonStdout(output: Record<string, unknown>): void {
4048
4426
  process.stdout.write(`${JSON.stringify(output)}\n`);
4049
4427
  }
4050
4428
 
4429
+ function redactMalformedHookPreview(rawInput: string): string {
4430
+ const withoutControls = rawInput.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
4431
+ const withoutAuthSecrets = redactAuthSecrets(withoutControls);
4432
+ return withoutAuthSecrets
4433
+ .replace(
4434
+ /(["']?(?:prompt|user_prompt|input|text)["']?\s*:\s*)(["'])(?:\\.|(?!\2)[^\\])*\2/gi,
4435
+ "$1$2[REDACTED]$2",
4436
+ )
4437
+ .replace(
4438
+ /(["']?(?:prompt|user_prompt|input|text)["']?\s*:\s*)(["'])(?:\\.|[^\\])*$/gi,
4439
+ "$1$2[REDACTED]$2",
4440
+ )
4441
+ .replace(
4442
+ /(["']?(?:prompt|user_prompt|input|text)["']?\s*:\s*)(?!["'])[^,}]*/gi,
4443
+ "$1[REDACTED]",
4444
+ );
4445
+ }
4446
+
4447
+ function buildRawInputLogFields(rawInput: string): Record<string, unknown> {
4448
+ if (!rawInput) return {};
4449
+ return {
4450
+ raw_input_length: Buffer.byteLength(rawInput, "utf-8"),
4451
+ raw_input_prefix: redactMalformedHookPreview(rawInput).slice(0, 240),
4452
+ };
4453
+ }
4454
+
4051
4455
  async function logNativeHookCliError(
4052
4456
  cwd: string,
4053
4457
  type: string,
4054
4458
  error: unknown,
4055
4459
  payload: CodexHookPayload = {},
4460
+ details: Record<string, unknown> = {},
4056
4461
  ): Promise<void> {
4057
4462
  const logsDir = join(cwd || process.cwd(), ".omx", "logs");
4058
4463
  await mkdir(logsDir, { recursive: true }).catch(() => {});
@@ -4067,6 +4472,7 @@ async function logNativeHookCliError(
4067
4472
  thread_id: readPayloadThreadId(payload) || undefined,
4068
4473
  turn_id: readPayloadTurnId(payload) || undefined,
4069
4474
  error: error instanceof Error ? error.message : String(error),
4475
+ ...details,
4070
4476
  }) + "\n",
4071
4477
  ).catch(() => {});
4072
4478
  }
@@ -4095,18 +4501,20 @@ function buildStopDispatchFailureOutput(error: unknown): Record<string, unknown>
4095
4501
  }
4096
4502
 
4097
4503
  export async function runCodexNativeHookCli(): Promise<void> {
4098
- const { payload, parseError } = await readStdinJson();
4504
+ const { payload, parseError, rawInput, oversized, rawHookEventName } = await readStdinJson();
4505
+ if (oversized) {
4506
+ writeNativeHookJsonStdout(await buildOversizedStdinHookOutput(rawHookEventName, process.cwd()));
4507
+ return;
4508
+ }
4099
4509
  if (parseError) {
4100
- await logNativeHookCliError(process.cwd(), "native_hook_stdin_parse_error", parseError);
4101
- writeNativeHookJsonStdout({
4102
- decision: "block",
4103
- reason: "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.",
4104
- hookSpecificOutput: {
4105
- hookEventName: "Unknown",
4106
- additionalContext:
4107
- `stdin JSON parsing failed inside codex-native-hook: ${parseError.message}. Emit valid JSON from the native hook caller before retrying.`,
4108
- },
4109
- });
4510
+ await logNativeHookCliError(
4511
+ process.cwd(),
4512
+ "native_hook_stdin_parse_error",
4513
+ parseError,
4514
+ {},
4515
+ buildRawInputLogFields(rawInput),
4516
+ );
4517
+ writeNativeHookJsonStdout(buildMalformedStdinHookOutput(parseError, rawInput));
4110
4518
  return;
4111
4519
  }
4112
4520