oh-my-codex 0.19.1 → 0.20.0

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 (212) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +3 -1
  4. package/crates/omx-api/src/lib.rs +3 -3
  5. package/crates/omx-api/tests/cli.rs +4 -4
  6. package/crates/omx-explore/src/main.rs +4 -4
  7. package/crates/omx-sparkshell/src/codex_bridge.rs +2 -2
  8. package/dist/agents/__tests__/definitions.test.js +3 -3
  9. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  10. package/dist/agents/__tests__/native-config.test.js +71 -42
  11. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  12. package/dist/agents/definitions.d.ts +1 -1
  13. package/dist/agents/definitions.d.ts.map +1 -1
  14. package/dist/agents/definitions.js +3 -3
  15. package/dist/agents/definitions.js.map +1 -1
  16. package/dist/agents/native-config.d.ts +2 -2
  17. package/dist/agents/native-config.d.ts.map +1 -1
  18. package/dist/agents/native-config.js +7 -5
  19. package/dist/agents/native-config.js.map +1 -1
  20. package/dist/autopilot/__tests__/planner-routing.test.js +19 -12
  21. package/dist/autopilot/__tests__/planner-routing.test.js.map +1 -1
  22. package/dist/autopilot/__tests__/ralplan-gate.test.js +151 -0
  23. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  24. package/dist/autopilot/planner-routing.d.ts.map +1 -1
  25. package/dist/autopilot/planner-routing.js +4 -2
  26. package/dist/autopilot/planner-routing.js.map +1 -1
  27. package/dist/autopilot/ralplan-gate.d.ts +1 -0
  28. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  29. package/dist/autopilot/ralplan-gate.js +38 -2
  30. package/dist/autopilot/ralplan-gate.js.map +1 -1
  31. package/dist/autoresearch/runtime.d.ts +2 -0
  32. package/dist/autoresearch/runtime.d.ts.map +1 -1
  33. package/dist/autoresearch/runtime.js +8 -0
  34. package/dist/autoresearch/runtime.js.map +1 -1
  35. package/dist/capabilities/__tests__/lockfile.test.d.ts +2 -0
  36. package/dist/capabilities/__tests__/lockfile.test.d.ts.map +1 -0
  37. package/dist/capabilities/__tests__/lockfile.test.js +115 -0
  38. package/dist/capabilities/__tests__/lockfile.test.js.map +1 -0
  39. package/dist/capabilities/lockfile.d.ts +102 -0
  40. package/dist/capabilities/lockfile.d.ts.map +1 -0
  41. package/dist/capabilities/lockfile.js +334 -0
  42. package/dist/capabilities/lockfile.js.map +1 -0
  43. package/dist/cli/__tests__/agents.test.js +5 -5
  44. package/dist/cli/__tests__/agents.test.js.map +1 -1
  45. package/dist/cli/__tests__/capabilities.test.d.ts +2 -0
  46. package/dist/cli/__tests__/capabilities.test.d.ts.map +1 -0
  47. package/dist/cli/__tests__/capabilities.test.js +135 -0
  48. package/dist/cli/__tests__/capabilities.test.js.map +1 -0
  49. package/dist/cli/__tests__/codex-plugin-layout.test.js +237 -6
  50. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  51. package/dist/cli/__tests__/doctor-context-window-warning.test.js +9 -9
  52. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  53. package/dist/cli/__tests__/doctor-invalid-config.test.js +1 -1
  54. package/dist/cli/__tests__/doctor-spark-routing.test.js +22 -6
  55. package/dist/cli/__tests__/doctor-spark-routing.test.js.map +1 -1
  56. package/dist/cli/__tests__/index.test.js +53 -18
  57. package/dist/cli/__tests__/index.test.js.map +1 -1
  58. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js +3 -3
  59. package/dist/cli/__tests__/plugin-marketplace-idempotent.test.js.map +1 -1
  60. package/dist/cli/__tests__/resume.test.js +95 -6
  61. package/dist/cli/__tests__/resume.test.js.map +1 -1
  62. package/dist/cli/__tests__/setup-install-mode.test.js +46 -18
  63. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  64. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +3 -3
  65. package/dist/cli/__tests__/setup-refresh.test.js +29 -7
  66. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  67. package/dist/cli/__tests__/uninstall.test.js +4 -4
  68. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  69. package/dist/cli/agents.js +1 -1
  70. package/dist/cli/agents.js.map +1 -1
  71. package/dist/cli/capabilities.d.ts +3 -0
  72. package/dist/cli/capabilities.d.ts.map +1 -0
  73. package/dist/cli/capabilities.js +118 -0
  74. package/dist/cli/capabilities.js.map +1 -0
  75. package/dist/cli/doctor.d.ts +1 -1
  76. package/dist/cli/doctor.d.ts.map +1 -1
  77. package/dist/cli/doctor.js +9 -4
  78. package/dist/cli/doctor.js.map +1 -1
  79. package/dist/cli/index.d.ts +7 -4
  80. package/dist/cli/index.d.ts.map +1 -1
  81. package/dist/cli/index.js +77 -19
  82. package/dist/cli/index.js.map +1 -1
  83. package/dist/cli/plugin-marketplace.d.ts +1 -0
  84. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  85. package/dist/cli/plugin-marketplace.js +31 -0
  86. package/dist/cli/plugin-marketplace.js.map +1 -1
  87. package/dist/cli/setup.d.ts.map +1 -1
  88. package/dist/cli/setup.js +7 -4
  89. package/dist/cli/setup.js.map +1 -1
  90. package/dist/config/__tests__/generator-idempotent.test.js +23 -23
  91. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  92. package/dist/config/__tests__/generator-notify.test.js +9 -9
  93. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  94. package/dist/config/__tests__/models.test.js +51 -26
  95. package/dist/config/__tests__/models.test.js.map +1 -1
  96. package/dist/config/generator.d.ts +1 -1
  97. package/dist/config/generator.d.ts.map +1 -1
  98. package/dist/config/models.d.ts +16 -6
  99. package/dist/config/models.d.ts.map +1 -1
  100. package/dist/config/models.js +19 -8
  101. package/dist/config/models.js.map +1 -1
  102. package/dist/hooks/__tests__/keyword-detector.test.js +12 -4
  103. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  104. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +1 -1
  105. package/dist/hooks/__tests__/prometheus-strict-contract.test.js +2 -2
  106. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  107. package/dist/hooks/__tests__/tmux-hook-engine.test.js +2 -2
  108. package/dist/leader/__tests__/contract.test.js +108 -1
  109. package/dist/leader/__tests__/contract.test.js.map +1 -1
  110. package/dist/leader/contract.d.ts +26 -0
  111. package/dist/leader/contract.d.ts.map +1 -1
  112. package/dist/leader/contract.js +170 -0
  113. package/dist/leader/contract.js.map +1 -1
  114. package/dist/modes/base.d.ts +1 -1
  115. package/dist/modes/base.d.ts.map +1 -1
  116. package/dist/pipeline/__tests__/stages.test.js +27 -1
  117. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  118. package/dist/pipeline/stages/ralplan.d.ts +2 -1
  119. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  120. package/dist/pipeline/stages/ralplan.js +1 -0
  121. package/dist/pipeline/stages/ralplan.js.map +1 -1
  122. package/dist/pipeline/stages/ultragoal.d.ts +5 -1
  123. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -1
  124. package/dist/pipeline/stages/ultragoal.js +14 -4
  125. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  126. package/dist/ralplan/__tests__/runtime.test.js +132 -0
  127. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  128. package/dist/ralplan/runtime.d.ts +17 -0
  129. package/dist/ralplan/runtime.d.ts.map +1 -1
  130. package/dist/ralplan/runtime.js +97 -1
  131. package/dist/ralplan/runtime.js.map +1 -1
  132. package/dist/scripts/__tests__/codex-native-hook.test.js +412 -0
  133. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  134. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  135. package/dist/scripts/codex-native-hook.js +238 -12
  136. package/dist/scripts/codex-native-hook.js.map +1 -1
  137. package/dist/state/__tests__/operations.test.js +450 -0
  138. package/dist/state/__tests__/operations.test.js.map +1 -1
  139. package/dist/state/__tests__/workflow-transition.test.js +14 -0
  140. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  141. package/dist/state/operations.d.ts.map +1 -1
  142. package/dist/state/operations.js +72 -11
  143. package/dist/state/operations.js.map +1 -1
  144. package/dist/state/terminal-normalization.d.ts +8 -0
  145. package/dist/state/terminal-normalization.d.ts.map +1 -0
  146. package/dist/state/terminal-normalization.js +127 -0
  147. package/dist/state/terminal-normalization.js.map +1 -0
  148. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  149. package/dist/state/workflow-transition-reconcile.js +3 -1
  150. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  151. package/dist/team/__tests__/delegation-policy.test.js +2 -2
  152. package/dist/team/__tests__/delegation-policy.test.js.map +1 -1
  153. package/dist/team/__tests__/model-contract.test.js +25 -25
  154. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  155. package/dist/team/__tests__/runtime.test.js +28 -28
  156. package/dist/team/__tests__/runtime.test.js.map +1 -1
  157. package/dist/team/__tests__/scaling.test.js +7 -7
  158. package/dist/team/__tests__/scaling.test.js.map +1 -1
  159. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +5 -5
  160. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +1 -1
  161. package/dist/team/__tests__/tmux-session.test.js +12 -12
  162. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  163. package/dist/team/__tests__/worker-bootstrap.test.js +57 -9
  164. package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
  165. package/dist/team/__tests__/worker-runtime-identity.test.js +3 -3
  166. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  167. package/dist/team/model-contract.d.ts +1 -1
  168. package/dist/team/model-contract.d.ts.map +1 -1
  169. package/dist/team/runtime.d.ts.map +1 -1
  170. package/dist/team/runtime.js +11 -0
  171. package/dist/team/runtime.js.map +1 -1
  172. package/dist/team/worker-bootstrap.d.ts +2 -0
  173. package/dist/team/worker-bootstrap.d.ts.map +1 -1
  174. package/dist/team/worker-bootstrap.js +6 -1
  175. package/dist/team/worker-bootstrap.js.map +1 -1
  176. package/dist/ultragoal/__tests__/artifacts.test.js +68 -1
  177. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  178. package/dist/ultragoal/artifacts.d.ts +5 -1
  179. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  180. package/dist/ultragoal/artifacts.js +41 -9
  181. package/dist/ultragoal/artifacts.js.map +1 -1
  182. package/dist/utils/__tests__/agents-model-table.test.js +7 -7
  183. package/dist/utils/__tests__/agents-model-table.test.js.map +1 -1
  184. package/dist/utils/__tests__/worktree-tool-context.test.d.ts +2 -0
  185. package/dist/utils/__tests__/worktree-tool-context.test.d.ts.map +1 -0
  186. package/dist/utils/__tests__/worktree-tool-context.test.js +118 -0
  187. package/dist/utils/__tests__/worktree-tool-context.test.js.map +1 -0
  188. package/dist/utils/worktree-tool-context.d.ts +25 -0
  189. package/dist/utils/worktree-tool-context.d.ts.map +1 -0
  190. package/dist/utils/worktree-tool-context.js +116 -0
  191. package/dist/utils/worktree-tool-context.js.map +1 -0
  192. package/package.json +1 -1
  193. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  194. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +96 -10
  195. package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +1 -1
  196. package/plugins/oh-my-codex/skills/code-review/SKILL.md +1 -1
  197. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +5 -5
  198. package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +3 -3
  199. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  200. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  201. package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +2 -2
  202. package/prompts/prometheus-strict-metis.md +1 -1
  203. package/skills/ai-slop-cleaner/SKILL.md +1 -1
  204. package/skills/code-review/SKILL.md +1 -1
  205. package/skills/ecomode/references/agent-tiers.md +1 -1
  206. package/skills/omx-setup/SKILL.md +5 -5
  207. package/skills/prometheus-strict/SKILL.md +3 -3
  208. package/skills/ralplan/SKILL.md +1 -1
  209. package/skills/team/SKILL.md +1 -1
  210. package/skills/ultrawork/references/agent-tiers.md +2 -2
  211. package/src/scripts/__tests__/codex-native-hook.test.ts +477 -0
  212. package/src/scripts/codex-native-hook.ts +272 -11
