oh-my-codex 0.18.8 → 0.18.10

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 (221) hide show
  1. package/Cargo.lock +12 -12
  2. package/Cargo.toml +1 -1
  3. package/README.md +4 -0
  4. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +2 -0
  5. package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +1 -0
  6. package/dist/autopilot/__tests__/deep-interview-gate.test.js +215 -0
  7. package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +1 -0
  8. package/dist/autopilot/__tests__/fsm.test.js +3 -0
  9. package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
  10. package/dist/autopilot/__tests__/ralplan-gate.test.js +148 -0
  11. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  12. package/dist/autopilot/deep-interview-gate.d.ts.map +1 -1
  13. package/dist/autopilot/deep-interview-gate.js +140 -0
  14. package/dist/autopilot/deep-interview-gate.js.map +1 -1
  15. package/dist/autopilot/fsm.js +2 -2
  16. package/dist/autopilot/fsm.js.map +1 -1
  17. package/dist/cli/__tests__/auth.test.js +37 -2
  18. package/dist/cli/__tests__/auth.test.js.map +1 -1
  19. package/dist/cli/__tests__/codex-feature-probe.test.d.ts +2 -0
  20. package/dist/cli/__tests__/codex-feature-probe.test.d.ts.map +1 -0
  21. package/dist/cli/__tests__/codex-feature-probe.test.js +46 -0
  22. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -0
  23. package/dist/cli/__tests__/codex-plugin-layout.test.js +1 -1
  24. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  25. package/dist/cli/__tests__/doctor-warning-copy.test.js +2 -0
  26. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  27. package/dist/cli/__tests__/index.test.js +288 -6
  28. package/dist/cli/__tests__/index.test.js.map +1 -1
  29. package/dist/cli/__tests__/launch-fallback.test.js +19 -5
  30. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  31. package/dist/cli/__tests__/package-bin-contract.test.js +39 -10
  32. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/question.test.js +26 -9
  34. package/dist/cli/__tests__/question.test.js.map +1 -1
  35. package/dist/cli/__tests__/resume.test.js +50 -1
  36. package/dist/cli/__tests__/resume.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-refresh.test.js +6 -2
  38. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  39. package/dist/cli/__tests__/sparkshell-packaging.test.js +45 -2
  40. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  41. package/dist/cli/__tests__/team-decompose.test.js +10 -5
  42. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  43. package/dist/cli/__tests__/team.test.js +45 -1
  44. package/dist/cli/__tests__/team.test.js.map +1 -1
  45. package/dist/cli/__tests__/ultragoal.test.js +75 -0
  46. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  47. package/dist/cli/__tests__/update.test.js +214 -17
  48. package/dist/cli/__tests__/update.test.js.map +1 -1
  49. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  50. package/dist/cli/auth.d.ts.map +1 -1
  51. package/dist/cli/auth.js +25 -1
  52. package/dist/cli/auth.js.map +1 -1
  53. package/dist/cli/codex-feature-probe.d.ts +5 -2
  54. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  55. package/dist/cli/codex-feature-probe.js +25 -9
  56. package/dist/cli/codex-feature-probe.js.map +1 -1
  57. package/dist/cli/index.d.ts +39 -5
  58. package/dist/cli/index.d.ts.map +1 -1
  59. package/dist/cli/index.js +184 -101
  60. package/dist/cli/index.js.map +1 -1
  61. package/dist/cli/setup.d.ts.map +1 -1
  62. package/dist/cli/setup.js +9 -1
  63. package/dist/cli/setup.js.map +1 -1
  64. package/dist/cli/team.d.ts +4 -0
  65. package/dist/cli/team.d.ts.map +1 -1
  66. package/dist/cli/team.js +43 -4
  67. package/dist/cli/team.js.map +1 -1
  68. package/dist/cli/ultragoal.d.ts.map +1 -1
  69. package/dist/cli/ultragoal.js +29 -0
  70. package/dist/cli/ultragoal.js.map +1 -1
  71. package/dist/cli/update.d.ts +20 -3
  72. package/dist/cli/update.d.ts.map +1 -1
  73. package/dist/cli/update.js +265 -23
  74. package/dist/cli/update.js.map +1 -1
  75. package/dist/cli/version.d.ts.map +1 -1
  76. package/dist/cli/version.js +5 -9
  77. package/dist/cli/version.js.map +1 -1
  78. package/dist/compat/__tests__/doctor-contract.test.js +12 -1
  79. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  80. package/dist/hooks/__tests__/agents-overlay.test.js +1 -0
  81. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  82. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +15 -0
  83. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  84. package/dist/hooks/__tests__/code-review-skill-contract.test.js +7 -3
  85. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  86. package/dist/hooks/__tests__/deep-interview-contract.test.js +46 -1
  87. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  88. package/dist/hooks/__tests__/skill-guidance-contract.test.js +14 -5
  89. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  90. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  91. package/dist/hooks/agents-overlay.js +2 -1
  92. package/dist/hooks/agents-overlay.js.map +1 -1
  93. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +112 -1
  94. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  95. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts +2 -0
  96. package/dist/hooks/extensibility/plugin-runner-stdin.d.ts.map +1 -0
  97. package/dist/hooks/extensibility/plugin-runner-stdin.js +16 -0
  98. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -0
  99. package/dist/hooks/extensibility/plugin-runner.js +2 -4
  100. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  101. package/dist/hud/__tests__/index.test.js +23 -2
  102. package/dist/hud/__tests__/index.test.js.map +1 -1
  103. package/dist/hud/__tests__/reconcile.test.js +387 -0
  104. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  105. package/dist/hud/__tests__/state.test.js +28 -0
  106. package/dist/hud/__tests__/state.test.js.map +1 -1
  107. package/dist/hud/__tests__/tmux.test.js +118 -7
  108. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  109. package/dist/hud/index.d.ts +6 -1
  110. package/dist/hud/index.d.ts.map +1 -1
  111. package/dist/hud/index.js +12 -3
  112. package/dist/hud/index.js.map +1 -1
  113. package/dist/hud/reconcile.d.ts +6 -2
  114. package/dist/hud/reconcile.d.ts.map +1 -1
  115. package/dist/hud/reconcile.js +58 -28
  116. package/dist/hud/reconcile.js.map +1 -1
  117. package/dist/hud/state.d.ts.map +1 -1
  118. package/dist/hud/state.js +4 -18
  119. package/dist/hud/state.js.map +1 -1
  120. package/dist/hud/tmux.d.ts +14 -1
  121. package/dist/hud/tmux.d.ts.map +1 -1
  122. package/dist/hud/tmux.js +129 -15
  123. package/dist/hud/tmux.js.map +1 -1
  124. package/dist/question/__tests__/renderer.test.js +566 -1
  125. package/dist/question/__tests__/renderer.test.js.map +1 -1
  126. package/dist/question/renderer.d.ts +9 -1
  127. package/dist/question/renderer.d.ts.map +1 -1
  128. package/dist/question/renderer.js +246 -70
  129. package/dist/question/renderer.js.map +1 -1
  130. package/dist/ralplan/consensus-gate.js +9 -1
  131. package/dist/ralplan/consensus-gate.js.map +1 -1
  132. package/dist/scripts/__tests__/codex-native-hook.test.js +322 -15
  133. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  134. package/dist/scripts/__tests__/run-test-files.test.js +115 -1
  135. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  136. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  137. package/dist/scripts/codex-native-hook.js +94 -20
  138. package/dist/scripts/codex-native-hook.js.map +1 -1
  139. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  140. package/dist/scripts/notify-hook/team-worker-stop.js +54 -21
  141. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  142. package/dist/scripts/run-test-files.js +218 -160
  143. package/dist/scripts/run-test-files.js.map +1 -1
  144. package/dist/state/__tests__/operations.test.js +463 -0
  145. package/dist/state/__tests__/operations.test.js.map +1 -1
  146. package/dist/team/__tests__/delivery-log.test.js +18 -0
  147. package/dist/team/__tests__/delivery-log.test.js.map +1 -1
  148. package/dist/team/__tests__/runtime.test.js +48 -0
  149. package/dist/team/__tests__/runtime.test.js.map +1 -1
  150. package/dist/team/__tests__/tmux-session.test.js +107 -0
  151. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  152. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  153. package/dist/team/__tests__/tmux-test-fixture.js +14 -2
  154. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  155. package/dist/team/__tests__/tmux-test-fixture.test.js +1 -0
  156. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  157. package/dist/team/__tests__/worker-bootstrap.test.js +54 -1
  158. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  159. package/dist/team/delivery-log.d.ts +1 -1
  160. package/dist/team/delivery-log.d.ts.map +1 -1
  161. package/dist/team/delivery-log.js.map +1 -1
  162. package/dist/team/repo-aware-decomposition.d.ts +4 -0
  163. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  164. package/dist/team/repo-aware-decomposition.js.map +1 -1
  165. package/dist/team/runtime.d.ts.map +1 -1
  166. package/dist/team/runtime.js +78 -9
  167. package/dist/team/runtime.js.map +1 -1
  168. package/dist/team/tmux-session.d.ts +1 -0
  169. package/dist/team/tmux-session.d.ts.map +1 -1
  170. package/dist/team/tmux-session.js +16 -5
  171. package/dist/team/tmux-session.js.map +1 -1
  172. package/dist/team/ultragoal-context.d.ts +12 -0
  173. package/dist/team/ultragoal-context.d.ts.map +1 -1
  174. package/dist/team/ultragoal-context.js +32 -8
  175. package/dist/team/ultragoal-context.js.map +1 -1
  176. package/dist/utils/__tests__/paths.test.js +23 -0
  177. package/dist/utils/__tests__/paths.test.js.map +1 -1
  178. package/dist/utils/__tests__/platform-command.test.js +16 -1
  179. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  180. package/dist/utils/__tests__/version.test.d.ts +2 -0
  181. package/dist/utils/__tests__/version.test.d.ts.map +1 -0
  182. package/dist/utils/__tests__/version.test.js +51 -0
  183. package/dist/utils/__tests__/version.test.js.map +1 -0
  184. package/dist/utils/paths.d.ts +8 -1
  185. package/dist/utils/paths.d.ts.map +1 -1
  186. package/dist/utils/paths.js +20 -6
  187. package/dist/utils/paths.js.map +1 -1
  188. package/dist/utils/platform-command.d.ts +9 -0
  189. package/dist/utils/platform-command.d.ts.map +1 -1
  190. package/dist/utils/platform-command.js +15 -0
  191. package/dist/utils/platform-command.js.map +1 -1
  192. package/dist/utils/toml.d.ts +4 -0
  193. package/dist/utils/toml.d.ts.map +1 -0
  194. package/dist/utils/toml.js +75 -0
  195. package/dist/utils/toml.js.map +1 -0
  196. package/dist/utils/version.d.ts +7 -0
  197. package/dist/utils/version.d.ts.map +1 -0
  198. package/dist/utils/version.js +67 -0
  199. package/dist/utils/version.js.map +1 -0
  200. package/dist/verification/__tests__/ci-rust-gates.test.js +8 -0
  201. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  202. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +16 -2
  203. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  204. package/package.json +4 -3
  205. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  206. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -0
  207. package/plugins/oh-my-codex/skills/code-review/SKILL.md +2 -2
  208. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +85 -11
  209. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +32 -17
  210. package/skills/autopilot/SKILL.md +3 -0
  211. package/skills/code-review/SKILL.md +2 -2
  212. package/skills/deep-interview/SKILL.md +85 -11
  213. package/skills/ultrawork/SKILL.md +32 -17
  214. package/src/scripts/__tests__/codex-native-hook.test.ts +391 -26
  215. package/src/scripts/__tests__/run-test-files.test.ts +138 -2
  216. package/src/scripts/codex-native-hook.ts +99 -17
  217. package/src/scripts/notify-hook/team-worker-stop.ts +58 -18
  218. package/src/scripts/prepare-build.js +83 -0
  219. package/src/scripts/run-test-files.ts +229 -150
  220. package/templates/AGENTS.md +40 -199
  221. package/src/scripts/postinstall-bootstrap.js +0 -23
