oh-my-codex 0.18.3 → 0.18.5

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 (166) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +1 -0
  4. package/dist/cli/__tests__/doctor-warning-copy.test.js +61 -3
  5. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  6. package/dist/cli/__tests__/explore.test.js +8 -7
  7. package/dist/cli/__tests__/explore.test.js.map +1 -1
  8. package/dist/cli/__tests__/index.test.js +63 -5
  9. package/dist/cli/__tests__/index.test.js.map +1 -1
  10. package/dist/cli/__tests__/question.test.js +81 -0
  11. package/dist/cli/__tests__/question.test.js.map +1 -1
  12. package/dist/cli/__tests__/setup-install-mode.test.js +56 -17
  13. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  14. package/dist/cli/__tests__/setup-scope.test.js +1 -1
  15. package/dist/cli/__tests__/sparkshell-cli.test.js +2 -2
  16. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  17. package/dist/cli/__tests__/ultragoal.test.js +60 -1
  18. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  19. package/dist/cli/doctor.d.ts.map +1 -1
  20. package/dist/cli/doctor.js +111 -14
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/explore.d.ts +1 -0
  23. package/dist/cli/explore.d.ts.map +1 -1
  24. package/dist/cli/explore.js +6 -0
  25. package/dist/cli/explore.js.map +1 -1
  26. package/dist/cli/index.d.ts +1 -1
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +11 -5
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/question.d.ts.map +1 -1
  31. package/dist/cli/question.js +31 -1
  32. package/dist/cli/question.js.map +1 -1
  33. package/dist/cli/setup.d.ts.map +1 -1
  34. package/dist/cli/setup.js +18 -54
  35. package/dist/cli/setup.js.map +1 -1
  36. package/dist/cli/ultragoal.d.ts.map +1 -1
  37. package/dist/cli/ultragoal.js +18 -4
  38. package/dist/cli/ultragoal.js.map +1 -1
  39. package/dist/config/__tests__/generator-idempotent.test.js +5 -5
  40. package/dist/config/generator.d.ts +8 -2
  41. package/dist/config/generator.d.ts.map +1 -1
  42. package/dist/config/generator.js +48 -4
  43. package/dist/config/generator.js.map +1 -1
  44. package/dist/hooks/__tests__/agents-overlay.test.js +9 -9
  45. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  46. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -1
  47. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  48. package/dist/hooks/__tests__/code-review-skill-contract.test.js +11 -2
  49. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  50. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -0
  51. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  52. package/dist/hooks/__tests__/explore-routing.test.js +10 -13
  53. package/dist/hooks/__tests__/explore-routing.test.js.map +1 -1
  54. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +13 -15
  55. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  56. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +33 -0
  57. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  58. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +60 -0
  59. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  60. package/dist/hooks/explore-routing.d.ts.map +1 -1
  61. package/dist/hooks/explore-routing.js +8 -14
  62. package/dist/hooks/explore-routing.js.map +1 -1
  63. package/dist/hud/__tests__/hud-tmux-injection.test.js +15 -10
  64. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  65. package/dist/hud/__tests__/index.test.js +125 -1
  66. package/dist/hud/__tests__/index.test.js.map +1 -1
  67. package/dist/hud/__tests__/reconcile.test.js +32 -8
  68. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  69. package/dist/hud/__tests__/render.test.js +160 -4
  70. package/dist/hud/__tests__/render.test.js.map +1 -1
  71. package/dist/hud/__tests__/state.test.js +120 -0
  72. package/dist/hud/__tests__/state.test.js.map +1 -1
  73. package/dist/hud/__tests__/tmux.test.js +32 -1
  74. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  75. package/dist/hud/constants.d.ts +2 -2
  76. package/dist/hud/constants.js +2 -2
  77. package/dist/hud/index.d.ts +1 -1
  78. package/dist/hud/index.d.ts.map +1 -1
  79. package/dist/hud/index.js +27 -3
  80. package/dist/hud/index.js.map +1 -1
  81. package/dist/hud/reconcile.d.ts.map +1 -1
  82. package/dist/hud/reconcile.js +15 -0
  83. package/dist/hud/reconcile.js.map +1 -1
  84. package/dist/hud/render.d.ts.map +1 -1
  85. package/dist/hud/render.js +42 -20
  86. package/dist/hud/render.js.map +1 -1
  87. package/dist/hud/state.d.ts.map +1 -1
  88. package/dist/hud/state.js +18 -0
  89. package/dist/hud/state.js.map +1 -1
  90. package/dist/hud/tmux.d.ts +1 -0
  91. package/dist/hud/tmux.d.ts.map +1 -1
  92. package/dist/hud/tmux.js +8 -0
  93. package/dist/hud/tmux.js.map +1 -1
  94. package/dist/hud/types.d.ts +2 -0
  95. package/dist/hud/types.d.ts.map +1 -1
  96. package/dist/hud/types.js.map +1 -1
  97. package/dist/question/__tests__/deep-interview.test.js +80 -7
  98. package/dist/question/__tests__/deep-interview.test.js.map +1 -1
  99. package/dist/question/__tests__/policy.test.js +115 -9
  100. package/dist/question/__tests__/policy.test.js.map +1 -1
  101. package/dist/question/autopilot-wait.d.ts +11 -0
  102. package/dist/question/autopilot-wait.d.ts.map +1 -0
  103. package/dist/question/autopilot-wait.js +140 -0
  104. package/dist/question/autopilot-wait.js.map +1 -0
  105. package/dist/question/deep-interview.d.ts.map +1 -1
  106. package/dist/question/deep-interview.js +4 -0
  107. package/dist/question/deep-interview.js.map +1 -1
  108. package/dist/question/policy.d.ts +1 -0
  109. package/dist/question/policy.d.ts.map +1 -1
  110. package/dist/question/policy.js +21 -0
  111. package/dist/question/policy.js.map +1 -1
  112. package/dist/scripts/__tests__/codex-native-hook.test.js +335 -3
  113. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  114. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  115. package/dist/scripts/codex-native-hook.js +45 -3
  116. package/dist/scripts/codex-native-hook.js.map +1 -1
  117. package/dist/scripts/notify-hook.js +13 -0
  118. package/dist/scripts/notify-hook.js.map +1 -1
  119. package/dist/subagents/__tests__/tracker.test.js +69 -0
  120. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  121. package/dist/subagents/tracker.d.ts +5 -0
  122. package/dist/subagents/tracker.d.ts.map +1 -1
  123. package/dist/subagents/tracker.js +16 -0
  124. package/dist/subagents/tracker.js.map +1 -1
  125. package/dist/team/__tests__/runtime.test.js +1 -0
  126. package/dist/team/__tests__/runtime.test.js.map +1 -1
  127. package/dist/team/__tests__/tmux-session.test.js +3 -3
  128. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  129. package/dist/team/tmux-session.d.ts.map +1 -1
  130. package/dist/team/tmux-session.js +9 -4
  131. package/dist/team/tmux-session.js.map +1 -1
  132. package/dist/ultragoal/__tests__/artifacts.test.js +208 -1
  133. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  134. package/dist/ultragoal/__tests__/docs-contract.test.js +5 -0
  135. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  136. package/dist/ultragoal/artifacts.d.ts +11 -0
  137. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  138. package/dist/ultragoal/artifacts.js +157 -12
  139. package/dist/ultragoal/artifacts.js.map +1 -1
  140. package/package.json +1 -1
  141. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  142. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +2 -2
  143. package/plugins/oh-my-codex/skills/code-review/SKILL.md +8 -4
  144. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -1
  145. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +4 -4
  146. package/plugins/oh-my-codex/skills/plan/SKILL.md +5 -5
  147. package/plugins/oh-my-codex/skills/ralph/SKILL.md +1 -1
  148. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -5
  149. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +10 -2
  150. package/prompts/executor.md +1 -1
  151. package/prompts/explore-harness.md +2 -2
  152. package/prompts/explore.md +1 -1
  153. package/prompts/planner.md +1 -1
  154. package/prompts/sisyphus-lite.md +1 -1
  155. package/skills/autopilot/SKILL.md +2 -2
  156. package/skills/code-review/SKILL.md +8 -4
  157. package/skills/deep-interview/SKILL.md +1 -1
  158. package/skills/omx-setup/SKILL.md +4 -4
  159. package/skills/plan/SKILL.md +5 -5
  160. package/skills/ralph/SKILL.md +1 -1
  161. package/skills/ralplan/SKILL.md +5 -5
  162. package/skills/ultragoal/SKILL.md +10 -2
  163. package/src/scripts/__tests__/codex-native-hook.test.ts +372 -3
  164. package/src/scripts/codex-native-hook.ts +50 -2
  165. package/src/scripts/notify-hook.ts +15 -0
  166. package/templates/AGENTS.md +3 -3