@@ -0,0 +1,135 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { describe, it } from "node:test";
8
+ const testDir = join(fileURLToPath(import.meta.url), "..");
9
+ const repoRoot = join(testDir, "..", "..", "..");
10
+ const omxBin = join(repoRoot, "dist", "cli", "omx.js");
11
+ async function withTempDir(run) {
12
+ const dir = await mkdtemp(join(tmpdir(), "omx-capabilities-cli-"));
13
+ try {
14
+ return await run(dir);
15
+ }
16
+ finally {
17
+ await rm(dir, { recursive: true, force: true });
18
+ }
19
+ }
20
+ function runOmx(cwd, args) {
21
+ return spawnSync(process.execPath, [omxBin, ...args], { cwd, encoding: "utf8" });
22
+ }
23
+ async function writeObservations(dir, observations) {
24
+ const path = join(dir, "observations.json");
25
+ await writeFile(path, JSON.stringify({ version: 1, kind: "omx_capability_observations", observations }, null, 2), "utf8");
26
+ return path;
27
+ }
28
+ const SUCCESS_OBSERVATIONS = [
29
+ {
30
+ fixture_id: "project-memory-write-required-arg",
31
+ tool_calls: [{ name: "project_memory_write", arguments: { memory: { note: "capability preflight fixture" } } }],
32
+ },
33
+ {
34
+ fixture_id: "project-memory-read-known-tool",
35
+ tool_calls: [{ name: "project_memory_read", arguments: {} }],
36
+ },
37
+ {
38
+ fixture_id: "tool-restraint-no-call",
39
+ tool_calls: [],
40
+ },
41
+ ];
42
+ function hasFailure(stdout, code) {
43
+ const parsed = JSON.parse(stdout);
44
+ return parsed.failures?.some((failure) => failure.code === code) ?? false;
45
+ }
46
+ describe("omx capabilities cli", () => {
47
+ it("routes local help", async () => {
48
+ await withTempDir(async (dir) => {
49
+ const result = runOmx(dir, ["capabilities", "--help"]);
50
+ assert.equal(result.status, 0);
51
+ assert.match(result.stdout, /omx capabilities lock/);
52
+ assert.doesNotMatch(result.stdout, /Launch Codex CLI/);
53
+ });
54
+ });
55
+ it("locks and checks with JSON output", async () => {
56
+ await withTempDir(async (dir) => {
57
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
58
+ assert.equal(lock.status, 0, lock.stderr);
59
+ const lockJson = JSON.parse(lock.stdout);
60
+ assert.equal(lockJson.ok, true);
61
+ assert.match(lockJson.lockfile, /omx-capabilities\.lock\.json$/);
62
+ const check = runOmx(dir, ["capabilities", "check", "--json"]);
63
+ assert.equal(check.status, 0, check.stderr);
64
+ const checkJson = JSON.parse(check.stdout);
65
+ assert.equal(checkJson.ok, true);
66
+ assert.equal(checkJson.observations_checked, false);
67
+ });
68
+ });
69
+ it("checks successful fixture observations", async () => {
70
+ await withTempDir(async (dir) => {
71
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
72
+ assert.equal(lock.status, 0, lock.stderr);
73
+ const observations = await writeObservations(dir, SUCCESS_OBSERVATIONS);
74
+ const check = runOmx(dir, ["capabilities", "check", "--observations", observations, "--require-observations", "--json"]);
75
+ assert.equal(check.status, 0, check.stderr);
76
+ const checkJson = JSON.parse(check.stdout);
77
+ assert.equal(checkJson.ok, true);
78
+ assert.equal(checkJson.observations_checked, true);
79
+ });
80
+ });
81
+ it("exits non-zero for required missing observations", async () => {
82
+ await withTempDir(async (dir) => {
83
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
84
+ assert.equal(lock.status, 0, lock.stderr);
85
+ const check = runOmx(dir, ["capabilities", "check", "--require-observations", "--json"]);
86
+ assert.notEqual(check.status, 0);
87
+ const checkJson = JSON.parse(check.stdout);
88
+ assert.equal(checkJson.ok, false);
89
+ assert.ok(checkJson.failures.some((failure) => failure.code === "observations_missing"));
90
+ });
91
+ });
92
+ it("reports missing required tool arguments", async () => {
93
+ await withTempDir(async (dir) => {
94
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
95
+ assert.equal(lock.status, 0, lock.stderr);
96
+ const observations = await writeObservations(dir, [
97
+ { fixture_id: "project-memory-write-required-arg", tool_calls: [{ name: "project_memory_write", arguments: {} }] },
98
+ SUCCESS_OBSERVATIONS[1],
99
+ SUCCESS_OBSERVATIONS[2],
100
+ ]);
101
+ const check = runOmx(dir, ["capabilities", "check", "--observations", observations, "--json"]);
102
+ assert.notEqual(check.status, 0);
103
+ assert.equal(hasFailure(check.stdout, "missing_required_arg"), true);
104
+ });
105
+ });
106
+ it("reports hallucinated tools", async () => {
107
+ await withTempDir(async (dir) => {
108
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
109
+ assert.equal(lock.status, 0, lock.stderr);
110
+ const observations = await writeObservations(dir, [
111
+ SUCCESS_OBSERVATIONS[0],
112
+ { fixture_id: "project-memory-read-known-tool", tool_calls: [{ name: "imaginary_tool", arguments: {} }] },
113
+ SUCCESS_OBSERVATIONS[2],
114
+ ]);
115
+ const check = runOmx(dir, ["capabilities", "check", "--observations", observations, "--json"]);
116
+ assert.notEqual(check.status, 0);
117
+ assert.equal(hasFailure(check.stdout, "hallucinated_tool"), true);
118
+ });
119
+ });
120
+ it("reports tool-restraint violations", async () => {
121
+ await withTempDir(async (dir) => {
122
+ const lock = runOmx(dir, ["capabilities", "lock", "--json"]);
123
+ assert.equal(lock.status, 0, lock.stderr);
124
+ const observations = await writeObservations(dir, [
125
+ SUCCESS_OBSERVATIONS[0],
126
+ SUCCESS_OBSERVATIONS[1],
127
+ { fixture_id: "tool-restraint-no-call", tool_calls: [{ name: "project_memory_read", arguments: {} }] },
128
+ ]);
129
+ const check = runOmx(dir, ["capabilities", "check", "--observations", observations, "--json"]);
130
+ assert.notEqual(check.status, 0);
131
+ assert.equal(hasFailure(check.stdout, "unexpected_tool_call"), true);
132
+ });
133
+ });
134
+ });
135
+ //# sourceMappingURL=capabilities.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.test.js","sourceRoot":"","sources":["../../../src/cli/__tests__/capabilities.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;AAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACjD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAEvD,KAAK,UAAU,WAAW,CAAI,GAAgC;IAC5D,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC;IACnE,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED,SAAS,MAAM,CAAC,GAAW,EAAE,IAAc;IACzC,OAAO,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;AACnF,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,GAAW,EAAE,YAAuB;IACnE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IAC5C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,6BAA6B,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC1H,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,oBAAoB,GAAG;IAC3B;QACE,UAAU,EAAE,mCAAmC;QAC/C,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,8BAA8B,EAAE,EAAE,EAAE,CAAC;KAChH;IACD;QACE,UAAU,EAAE,gCAAgC;QAC5C,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;KAC7D;IACD;QACE,UAAU,EAAE,wBAAwB;QACpC,UAAU,EAAE,EAAE;KACf;CACF,CAAC;AAEF,SAAS,UAAU,CAAC,MAAc,EAAE,IAAY;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAA4C,CAAC;IAC7E,OAAO,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,CAAC;AAC5E,CAAC;AAED,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACjC,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAC;YAEvD,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;YACrD,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAChC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,+BAA+B,CAAC,CAAC;YAEjE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC/D,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;YAExE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,wBAAwB,EAAE,QAAQ,CAAC,CAAC,CAAC;YAEzH,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAE1C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,wBAAwB,EAAE,QAAQ,CAAC,CAAC,CAAC;YAEzF,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YACjC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3C,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAClC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAyB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,sBAAsB,CAAC,CAAC,CAAC;QAC7G,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE;gBAChD,EAAE,UAAU,EAAE,mCAAmC,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE;gBAClH,oBAAoB,CAAC,CAAC,CAAC;gBACvB,oBAAoB,CAAC,CAAC,CAAC;aACxB,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC;YAE/F,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YACjC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE;gBAChD,oBAAoB,CAAC,CAAC,CAAC;gBACvB,EAAE,UAAU,EAAE,gCAAgC,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE;gBACzG,oBAAoB,CAAC,CAAC,CAAC;aACxB,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC;YAE/F,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YACjC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAAE,IAAI,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE;gBAChD,oBAAoB,CAAC,CAAC,CAAC;gBACvB,oBAAoB,CAAC,CAAC,CAAC;gBACvB,EAAE,UAAU,EAAE,wBAAwB,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE;aACvG,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC;YAE/F,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YACjC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAAE,IAAI,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -148,6 +148,8 @@ async function assertPluginHookLaunchesPostCompactFromCache() {
148
148
  OMX_ROOT: join(cacheRoot, '.omx-root'),