@@ -4,22 +4,23 @@ description: Parallel execution engine for high-throughput task completion
4
4
  ---
5
5
 
6
6
  <Purpose>
7
- Ultrawork is a parallel execution engine for high-throughput task completion. It is a component, not a standalone persistence mode: it provides parallelism, context discipline, and smart delegation guidance, but not Ralph's persistence loop, architect sign-off, or long-running completion guarantees.
7
+ Ultrawork is a parallel execution engine for high-throughput task completion. It is a component, not a standalone persistence or verification mode: it provides parallelism, context discipline, and smart delegation guidance, but not durable goal tracking, Team's tmux worker lifecycle, Ralph's legacy persistence loop, architect sign-off, or long-running completion guarantees.
8
8
  </Purpose>
9
9
 
10
10
  <Use_When>
11
11
  - Multiple independent tasks can run simultaneously
12
12
  - User says "ulw", "ultrawork", or explicitly wants parallel execution
13
13
  - Task benefits from concurrent execution plus lightweight evidence before wrap-up
14
- - You need a direct-tool lane plus optional background evidence lanes without entering Ralph
14
+ - You need a direct-tool lane plus optional background evidence lanes without entering Team or a durable goal workflow
15
15
  </Use_When>
16
16
 
17
17
  <Do_Not_Use_When>