@@ -24,6 +24,7 @@ import {
24
24
  import { writeSessionStart } from "../../hooks/session.js";
25
25
  import { resetTriageConfigCache } from "../../hooks/triage-config.js";
26
26
  import { executeStateOperation } from "../../state/operations.js";
27
+ import { HUD_TMUX_HEIGHT_LINES } from "../../hud/constants.js";
27
28
  import { OMX_TMUX_HUD_OWNER_ENV } from "../../hud/reconcile.js";
28
29
  import { OMX_TMUX_HUD_LEADER_PANE_ENV } from "../../hud/tmux.js";
29
30
  import { readAllState } from "../../hud/state.js";
@@ -66,6 +67,26 @@ async function writeJson(path: string, value: unknown): Promise<void> {
66
67
  await writeFile(path, JSON.stringify(value, null, 2));
67
68
  }
68
69
 
70
+ async function setTeamPaneIds(
71
+ cwd: string,
72
+ teamName: string,
73
+ paneIds: { leaderPaneId: string; workerPaneIds: Record<string, string> },
74
+ ): Promise<void> {
75
+ for (const fileName of ["config.json", "manifest.v2.json"]) {
76
+ const filePath = join(cwd, ".omx", "state", "team", teamName, fileName);
77
+ const parsed = JSON.parse(await readFile(filePath, "utf-8")) as {
78
+ leader_pane_id?: string | null;
79
+ workers?: Array<{ name?: string; pane_id?: string | null }>;
80
+ };
81
+ parsed.leader_pane_id = paneIds.leaderPaneId;
82
+ parsed.workers = (parsed.workers ?? []).map((worker) => ({
83
+ ...worker,
84
+ pane_id: worker.name ? paneIds.workerPaneIds[worker.name] ?? worker.pane_id ?? null : worker.pane_id ?? null,
85
+ }));
86
+ await writeJson(filePath, parsed);
87
+ }
88
+ }
89
+
69
90
  async function withIsolatedHome<T>(prefix: string, run: (homeDir: string) => Promise<T>): Promise<T> {
70
91
  const homeDir = await mkdtemp(join(tmpdir(), `omx-native-hook-home-${prefix}-`));
71
92
  const previousHome = process.env.HOME;
@@ -243,6 +264,7 @@ const DEFAULT_AUTO_NUDGE_RESPONSE =
243
264
 
244
265
  const TEAM_ENV_KEYS = [
245
266
  "OMX_TEAM_WORKER",
267
+ "OMX_TEAM_INTERNAL_WORKER",
246
268
  "OMX_TEAM_STATE_ROOT",
247
269
  "OMX_TEAM_LEADER_CWD",
248
270
  "OMX_SESSION_ID",
@@ -2362,6 +2384,38 @@ standardMaxRounds = 15
2362
2384
  }
2363
2385
  });