149
149
  OMX_SESSION_ID: 'omx-plugin-hook-postcompact-smoke',
150
150
  OMX_SOURCE_CWD: cacheRoot,
151
+ OMX_ENTRY_PATH: omxBin,
152
+ OMX_CODEX_LAUNCH_ID: 'omx-plugin-hook-postcompact-smoke-launch',
151
153
  OMX_STARTUP_CWD: cacheRoot,
152
154
  },
153
155
  });
@@ -197,6 +199,8 @@ async function assertPluginHookDelegatesPostCompactToPinnedCommand() {
197
199
  OMX_ROOT: join(cacheRoot, '.omx-root'),
198
200
  OMX_SESSION_ID: 'omx-plugin-hook-postcompact-delegate',
199
201
  OMX_SOURCE_CWD: cacheRoot,
202
+ OMX_ENTRY_PATH: omxBin,
203
+ OMX_CODEX_LAUNCH_ID: 'omx-plugin-hook-postcompact-delegate-launch',
200
204
  OMX_STARTUP_CWD: cacheRoot,
201
205
  },
202
206
  });
@@ -254,10 +258,25 @@ async function withPluginCacheCopy(run) {
254
258
  }
255
259
  function pluginHookEnv(overrides = {}) {
256
260
  const env = { ...process.env };
257
- for (const key of ['OMX_TEAM_STATE_ROOT', 'OMX_ROOT', 'OMX_STATE_ROOT', 'OMX_SESSION_ID', 'CODEX_SESSION_ID']) {
261
+ for (const key of [
262
+ 'OMX_TEAM_STATE_ROOT',
263
+ 'OMX_ROOT',
264
+ 'OMX_STATE_ROOT',
265
+ 'OMX_SESSION_ID',
266
+ 'CODEX_SESSION_ID',
267
+ 'OMX_ENTRY_PATH',
268
+ 'OMX_CODEX_LAUNCH_ID',
269
+ 'OMX_STARTUP_CWD',
270
+ ]) {
258
271
  delete env[key];
259
272
  }
260
- return { ...env, ...overrides };
273
+ return {
274
+ ...env,
275
+ OMX_ENTRY_PATH: omxBin,
276
+ OMX_CODEX_LAUNCH_ID: 'omx-plugin-layout-launch',
277
+ OMX_STARTUP_CWD: root,
278
+ ...overrides,
279
+ };
261
280
  }
262
281
  function runPluginNativeHook(cachePluginRoot, input, env = {}) {
263
282
  return spawnSync(process.execPath, [join(cachePluginRoot, 'hooks', 'codex-native-hook.mjs')], {
@@ -336,6 +355,97 @@ describe('official Codex plugin layout', () => {
336
355
  assert.match(launcher, /return \{ \.\.\.options, shell: true, windowsHide: true \};/);
337
356
  assert.doesNotMatch(launcher, /shell:\s*process\.platform === 'win32'/);
338
357
  });
358
+ it('no-ops plugin hooks when Codex was not launched through omx', async () => {
359
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
360
+ const calledPath = join(cacheRoot, 'called.txt');
361
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'record-called.cmd' : 'record-called.sh');
362
+ if (process.platform === 'win32') {
363
+ await writeFile(commandPath, `@echo off\r\necho called > "${calledPath}"\r\necho {}\r\n`, 'utf-8');
364
+ }
365
+ else {
366
+ await writeFile(commandPath, `#!/bin/sh\necho called > "${calledPath}"\nprintf '{}\\n'\n`, 'utf-8');
367
+ await chmod(commandPath, 0o755);
368
+ }
369
+ const userPrompt = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt: '$ralplan smoke' }), {
370
+ OMX_ENTRY_PATH: '',
371
+ OMX_CODEX_LAUNCH_ID: '',
372
+ OMX_NATIVE_HOOK_COMMAND: commandPath,
373
+ });
374
+ assert.equal(userPrompt.status, 0, userPrompt.stderr || userPrompt.stdout);
375
+ assert.equal(userPrompt.stdout, '');
376
+ await assert.rejects(readFile(calledPath, 'utf-8'), { code: 'ENOENT' });
377
+ const stop = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'Stop', session_id: 'plain-codex-stop' }), {
378
+ OMX_ENTRY_PATH: '',
379
+ OMX_CODEX_LAUNCH_ID: '',
380
+ OMX_NATIVE_HOOK_COMMAND: commandPath,
381
+ });
382
+ assert.equal(stop.status, 0, stop.stderr || stop.stdout);
383
+ assert.deepEqual(parseSingleJsonStdout(stop.stdout), {});
384
+ await assert.rejects(readFile(calledPath, 'utf-8'), { code: 'ENOENT' });
385
+ const oversizedUserPrompt = runPluginNativeHook(cachePluginRoot, JSON.stringify({
386
+ hook_event_name: 'UserPromptSubmit',
387
+ prompt: 'x'.repeat(2 * 1024 * 1024),
388
+ }), {
389
+ OMX_ENTRY_PATH: '',
390
+ OMX_CODEX_LAUNCH_ID: '',
391
+ OMX_NATIVE_HOOK_COMMAND: commandPath,
392
+ });
393
+ assert.equal(oversizedUserPrompt.error, undefined, oversizedUserPrompt.error?.message ?? '');
394
+ assert.equal(oversizedUserPrompt.status, 0, oversizedUserPrompt.stderr || oversizedUserPrompt.stdout);
395
+ assert.equal(oversizedUserPrompt.stdout, '');
396
+ await assert.rejects(readFile(calledPath, 'utf-8'), { code: 'ENOENT' });
397
+ const oversizedStop = runPluginNativeHook(cachePluginRoot, JSON.stringify({
398
+ hook_event_name: 'Stop',
399
+ session_id: 'plain-codex-oversized-stop',
400
+ padding: 'x'.repeat(2 * 1024 * 1024),
401
+ }), {
402
+ OMX_ENTRY_PATH: '',
403
+ OMX_CODEX_LAUNCH_ID: '',
404
+ OMX_NATIVE_HOOK_COMMAND: commandPath,
405
+ });
406
+ assert.equal(oversizedStop.error, undefined, oversizedStop.error?.message ?? '');
407
+ assert.equal(oversizedStop.status, 0, oversizedStop.stderr || oversizedStop.stdout);
408
+ assert.deepEqual(parseSingleJsonStdout(oversizedStop.stdout), {});
409
+ await assert.rejects(readFile(calledPath, 'utf-8'), { code: 'ENOENT' });
410
+ });
411
+ });
412
+ it('no-ops plugin hooks for nested plain Codex sessions that inherit omx launch env', async () => {
413
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
414
+ const calledPath = join(cacheRoot, 'called.txt');
415
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'record-inherited-called.cmd' : 'record-inherited-called.sh');
416
+ if (process.platform === 'win32') {
417
+ await writeFile(commandPath, `@echo off\r\necho %* > "${calledPath}"\r\necho {}\r\n`, 'utf-8');
418
+ }
419
+ else {
420
+ await writeFile(commandPath, `#!/bin/sh\necho "$@" > "${calledPath}"\nprintf '{}\\n'\n`, 'utf-8');
421
+ await chmod(commandPath, 0o755);
422
+ }
423
+ const inheritedEnv = {
424
+ OMX_ROOT: join(cacheRoot, '.omx-root'),
425
+ OMX_ENTRY_PATH: omxBin,
426
+ OMX_CODEX_LAUNCH_ID: 'inherited-launch-token',
427
+ OMX_NATIVE_HOOK_COMMAND: commandPath,
428
+ };
429
+ const owner = runPluginNativeHook(cachePluginRoot, JSON.stringify({
430
+ hook_event_name: 'UserPromptSubmit',
431
+ session_id: 'owner-codex-session',
432
+ session_pid: 111,
433
+ prompt: '$ralplan smoke',
434
+ }), inheritedEnv);
435
+ assert.equal(owner.status, 0, owner.stderr || owner.stdout);
436
+ assert.equal((await readFile(calledPath, 'utf-8')).trim(), 'codex-native-hook');
437
+ await rm(calledPath, { force: true });
438
+ const nestedPlainCodex = runPluginNativeHook(cachePluginRoot, JSON.stringify({
439
+ hook_event_name: 'UserPromptSubmit',
440
+ session_id: 'nested-plain-codex-session',
441
+ session_pid: 222,
442
+ prompt: '$ralplan nested',
443
+ }), inheritedEnv);
444
+ assert.equal(nestedPlainCodex.status, 0, nestedPlainCodex.stderr || nestedPlainCodex.stdout);
445
+ assert.equal(nestedPlainCodex.stdout, '');
446
+ await assert.rejects(readFile(calledPath, 'utf-8'), { code: 'ENOENT' });
447
+ });
448
+ });
339
449
  it('emits Stop JSON when the plugin hook pinned launcher is invalid', async () => {
340
450
  await withPluginCacheCopy(async (cachePluginRoot) => {
341
451
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
@@ -428,6 +538,125 @@ exit 7
428
538
  assert.doesNotMatch(result.stdout, /plugin_stop_hook_launcher/);
429
539
  });
430
540
  });