18
- - Task requires guaranteed completion with persistence, architect verification, or deslop/reverification -- use `ralph` instead (Ralph includes ultrawork)
19
- - Task requires a full autonomous pipeline -- use `autopilot` instead (autopilot defaults to Ultragoal, with Team/parallel execution used only when needed)
20
- - There is only one sequential task with no parallelism opportunity -- execute directly or delegate to a single `executor`
18
+ - Task needs durable goal tracking, ledger checkpoints, or resume across stories -- use `ultragoal` instead
19
+ - Task needs coordinated tmux workers, shared task state, mailbox/dispatch coordination, or long-running parallel execution -- use `team` instead
20
+ - Task requires a full autonomous pipeline -- use `autopilot` instead (default loop: `deep-interview -> ralplan -> ultragoal`, with `team` only when needed)
21
+ - Task intentionally requires the legacy persistent single-owner completion/verification loop -- use `ralph` explicitly; do not present it as the default durable path
22
+ - There is only one sequential task with no parallelism opportunity -- execute directly, use `ultragoal` for durable tracking, or delegate to a single `executor`
21
23
  - The request is still in plan-consensus mode -- keep planning artifacts in `ralplan` until execution is explicitly authorized
22
- - User needs session persistence for resume -- use `ralph`, which adds persistence on top of ultrawork
23
24
  </Do_Not_Use_When>
24
25
 
25
26
  <Why_This_Exists>
@@ -138,8 +139,12 @@ Why bad: No verification output, no acceptance evidence, and no manual QA note w
138
139
  </Examples>
139
140
 
140
141
  <Escalation_And_Stop_Conditions>
141
- - When ultrawork is invoked directly (not via Ralph), apply lightweight verification only -- build/typecheck passes when relevant, affected tests pass, and manual QA notes are captured when needed.
142
- - Ralph owns persistence, architect verification, deslop, and the full verified-completion promise. Do not claim those guarantees from direct ultrawork alone.
142
+ - When ultrawork is invoked directly, apply lightweight verification only -- build/typecheck passes when relevant, affected tests pass, and manual QA notes are captured when needed.
143
+ - Ultrawork does not own persistence, durable ledgers, architect verification, deslop, full QA, or the full verified-completion promise. Do not claim those guarantees from direct ultrawork alone.
144
+ - Escalate to `ultragoal` when the work needs durable goal state, story checkpoints, or resume across implementation steps.
145
+ - Escalate to `team` when the work needs coordinated tmux workers, shared task state, or durable multi-worker lifecycle control.
146
+ - Escalate to explicitly requested `ralph` only for the supported legacy single-owner persistence/verification fallback.
147
+ - Ralph owns persistence, architect verification, deslop, and the full verified-completion promise only when explicitly selected as the supported legacy fallback; direct ultrawork does not own those guarantees.
143
148
  - If a task fails repeatedly across retries, report the issue rather than retrying indefinitely.
