oh-my-codex 0.18.7 → 0.18.8

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 (259) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +5 -5
  4. package/crates/omx-sparkshell/tests/execution.rs +1 -1
  5. package/dist/agents/__tests__/native-config.test.js +42 -1
  6. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  7. package/dist/agents/definitions.d.ts +8 -0
  8. package/dist/agents/definitions.d.ts.map +1 -1
  9. package/dist/agents/definitions.js +1 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/agents/native-config.d.ts +5 -1
  12. package/dist/agents/native-config.d.ts.map +1 -1
  13. package/dist/agents/native-config.js +17 -2
  14. package/dist/agents/native-config.js.map +1 -1
  15. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  16. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  17. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  18. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  19. package/dist/cli/__tests__/index.test.js +61 -5
  20. package/dist/cli/__tests__/index.test.js.map +1 -1
  21. package/dist/cli/__tests__/package-bin-contract.test.js +8 -4
  22. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  23. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  24. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  25. package/dist/cli/__tests__/ralph.test.js +14 -0
  26. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  27. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  28. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  29. package/dist/cli/__tests__/setup-refresh.test.js +65 -0
  30. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  31. package/dist/cli/__tests__/state.test.js +21 -0
  32. package/dist/cli/__tests__/state.test.js.map +1 -1
  33. package/dist/cli/__tests__/team.test.js +2 -2
  34. package/dist/cli/__tests__/update.test.js +110 -2
  35. package/dist/cli/__tests__/update.test.js.map +1 -1
  36. package/dist/cli/doctor.d.ts.map +1 -1
  37. package/dist/cli/doctor.js +8 -1
  38. package/dist/cli/doctor.js.map +1 -1
  39. package/dist/cli/index.d.ts +11 -2
  40. package/dist/cli/index.d.ts.map +1 -1
  41. package/dist/cli/index.js +108 -15
  42. package/dist/cli/index.js.map +1 -1
  43. package/dist/cli/plugin-marketplace.d.ts +14 -2
  44. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  45. package/dist/cli/plugin-marketplace.js +62 -15
  46. package/dist/cli/plugin-marketplace.js.map +1 -1
  47. package/dist/cli/ralph.d.ts.map +1 -1
  48. package/dist/cli/ralph.js +3 -1
  49. package/dist/cli/ralph.js.map +1 -1
  50. package/dist/cli/setup-preferences.d.ts +2 -0
  51. package/dist/cli/setup-preferences.d.ts.map +1 -1
  52. package/dist/cli/setup-preferences.js +4 -0
  53. package/dist/cli/setup-preferences.js.map +1 -1
  54. package/dist/cli/setup.d.ts +3 -0
  55. package/dist/cli/setup.d.ts.map +1 -1
  56. package/dist/cli/setup.js +166 -27
  57. package/dist/cli/setup.js.map +1 -1
  58. package/dist/cli/state.d.ts.map +1 -1
  59. package/dist/cli/state.js +8 -1
  60. package/dist/cli/state.js.map +1 -1
  61. package/dist/cli/tmux-hook.d.ts.map +1 -1
  62. package/dist/cli/tmux-hook.js +16 -0
  63. package/dist/cli/tmux-hook.js.map +1 -1
  64. package/dist/cli/update.d.ts +2 -0
  65. package/dist/cli/update.d.ts.map +1 -1
  66. package/dist/cli/update.js +47 -3
  67. package/dist/cli/update.js.map +1 -1
  68. package/dist/config/__tests__/generator-notify.test.js +1 -0
  69. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  70. package/dist/config/generator.d.ts +2 -2
  71. package/dist/config/generator.d.ts.map +1 -1
  72. package/dist/config/generator.js +2 -2
  73. package/dist/config/generator.js.map +1 -1
  74. package/dist/config/team-mode.d.ts +12 -0
  75. package/dist/config/team-mode.d.ts.map +1 -0
  76. package/dist/config/team-mode.js +91 -0
  77. package/dist/config/team-mode.js.map +1 -0
  78. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  79. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  80. package/dist/hooks/__tests__/code-review-skill-contract.test.js +8 -0
  81. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  82. package/dist/hooks/__tests__/keyword-detector.test.js +423 -3
  83. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  84. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +1 -1
  85. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  86. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  87. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  88. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  89. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  90. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  91. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  92. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  93. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  94. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  95. package/dist/hooks/agents-overlay.js +36 -50
  96. package/dist/hooks/agents-overlay.js.map +1 -1
  97. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  98. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  99. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  100. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  101. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  102. package/dist/hooks/keyword-detector.js +258 -12
  103. package/dist/hooks/keyword-detector.js.map +1 -1
  104. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  105. package/dist/hooks/prompt-guidance-contract.js +6 -0
  106. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  107. package/dist/hooks/session.d.ts +1 -0
  108. package/dist/hooks/session.d.ts.map +1 -1
  109. package/dist/hooks/session.js.map +1 -1
  110. package/dist/hud/__tests__/authority.test.js +435 -32
  111. package/dist/hud/__tests__/authority.test.js.map +1 -1
  112. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  113. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  114. package/dist/hud/__tests__/index.test.js +42 -0
  115. package/dist/hud/__tests__/index.test.js.map +1 -1
  116. package/dist/hud/__tests__/reconcile.test.js +521 -15
  117. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  118. package/dist/hud/__tests__/render.test.js +61 -0
  119. package/dist/hud/__tests__/render.test.js.map +1 -1
  120. package/dist/hud/__tests__/state.test.js +132 -4
  121. package/dist/hud/__tests__/state.test.js.map +1 -1
  122. package/dist/hud/__tests__/tmux.test.js +180 -21
  123. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  124. package/dist/hud/authority.d.ts +5 -0
  125. package/dist/hud/authority.d.ts.map +1 -1
  126. package/dist/hud/authority.js +324 -28
  127. package/dist/hud/authority.js.map +1 -1
  128. package/dist/hud/index.d.ts +3 -2
  129. package/dist/hud/index.d.ts.map +1 -1
  130. package/dist/hud/index.js +42 -19
  131. package/dist/hud/index.js.map +1 -1
  132. package/dist/hud/reconcile.d.ts +3 -3
  133. package/dist/hud/reconcile.d.ts.map +1 -1
  134. package/dist/hud/reconcile.js +128 -19
  135. package/dist/hud/reconcile.js.map +1 -1
  136. package/dist/hud/render.d.ts.map +1 -1
  137. package/dist/hud/render.js +35 -0
  138. package/dist/hud/render.js.map +1 -1
  139. package/dist/hud/state.d.ts.map +1 -1
  140. package/dist/hud/state.js +61 -62
  141. package/dist/hud/state.js.map +1 -1
  142. package/dist/hud/tmux.d.ts +24 -6
  143. package/dist/hud/tmux.d.ts.map +1 -1
  144. package/dist/hud/tmux.js +136 -38
  145. package/dist/hud/tmux.js.map +1 -1
  146. package/dist/hud/types.d.ts +11 -0
  147. package/dist/hud/types.d.ts.map +1 -1
  148. package/dist/hud/types.js.map +1 -1
  149. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  150. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  151. package/dist/mcp/state-paths.d.ts +32 -0
  152. package/dist/mcp/state-paths.d.ts.map +1 -1
  153. package/dist/mcp/state-paths.js +113 -17
  154. package/dist/mcp/state-paths.js.map +1 -1
  155. package/dist/mcp/state-server.d.ts +4 -4
  156. package/dist/scripts/__tests__/codex-native-hook.test.js +593 -11
  157. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  158. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  159. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  160. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  161. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  162. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  163. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  164. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  165. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  166. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  167. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  168. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  169. package/dist/scripts/codex-native-hook.js +88 -31
  170. package/dist/scripts/codex-native-hook.js.map +1 -1
  171. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  172. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  173. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  174. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  175. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  176. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  177. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  178. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  179. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  180. package/dist/scripts/notify-hook/state-io.js +62 -38
  181. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  182. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  183. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  184. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  185. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  186. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  187. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  188. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  189. package/dist/scripts/notify-hook.js +75 -11
  190. package/dist/scripts/notify-hook.js.map +1 -1
  191. package/dist/scripts/run-test-files.js +193 -22
  192. package/dist/scripts/run-test-files.js.map +1 -1
  193. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  194. package/dist/scripts/sync-plugin-mirror.js +61 -3
  195. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  196. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  197. package/dist/scripts/verify-native-agents.js +58 -1
  198. package/dist/scripts/verify-native-agents.js.map +1 -1
  199. package/dist/state/__tests__/operations.test.js +113 -0
  200. package/dist/state/__tests__/operations.test.js.map +1 -1
  201. package/dist/state/__tests__/skill-active.test.js +3 -16
  202. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  203. package/dist/state/__tests__/workflow-transition.test.js +25 -0
  204. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  205. package/dist/state/operations.d.ts.map +1 -1
  206. package/dist/state/operations.js +57 -2
  207. package/dist/state/operations.js.map +1 -1
  208. package/dist/state/skill-active.d.ts.map +1 -1
  209. package/dist/state/skill-active.js +7 -39
  210. package/dist/state/skill-active.js.map +1 -1
  211. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  212. package/dist/state/workflow-transition-reconcile.js +10 -14
  213. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  214. package/dist/team/__tests__/runtime.test.js +1 -1
  215. package/dist/team/__tests__/runtime.test.js.map +1 -1
  216. package/dist/team/__tests__/scaling.test.js +9 -4
  217. package/dist/team/__tests__/scaling.test.js.map +1 -1
  218. package/dist/team/__tests__/tmux-session.test.js +195 -2
  219. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  220. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  221. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  222. package/dist/team/scaling.d.ts.map +1 -1
  223. package/dist/team/scaling.js +3 -2
  224. package/dist/team/scaling.js.map +1 -1
  225. package/dist/team/tmux-session.d.ts +2 -0
  226. package/dist/team/tmux-session.d.ts.map +1 -1
  227. package/dist/team/tmux-session.js +142 -12
  228. package/dist/team/tmux-session.js.map +1 -1
  229. package/dist/verification/__tests__/ci-rust-gates.test.js +81 -1
  230. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  231. package/package.json +8 -8
  232. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  233. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  234. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  235. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -1
  236. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  237. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  238. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  239. package/skills/autopilot/SKILL.md +3 -1
  240. package/skills/code-review/SKILL.md +7 -7
  241. package/skills/ralph/SKILL.md +22 -22
  242. package/skills/ultraqa/SKILL.md +9 -0
  243. package/src/scripts/__tests__/codex-native-hook.test.ts +686 -13
  244. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  245. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  246. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  247. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  248. package/src/scripts/codex-native-hook.ts +105 -28
  249. package/src/scripts/demo-team-e2e.sh +10 -7
  250. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  251. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  252. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  253. package/src/scripts/notify-hook/state-io.ts +75 -37
  254. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  255. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  256. package/src/scripts/notify-hook.ts +91 -4
  257. package/src/scripts/run-test-files.ts +192 -22
  258. package/src/scripts/sync-plugin-mirror.ts +98 -9
  259. package/src/scripts/verify-native-agents.ts +65 -1