2364
2386
 
2387
+ it("does not repeat ultragoal Stop recovery after a safe completed-aggregate microgoal blocker is recorded", async () => {
2388
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-aggregate-blocked-stop-"));
2389
+ try {
2390
+ await writeJson(join(cwd, ".omx", "ultragoal", "goals.json"), {
2391
+ version: 1,
2392
+ codexGoalMode: "aggregate",
2393
+ activeGoalId: "G001-demo",
2394
+ goals: [{
2395
+ id: "G001-demo",
2396
+ status: "in_progress",
2397
+ objective: "Demo goal",
2398
+ failureReason: "aggregate Codex goal already complete and unreconcilable while repo-native .omx/ultragoal/goals.json still has an in-progress microgoal; stop the recovery loop",
2399
+ }],
2400
+ });
2401
+
2402
+ const result = await dispatchCodexNativeHook({
2403
+ hook_event_name: "Stop",
2404
+ cwd,
2405
+ session_id: "sess-ultragoal-aggregate-blocked-stop",
2406
+ thread_id: "thread-ultragoal-aggregate-blocked-stop",
2407
+ stop_hook_active: true,
2408
+ last_assistant_message: "Goal complete.",
2409
+ }, { cwd });
2410
+
2411
+ assert.notEqual(result.outputJson?.decision, "block");
2412
+ assert.notEqual(result.outputJson?.stopReason, "ultragoal_codex_goal_snapshot_required");
2413
+ assert.doesNotMatch(JSON.stringify(result.outputJson), /omx ultragoal checkpoint --goal-id G001-demo --status complete/);
2414
+ } finally {
2415
+ await rm(cwd, { recursive: true, force: true });
2416
+ }
2417
+ });
2418
+
2365
2419
 
2366
2420
  it("does not block ultragoal Stop after task-scoped reconciliation finishes exploded bookkeeping", async () => {
2367
2421
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-ultragoal-reconciled-stop-"));
@@ -3852,6 +3906,198 @@ export async function onHookEvent(event) {
3852
3906
  }
3853
3907
  });
3854
3908
 