144
149
  - Escalate to the user when tasks have unclear dependencies, conflicting requirements, or a materially branching acceptance target.
145
150
  </Escalation_And_Stop_Conditions>
@@ -159,17 +164,27 @@ Why bad: No verification output, no acceptance evidence, and no manual QA note w
159
164
  ## Relationship to Other Modes
160
165
 
161
166
  ```
162
- ralph (persistence + verified completion wrapper)
163
- \-- includes: ultrawork (this skill)
164
- \-- provides: high-throughput execution + lightweight evidence
167
+ ultrawork (this skill)
168
+ \-- provides: in-session parallel execution discipline + lightweight evidence
165
169
 
166
- autopilot (autonomous execution)
167
- \-- includes: ralph
168
- \-- includes: ultrawork (this skill)
170
+ ultragoal (durable goal execution)
171
+ \-- owns: goal ledger, checkpoints, resume across stories, final gate discipline
172
+ \-- may use: team for parallel lanes when a story benefits from coordinated workers
169
173
 
170
- ecomode (token efficiency)
171
- \-- modifies: ultrawork's model selection
174
+ team (tmux coordinated execution)
175
+ \-- owns: worker panes, shared task state, mailbox/dispatch, lifecycle control
176
+ \-- can return: checkpoint-ready evidence to an Ultragoal leader
177
+
178
+ autopilot (strict autonomous delivery loop)
179
+ \-- default flow: deep-interview -> ralplan -> ultragoal -> code-review -> ultraqa
180
+ \-- may use: team only when an Ultragoal story needs parallel execution
181
+
182
+ ralph (supported legacy explicit fallback)
183
+ \-- owns: single-owner persistence loop + architect verification when intentionally selected
184
+
185
+ ecomode (deprecated compatibility-only)
186
+ \-- do not route users there from ultrawork; it is not the current model-selection path
172
187
  ```
173
188
 
174
- Ultrawork is the parallelism and execution-discipline layer. Ralph adds persistence, architect verification, deslop, and retry-until-done behavior. Autopilot adds the broader autonomous lifecycle pipeline. Ecomode adjusts ultrawork's model routing to favor cheaper models.
189
+ Ultrawork is the parallelism and execution-discipline layer. Ultragoal is the current default durable goal/ledger follow-up. Team is the coordinated tmux parallel runtime, often nested under an Ultragoal story when durable work needs multiple lanes. Autopilot orchestrates the full default lifecycle through deep-interview, ralplan, ultragoal, code-review, and ultraqa. Ralph remains active as an explicit legacy fallback for persistent single-owner verification, but it is not the recommended default durable path. Ecomode is deprecated compatibility-only and should not be advertised as the ultrawork model-selection route.
175
190
  </Advanced>
@@ -441,23 +441,77 @@ describe("codex native hook dispatch", () => {
441
441
  );
442
442
  });
443
443
 
444
- it("emits schema-safe JSON stdout when CLI stdin is malformed", () => {
445
- const stdout = runNativeHookCli("{");
444
+ it("emits Stop-schema-safe block JSON when unidentifiable malformed stdin has native Stop runtime surface", async () => {
445
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-stop-surface-"));
446
+ try {
447
+ await mkdir(join(cwd, ".omx"), { recursive: true });
448
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
449
+ cwd,
450
+ input: "{",
451
+ encoding: "utf-8",
452
+ stdio: ["pipe", "pipe", "pipe"],
453
+ });
446
454
 
447
- const output = parseSingleJsonStdout(stdout) as {
448
- continue?: boolean;
449
- stopReason?: string;
450
- systemMessage?: string;
451
- hookSpecificOutput?: unknown;
452
- };
455
+ assert.equal(result.status, 0, result.stderr || result.stdout);
456
+ assert.equal(result.stderr, "");
457
+ const output = parseSingleJsonStdout(result.stdout) as {
458
+ decision?: string;
459
+ continue?: boolean;
460
+ reason?: string;
461
+ stopReason?: string;
462
+ systemMessage?: string;
463
+ hookSpecificOutput?: unknown;
464
+ };
453
465
 
454
- assert.equal(output.continue, false);
455
- assert.equal(output.stopReason, "native_hook_stdin_parse_error");
456
- assert.equal(output.hookSpecificOutput, undefined);
457
- assert.match(
458
- String(output.systemMessage ?? ""),
459
- /stdin JSON parsing failed inside codex-native-hook:/,
460
- );
466
+ assert.equal(output.decision, "block");
467
+ assert.equal(output.continue, undefined);
468
+ assert.equal(
469
+ output.reason,
470
+ "OMX native hook received malformed JSON input. Preserve runtime state, inspect the emitting hook payload yourself, and retry with valid JSON.",
471
+ );
472
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
473
+ assert.equal(output.hookSpecificOutput, undefined);
474
+ assert.match(
475
+ String(output.systemMessage ?? ""),
476
+ /stdin JSON parsing failed inside codex-native-hook:/,
477
+ );
478
+ } finally {
479
+ await rm(cwd, { recursive: true, force: true });
480
+ }
481
+ });
482
+
483
+ it("preserves non-Stop fail-closed JSON when malformed stdin identifies a non-Stop hook", async () => {
484
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-cli-malformed-nonstop-"));
485
+ try {
486
+ await mkdir(join(cwd, ".omx"), { recursive: true });
487
+ const result = spawnSync(process.execPath, [nativeHookScriptPath()], {
488
+ cwd,
489
+ input: '{hook_event_name:"PreToolUse",',
490
+ encoding: "utf-8",
491
+ stdio: ["pipe", "pipe", "pipe"],
492
+ });
493
+
494
+ assert.equal(result.status, 0, result.stderr || result.stdout);
495
+ assert.equal(result.stderr, "");
496
+ const output = parseSingleJsonStdout(result.stdout) as {
497
+ continue?: boolean;
498
+ decision?: string;
499
+ stopReason?: string;
500
+ systemMessage?: string;
501
+ hookSpecificOutput?: unknown;
502
+ };
503
+
504
+ assert.equal(output.continue, false);
505
+ assert.equal(output.decision, undefined);
506
+ assert.equal(output.stopReason, "native_hook_stdin_parse_error");
507
+ assert.equal(output.hookSpecificOutput, undefined);
508
+ assert.match(
509
+ String(output.systemMessage ?? ""),
510
+ /stdin JSON parsing failed inside codex-native-hook:/,
511
+ );
512
+ } finally {
513
+ await rm(cwd, { recursive: true, force: true });
514
+ }
461
515
  });