@@ -48,6 +48,7 @@ import {
48
48
  type SkillActiveState,
49
49
  } from "../hooks/keyword-detector.js";
50
50
  import { buildDeepInterviewConfigInstruction } from "../hooks/deep-interview-config-instruction.js";
51
+ import { readTeamModeConfig } from "../config/team-mode.js";
51
52
  import {
52
53
  detectNativeStopStallPattern,
53
54
  loadAutoNudgeConfig,
@@ -189,6 +190,25 @@ function resolveHudReconcileSessionId(
189
190
  return canonicalSessionId || sessionIdForState || undefined;
190
191
  }
191
192
 
193
+ function resolveHudReconcileSessionIds(
194
+ currentSessionState: SessionState | null,
195
+ canonicalSessionId: string | null,
196
+ sessionIdForState: string | null,
197
+ nativeSessionId: string | null,
198
+ ): string[] {
199
+ const ownerOmxSessionId = safeString(currentSessionState?.owner_omx_session_id).trim();
200
+ return uniqueNonEmpty([
201
+ resolveHudReconcileSessionId(currentSessionState, canonicalSessionId, sessionIdForState),
202
+ canonicalSessionId ?? undefined,
203
+ sessionIdForState ?? undefined,
204
+ nativeSessionId ?? undefined,
205
+ safeString(currentSessionState?.session_id),
206
+ safeString(currentSessionState?.native_session_id),
207
+ OMX_OWNER_SESSION_ID_PATTERN.test(ownerOmxSessionId) ? ownerOmxSessionId : undefined,
208
+ safeString(currentSessionState?.owner_codex_session_id),
209
+ ]);
210
+ }
211
+
192
212
  function safeContextSnippet(value: unknown, maxLength = 300): string {
193
213
  const text = safeString(value).replace(/\s+/g, " ").trim();
194
214
  if (text.length <= maxLength) return text;
@@ -1730,6 +1750,7 @@ function buildNativeOutsideTmuxTeamPromptBlockState(
1730
1750
  threadId?: string,
1731
1751
  turnId?: string,
1732
1752
  ): SkillActiveState | null {
1753
+ if (!readTeamModeConfig(cwd).enabled) return null;
1733
1754
  const match = detectPrimaryKeyword(prompt);
1734
1755
  if (match?.skill !== "team") return null;
1735
1756
 
@@ -1765,11 +1786,14 @@ function buildSkillStateCliInstruction(mode: string, statePath: string): string
1765
1786
 
1766
1787
  function buildAutopilotPromptActivationNote(
1767
1788
  skillState?: SkillActiveState | null,
1768
- options: { markedQuestionAnswer?: boolean } = {},
1789
+ options: { markedQuestionAnswer?: boolean; cwd?: string } = {},
1769
1790
  ): string | null {
1770
1791
  if (skillState?.initialized_mode !== "autopilot") return null;
1792
+ const teamHandoff = readTeamModeConfig(options.cwd).enabled
1793
+ ? " (+ $team if needed)"
1794
+ : "";
1771
1795
  return [
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).",
1796
+ `Autopilot protocol: the durable default chain is $deep-interview -> $ralplan -> $ultragoal${teamHandoff} -> $code-review -> $ultraqa (deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa).`,
1773
1797
  "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
1798
  "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
1799
  options.markedQuestionAnswer
@@ -1782,6 +1806,12 @@ function buildAutopilotPromptActivationNote(
1782
1806
  ].filter(Boolean).join(" ");
1783
1807
  }
1784
1808
 
1809
+ function formatExecutionHandoffList(cwd: string): string {
1810
+ return readTeamModeConfig(cwd).enabled
1811
+ ? "`$ultragoal`, `$team`, or `$ralph`"
1812
+ : "`$ultragoal` or `$ralph`";
1813
+ }
1814
+
1785
1815
  function buildAdditionalContextMessage(
1786
1816
  prompt: string,
1787
1817
  skillState?: SkillActiveState | null,
@@ -1790,8 +1820,9 @@ function buildAdditionalContextMessage(
1790
1820
  ): string | null {
1791
1821
  if (!prompt) return null;
1792
1822
  const promptPriorityMessage = buildPromptPriorityMessage(prompt);
1793
- const matches = detectKeywords(prompt);
1794
- const match = detectPrimaryKeyword(prompt);
1823
+ const teamMode = readTeamModeConfig(cwd);
1824
+ const matches = detectKeywords(prompt).filter((entry) => teamMode.enabled || entry.skill !== "team");
1825
+ const match = matches[0] ?? null;
1795
1826
  if (!match) {
1796
1827
  const continuedSkill = safeString(skillState?.skill).trim();
1797
1828
  if (!continuedSkill) return promptPriorityMessage;
@@ -1800,7 +1831,7 @@ function buildAdditionalContextMessage(
1800
1831
  : null;
1801
1832
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1802
1833
  const markedQuestionAnswer = /^\s*\[omx question answered\]/i.test(prompt);
1803
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer });
1834
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer, cwd });
1804
1835
  return [
1805
1836
  `OMX native UserPromptSubmit continued active workflow skill "${continuedSkill}".`,
1806
1837
  promptPriorityMessage,
@@ -1826,7 +1857,7 @@ function buildAdditionalContextMessage(
1826
1857
  ? buildDeepInterviewQuestionBridgeInstruction(cwd, payload)
1827
1858
  : null;
1828
1859
  const deepInterviewConfigPromptActivationNote = buildDeepInterviewConfigInstruction(cwd, skillState);
1829
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true });
1860
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { markedQuestionAnswer: true, cwd });
1830
1861
  return [
1831
1862
  `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
1863
  promptPriorityMessage,
@@ -1859,7 +1890,7 @@ function buildAdditionalContextMessage(
1859
1890
  const ultragoalPromptActivationNote = match.skill === "ultragoal"
1860
1891
  ? "Ultragoal protocol: use `omx ultragoal create-goals` / `complete-goals` / `checkpoint` for `.omx/ultragoal` artifacts, then use Codex goal model tools only from the active agent handoff (`get_goal`, `create_goal`, `update_goal`) and never overwrite a different active Codex goal. Ultragoal does not call `/goal clear`; for multiple sequential ultragoal runs in one Codex session/thread, manually clear the completed Codex goal in the UI before creating the next aggregate goal."
1861
1892
  : null;
1862
- const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState);
1893
+ const autopilotPromptActivationNote = buildAutopilotPromptActivationNote(skillState, { cwd });
1863
1894
  const combinedTransitionMessage = (() => {
1864
1895
  if (!skillState?.transition_message) return null;
1865
1896
  if (matches.length <= 1 || activeSkills.length <= 1) return skillState.transition_message;
@@ -2464,8 +2495,11 @@ function readPayloadTurnId(payload: CodexHookPayload): string {
2464
2495
  async function resolveInternalSessionIdForPayload(
2465
2496
  cwd: string,
2466
2497
  payloadSessionId: string,
2498
+ stateDir?: string,
2467
2499
  ): Promise<string> {
2468
- const currentSession = await readUsableSessionState(cwd);
2500
+ const currentSession = stateDir
2501
+ ? await readUsableSessionStateFromStateDir(cwd, stateDir)
2502
+ : await readUsableSessionState(cwd);
2469
2503
  const canonicalSessionId = safeString(currentSession?.session_id).trim();
2470
2504
  if (!canonicalSessionId) return payloadSessionId;
2471
2505
 
@@ -2476,6 +2510,22 @@ async function resolveInternalSessionIdForPayload(
2476
2510
  return payloadSessionId;
2477
2511
  }
2478
2512
 
2513
+ async function readUsableSessionStateFromStateDir(
2514
+ cwd: string,
2515
+ stateDir: string,
2516
+ ): Promise<SessionState | null> {
2517
+ const sessionPath = join(stateDir, "session.json");
2518
+ if (!existsSync(sessionPath)) return null;
2519
+
2520
+ try {
2521
+ const content = await readFile(sessionPath, "utf-8");
2522
+ const state = JSON.parse(content) as SessionState;
2523
+ return isSessionStateUsable(state, cwd) ? state : null;
2524
+ } catch {
2525
+ return null;
2526
+ }
2527
+ }
2528
+
2479
2529
  async function readStopSessionPinnedState(
2480
2530
  fileName: string,
2481
2531
  cwd: string,
@@ -2514,7 +2564,8 @@ const PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES = new Set([
2514
2564
  const DEEP_INTERVIEW_IMPLEMENTATION_TOOL_NAMES = PLANNING_MODE_IMPLEMENTATION_TOOL_NAMES;
2515
2565
 
2516
2566
  const RALPLAN_EXECUTION_HANDOFF_SKILLS = new Set([
2517
- "autopilot",
2567
+ // Autopilot is intentionally excluded: it supervises planning phases such as
2568
+ // ralplan/replan and is not by itself an execution authorization.
2518
2569
  "autoresearch",
2519
2570
  "ralph",
2520
2571
  "team",
@@ -2546,7 +2597,7 @@ function isActiveAutopilotRalplanPhase(state: Record<string, unknown> | null): b
2546
2597
  const mode = safeString(state.mode).trim();
2547
2598
  if (mode && mode !== "autopilot") return false;
2548
2599
  const phase = safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase();
2549
- return phase === "ralplan";
2600
+ return phase === "ralplan" || phase === "replan" || phase === "autopilot:replan";
2550
2601
  }
2551
2602
 
2552
2603
  function hasExplicitExecutionHandoffSkill(
@@ -2649,7 +2700,7 @@ async function readActiveDeepInterviewStateForPreToolUse(
2649
2700
  const canonicalState = sessionId
2650
2701
  ? await readVisibleSkillActiveStateForStateDir(stateDir, sessionId)
2651
2702
  : await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
2652
- if (!canonicalState) return modeState;
2703
+ if (!canonicalState) return null;
2653
2704
  const hasActiveDeepInterviewSkill = listActiveSkills(canonicalState).some((entry) => (
2654
2705
  entry.skill === "deep-interview"
2655
2706
  && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
@@ -2671,7 +2722,7 @@ async function readActiveRalplanStateForPreToolUse(
2671
2722
  : await readSkillActiveState(join(stateDir, SKILL_ACTIVE_STATE_FILE));
2672
2723
  if (isActiveRalplanPhase(modeState) && modeState && modeStateMatchesSkillStopContext(modeState, cwd, sessionId)) {
2673
2724
  if (hasExplicitExecutionHandoffSkill(canonicalState, sessionId, threadId)) return null;
2674
- if (!canonicalState) return modeState;
2725
+ if (!canonicalState) return null;
2675
2726
  const hasActiveRalplanSkill = listActiveSkills(canonicalState).some((entry) => (
2676
2727
  entry.skill === "ralplan"
2677
2728
  && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
@@ -2686,7 +2737,7 @@ async function readActiveRalplanStateForPreToolUse(
2686
2737
  if (!modeStateMatchesSkillStopContext(autopilotState, cwd, sessionId)) return null;
2687
2738
  const terminalAutopilotRunState = await readCanonicalTerminalRunStateForStop(cwd, sessionId, "autopilot");
2688
2739
  if (terminalAutopilotRunState) return null;
2689
- if (!canonicalState) return autopilotState;
2740
+ if (!canonicalState) return null;
2690
2741
  const hasActiveAutopilotSkill = listActiveSkills(canonicalState).some((entry) => (
2691
2742
  entry.skill === "autopilot"
2692
2743
  && matchesSkillStopContext(entry, canonicalState, sessionId, threadId)
@@ -2705,8 +2756,9 @@ async function buildRalplanPreToolUseBoundaryOutput(
2705
2756
  payload: CodexHookPayload,
2706
2757
  cwd: string,
2707
2758
  stateDir: string,
2759
+ resolvedSessionId?: string,
2708
2760
  ): Promise<Record<string, unknown> | null> {
2709
- const sessionId = readPayloadSessionId(payload);
2761
+ const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
2710
2762
  const threadId = readPayloadThreadId(payload);
2711
2763
  const activeState = await readActiveRalplanStateForPreToolUse(cwd, stateDir, sessionId, threadId);
2712
2764
  if (!activeState) return null;
@@ -2726,13 +2778,21 @@ async function buildRalplanPreToolUseBoundaryOutput(
2726
2778
  if (!blocked) return null;
2727
2779
 
2728
2780
  const phase = formatPhase(activeState.current_phase ?? activeState.currentPhase, "planning");
2781
+ const activeMode = safeString(activeState.mode).trim().toLowerCase();
2782
+ const planningModeLabel = activeMode === "autopilot" ? "Autopilot planning" : "Ralplan";
2783
+ const planningModeDescription = activeMode === "autopilot"
2784
+ ? "Autopilot is supervising a planning phase"
2785
+ : "Ralplan is consensus-planning mode";
2729
2786
  return {
2730
2787
  decision: "block",
2731
- reason: `Ralplan is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated.`,
2788
+ reason: `${planningModeLabel} is active (phase: ${phase}); implementation/write tools are blocked until an explicit execution handoff workflow is activated.`,
2732
2789
  hookSpecificOutput: {
2733
2790
  hookEventName: "PreToolUse",
2734
2791
  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.",
2792
+ `${planningModeDescription}. `
2793
+ + "Write only planning artifacts under `.omx/context/`, `.omx/plans/`, `.omx/specs/`, or required `.omx/state/` files. "
2794
+ + "Do not edit implementation files or run implementation-focused writes from planning phases. "
2795
+ + `To execute, first process an explicit handoff such as ${formatExecutionHandoffList(cwd)}, which must emit terminal planning state before implementation begins.`,
2736
2796
  },
2737
2797
  };
2738
2798
  }
@@ -2741,8 +2801,9 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
2741
2801
  payload: CodexHookPayload,
2742
2802
  cwd: string,
2743
2803
  stateDir: string,
2804
+ resolvedSessionId?: string,
2744
2805
  ): Promise<Record<string, unknown> | null> {
2745
- const sessionId = readPayloadSessionId(payload);
2806
+ const sessionId = safeString(resolvedSessionId ?? readPayloadSessionId(payload)).trim();
2746
2807
  const threadId = readPayloadThreadId(payload);
2747
2808
  const activeState = await readActiveDeepInterviewStateForPreToolUse(cwd, stateDir, sessionId, threadId);
2748
2809
  if (!activeState) return null;
@@ -2768,7 +2829,7 @@ async function buildDeepInterviewPreToolUseBoundaryOutput(
2768
2829
  hookSpecificOutput: {
2769
2830
  hookEventName: "PreToolUse",
2770
2831
  additionalContext:
2771
- "Deep-interview is requirements/spec mode. Treat detailed user answers as interview/spec material, not implicit implementation authorization. You may write only deep-interview artifacts under `.omx/context/`, `.omx/interviews/`, `.omx/specs/`, or required `.omx/state/` files. To implement, first ask for or process an explicit transition such as `$ralplan`, `$autopilot`, `$ralph`, `$team`, or `$ultragoal`.",
2832
+ `Deep-interview is requirements/spec mode. Treat detailed user answers as interview/spec material, not implicit implementation authorization. You may write only deep-interview artifacts under \`.omx/context/\`, \`.omx/interviews/\`, \`.omx/specs/\`, or required \`.omx/state/\` files. To implement, first ask for or process an explicit transition such as \`$ralplan\`, \`$autopilot\`, ${formatExecutionHandoffList(cwd)}.`,
2772
2833
  },
2773
2834
  };
2774
2835
  }