541
+ it('preserves valid pretty-printed Stop child JSON', async () => {
542
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
543
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'pretty-valid-json.cmd' : 'pretty-valid-json.sh');
544
+ if (process.platform === 'win32') {
545
+ await writeFile(commandPath, '@echo off\r\necho {\r\necho "decision": "block",\r\necho "stopReason": "pretty_child_json"\r\necho }\r\nexit /b 0\r\n', 'utf-8');
546
+ }
547
+ else {
548
+ await writeFile(commandPath, `#!/bin/sh
549
+ printf '{\n'
550
+ printf ' "decision": "block",\n'
551
+ printf ' "stopReason": "pretty_child_json"\n'
552
+ printf '}\n'
553
+ `, 'utf-8');
554
+ await chmod(commandPath, 0o755);
555
+ }
556
+ const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'Stop', session_id: 'sess-plugin-pretty-valid-json-stop' }), { OMX_NATIVE_HOOK_COMMAND: commandPath });
557
+ assert.equal(result.status, 0, result.stderr || result.stdout);
558
+ const output = parseSingleJsonStdout(result.stdout);
559
+ assert.equal(output.decision, 'block');
560
+ assert.equal(output.stopReason, 'pretty_child_json');
561
+ assert.doesNotMatch(result.stdout, /plugin_stop_hook_launcher_invalid_stdout/);
562
+ });
563
+ });
564
+ it('preserves valid Stop child JSON when stdout includes prior launcher noise', async () => {
565
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
566
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'noisy-valid-json.cmd' : 'noisy-valid-json.sh');
567
+ if (process.platform === 'win32') {
568
+ await writeFile(commandPath, '@echo off\r\necho runtime notice before json\r\necho {"decision":"block","stopReason":"autopilot_ultragoal"}\r\nexit /b 0\r\n', 'utf-8');
569
+ }
570
+ else {
571
+ await writeFile(commandPath, `#!/bin/sh
572
+ printf 'runtime notice before json\n'
573
+ printf '{"decision":"block","stopReason":"autopilot_ultragoal"}\n'
574
+ `, 'utf-8');
575
+ await chmod(commandPath, 0o755);
576
+ }
577
+ const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({
578
+ hook_event_name: 'Stop',
579
+ session_id: 'omx-1783508412223-c32f1l',
580
+ cwd: cacheRoot,
581
+ }), { OMX_NATIVE_HOOK_COMMAND: commandPath });
582
+ assert.equal(result.status, 0, result.stderr || result.stdout);
583
+ assert.doesNotMatch(result.stdout, /runtime notice before json/);
584
+ const output = parseSingleJsonStdout(result.stdout);
585
+ assert.equal(output.decision, 'block');
586
+ assert.equal(output.stopReason, 'autopilot_ultragoal');
587
+ assert.doesNotMatch(result.stdout, /plugin_stop_hook_launcher_invalid_stdout/);
588
+ });
589
+ });
590
+ it('rejects Stop child JSON when later stdout noise follows it', async () => {
591
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
592
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'valid-json-trailing-noise.cmd' : 'valid-json-trailing-noise.sh');
593
+ if (process.platform === 'win32') {
594
+ await writeFile(commandPath, '@echo off\r\necho {"decision":"block","stopReason":"child_valid_json"}\r\necho trailing runtime noise\r\nexit /b 0\r\n', 'utf-8');
595
+ }
596
+ else {
597
+ await writeFile(commandPath, `#!/bin/sh
598
+ printf '{"decision":"block","stopReason":"child_valid_json"}\n'
599
+ printf 'trailing runtime noise\n'
600
+ `, 'utf-8');
601
+ await chmod(commandPath, 0o755);
602
+ }
603
+ const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'Stop', session_id: 'sess-plugin-trailing-noise-stop' }), { OMX_NATIVE_HOOK_COMMAND: commandPath });
604
+ assert.equal(result.status, 0, result.stderr || result.stdout);
605
+ assert.doesNotMatch(result.stdout, /trailing runtime noise/);
606
+ const output = parseSingleJsonStdout(result.stdout);
607
+ assert.equal(output.decision, 'block');
608
+ assert.equal(output.stopReason, 'plugin_stop_hook_launcher_invalid_stdout');
609
+ });
610
+ });
611
+ it('rejects Stop child JSON when an earlier JSON decision is followed by noise and final JSON', async () => {
612
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
613
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'json-noise-json.cmd' : 'json-noise-json.sh');
614
+ if (process.platform === 'win32') {
615
+ await writeFile(commandPath, '@echo off\r\necho {"decision":"block","stopReason":"first_json"}\r\necho trailing runtime noise\r\necho {"decision":"block","stopReason":"second_json"}\r\nexit /b 0\r\n', 'utf-8');
616
+ }
617
+ else {
618
+ await writeFile(commandPath, `#!/bin/sh
619
+ printf '{"decision":"block","stopReason":"first_json"}\n'
620
+ printf 'trailing runtime noise\n'
621
+ printf '{"decision":"block","stopReason":"second_json"}\n'
622
+ `, 'utf-8');
623
+ await chmod(commandPath, 0o755);
624
+ }
625
+ const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'Stop', session_id: 'sess-plugin-json-noise-json-stop' }), { OMX_NATIVE_HOOK_COMMAND: commandPath });
626
+ assert.equal(result.status, 0, result.stderr || result.stdout);
627
+ assert.doesNotMatch(result.stdout, /first_json/);
628
+ assert.doesNotMatch(result.stdout, /second_json/);
629
+ assert.doesNotMatch(result.stdout, /trailing runtime noise/);
630
+ const output = parseSingleJsonStdout(result.stdout);
631
+ assert.equal(output.decision, 'block');
632
+ assert.equal(output.stopReason, 'plugin_stop_hook_launcher_invalid_stdout');
633
+ });
634
+ });
635
+ it('rejects pretty Stop child JSON followed by final JSON log noise', async () => {
636
+ await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
637
+ const commandPath = join(cacheRoot, process.platform === 'win32' ? 'pretty-json-final-log.cmd' : 'pretty-json-final-log.sh');
638
+ if (process.platform === 'win32') {
639
+ await writeFile(commandPath, '@echo off\r\necho {\r\necho "decision": "block",\r\necho "stopReason": "real_block"\r\necho }\r\necho {"level":"info"}\r\nexit /b 0\r\n', 'utf-8');
640
+ }
641
+ else {
642
+ await writeFile(commandPath, `#!/bin/sh
643
+ printf '{\n'
644
+ printf ' "decision": "block",\n'
645
+ printf ' "stopReason": "real_block"\n'
646
+ printf '}\n'
647
+ printf '{"level":"info"}\n'
648
+ `, 'utf-8');
649
+ await chmod(commandPath, 0o755);
650
+ }
651
+ const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({ hook_event_name: 'Stop', session_id: 'sess-plugin-pretty-json-final-log-stop' }), { OMX_NATIVE_HOOK_COMMAND: commandPath });
652
+ assert.equal(result.status, 0, result.stderr || result.stdout);
653
+ assert.doesNotMatch(result.stdout, /real_block/);
654
+ assert.doesNotMatch(result.stdout, /level/);
655
+ const output = parseSingleJsonStdout(result.stdout);
656
+ assert.equal(output.decision, 'block');
657
+ assert.equal(output.stopReason, 'plugin_stop_hook_launcher_invalid_stdout');
658
+ });
659
+ });
431
660
  it('replaces oversized Stop child stdout with fallback Stop JSON', async () => {
432
661
  await withPluginCacheCopy(async (cachePluginRoot, cacheRoot) => {
433
662
  const commandPath = join(cacheRoot, process.platform === 'win32' ? 'oversized-stop-stdout.cmd' : 'oversized-stop-stdout.sh');
@@ -451,7 +680,7 @@ head -c 1100000 /dev/zero | tr '\0' x
451
680
  it('emits Stop JSON for malformed Stop-looking stdin before invalid launcher failure', async () => {
452
681
  await withPluginCacheCopy(async (cachePluginRoot) => {
453
682
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
454
- const result = runPluginNativeHook(cachePluginRoot, '{"hook_event_name":"Stop",');
683
+ const result = runPluginNativeHook(cachePluginRoot, '{"hook_event_name":"Stop","session_id":"sess-plugin-malformed-stop",');
455
684
  assert.equal(result.status, 0, result.stderr || result.stdout);
456
685
  const output = parseSingleJsonStdout(result.stdout);
457
686
  assert.equal(output.decision, 'block');
@@ -461,7 +690,7 @@ head -c 1100000 /dev/zero | tr '\0' x
461
690
  it('emits Stop JSON for the core-supported name alias before invalid launcher failure', async () => {
462
691
  await withPluginCacheCopy(async (cachePluginRoot) => {
463
692
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
464
- const result = runPluginNativeHook(cachePluginRoot, '{"name":"Stop",');
693
+ const result = runPluginNativeHook(cachePluginRoot, '{"name":"Stop","session_id":"sess-plugin-malformed-name-stop",');
465
694
  assert.equal(result.status, 0, result.stderr || result.stdout);
466
695
  const output = parseSingleJsonStdout(result.stdout);
467
696
  assert.equal(output.decision, 'block');
@@ -494,6 +723,7 @@ head -c 1100000 /dev/zero | tr '\0' x
494
723
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
495
724
  const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({
496
725
  hook_event_name: 'UserPromptSubmit',
726
+ session_id: 'sess-plugin-invalid-launcher-user-prompt',
497
727
  prompt: 'hello',
498
728
  }));
499
729
  assert.equal(result.status, 1);
@@ -531,6 +761,7 @@ head -c 1100000 /dev/zero | tr '\0' x
531
761
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
532
762
  const result = runPluginNativeHook(cachePluginRoot, JSON.stringify({
533
763
  hook_event_name: 'PreToolUse',
764
+ session_id: 'sess-plugin-nested-stop-text',
534
765
  tool_input: { name: 'Stop' },
535
766
  }));
536
767
  assert.equal(result.status, 1);
@@ -541,7 +772,7 @@ head -c 1100000 /dev/zero | tr '\0' x
541
772
  it('does not classify malformed non-Stop plugin JSON with nested Stop text as Stop', async () => {
542
773
  await withPluginCacheCopy(async (cachePluginRoot) => {
543
774
  await writeFile(join(cachePluginRoot, 'hooks', 'omx-command.json'), '{"command":', 'utf-8');
544
- const result = runPluginNativeHook(cachePluginRoot, '{"hook_event_name":"PreToolUse","tool_input":{"name":"Stop"},');
775
+ const result = runPluginNativeHook(cachePluginRoot, '{"hook_event_name":"PreToolUse","session_id":"sess-plugin-malformed-non-stop","tool_input":{"name":"Stop"},');
545
776
  assert.equal(result.status, 1);
546
777
  assert.equal(result.stdout, '');
547
778
  assert.match(result.stderr, /invalid plugin hook launcher/);
@@ -681,7 +912,7 @@ head -c 1100000 /dev/zero | tr '\0' x
681
912
  });
682
913
  it('fails oversized non-Stop plugin stdin without Stop JSON', async () => {
683
914
  await withPluginCacheCopy(async (cachePluginRoot) => {
684
- const result = runPluginNativeHook(cachePluginRoot, 'x'.repeat(1024 * 1024 + 1));
915
+ const result = runPluginNativeHook(cachePluginRoot, `{"hook_event_name":"UserPromptSubmit","session_id":"sess-plugin-oversized-non-stop","padding":"${'x'.repeat(1024 * 1024 + 1)}`);
685
916
  assert.equal(result.status, 1);
686
917
  assert.equal(result.stdout, '');
687
918
  assert.match(result.stderr, /plugin hook stdin exceeded/);