462
516
 
463
517
  it("redacts unterminated prompt-like malformed stdin fields", async () => {
@@ -1867,6 +1921,51 @@ describe("codex native hook dispatch", () => {
1867
1921
  }
1868
1922
  });
1869
1923
 
1924
+ it("includes repo-local .omx project-memory during SessionStart when OMX_ROOT is boxed", async () => {
1925
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-boxed-memory-"));
1926
+ const boxedRoot = await mkdtemp(join(tmpdir(), "omx-native-hook-boxed-root-"));
1927
+ const previousOmxRoot = process.env.OMX_ROOT;
1928
+ try {
1929
+ process.env.OMX_ROOT = boxedRoot;
1930
+ await writeJson(join(cwd, ".omx", "project-memory.json"), {
1931
+ techStack: "Repo-local CLI memory",
1932
+ conventions: "SessionStart should load CLI-written project memory",
1933
+ directives: [
1934
+ { directive: "Prefer repo-local .omx project memory over boxed runtime fallback.", priority: "high" },
1935
+ ],
1936
+ });
1937
+ await writeJson(join(boxedRoot, ".omx", "project-memory.json"), {
1938
+ techStack: "Boxed runtime memory should not win",
1939
+ notes: [{ category: "runtime", content: "stale boxed runtime note", timestamp: new Date().toISOString() }],
1940
+ });
1941
+
1942
+ const result = await dispatchCodexNativeHook(
1943
+ {
1944
+ hook_event_name: "SessionStart",
1945
+ cwd,
1946
+ session_id: "sess-boxed-memory-1",
1947
+ },
1948
+ { cwd, sessionOwnerPid: 43210 },
1949
+ );
1950
+
1951
+ const additionalContext = String(
1952
+ (result.outputJson as { hookSpecificOutput?: { additionalContext?: string } })?.hookSpecificOutput?.additionalContext ?? "",
1953
+ );
1954
+ assert.match(additionalContext, /\[Project memory\]/);
1955
+ assert.match(additionalContext, /source: \.omx\/project-memory\.json/);
1956
+ assert.match(additionalContext, /Repo-local CLI memory/);
1957
+ assert.match(additionalContext, /SessionStart should load CLI-written project memory/);
1958
+ assert.match(additionalContext, /Prefer repo-local \.omx project memory over boxed runtime fallback\./);
1959
+ assert.doesNotMatch(additionalContext, /Boxed runtime memory should not win/);
1960
+ assert.doesNotMatch(additionalContext, /stale boxed runtime note/);
1961
+ } finally {
1962
+ if (previousOmxRoot === undefined) delete process.env.OMX_ROOT;
1963
+ else process.env.OMX_ROOT = previousOmxRoot;
1964
+ await rm(cwd, { recursive: true, force: true });
1965
+ await rm(boxedRoot, { recursive: true, force: true });
1966
+ }
1967
+ });
1968
+
1870
1969
  it("prefers repository project-memory.json during SessionStart while preserving legacy wiki guidance", async () => {
1871
1970
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-session-root-memory-legacy-wiki-"));
1872
1971
  try {
@@ -5804,6 +5903,76 @@ exit 0
5804
5903
  }
5805
5904
  });
5806
5905
 
5906
+ it("allows null-device fd redirects while deep-interview blocks real Bash writes", async () => {
5907
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-null-redirect-"));
5908
+ try {
5909
+ const stateDir = join(cwd, ".omx", "state");
5910
+ const sessionDir = join(stateDir, "sessions", "sess-di-null-redirect");
5911
+ await mkdir(sessionDir, { recursive: true });
5912
+ await writeJson(join(stateDir, "session.json"), { session_id: "sess-di-null-redirect", cwd });
5913
+ await writeJson(join(sessionDir, "skill-active-state.json"), {
5914
+ version: 1,
5915
+ active: true,
5916
+ skill: "deep-interview",
5917
+ phase: "planning",
5918
+ session_id: "sess-di-null-redirect",
5919
+ active_skills: [{ skill: "deep-interview", phase: "planning", active: true, session_id: "sess-di-null-redirect" }],
5920
+ });
5921
+ await writeJson(join(sessionDir, "deep-interview-state.json"), {
5922
+ active: true,
5923
+ mode: "deep-interview",
5924
+ current_phase: "intent-first",
5925
+ session_id: "sess-di-null-redirect",
5926
+ });
5927
+
5928
+ const allowedCommands = [
5929
+ "find application -type d -name 'bug-tracking*' 2>/dev/null | head -20",
5930
+ "find application -type d -name 'bug-tracking*' 2> /dev/null | head -20",
5931
+ "find application -type d -name 'bug-tracking*' 2>NUL | head -20",
5932
+ "find application -type d -name 'bug-tracking*' 1>/dev/null",
5933
+ "find application -type d -name 'bug-tracking*' &>/dev/null",
5934
+ ];
5935
+
5936
+ for (const [index, command] of allowedCommands.entries()) {
5937
+ const result = await dispatchCodexNativeHook(
5938
+ {
5939
+ hook_event_name: "PreToolUse",
5940
+ cwd,
5941
+ session_id: "sess-di-null-redirect",
5942
+ tool_name: "Bash",
5943
+ tool_use_id: `tool-di-null-redirect-${index}`,
5944
+ tool_input: { command },
5945
+ },
5946
+ { cwd },
5947
+ );
5948
+ assert.equal(result.outputJson, null, command);
5949
+ }
5950
+
5951
+ const blockedCommands = [
5952
+ "find application -type d -name 'bug-tracking*' 2>errors.log | head -20",
5953
+ "find application -type d -name 'bug-tracking*' > /tmp/bug-tracking.txt",
5954
+ "find application -type d -name 'bug-tracking*' | tee /dev/null",
5955
+ ];
5956
+
5957
+ for (const [index, command] of blockedCommands.entries()) {
5958
+ const result = await dispatchCodexNativeHook(
5959
+ {
5960
+ hook_event_name: "PreToolUse",
5961
+ cwd,
5962
+ session_id: "sess-di-null-redirect",
5963
+ tool_name: "Bash",
5964
+ tool_use_id: `tool-di-real-redirect-${index}`,
5965
+ tool_input: { command },
5966
+ },
5967
+ { cwd },
5968
+ );
5969
+ assert.equal((result.outputJson as { decision?: string } | null)?.decision, "block", command);
5970
+ }
5971
+ } finally {
5972
+ await rm(cwd, { recursive: true, force: true });
5973
+ }
5974
+ });
5975
+
5807
5976
  it("allows implementation tools after an explicit deep-interview handoff deactivates the mode", async () => {
5808
5977
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-pretool-deep-interview-handoff-"));
5809
5978
  try {
@@ -8296,6 +8465,52 @@ exit 0
8296
8465
  }
8297
8466
  });
8298
8467
 
8468
+ it("suppresses parent Autopilot Stop continuation in side conversations", async () => {
8469
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-side-conversation-"));
8470
+ try {
8471
+ const stateDir = join(cwd, ".omx", "state");
8472
+ const sessionId = "sess-stop-autopilot-side-conversation";
8473
+ const transcriptPath = join(cwd, "side-conversation-rollout.jsonl");
8474
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
8475
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
8476
+ active: true,
8477
+ mode: "autopilot",
8478
+ current_phase: "deep-interview",
8479
+ });
8480
+ await writeFile(
8481
+ transcriptPath,
8482
+ `${JSON.stringify({
8483
+ type: "message",
8484
+ role: "user",
8485
+ content: [
8486
+ "Side conversation boundary.",
8487
+ "Everything before this boundary is inherited history from the parent thread. It is reference context only. It is not your current task.",
8488
+ "Only messages submitted after this boundary are active user instructions for this side conversation.",
8489
+ "You are a side-conversation assistant, separate from the main thread.",
8490
+ ].join("\n\n"),
8491
+ })}\n`,
8492
+ "utf-8",
8493
+ );
8494
+
8495
+ const result = await dispatchCodexNativeHook(
8496
+ {
8497
+ hook_event_name: "Stop",
8498
+ cwd,
8499
+ session_id: sessionId,
8500
+ thread_id: "thread-stop-autopilot-side-conversation",
8501
+ transcript_path: transcriptPath,
8502
+ last_assistant_message: "Waiting for a new side-conversation question.",
8503
+ },
8504
+ { cwd },
8505
+ );
8506
+
8507
+ assert.equal(result.omxEventName, "stop");
8508
+ assert.equal(result.outputJson, null);
8509
+ } finally {
8510
+ await rm(cwd, { recursive: true, force: true });
8511
+ }
8512
+ });
8513
+
8299
8514
  it("requires Autopilot code review after a compact-boundary Stop exemption", async () => {
8300
8515
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-autopilot-review-compact-"));
8301
8516
  try {
@@ -9037,7 +9252,7 @@ exit 0
9037
9252
  }
9038
9253
  });