@@ -3027,6 +3088,7 @@ async function reconcileStaleRootSkillActiveStateForStop(
3027
3088
  function buildRalplanContinuationStatus(
3028
3089
  blocker: { phase: string; latestPlanPath?: string; planningComplete?: boolean; runOutcome?: string },
3029
3090
  activeSubagentCount: number,
3091
+ cwd: string,
3030
3092
  ): { reason: string; systemMessage: string; stopReasonSuffix: string } {
3031
3093
  const phase = blocker.phase || "planning";
3032
3094
  const artifact = blocker.latestPlanPath
@@ -3062,7 +3124,7 @@ function buildRalplanContinuationStatus(
3062
3124
  }
3063
3125
 
3064
3126
  const completeHint = blocker.planningComplete
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."
3127
+ ? ` 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 ${formatExecutionHandoffList(cwd).replaceAll("`", "")} handoff.`
3066
3128
  : "";
3067
3129
 
3068
3130
  return {
@@ -3282,6 +3344,11 @@ async function maybeBuildOrdinaryStopNoProgressOutput(
3282
3344
  stateDir: string,
3283
3345
  canonicalSessionId?: string,
3284
3346
  ): Promise<Record<string, unknown> | null> {
3347
+ const lastAssistantMessage = safeString(
3348
+ payload.last_assistant_message ?? payload.lastAssistantMessage,
3349
+ ).trim();
3350
+ if (!lastAssistantMessage) return null;
3351
+
3285
3352
  const statePath = join(stateDir, NATIVE_STOP_STATE_FILE);
3286
3353
  const state = await readJsonIfExists(statePath) ?? {};
3287
3354
  const sessions = safeObject(state.sessions);
@@ -3313,9 +3380,6 @@ async function maybeBuildOrdinaryStopNoProgressOutput(
3313
3380
  await mkdir(stateDir, { recursive: true });
3314
3381
  await writeFile(statePath, JSON.stringify({ ...state, sessions }, null, 2));
3315
3382
 
3316
- const stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
3317
- if (!stopHookActive) return null;
3318
-
3319
3383
  const maxRepeats = parseBoundedPositiveInteger(
3320
3384
  process.env.OMX_NATIVE_STOP_NO_PROGRESS_MAX_REPEATS,
3321
3385
  ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS,
@@ -3520,7 +3584,7 @@ async function buildSkillStopOutput(
3520
3584
  const activeSubagentCount = subagentSummary?.activeSubagentThreadIds.length ?? 0;
3521
3585
 
3522
3586
  if (blocker.skill === "ralplan") {
3523
- const status = buildRalplanContinuationStatus(blocker, activeSubagentCount);
3587
+ const status = buildRalplanContinuationStatus(blocker, activeSubagentCount, cwd);
3524
3588
  return {
3525
3589
  decision: "block",
3526
3590
  reason: status.reason,
@@ -3978,7 +4042,9 @@ export async function dispatchCodexNativeHook(
3978
4042
  // Native hooks must use the same authoritative runtime state root as HUD/MCP
3979
4043
  // when boxed/team roots are active; do not bypass it with cwd/.omx/state.
3980
4044
  const stateDir = getBaseStateDir(cwd);
3981
- await mkdir(stateDir, { recursive: true });
4045
+ if (hookEventName !== "Stop") {
4046
+ await mkdir(stateDir, { recursive: true });
4047
+ }
3982
4048
 
3983
4049
  const omxEventName = mapCodexHookEventToOmxEvent(hookEventName);
3984
4050
  let skillState: SkillActiveState | null = null;
@@ -4179,16 +4245,23 @@ export async function dispatchCodexNativeHook(
4179
4245
  triageAdditionalContext = null;
4180
4246
  }
4181
4247
  }
4248
+ const skipHudReconcileForDoctorSmoke = process.env.OMX_NATIVE_HOOK_DOCTOR_SMOKE === "1";
4182
4249
  const skipHudReconcileForTeamWorkerPane = !isSubagentPromptSubmit
4183
4250
  && await isConfirmedTeamWorkerPromptSubmitPane(cwd).catch(() => false);
4184
- if (!skipHudReconcileForTeamWorkerPane) {
4251
+ if (!skipHudReconcileForDoctorSmoke && !skipHudReconcileForTeamWorkerPane) {
4185
4252
  const reconcileHudForPromptSubmitFn = options.reconcileHudForPromptSubmitFn ?? reconcileHudForPromptSubmit;
4186
4253
  const hudSessionId = resolveHudReconcileSessionId(
4187
4254
  currentSessionState,
4188
4255
  canonicalSessionId,
4189
4256
  sessionIdForState,
4190
4257
  );
4191
- await reconcileHudForPromptSubmitFn(cwd, { sessionId: hudSessionId }).catch(() => {});
4258
+ const hudSessionIds = resolveHudReconcileSessionIds(
4259
+ currentSessionState,
4260
+ canonicalSessionId,
4261
+ sessionIdForState,
4262
+ nativeSessionId,
4263
+ );
4264
+ await reconcileHudForPromptSubmitFn(cwd, { sessionId: hudSessionId, sessionIds: hudSessionIds }).catch(() => {});
4192
4265
  }
4193
4266
  }
4194
4267
 
@@ -4248,8 +4321,12 @@ export async function dispatchCodexNativeHook(
4248
4321
  };
4249
4322
  }
4250
4323
  } else if (hookEventName === "PreToolUse") {
4251
- outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir)
4252
- ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir)
4324
+ const payloadSessionId = readPayloadSessionId(payload);
4325
+ const preToolUseSessionId = payloadSessionId
4326
+ ? await resolveInternalSessionIdForPayload(cwd, payloadSessionId, stateDir)
4327
+ : "";
4328
+ outputJson = await buildDeepInterviewPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
4329
+ ?? await buildRalplanPreToolUseBoundaryOutput(payload, cwd, stateDir, preToolUseSessionId)
4253
4330
  ?? buildNativePreToolUseOutput(payload);
4254
4331
  } else if (hookEventName === "PostToolUse") {
4255
4332
  if (detectMcpTransportFailure(payload)) {
@@ -89,10 +89,8 @@ OMX_TEAM_WORKER_LAUNCH_ARGS="${OMX_TEAM_WORKER_LAUNCH_ARGS:--c model_reasoning_e
89
89
  TEAM_STARTED=0
90
90
  cleanup() {
91
91
  if ((TEAM_STARTED == 1)); then
92
- echo "[cleanup] shutting down team: $TEAM_NAME"
93
- omx team shutdown "$TEAM_NAME" >/dev/null 2>&1 || true
94
- echo "[cleanup] cleaning state for team: $TEAM_NAME"
95
- omx team api cleanup --input "{\"team_name\":\"$TEAM_NAME\"}" --json >/dev/null 2>&1 || true
92
+ echo "[cleanup] force-cleaning demo team: $TEAM_NAME"
93
+ omx team api cleanup --input "{\"team_name\":\"$TEAM_NAME\",\"force\":true,\"confirm_issues\":true}" --json >/dev/null 2>&1 || true
96
94
  fi
97
95
  }
98
96
  trap cleanup EXIT
@@ -106,7 +104,13 @@ echo "OMX_TEAM_WORKER_CLI_MAP=$OMX_TEAM_WORKER_CLI_MAP"
106
104
  echo "OMX_TEAM_WORKER_LAUNCH_ARGS=$OMX_TEAM_WORKER_LAUNCH_ARGS"
107
105
 
108
106
  echo "[1/8] start team (${WORKER_COUNT} mixed workers)"
109
- omx team "${WORKER_COUNT}:executor" "$TEAM_TASK"
107
+ START_OUTPUT="$(omx team "${WORKER_COUNT}:executor" "$TEAM_TASK")"
108
+ echo "$START_OUTPUT"
109
+ ACTUAL_TEAM_NAME="$(echo "$START_OUTPUT" | sed -nE 's/^Team started: ([^[:space:]]+)$/\1/p' | head -n 1)"
110
+ if [[ -n "$ACTUAL_TEAM_NAME" && "$ACTUAL_TEAM_NAME" != "$TEAM_NAME" ]]; then
111
+ echo "TEAM_NAME_RESOLVED=$ACTUAL_TEAM_NAME"
112
+ TEAM_NAME="$ACTUAL_TEAM_NAME"
113
+ fi
110
114
  TEAM_STARTED=1
111
115
 
112
116
  echo "[2/8] status"
@@ -174,8 +178,7 @@ SUMMARY_JSON="$(omx team api get-summary --input "$SUMMARY_INPUT" --json)"
174
178
  echo "$SUMMARY_JSON" | jq -e '.schema_version == "1.0" and .operation == "get-summary" and .ok == true' >/dev/null
175
179
 
176
180
  echo "[8/8] shutdown + cleanup"
177
- omx team shutdown "$TEAM_NAME"
178
- omx team api cleanup --input "{\"team_name\":\"$TEAM_NAME\"}" --json >/dev/null
181
+ omx team api cleanup --input "{\"team_name\":\"$TEAM_NAME\",\"force\":true,\"confirm_issues\":true}" --json >/dev/null
179
182
  TEAM_STARTED=0
180
183
 
181
184
  echo "E2E demo complete."
@@ -9,7 +9,7 @@ if (build.status !== 0) {
9
9
  }
10
10
 
11
11
  const test = spawnSync('node', [
12
- '--test',
12
+ 'dist/scripts/run-test-files.js',
13
13
  'dist/autoresearch/__tests__/contracts.test.js',
14
14
  'dist/autoresearch/__tests__/runtime.test.js',
15
15
  'dist/cli/__tests__/autoresearch.test.js',
@@ -269,7 +269,9 @@ export async function syncSkillStateFromTurn(stateDir, payload) {
269
269
 
270
270
 
271
271
  export async function isDeepInterviewStateActive(stateDir, sessionId) {
272
- const modeState = await readScopedJsonIfExists(stateDir, 'deep-interview-state.json', sessionId, null);
272
+ const modeState = typeof sessionId === 'string' && sessionId.trim()
273
+ ? await readScopedJsonIfExists(stateDir, 'deep-interview-state.json', sessionId, null)
274
+ : await readJsonIfExists(join(stateDir, 'deep-interview-state.json'), null);
273
275
  return Boolean(modeState && modeState.active === true);
274
276
  }
275
277
 
@@ -1,8 +1,7 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'fs/promises';
3
- import { dirname, join, resolve } from 'path';
3
+ import { dirname, join } from 'path';
4
4
  import { captureTmuxPaneFromEnv } from '../../state/mode-state-context.js';
5
- import { isSessionStateUsable } from '../../hooks/session.js';
6
5
  import { resolveCodexPane } from '../tmux-hook-engine.js';
7
6
  import { safeString } from './utils.js';
8
7
 
@@ -222,15 +221,10 @@ function readSessionIdFromEnvironment(env: NodeJS.ProcessEnv = process.env): str
222
221
 
223
222
  async function readCurrentOmxSessionId(stateDir: string, env: NodeJS.ProcessEnv = process.env): Promise<string> {
224
223
  const envSessionId = readSessionIdFromEnvironment(env);
225
- if (envSessionId) {
226
- const envScopedDir = join(stateDir, 'sessions', envSessionId);
227
- if (existsSync(envScopedDir)) return envSessionId;
228
- }
224
+ if (envSessionId) return envSessionId;
229
225
 
230
- const cwd = resolve(stateDir, '..', '..');
231
226
  const session = await readJson(join(stateDir, 'session.json'));
232
227
  if (!session || typeof session !== 'object') return '';
233
- if (!isSessionStateUsable(session as any, cwd)) return '';
234
228
  const sessionId = safeString(session?.session_id).trim();
235
229
  return SESSION_ID_PATTERN.test(sessionId) ? sessionId : '';
236
230
  }
@@ -3,12 +3,10 @@
3
3
  */
4
4
 
5
5
  import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
6
- import { dirname, join, resolve } from 'path';
7
- import { existsSync } from 'fs';
8
- import { isSessionStateUsable } from '../../hooks/session.js';
6
+ import { dirname, join } from 'path';
7
+ import { validateSessionId } from '../../mcp/state-paths.js';
9
8
  import { asNumber, safeString } from './utils.js';
10
9
 
11
- const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
12
10
 
13
11
  export { readdir };
14
12
 
@@ -25,46 +23,90 @@ function isSafeStateFileName(fileName: string): boolean {
25
23
  && !fileName.includes('\\');
26
24
  }
27
25
 
28
- function readSessionIdFromEnvironment(env: NodeJS.ProcessEnv = process.env): string | undefined {
29
- const candidates = [env.OMX_SESSION_ID, env.CODEX_SESSION_ID, env.SESSION_ID];
30
- for (const candidate of candidates) {
31
- const sessionId = safeString(candidate).trim();
32
- if (!SESSION_ID_PATTERN.test(sessionId)) continue;
33
- return sessionId;
26
+ interface SessionMetadata {
27
+ sessionId?: string;
28
+ nativeAliases: string[];
29
+ }
30
+
31
+ async function readSessionMetadataFromBaseStateDir(baseStateDir: string): Promise<SessionMetadata> {
32
+ const session = await readJsonIfExists(join(baseStateDir, 'session.json'), null);
33
+ let sessionId: string | undefined;
34
+ try {
35
+ sessionId = validateSessionId(session?.session_id);
36
+ } catch {
37
+ sessionId = undefined;
34
38
  }
35
- return undefined;
39
+ const nativeAliases = [
40
+ session?.native_session_id,
41
+ session?.codex_session_id,
42
+ session?.previous_native_session_id,
43
+ ]
44
+ .map((value) => safeString(value).trim())
45
+ .filter(Boolean);
46
+ return { sessionId, nativeAliases: [...new Set(nativeAliases)] };
36
47
  }
37
48
 
38
- export async function readCurrentSessionId(baseStateDir: string): Promise<string | undefined> {
39
- const envSessionId = readSessionIdFromEnvironment();
40
- if (envSessionId) {
41
- const envScopedDir = join(baseStateDir, 'sessions', envSessionId);
42
- if (existsSync(envScopedDir)) return envSessionId;
49
+ function readSessionIdFromEnvironment(): string | undefined {
50
+ for (const candidate of [process.env.OMX_SESSION_ID, process.env.CODEX_SESSION_ID, process.env.SESSION_ID]) {
51
+ if (typeof candidate !== 'string') continue;
52
+ const trimmed = candidate.trim();
53
+ if (!trimmed) continue;
54
+ try {
55
+ const sessionId = validateSessionId(trimmed);
56
+ if (sessionId) return sessionId;
57
+ } catch {
58
+ continue;
59
+ }
43
60
  }
61
+ return undefined;
62
+ }
44
63
 
45
- const cwd = resolve(baseStateDir, '..', '..');
46
- const session = await readJsonIfExists(join(baseStateDir, 'session.json'), null);
47
- if (!session || typeof session !== 'object') return undefined;
48
- if (!isSessionStateUsable(session, cwd)) return undefined;
49
- const sessionId = safeString(session?.session_id);
50
- return SESSION_ID_PATTERN.test(sessionId) ? sessionId : undefined;
64
+ function resolveCanonicalSessionId(candidate: string | undefined, metadata: SessionMetadata): string | undefined {
65
+ if (!candidate) return undefined;
66
+ return metadata.sessionId && metadata.nativeAliases.includes(candidate)
67
+ ? metadata.sessionId
68
+ : candidate;
51
69
  }
52
70
 
53
- export async function resolveScopedStateDir(
71
+ async function resolveBaseScopedStateDir(
54
72
  baseStateDir: string,
55
73
  explicitSessionId?: string,
56
74
  ): Promise<string> {
57
- const normalizedExplicit = safeString(explicitSessionId).trim();
58
- if (SESSION_ID_PATTERN.test(normalizedExplicit)) {
59
- return join(baseStateDir, 'sessions', normalizedExplicit);
60
- }
75
+ const normalizedExplicit = typeof explicitSessionId === 'string' && explicitSessionId.trim()
76
+ ? explicitSessionId.trim()
77
+ : undefined;
78
+ const validatedExplicit = validateSessionId(normalizedExplicit);
79
+ const metadata = await readSessionMetadataFromBaseStateDir(baseStateDir);
80
+ const sessionId = resolveCanonicalSessionId(validatedExplicit, metadata)
81
+ ?? resolveCanonicalSessionId(readSessionIdFromEnvironment(), metadata)
82
+ ?? metadata.sessionId;
83
+ return sessionId ? join(baseStateDir, 'sessions', sessionId) : baseStateDir;
84
+ }
61
85
 
62
- const currentSessionId = await readCurrentSessionId(baseStateDir);
63
- if (currentSessionId) {
64
- return join(baseStateDir, 'sessions', currentSessionId);
65
- }
86
+ async function resolveBaseScopedStateDirs(
87
+ baseStateDir: string,
88
+ explicitSessionId?: string,
89
+ options: { includeRootFallback?: boolean } = {},
90
+ ): Promise<string[]> {
91
+ const scopedDir = await resolveBaseScopedStateDir(baseStateDir, explicitSessionId);
92
+ return options.includeRootFallback === true && scopedDir !== baseStateDir
93
+ ? [scopedDir, baseStateDir]
94
+ : [scopedDir];
95
+ }
96
+
97
+
98
+
99
+
100
+ export async function readCurrentSessionId(baseStateDir: string): Promise<string | undefined> {
101
+ const metadata = await readSessionMetadataFromBaseStateDir(baseStateDir);
102
+ return resolveCanonicalSessionId(readSessionIdFromEnvironment(), metadata) ?? metadata.sessionId;
103
+ }
66
104
 
67
- return baseStateDir;
105
+ export async function resolveScopedStateDir(
106
+ baseStateDir: string,
107
+ explicitSessionId?: string,
108
+ ): Promise<string> {
109
+ return resolveBaseScopedStateDir(baseStateDir, explicitSessionId);
68
110
  }
69
111
 
70
112
  export async function getScopedStateDirsForCurrentSession(
@@ -72,11 +114,7 @@ export async function getScopedStateDirsForCurrentSession(
72
114
  explicitSessionId?: string,
73
115
  options: { includeRootFallback?: boolean } = {},
74
116
  ): Promise<string[]> {
75
- const scopedDir = await resolveScopedStateDir(baseStateDir, explicitSessionId);
76
- if (scopedDir === baseStateDir || options.includeRootFallback !== true) {
77
- return [scopedDir];
78
- }
79
- return [scopedDir, baseStateDir];
117
+ return resolveBaseScopedStateDirs(baseStateDir, explicitSessionId, options);
80
118
  }
81
119
 
82
120
  export async function getScopedStatePath(
@@ -25,6 +25,7 @@ import { writeTeamLeaderAttention } from '../../team/state.js';
25
25
  import { readLatestTeamProgressEvidenceMs } from '../../team/progress-evidence.js';
26
26
  import { validateSessionId } from '../../mcp/state-paths.js';
27
27
  import { TEAM_NAME_SAFE_PATTERN } from '../../team/contracts.js';
28
+ import { isDeepInterviewStateActive } from './auto-nudge.js';
28
29
  const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
29
30
  const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
30
31
  const LEADER_PANE_SAME_CLASSIFIED_STATE_SUPPRESSED_REASON = 'pane_already_shows_same_classified_state';
@@ -571,6 +572,12 @@ export async function maybeNudgeTeamLeader({
571
572
 
572
573
  const candidateTeamNames = new Set();
573
574
  const currentSessionId = await resolveCurrentSessionId(stateDir);
575
+ const deepInterviewActive = currentSessionId
576
+ ? await isDeepInterviewStateActive(stateDir, currentSessionId).catch(() => false)
577
+ : await isDeepInterviewStateActive(stateDir, undefined).catch(() => false);
578
+ if (deepInterviewActive) {
579
+ return;
580
+ }
574
581
  try {
575
582
  const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir);
576
583
  const candidateStateDirs = [...new Set([...scopedDirs, stateDir])];