3909
+ it("skips prompt-submit HUD reconciliation for confirmed team worker panes", async () => {
3910
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-skip-"));
3911
+ try {
3912
+ const teamName = "hud-worker-skip";
3913
+ await initTeamState(teamName, "skip worker HUD reconcile", "executor", 1, cwd);
3914
+ await setTeamPaneIds(cwd, teamName, {
3915
+ leaderPaneId: "%42",
3916
+ workerPaneIds: { "worker-1": "%10" },
3917
+ });
3918
+ process.env.TMUX = "1";
3919
+ process.env.TMUX_PANE = "%10";
3920
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3921
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3922
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3923
+
3924
+ let reconcileCalls = 0;
3925
+ const result = await dispatchCodexNativeHook(
3926
+ {
3927
+ hook_event_name: "UserPromptSubmit",
3928
+ cwd,
3929
+ session_id: "sess-hud-team-worker",
3930
+ prompt: "$ralplan prepare plan",
3931
+ },
3932
+ {
3933
+ cwd,
3934
+ reconcileHudForPromptSubmitFn: async () => {
3935
+ reconcileCalls += 1;
3936
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3937
+ },
3938
+ },
3939
+ );
3940
+
3941
+ assert.equal(result.omxEventName, "keyword-detector");
3942
+ assert.equal(reconcileCalls, 0);
3943
+ } finally {
3944
+ await rm(cwd, { recursive: true, force: true });
3945
+ }
3946
+ });
3947
+
3948
+ it("preserves prompt-submit HUD reconciliation for team leader panes", async () => {
3949
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-leader-preserve-"));
3950
+ try {
3951
+ const teamName = "hud-leader-keep";
3952
+ await initTeamState(teamName, "preserve leader HUD reconcile", "executor", 1, cwd);
3953
+ await setTeamPaneIds(cwd, teamName, {
3954
+ leaderPaneId: "%42",
3955
+ workerPaneIds: { "worker-1": "%10" },
3956
+ });
3957
+ process.env.TMUX = "1";
3958
+ process.env.TMUX_PANE = "%42";
3959
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3960
+
3961
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
3962
+ const result = await dispatchCodexNativeHook(
3963
+ {
3964
+ hook_event_name: "UserPromptSubmit",
3965
+ cwd,
3966
+ session_id: "sess-hud-team-leader",
3967
+ prompt: "$ralplan prepare plan",
3968
+ },
3969
+ {
3970
+ cwd,
3971
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
3972
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
3973
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
3974
+ },
3975
+ },
3976
+ );
3977
+
3978
+ assert.equal(result.omxEventName, "keyword-detector");
3979
+ assert.deepEqual(reconcileCall, { cwd, sessionId: "sess-hud-team-leader" });
3980
+ } finally {
3981
+ await rm(cwd, { recursive: true, force: true });
3982
+ }
3983
+ });
3984
+
3985
+ it("preserves prompt-submit HUD reconciliation when worker pane detection is ambiguous", async () => {
3986
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-team-worker-ambiguous-"));
3987
+ try {
3988
+ const teamName = "hud-worker-ambiguous";
3989
+ await initTeamState(teamName, "fail closed for ambiguous worker HUD reconcile", "executor", 1, cwd);
3990
+ await setTeamPaneIds(cwd, teamName, {
3991
+ leaderPaneId: "%42",
3992
+ workerPaneIds: { "worker-1": "%10" },
3993
+ });
3994
+ process.env.TMUX = "1";
3995
+ process.env.TMUX_PANE = "%99";
3996
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
3997
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
3998
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
3999
+
4000
+ let reconcileCalls = 0;
4001
+ const result = await dispatchCodexNativeHook(
4002
+ {
4003
+ hook_event_name: "UserPromptSubmit",
4004
+ cwd,
4005
+ session_id: "sess-hud-team-worker-ambiguous",
4006
+ prompt: "$ralplan prepare plan",
4007
+ },
4008
+ {
4009
+ cwd,
4010
+ reconcileHudForPromptSubmitFn: async () => {
4011
+ reconcileCalls += 1;
4012
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
4013
+ },
4014
+ },
4015
+ );
4016
+
4017
+ assert.equal(result.omxEventName, "keyword-detector");
4018
+ assert.equal(reconcileCalls, 1);
4019
+ } finally {
4020
+ await rm(cwd, { recursive: true, force: true });
4021
+ }
4022
+ });
4023
+
4024
+ it("preserves prompt-submit HUD reconciliation for native subagents even with worker pane env", async () => {
4025
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-subagent-worker-preserve-"));
4026
+ try {
4027
+ const teamName = "hud-subagent-keep";
4028
+ await initTeamState(teamName, "preserve subagent HUD reconcile", "executor", 1, cwd);
4029
+ await setTeamPaneIds(cwd, teamName, {
4030
+ leaderPaneId: "%42",
4031
+ workerPaneIds: { "worker-1": "%10" },
4032
+ });
4033
+ const stateDir = join(cwd, ".omx", "state");
4034
+ const canonicalSessionId = "sess-subagent-hud-parent";
4035
+ const leaderNativeSessionId = "native-subagent-hud-parent";
4036
+ const childNativeSessionId = "native-subagent-hud-child";
4037
+ const nowIso = new Date().toISOString();
4038
+ await writeJson(join(stateDir, "session.json"), {
4039
+ session_id: canonicalSessionId,
4040
+ native_session_id: leaderNativeSessionId,
4041
+ });
4042
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
4043
+ schemaVersion: 1,
4044
+ sessions: {
4045
+ [canonicalSessionId]: {
4046
+ session_id: canonicalSessionId,
4047
+ leader_thread_id: leaderNativeSessionId,
4048
+ updated_at: nowIso,
4049
+ threads: {
4050
+ [leaderNativeSessionId]: {
4051
+ thread_id: leaderNativeSessionId,
4052
+ kind: "leader",
4053
+ first_seen_at: nowIso,
4054
+ last_seen_at: nowIso,
4055
+ turn_count: 1,
4056
+ },
4057
+ [childNativeSessionId]: {
4058
+ thread_id: childNativeSessionId,
4059
+ kind: "subagent",
4060
+ first_seen_at: nowIso,
4061
+ last_seen_at: nowIso,
4062
+ turn_count: 1,
4063
+ mode: "verifier",
4064
+ },
4065
+ },
4066
+ },
4067
+ },
4068
+ });
4069
+ process.env.TMUX = "1";
4070
+ process.env.TMUX_PANE = "%10";
4071
+ process.env.OMX_TEAM_INTERNAL_WORKER = `${teamName}/worker-1`;
4072
+ process.env.OMX_TEAM_WORKER = `${teamName}/worker-1`;
4073
+ process.env[OMX_TMUX_HUD_OWNER_ENV] = "1";
4074
+
4075
+ let reconcileCall: { cwd: string; sessionId?: string } | null = null;
4076
+ const result = await dispatchCodexNativeHook(
4077
+ {
4078
+ hook_event_name: "UserPromptSubmit",
4079
+ cwd,
4080
+ session_id: childNativeSessionId,
4081
+ thread_id: childNativeSessionId,
4082
+ turn_id: "turn-subagent-hud-child",
4083
+ prompt: "Review the worker patch literally; do not activate $ralplan.",
4084
+ },
4085
+ {
4086
+ cwd,
4087
+ reconcileHudForPromptSubmitFn: async (hookCwd, deps = {}) => {
4088
+ reconcileCall = { cwd: hookCwd, sessionId: deps.sessionId };
4089
+ return { status: "recreated", paneId: "%9", desiredHeight: 3, duplicateCount: 0 };
4090
+ },
4091
+ },
4092
+ );
4093
+
4094
+ assert.equal(result.outputJson, null);
4095
+ assert.deepEqual(reconcileCall, { cwd, sessionId: canonicalSessionId });
4096
+ } finally {
4097
+ await rm(cwd, { recursive: true, force: true });
4098
+ }
4099
+ });
4100
+
3855
4101
  it("runs prompt-submit HUD reconciliation as a best-effort tmux-only side effect", async () => {
3856
4102
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-hud-reconcile-"));
3857
4103
  const originalTmux = process.env.TMUX;
@@ -3908,8 +4154,8 @@ esac
3908
4154
  assert.equal(result.omxEventName, "keyword-detector");
3909
4155
  const tmuxCalls = await readFile(tmuxLog, "utf-8");
3910
4156
  assert.match(tmuxCalls, /list-panes -t %1 -F/);
3911
- assert.match(tmuxCalls, /split-window -v -l 3 -d -t %1 -c/);
3912
- assert.match(tmuxCalls, /resize-pane -t %9 -y 3/);
4157
+ assert.match(tmuxCalls, new RegExp(`split-window -v -l ${HUD_TMUX_HEIGHT_LINES} -d -t %1 -c`));
4158
+ assert.match(tmuxCalls, new RegExp(`resize-pane -t %9 -y ${HUD_TMUX_HEIGHT_LINES}`));
3913
4159
  assert.match(tmuxCalls, /dist\/cli\/omx\.js' hud --watch --preset=focused/);
3914
4160
  assert.doesNotMatch(tmuxCalls, /\/tmp\/codex-host-binary' hud --watch/);
3915
4161
  } finally {
@@ -3990,7 +4236,7 @@ esac
3990
4236
  assert.equal(result.omxEventName, "keyword-detector");
3991
4237
  const tmuxCalls = await readFile(tmuxLog, "utf-8");
3992
4238
  assert.match(tmuxCalls, /list-panes -t %1 -F/);
3993
- assert.match(tmuxCalls, /resize-pane -t %2 -y 3/);
4239
+ assert.match(tmuxCalls, new RegExp(`resize-pane -t %2 -y ${HUD_TMUX_HEIGHT_LINES}`));
3994
4240
  assert.doesNotMatch(tmuxCalls, /split-window/);
3995
4241
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", canonicalSessionId, "ralplan-state.json")), true);
3996
4242
  assert.equal(existsSync(join(cwd, ".omx", "state", "sessions", nativeSessionId, "ralplan-state.json")), false);
@@ -9264,6 +9510,70 @@ exit 0
9264
9510
  }
9265
9511
  });
9266
9512
 
9513
+ it("does not report ralplan subagent waiting when notify-fallback already recorded completion", async () => {
9514
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-skill-subagent-complete-"));
9515
+ try {
9516
+ const stateDir = join(cwd, ".omx", "state");
9517
+ const now = new Date().toISOString();
9518
+ await mkdir(join(stateDir, "sessions", "sess-stop-skill-subagent-complete"), { recursive: true });
9519
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-stop-skill-subagent-complete" });
9520
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "skill-active-state.json"), {
9521
+ active: true,
9522
+ skill: "ralplan",
9523
+ phase: "planning",
9524
+ });
9525
+ await writeJson(join(stateDir, "sessions", "sess-stop-skill-subagent-complete", "ralplan-state.json"), {
9526
+ active: true,
9527
+ current_phase: "planning",
9528
+ });
9529
+ await writeJson(join(stateDir, "subagent-tracking.json"), {
9530
+ schemaVersion: 1,
9531
+ sessions: {
9532
+ "sess-stop-skill-subagent-complete": {
9533
+ session_id: "sess-stop-skill-subagent-complete",
9534
+ leader_thread_id: "leader-1",
9535
+ updated_at: now,
9536
+ threads: {
9537
+ "leader-1": {
9538
+ thread_id: "leader-1",
9539
+ kind: "leader",
9540
+ first_seen_at: now,
9541
+ last_seen_at: now,
9542
+ turn_count: 1,
9543
+ },
9544
+ "sub-1": {
9545
+ thread_id: "sub-1",
9546
+ kind: "subagent",
9547
+ first_seen_at: now,
9548
+ last_seen_at: now,
9549
+ completed_at: now,
9550
+ last_completed_turn_id: "turn-complete-1",
9551
+ completion_source: "notify-fallback-watcher",
9552
+ turn_count: 2,
9553
+ },
9554
+ },
9555
+ },
9556
+ },
9557
+ });
9558
+
9559
+ const result = await dispatchCodexNativeHook(
9560
+ {
9561
+ hook_event_name: "Stop",
9562
+ cwd,
9563
+ session_id: "sess-stop-skill-subagent-complete",
9564
+ },
9565
+ { cwd },
9566
+ );
9567
+
9568
+ assert.equal(result.omxEventName, "stop");
9569
+ assert.equal(result.outputJson?.decision, "block");
9570
+ assert.doesNotMatch(String(result.outputJson?.reason ?? ""), /waiting for 1 active native subagent thread/);
9571
+ assert.equal(result.outputJson?.stopReason, "skill_ralplan_planning_continue_artifact");
9572
+ } finally {
9573
+ await rm(cwd, { recursive: true, force: true });
9574
+ }
9575
+ });
9576
+
9267
9577
  it("does not block on stale root ralplan skill when the explicit session-scoped canonical skill state is absent", async () => {
9268
9578
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-stale-root-skill-"));
9269
9579
  try {
@@ -13833,3 +14143,62 @@ describe("codex native hook triage integration", () => {
13833
14143
  }
13834
14144
  });
13835
14145
  });