9039
9254
 
9040
- it("queues worker Stop leader nudge with Tab and submit when leader pane is busy", async () => {
9255
+ it("steers worker Stop leader nudge directly when leader pane is busy", async () => {
9041
9256
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-stop-team-worker-busy-leader-"));
9042
9257
  const prevTeamWorker = process.env.OMX_TEAM_WORKER;
9043
9258
  const prevTeamStateRoot = process.env.OMX_TEAM_STATE_ROOT;
@@ -9110,14 +9325,11 @@ exit 0
9110
9325
  assert.equal(result.outputJson, null);
9111
9326
  const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9112
9327
  assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
9113
- assert.match(tmuxLog, /send-keys -t %42 Tab/);
9114
- assert.match(tmuxLog, /send-keys -t %42 C-m/);
9115
- assert.ok(
9116
- tmuxLog.indexOf("send-keys -t %42 Tab") < tmuxLog.indexOf("send-keys -t %42 C-m"),
9117
- "busy worker-stop nudge should press Tab before C-m",
9118
- );
9328
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 Tab/);
9329
+ const submits = tmuxLog.match(/send-keys -t %42 C-m/g) || [];
9330
+ assert.equal(submits.length, 2, "busy worker-stop nudge should submit directly as steering, not queue via Tab");
9119
9331
  const nudgeState = JSON.parse(await readFile(join(workerDir, "worker-stop-nudge.json"), "utf-8"));
9120
- assert.equal(nudgeState.delivery, "queued");
9332
+ assert.equal(nudgeState.delivery, "steered");
9121
9333
  } finally {
9122
9334
  if (typeof prevTeamWorker === "string") process.env.OMX_TEAM_WORKER = prevTeamWorker;
9123
9335
  else delete process.env.OMX_TEAM_WORKER;
@@ -9253,6 +9465,14 @@ exit 0
9253
9465
  });
9254
9466
  assert.equal(shutdownResult.result, "team_state_gone_or_shutdown");
9255
9467
  assert.equal(existsSync(join(stateDir, "team", "shutdown-team", "worker-stop-nudge.json")), false);
9468
+ const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
9469
+ const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
9470
+ .trim()
9471
+ .split("\n")
9472
+ .map((line) => JSON.parse(line));
9473
+ const suppressedEvents = deliveryEvents.filter((event) => event.reason === "team_state_gone_or_shutdown");
9474
+ assert.equal(suppressedEvents.length, 2, "late closed-team Stop nudges should be diagnostics, not queued prompts");
9475
+ assert.equal(suppressedEvents.every((event) => event.result === "suppressed" && event.transport === "none"), true);
9256
9476
  } finally {
9257
9477
  await rm(cwd, { recursive: true, force: true });
9258
9478
  }
@@ -9293,13 +9513,13 @@ exit 0
9293
9513
  workerContext: { teamName, workerName: "worker-2" },
9294
9514
  });
9295
9515
 
9296
- assert.equal(result.result, "queued");
9516
+ assert.equal(result.result, "steered");
9297
9517
  const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9298
9518
  assert.match(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-2 native Stop allowed/);
9299
- assert.match(tmuxLog, /send-keys -t %42 Tab/);
9519
+ assert.doesNotMatch(tmuxLog, /send-keys -t %42 Tab/);
9300
9520
  const teamNudgeState = JSON.parse(await readFile(join(teamDir, "worker-stop-nudge.json"), "utf-8"));
9301
9521
  assert.equal(teamNudgeState.worker, "worker-2");
9302
- assert.equal(teamNudgeState.delivery, "queued");
9522
+ assert.equal(teamNudgeState.delivery, "steered");
9303
9523
  } finally {
9304
9524
  if (typeof prevPath === "string") process.env.PATH = prevPath;
9305
9525
  else delete process.env.PATH;
@@ -9432,6 +9652,21 @@ exit 0
9432
9652
  assert.equal(existsSync(teamDir), false, "deferred worker Stop recording must not recreate removed team state");
9433
9653
  const tmuxLog = await readFile(tmuxLogPath, "utf-8");
9434
9654
  assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l \[OMX\] worker-1 native Stop allowed/);
9655
+ const deliveryLogPath = join(logsDir, `team-delivery-${new Date().toISOString().split("T")[0]}.jsonl`);
9656
+ const deliveryEvents = (await readFile(deliveryLogPath, "utf-8"))
9657
+ .trim()
9658
+ .split("\n")
9659
+ .map((line) => JSON.parse(line));
9660
+ assert.equal(
9661
+ deliveryEvents.some((event) =>
9662
+ event.team === teamName
9663
+ && event.result === "suppressed"
9664
+ && event.transport === "none"
9665
+ && event.reason === "team_state_gone_or_shutdown"
9666
+ ),
9667
+ true,
9668
+ "teardown-race worker Stop nudges should be diagnostic suppression events, not queued prompts",
9669
+ );
9435
9670
  } finally {
9436
9671
  if (typeof prevPath === "string") process.env.PATH = prevPath;
9437
9672
  else delete process.env.PATH;
@@ -13932,6 +14167,136 @@ exit 0
13932
14167
  }
13933
14168
  });
13934
14169
 