14146
+
14147
+ describe('native Stop autopilot deep-interview wait', () => {
14148
+ it('does not force continued execution while autopilot is waiting on a deep-interview omx question', async () => {
14149
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-native-hook-autopilot-question-wait-'));
14150
+ try {
14151
+ const sessionId = 'sess-autopilot-wait';
14152
+ const sessionDir = join(cwd, '.omx', 'state', 'sessions', sessionId);
14153
+ await writeJson(join(cwd, '.omx', 'state', 'session.json'), { session_id: sessionId });
14154
+ await writeJson(join(sessionDir, 'autopilot-state.json'), {
14155
+ mode: 'autopilot',
14156
+ active: true,
14157
+ current_phase: 'waiting-for-user',
14158
+ run_outcome: 'blocked_on_user',
14159
+ lifecycle_outcome: 'askuserQuestion',
14160
+ session_id: sessionId,
14161
+ state: {
14162
+ deep_interview_question: {
14163
+ status: 'waiting_for_user',
14164
+ source: 'omx-question',
14165
+ obligation_id: 'obligation-stop-1',
14166
+ previous_phase: 'deep-interview',
14167
+ },
14168
+ },
14169
+ });
14170
+ await writeJson(join(sessionDir, 'deep-interview-state.json'), {
14171
+ mode: 'deep-interview',
14172
+ active: false,
14173
+ current_phase: 'intent-first',
14174
+ lifecycle_outcome: 'askuserQuestion',
14175
+ run_outcome: 'blocked_on_user',
14176
+ session_id: sessionId,
14177
+ question_enforcement: {
14178
+ obligation_id: 'obligation-stop-1',
14179
+ source: 'omx-question',
14180
+ status: 'pending',
14181
+ lifecycle_outcome: 'askuserQuestion',
14182
+ requested_at: '2026-04-19T00:00:00.000Z',
14183
+ },
14184
+ });
14185
+ await writeJson(join(sessionDir, 'skill-active-state.json'), {
14186
+ active: true,
14187
+ skill: 'autopilot',
14188
+ phase: 'deep-interview',
14189
+ session_id: sessionId,
14190
+ active_skills: [{ skill: 'autopilot', phase: 'deep-interview', active: true, session_id: sessionId }],
14191
+ });
14192
+
14193
+ const result = await dispatchCodexNativeHook({
14194
+ hook_event_name: 'Stop',
14195
+ session_id: sessionId,
14196
+ thread_id: 'thread-autopilot-wait',
14197
+ }, { cwd });
14198
+
14199
+ assert.equal(result.outputJson, null);
14200
+ } finally {
14201
+ await rm(cwd, { recursive: true, force: true });
14202
+ }
14203
+ });
14204
+ });
@@ -30,6 +30,7 @@ import {
30
30
  import {
31
31
  appendTeamEvent,
32
32
  readTeamLeaderAttention,
33
+ readTeamConfig,
33
34
  readTeamManifestV2,
34
35
  readTeamPhase,
35
36
  writeTeamLeaderAttention,
@@ -100,6 +101,7 @@ import {
100
101
  isPendingDeepInterviewQuestionEnforcement,
101
102
  reconcileDeepInterviewQuestionEnforcementFromAnsweredRecords,
102
103
  } from "../question/deep-interview.js";
104
+ import { readAutopilotDeepInterviewQuestionWaitState } from "../question/autopilot-wait.js";
103
105
  import {
104
106
  buildDocumentRefreshAdvisoryOutput,
105
107
  evaluateFinalHandoffDocumentRefresh,
@@ -1873,6 +1875,27 @@ async function resolveTeamStateDirForWorkerContext(
1873
1875
  return null;
1874
1876
  }
1875
1877
 
1878
+ async function isConfirmedTeamWorkerPromptSubmitPane(cwd: string): Promise<boolean> {
1879
+ const workerContext =
1880
+ parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_INTERNAL_WORKER))
1881
+ || parseTeamWorkerEnv(safeString(process.env.OMX_TEAM_WORKER));
1882
+ if (!workerContext) return false;
1883
+
1884
+ const currentPaneId = safeString(process.env.TMUX_PANE).trim();
1885
+ if (!currentPaneId) return false;
1886
+
1887
+ const config = await readTeamConfig(workerContext.teamName, cwd).catch(() => null);
1888
+ if (!config) return false;
1889
+
1890
+ const leaderPaneId = safeString(config.leader_pane_id).trim();
1891
+ if (leaderPaneId && leaderPaneId === currentPaneId) return false;
1892
+
1893
+ const workerPaneId = safeString(
1894
+ config.workers.find((worker) => worker.name === workerContext.workerName)?.pane_id,
1895
+ ).trim();
1896
+ return workerPaneId !== "" && workerPaneId === currentPaneId;
1897
+ }
1898
+
1876
1899
 
1877
1900
  type TeamWorkerStopDecision =
1878
1901
  | {
@@ -2033,6 +2056,9 @@ async function buildModeBasedStopOutput(
2033
2056
  if (await readCanonicalTerminalRunStateForStop(cwd, sessionId, mode)) {
2034
2057
  return null;
2035
2058
  }
2059
+ if (mode === "autopilot" && await readAutopilotDeepInterviewQuestionWaitState(cwd, sessionId)) {
2060
+ return null;
2061
+ }
2036
2062
  const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
2037
2063
  if (!state || !shouldContinueRun(state)) return null;
2038
2064
  const phase = formatPhase(state.current_phase);
@@ -2077,6 +2103,18 @@ function reportsBlockedPerformanceGoalObjectiveMismatch(state: unknown): boolean
2077
2103
  return /objective mismatch/i.test(evidence);
2078
2104
  }
2079
2105
 
2106
+ function reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(goal: Record<string, unknown>): boolean {
2107
+ const evidence = [
2108
+ safeString(goal.failureReason),
2109
+ safeString(goal.blockedReason),
2110
+ safeString(goal.evidence),
2111
+ ].join(" ");
2112
+ return /aggregate codex goal/i.test(evidence)
2113
+ && /\bcomplete(?:d)?\b/i.test(evidence)
2114
+ && /microgoal/i.test(evidence)
2115
+ && /\b(?:unreconcilable|mismatch|loop|already complete|already completed|blocks?)\b/i.test(evidence);
2116
+ }
2117
+
2080
2118
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
2081
2119
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
2082
2120
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -2085,6 +2123,9 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
2085
2123
  const activeUltragoal = aggregateProductComplete
2086
2124
  ? undefined
2087
2125
  : ultragoals.find((goal) => safeString(goal.status) === "in_progress" || safeString(goal.id) === safeString(ultragoal?.activeGoalId));
2126
+ if (activeUltragoal && reportsBlockedUltragoalCompletedAggregateMicrogoalLoop(activeUltragoal)) {
2127
+ return null;
2128
+ }
2088
2129
  if (activeUltragoal) {
2089
2130
  const goalId = safeString(activeUltragoal.id) || "<goal-id>";
2090
2131
  return {
@@ -2847,6 +2888,9 @@ async function buildDeepInterviewQuestionStopOutput(
2847
2888
  threadId: string,
2848
2889
  ): Promise<{ output: Record<string, unknown>; obligationId: string } | null> {
2849
2890
  await reconcileDeepInterviewQuestionEnforcementFromAnsweredRecords(cwd, sessionId);
2891
+ if (await readAutopilotDeepInterviewQuestionWaitState(cwd, sessionId)) {
2892
+ return null;
2893
+ }
2850
2894
  const modeState = await readStopSessionPinnedState("deep-interview-state.json", cwd, sessionId, stateDir);
2851
2895
  if (!modeState) return null;
2852
2896
 
@@ -3878,8 +3922,12 @@ export async function dispatchCodexNativeHook(
3878
3922
  triageAdditionalContext = null;
3879
3923
  }
3880
3924
  }
3881
- const reconcileHudForPromptSubmitFn = options.reconcileHudForPromptSubmitFn ?? reconcileHudForPromptSubmit;
3882
- await reconcileHudForPromptSubmitFn(cwd, { sessionId: canonicalSessionId || sessionIdForState || undefined }).catch(() => {});
3925
+ const skipHudReconcileForTeamWorkerPane = !isSubagentPromptSubmit
3926
+ && await isConfirmedTeamWorkerPromptSubmitPane(cwd).catch(() => false);
3927
+ if (!skipHudReconcileForTeamWorkerPane) {
3928
+ const reconcileHudForPromptSubmitFn = options.reconcileHudForPromptSubmitFn ?? reconcileHudForPromptSubmit;
3929
+ await reconcileHudForPromptSubmitFn(cwd, { sessionId: canonicalSessionId || sessionIdForState || undefined }).catch(() => {});
3930
+ }
3883
3931
  }
3884
3932
 
3885
3933
  if (omxEventName && !skipCanonicalSessionStartContext && !suppressNoisySubagentLifecycleDispatch) {
@@ -272,6 +272,14 @@ function isTurnCompletePayload(payload: Record<string, unknown>): boolean {
272
272
  return type === '' || type === 'agent-turn-complete' || type === 'turn-complete';
273
273
  }
274
274
 
275
+ function isNotifyFallbackTaskCompletePayload(payload: Record<string, unknown>): boolean {
276
+ const source = safeString(payload.source || '').trim();
277
+ if (source !== 'notify-fallback-watcher') return false;
278
+ return normalizeInputMessages(payload).some((message) => (
279
+ message.includes('[notify-fallback] synthesized from rollout task_complete')
280
+ ));
281
+ }
282
+
275
283
  async function main() {
276
284
  const rawPayload = process.argv[process.argv.length - 1];
277
285
  if (!rawPayload || rawPayload.startsWith('-')) {
@@ -294,6 +302,7 @@ async function main() {
294
302
  const inputMessages = normalizeInputMessages(payload);
295
303
  const latestUserInput = safeString(inputMessages.length > 0 ? inputMessages[inputMessages.length - 1] : '');
296
304
  const isTurnComplete = isTurnCompletePayload(payload);
305
+ const isNotifyFallbackTaskComplete = isNotifyFallbackTaskCompletePayload(payload);
297
306
 
298
307
  // Team worker detection via environment variable
299
308
  const teamWorkerEnv = process.env.OMX_TEAM_INTERNAL_WORKER || process.env.OMX_TEAM_WORKER; // e.g., "fix-ts/worker-1"
@@ -358,6 +367,12 @@ async function main() {
358
367
  ...(turnId ? { turnId } : {}),
359
368
  timestamp: new Date().toISOString(),
360
369
  mode: safeString(payload.mode || ''),
370
+ ...(isNotifyFallbackTaskComplete
371
+ ? {
372
+ completed: true,
373
+ completionSource: 'notify-fallback-watcher',
374
+ }
375
+ : {}),
361
376
  });
362
377
  }
363
378
  } catch {
@@ -274,10 +274,10 @@ Verification loop: define the claim and success criteria, run the smallest valid
274
274
  Mode selection: use `$deep-interview` for unclear intent/boundaries; `$ralplan` for consensus on architecture, tradeoffs, or tests; `$team` for approved multi-lane work; `$ralph` for persistent single-owner completion/verification loops; otherwise execute directly in solo mode. Switch modes only when evidence shows the current lane is mismatched or blocked.
275
275
 
276
276
  Command routing:
277
- - When `USE_OMX_EXPLORE_CMD` enables advisory routing, strongly prefer `omx explore` as the default surface for simple read-only repository lookup tasks (files, symbols, patterns, relationships).
278
- - For simple file/symbol lookups, use `omx explore` FIRST before attempting full code analysis.
277
+ - `omx explore` is deprecated and MUST NOT be recommended as the default surface for simple read-only repository lookup tasks. Use normal Codex repository inspection tools/subagents for file, symbol, pattern, relationship, and implementation discovery.
278
+ - `USE_OMX_EXPLORE_CMD` is compatibility-only for legacy callers; it does not make `omx explore` preferred for new work.
279
279
 
280
- Use `omx explore --prompt ...` for simple read-only lookups through the shell-only, allowlisted, read-only path. Use `omx sparkshell` for noisy read-only shell commands, bounded verification, repo-wide listing/search, or explicit `omx sparkshell --tmux-pane` summaries. Treat sparkshell as explicit opt-in. When to use what: keep ambiguous, implementation-heavy, edit-heavy, diagnostics, tests, MCP/web, and complex shell work on the normal path; if `omx explore` or `omx sparkshell` is incomplete, retry narrower or gracefully fall back to the normal path.
280
+ Use `omx sparkshell` for explicit shell-native read-only commands, bounded verification, repo-wide listing/search, or explicit `omx sparkshell --tmux-pane` summaries. Treat sparkshell as explicit opt-in. When to use what: keep ambiguous, implementation-heavy, edit-heavy, diagnostics, tests, MCP/web, and complex shell work on the normal path; if `omx sparkshell` is incomplete, retry narrower or gracefully fall back to the normal path.
281
281
 
282
282
  Leader vs worker:
283
283
  - The leader chooses the mode, keeps the brief current, delegates bounded work, and owns verification plus stop/escalate calls.