14170
+ it("blocks implementation writes when Autopilot ralplan is visible only in skill-active phase", async () => {
14171
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-skill-ralplan-pretool-block-"));
14172
+ try {
14173
+ const stateDir = join(cwd, ".omx", "state");
14174
+ const sessionId = "sess-autopilot-skill-ralplan-pretool-block";
14175
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14176
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14177
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
14178
+ active: true,
14179
+ skill: "autopilot",
14180
+ phase: "autopilot:ralplan",
14181
+ session_id: sessionId,
14182
+ active_skills: [{ skill: "autopilot", phase: "autopilot:ralplan", active: true, session_id: sessionId }],
14183
+ });
14184
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
14185
+ active: true,
14186
+ mode: "autopilot",
14187
+ current_phase: "planning",
14188
+ session_id: sessionId,
14189
+ state: {
14190
+ handoff_artifacts: {
14191
+ ralplan_consensus_gate: { required: true, complete: false },
14192
+ },
14193
+ },
14194
+ });
14195
+
14196
+ const result = await dispatchCodexNativeHook(
14197
+ {
14198
+ hook_event_name: "PreToolUse",
14199
+ cwd,
14200
+ session_id: sessionId,
14201
+ thread_id: "thread-autopilot-skill-ralplan-pretool-block",
14202
+ tool_name: "Edit",
14203
+ tool_input: { file_path: "src/runtime.ts" },
14204
+ },
14205
+ { cwd },
14206
+ );
14207
+
14208
+ assert.equal(result.omxEventName, "pre-tool-use");
14209
+ assert.equal(result.outputJson?.decision, "block");
14210
+ assert.match(String(result.outputJson?.reason ?? ""), /Autopilot planning is active .*implementation\/write tools are blocked/i);
14211
+ } finally {
14212
+ await rm(cwd, { recursive: true, force: true });
14213
+ }
14214
+ });
14215
+
14216
+ it("ignores stale Autopilot ralplan skill mirrors after detail state leaves planning", async () => {
14217
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-stale-ralplan-mirror-"));
14218
+ try {
14219
+ const stateDir = join(cwd, ".omx", "state");
14220
+ const sessionId = "sess-autopilot-stale-ralplan-mirror";
14221
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14222
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14223
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
14224
+ active: true,
14225
+ skill: "autopilot",
14226
+ phase: "autopilot:ralplan",
14227
+ session_id: sessionId,
14228
+ active_skills: [{ skill: "autopilot", phase: "autopilot:ralplan", active: true, session_id: sessionId }],
14229
+ });
14230
+
14231
+ for (const phase of ["ultragoal", "code-review", "completing", "complete"]) {
14232
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
14233
+ active: true,
14234
+ mode: "autopilot",
14235
+ current_phase: phase,
14236
+ session_id: sessionId,
14237
+ });
14238
+
14239
+ const result = await dispatchCodexNativeHook(
14240
+ {
14241
+ hook_event_name: "PreToolUse",
14242
+ cwd,
14243
+ session_id: sessionId,
14244
+ thread_id: "thread-autopilot-stale-ralplan-mirror",
14245
+ tool_name: "Edit",
14246
+ tool_input: { file_path: "src/runtime.ts" },
14247
+ },
14248
+ { cwd },
14249
+ );
14250
+
14251
+ assert.equal(result.omxEventName, "pre-tool-use");
14252
+ assert.equal(result.outputJson, null, `stale skill-active ralplan mirror must not block when Autopilot detail phase is ${phase}`);
14253
+ }
14254
+ } finally {
14255
+ await rm(cwd, { recursive: true, force: true });
14256
+ }
14257
+ });
14258
+
14259
+ it("allows explicit blank Autopilot detail phase to use a ralplan skill mirror", async () => {
14260
+ const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-blank-phase-mirror-"));
14261
+ try {
14262
+ const stateDir = join(cwd, ".omx", "state");
14263
+ const sessionId = "sess-autopilot-blank-phase-mirror";
14264
+ await mkdir(join(stateDir, "sessions", sessionId), { recursive: true });
14265
+ await writeJson(join(stateDir, "session.json"), { session_id: sessionId });
14266
+ await writeJson(join(stateDir, "sessions", sessionId, "skill-active-state.json"), {
14267
+ active: true,
14268
+ skill: "autopilot",
14269
+ phase: "autopilot:ralplan",
14270
+ session_id: sessionId,
14271
+ active_skills: [{ skill: "autopilot", phase: "autopilot:ralplan", active: true, session_id: sessionId }],
14272
+ });
14273
+ await writeJson(join(stateDir, "sessions", sessionId, "autopilot-state.json"), {
14274
+ active: true,
14275
+ mode: "autopilot",
14276
+ current_phase: "",
14277
+ session_id: sessionId,
14278
+ });
14279
+
14280
+ const result = await dispatchCodexNativeHook(
14281
+ {
14282
+ hook_event_name: "PreToolUse",
14283
+ cwd,
14284
+ session_id: sessionId,
14285
+ thread_id: "thread-autopilot-blank-phase-mirror",
14286
+ tool_name: "Edit",
14287
+ tool_input: { file_path: "src/runtime.ts" },
14288
+ },
14289
+ { cwd },
14290
+ );
14291
+
14292
+ assert.equal(result.omxEventName, "pre-tool-use");
14293
+ assert.equal(result.outputJson?.decision, "block");
14294
+ assert.match(String(result.outputJson?.reason ?? ""), /Autopilot planning is active .*implementation\/write tools are blocked/i);
14295
+ } finally {
14296
+ await rm(cwd, { recursive: true, force: true });
14297
+ }
14298
+ });
14299
+
13935
14300
  it("does not block implementation writes from Autopilot ralplan detail state without canonical skill state", async () => {
13936
14301
  const cwd = await mkdtemp(join(tmpdir(), "omx-native-hook-autopilot-ralplan-no-canonical-"));
13937
14302
  try {