oh-my-codex 0.17.3 → 0.18.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 (158) hide show
  1. package/Cargo.lock +13 -5
  2. package/Cargo.toml +2 -1
  3. package/README.md +1 -0
  4. package/crates/omx-api/Cargo.toml +19 -0
  5. package/crates/omx-api/src/lib.rs +2940 -0
  6. package/crates/omx-api/src/main.rs +10 -0
  7. package/crates/omx-api/tests/cli.rs +558 -0
  8. package/crates/omx-explore/src/main.rs +4 -0
  9. package/crates/omx-sparkshell/src/codex_bridge.rs +437 -123
  10. package/crates/omx-sparkshell/src/exec.rs +4 -0
  11. package/crates/omx-sparkshell/src/main.rs +738 -29
  12. package/crates/omx-sparkshell/src/prompt.rs +25 -3
  13. package/crates/omx-sparkshell/src/redaction.rs +241 -0
  14. package/crates/omx-sparkshell/tests/execution.rs +479 -238
  15. package/dist/cli/__tests__/api.test.d.ts +2 -0
  16. package/dist/cli/__tests__/api.test.d.ts.map +1 -0
  17. package/dist/cli/__tests__/api.test.js +175 -0
  18. package/dist/cli/__tests__/api.test.js.map +1 -0
  19. package/dist/cli/__tests__/ask.test.js +72 -5
  20. package/dist/cli/__tests__/ask.test.js.map +1 -1
  21. package/dist/cli/__tests__/autoresearch-goal.test.js +14 -1
  22. package/dist/cli/__tests__/autoresearch-goal.test.js.map +1 -1
  23. package/dist/cli/__tests__/explore.test.js +23 -0
  24. package/dist/cli/__tests__/explore.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +123 -5
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/launch-fallback.test.js +76 -0
  28. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  29. package/dist/cli/__tests__/package-bin-contract.test.js +4 -3
  30. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  31. package/dist/cli/__tests__/setup-install-mode.test.js +138 -0
  32. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  33. package/dist/cli/__tests__/sparkshell-cli.test.js +5 -0
  34. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  35. package/dist/cli/__tests__/version-sync-contract.test.js +4 -0
  36. package/dist/cli/__tests__/version-sync-contract.test.js.map +1 -1
  37. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  38. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  39. package/dist/cli/api.d.ts +26 -0
  40. package/dist/cli/api.d.ts.map +1 -0
  41. package/dist/cli/api.js +153 -0
  42. package/dist/cli/api.js.map +1 -0
  43. package/dist/cli/explore.d.ts +2 -0
  44. package/dist/cli/explore.d.ts.map +1 -1
  45. package/dist/cli/explore.js +43 -1
  46. package/dist/cli/explore.js.map +1 -1
  47. package/dist/cli/index.d.ts +10 -4
  48. package/dist/cli/index.d.ts.map +1 -1
  49. package/dist/cli/index.js +128 -10
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/native-assets.d.ts +2 -1
  52. package/dist/cli/native-assets.d.ts.map +1 -1
  53. package/dist/cli/native-assets.js +1 -0
  54. package/dist/cli/native-assets.js.map +1 -1
  55. package/dist/cli/sparkshell.d.ts.map +1 -1
  56. package/dist/cli/sparkshell.js +20 -3
  57. package/dist/cli/sparkshell.js.map +1 -1
  58. package/dist/config/generator.d.ts.map +1 -1
  59. package/dist/config/generator.js +90 -0
  60. package/dist/config/generator.js.map +1 -1
  61. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts +2 -0
  62. package/dist/hooks/__tests__/best-practice-research-skill.test.d.ts.map +1 -0
  63. package/dist/hooks/__tests__/best-practice-research-skill.test.js +27 -0
  64. package/dist/hooks/__tests__/best-practice-research-skill.test.js.map +1 -0
  65. package/dist/hooks/__tests__/keyword-detector.test.js +11 -0
  66. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  67. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +6 -0
  68. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  69. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +4 -0
  70. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  71. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  72. package/dist/hooks/keyword-registry.js +1 -0
  73. package/dist/hooks/keyword-registry.js.map +1 -1
  74. package/dist/hud/__tests__/reconcile.test.js +2 -2
  75. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  76. package/dist/hud/__tests__/tmux.test.js +23 -18
  77. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  78. package/dist/hud/tmux.d.ts.map +1 -1
  79. package/dist/hud/tmux.js +7 -6
  80. package/dist/hud/tmux.js.map +1 -1
  81. package/dist/mcp/__tests__/bootstrap.test.js +75 -1
  82. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  83. package/dist/mcp/bootstrap.d.ts +3 -1
  84. package/dist/mcp/bootstrap.d.ts.map +1 -1
  85. package/dist/mcp/bootstrap.js +71 -2
  86. package/dist/mcp/bootstrap.js.map +1 -1
  87. package/dist/scripts/__tests__/codex-native-hook.test.js +737 -26
  88. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  89. package/dist/scripts/__tests__/notify-dispatcher.test.js +183 -1
  90. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  91. package/dist/scripts/__tests__/smoke-packed-install.test.js +4 -1
  92. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  93. package/dist/scripts/build-api.d.ts +2 -0
  94. package/dist/scripts/build-api.d.ts.map +1 -0
  95. package/dist/scripts/build-api.js +44 -0
  96. package/dist/scripts/build-api.js.map +1 -0
  97. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  98. package/dist/scripts/codex-native-hook.js +208 -8
  99. package/dist/scripts/codex-native-hook.js.map +1 -1
  100. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  101. package/dist/scripts/codex-native-pre-post.js +89 -24
  102. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  103. package/dist/scripts/notify-dispatcher.js +88 -0
  104. package/dist/scripts/notify-dispatcher.js.map +1 -1
  105. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  106. package/dist/scripts/notify-hook/team-dispatch.js +27 -9
  107. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  108. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  109. package/dist/scripts/notify-hook/team-leader-nudge.js +26 -11
  110. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  111. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -0
  112. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  113. package/dist/scripts/notify-hook/team-tmux-guard.js +38 -0
  114. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  115. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  116. package/dist/scripts/notify-hook/team-worker-stop.js +27 -14
  117. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  118. package/dist/scripts/run-provider-advisor.js +9 -3
  119. package/dist/scripts/run-provider-advisor.js.map +1 -1
  120. package/dist/scripts/smoke-packed-install.d.ts +1 -1
  121. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  122. package/dist/scripts/smoke-packed-install.js +2 -0
  123. package/dist/scripts/smoke-packed-install.js.map +1 -1
  124. package/dist/team/__tests__/runtime.test.js +2 -2
  125. package/dist/team/__tests__/runtime.test.js.map +1 -1
  126. package/dist/team/__tests__/tmux-session.test.js +96 -19
  127. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  128. package/dist/team/tmux-session.d.ts +1 -0
  129. package/dist/team/tmux-session.d.ts.map +1 -1
  130. package/dist/team/tmux-session.js +34 -10
  131. package/dist/team/tmux-session.js.map +1 -1
  132. package/dist/verification/__tests__/ci-rust-gates.test.js +85 -10
  133. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  134. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +1 -0
  135. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  136. package/package.json +4 -3
  137. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  138. package/plugins/oh-my-codex/skills/best-practice-research/SKILL.md +83 -0
  139. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +1 -0
  140. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +1 -1
  141. package/prompts/researcher.md +15 -10
  142. package/skills/best-practice-research/SKILL.md +83 -0
  143. package/skills/deep-interview/SKILL.md +1 -0
  144. package/skills/ralplan/SKILL.md +1 -1
  145. package/src/scripts/__tests__/codex-native-hook.test.ts +810 -4
  146. package/src/scripts/__tests__/notify-dispatcher.test.ts +223 -1
  147. package/src/scripts/__tests__/smoke-packed-install.test.ts +8 -2
  148. package/src/scripts/build-api.ts +48 -0
  149. package/src/scripts/codex-native-hook.ts +262 -10
  150. package/src/scripts/codex-native-pre-post.ts +103 -24
  151. package/src/scripts/notify-dispatcher.ts +97 -0
  152. package/src/scripts/notify-hook/team-dispatch.ts +27 -8
  153. package/src/scripts/notify-hook/team-leader-nudge.ts +25 -11
  154. package/src/scripts/notify-hook/team-tmux-guard.ts +42 -0
  155. package/src/scripts/notify-hook/team-worker-stop.ts +24 -13
  156. package/src/scripts/run-provider-advisor.ts +11 -3
  157. package/src/scripts/smoke-packed-install.ts +2 -0
  158. package/templates/catalog-manifest.json +7 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync } from "node:fs";
3
+ import { chmodSync, existsSync } from "node:fs";
4
4
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
@@ -91,6 +91,228 @@ describe("notify dispatcher previousNotify guard", () => {
91
91
  }
92
92
  });
93
93
 
94
+ it("skips stale turn-ended wrappers whose previousNotify is an OMX dispatcher", () => {
95
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-wrapper-"));
96
+ try {
97
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
98
+ mkdirSync(oldPkgScripts, { recursive: true });
99
+ const stalePreviousMarker = join(wd, "stale-wrapper-ran");
100
+ const omxMarker = join(wd, "omx-ran");
101
+ const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
102
+ const turnEndedWrapper = join(wd, "SkyComputerUseClient");
103
+ const omxHook = join(wd, "current-notify-hook.js");
104
+ writeFileSync(
105
+ turnEndedWrapper,
106
+ `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
107
+ );
108
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
109
+ const metadataPath = join(wd, "notify-dispatch.json");
110
+ writeFileSync(
111
+ metadataPath,
112
+ JSON.stringify({
113
+ managedBy: "oh-my-codex",
114
+ version: 1,
115
+ previousNotify: [
116
+ process.execPath,
117
+ turnEndedWrapper,
118
+ "turn-ended",
119
+ "--previous-notify",
120
+ JSON.stringify([
121
+ process.execPath,
122
+ staleDispatcher,
123
+ "--metadata",
124
+ metadataPath,
125
+ ]),
126
+ ],
127
+ omxNotify: [process.execPath, omxHook],
128
+ }),
129
+ );
130
+
131
+ runDispatcher(metadataPath);
132
+
133
+ assert.equal(existsSync(stalePreviousMarker), false);
134
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
135
+ } finally {
136
+ rmSync(wd, { recursive: true, force: true });
137
+ }
138
+ });
139
+
140
+ it("skips stale turn-ended wrappers whose previousNotify text is an OMX hook", () => {
141
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-wrapper-text-"));
142
+ try {
143
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
144
+ mkdirSync(oldPkgScripts, { recursive: true });
145
+ const stalePreviousMarker = join(wd, "stale-wrapper-ran");
146
+ const omxMarker = join(wd, "omx-ran");
147
+ const staleHook = join(oldPkgScripts, "notify-hook.js");
148
+ const turnEndedWrapper = join(wd, "SkyComputerUseClient");
149
+ const omxHook = join(wd, "current-notify-hook.js");
150
+ writeFileSync(
151
+ turnEndedWrapper,
152
+ `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
153
+ );
154
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
155
+ const metadataPath = join(wd, "notify-dispatch.json");
156
+ writeFileSync(
157
+ metadataPath,
158
+ JSON.stringify({
159
+ managedBy: "oh-my-codex",
160
+ version: 1,
161
+ previousNotify: [
162
+ process.execPath,
163
+ turnEndedWrapper,
164
+ "turn-ended",
165
+ `--previous-notify=node ${staleHook}`,
166
+ ],
167
+ omxNotify: [process.execPath, omxHook],
168
+ }),
169
+ );
170
+
171
+ runDispatcher(metadataPath);
172
+
173
+ assert.equal(existsSync(stalePreviousMarker), false);
174
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
175
+ } finally {
176
+ rmSync(wd, { recursive: true, force: true });
177
+ }
178
+ });
179
+
180
+ it("skips SkyComputerUseClient wrappers with quoted nested previousNotify self-references", () => {
181
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-nested-wrapper-"));
182
+ try {
183
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
184
+ mkdirSync(oldPkgScripts, { recursive: true });
185
+ const stalePreviousMarker = join(wd, "nested-wrapper-ran");
186
+ const omxMarker = join(wd, "omx-ran");
187
+ const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
188
+ const turnEndedWrapper = join(wd, "SkyComputerUseClient");
189
+ const omxHook = join(wd, "current-notify-hook.js");
190
+ writeFileSync(
191
+ turnEndedWrapper,
192
+ `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
193
+ );
194
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
195
+ const metadataPath = join(wd, "notify-dispatch.json");
196
+ const nestedSelfReference = JSON.stringify([
197
+ process.execPath,
198
+ turnEndedWrapper,
199
+ "turn-ended",
200
+ "--previous-notify",
201
+ JSON.stringify([process.execPath, staleDispatcher, "--metadata", metadataPath]),
202
+ ]);
203
+ writeFileSync(
204
+ metadataPath,
205
+ JSON.stringify({
206
+ managedBy: "oh-my-codex",
207
+ version: 1,
208
+ previousNotify: [
209
+ process.execPath,
210
+ turnEndedWrapper,
211
+ "turn-ended",
212
+ "--previous-notify",
213
+ JSON.stringify(nestedSelfReference),
214
+ ],
215
+ omxNotify: [process.execPath, omxHook],
216
+ }),
217
+ );
218
+
219
+ runDispatcher(metadataPath);
220
+
221
+ assert.equal(existsSync(stalePreviousMarker), false);
222
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
223
+ } finally {
224
+ rmSync(wd, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ it("skips wrapper metadata objects with encoded OMX dispatcher payloads", () => {
229
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-object-wrapper-"));
230
+ try {
231
+ const oldPkgScripts = join(wd, "global", "oh-my-codex", "dist", "scripts");
232
+ mkdirSync(oldPkgScripts, { recursive: true });
233
+ const stalePreviousMarker = join(wd, "object-wrapper-ran");
234
+ const omxMarker = join(wd, "omx-ran");
235
+ const staleDispatcher = join(oldPkgScripts, "notify-dispatcher.js");
236
+ const turnEndedWrapper = join(wd, "SkyComputerUseClient");
237
+ const omxHook = join(wd, "current-notify-hook.js");
238
+ writeFileSync(
239
+ turnEndedWrapper,
240
+ `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "ran");\n`,
241
+ );
242
+ writeFileSync(omxHook, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(omxMarker)}, "ran");\n`);
243
+ const metadataPath = join(wd, "notify-dispatch.json");
244
+ writeFileSync(
245
+ metadataPath,
246
+ JSON.stringify({
247
+ managedBy: "oh-my-codex",
248
+ version: 1,
249
+ previousNotify: [
250
+ process.execPath,
251
+ turnEndedWrapper,
252
+ "turn-ended",
253
+ `--previous-notify=${JSON.stringify({ previousNotify: JSON.stringify([process.execPath, staleDispatcher]) })}`,
254
+ ],
255
+ omxNotify: [process.execPath, omxHook],
256
+ }),
257
+ );
258
+
259
+ runDispatcher(metadataPath);
260
+
261
+ assert.equal(existsSync(stalePreviousMarker), false);
262
+ assert.equal(readFileSync(omxMarker, "utf-8"), "ran");
263
+ } finally {
264
+ rmSync(wd, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ it("skips reporter-shaped SkyComputerUseClient previousNotify dispatcher recursion", () => {
269
+ const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-reporter-wrapper-"));
270
+ try {
271
+ const pkgScripts = join(wd, "pkg-without-managed-name", "dist", "scripts");
272
+ mkdirSync(pkgScripts, { recursive: true });
273
+ const stalePreviousMarker = join(wd, "skycomputer-ran");
274
+ const omxMarker = join(wd, "omx-ran");
275
+ const dispatcher = join(pkgScripts, "notify-dispatcher.js");
276
+ const turnEndedWrapper = join(wd, "SkyComputerUseClient");
277
+ const omxHook = join(wd, "current-notify-hook.js");
278
+ writeFileSync(dispatcher, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(stalePreviousMarker)}, "dispatcher");\n`);
279
+ writeFileSync(
280
+ turnEndedWrapper,
281
+ `#!/usr/bin/env node\nimport { appendFileSync } from "node:fs"; appendFileSync(${JSON.stringify(stalePreviousMarker)}, "wrapper\\n");\n`,
282
+ );
283
+ chmodSync(turnEndedWrapper, 0o755);
284
+ writeFileSync(omxHook, `import { appendFileSync } from "node:fs"; appendFileSync(${JSON.stringify(omxMarker)}, "omx\\n");\n`);
285
+ const metadataPath = join(wd, "notify-dispatch.json");
286
+ writeFileSync(
287
+ metadataPath,
288
+ JSON.stringify({
289
+ managedBy: "oh-my-codex",
290
+ version: 1,
291
+ previousNotify: [
292
+ turnEndedWrapper,
293
+ "turn-ended",
294
+ "--previous-notify",
295
+ JSON.stringify([
296
+ "node",
297
+ dispatcher,
298
+ "--metadata",
299
+ metadataPath,
300
+ ]),
301
+ ],
302
+ omxNotify: [process.execPath, omxHook],
303
+ dispatcherNotify: ["node", dispatcher, "--metadata", metadataPath],
304
+ }),
305
+ );
306
+
307
+ runDispatcher(metadataPath);
308
+
309
+ assert.equal(existsSync(stalePreviousMarker), false);
310
+ assert.equal(readFileSync(omxMarker, "utf-8"), "omx\n");
311
+ } finally {
312
+ rmSync(wd, { recursive: true, force: true });
313
+ }
314
+ });
315
+
94
316
  it("preserves and runs real user previousNotify entries", () => {
95
317
  const wd = mkdtempSync(join(tmpdir(), "omx-notify-dispatcher-user-"));
96
318
  try {
@@ -17,10 +17,16 @@ test('packed install smoke stays limited to boot + core commands', () => {
17
17
  assert.deepEqual(PACKED_INSTALL_SMOKE_CORE_COMMANDS, [
18
18
  ['--help'],
19
19
  ['version'],
20
+ ['api', '--help'],
21
+ ['sparkshell', '--help'],
20
22
  ]);
21
23
  assert.equal(
22
- PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('explore') || argv.includes('sparkshell')),
23
- false,
24
+ PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('api')),
25
+ true,
26
+ );
27
+ assert.equal(
28
+ PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('sparkshell')),
29
+ true,
24
30
  );
25
31
  });
26
32
 
@@ -0,0 +1,48 @@
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { arch, platform } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const projectRoot = join(__dirname, '..', '..');
10
+ const nativeRoot = join(projectRoot, 'crates', 'omx-api');
11
+ const manifestPath = process.env.OMX_API_MANIFEST ?? join(nativeRoot, 'Cargo.toml');
12
+ const binaryName = platform() === 'win32' ? 'omx-api.exe' : 'omx-api';
13
+ const releaseBinaryPath = join(projectRoot, 'target', 'release', binaryName);
14
+ const stagedBinaryRoot = process.env.OMX_API_STAGE_DIR
15
+ ? join(process.env.OMX_API_STAGE_DIR, `${platform()}-${arch()}`)
16
+ : join(projectRoot, 'bin', 'native', `${platform()}-${arch()}`);
17
+ const packagedBinaryDir = stagedBinaryRoot;
18
+ const packagedBinaryPath = join(packagedBinaryDir, binaryName);
19
+ const extraArgs = process.argv.slice(2);
20
+ const args = ['build', '--manifest-path', manifestPath, '--release', ...extraArgs];
21
+
22
+ if (!existsSync(manifestPath)) {
23
+ console.error(`omx api build: missing Rust manifest at ${manifestPath}`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = spawnSync('cargo', args, {
28
+ cwd: projectRoot,
29
+ stdio: 'inherit',
30
+ env: process.env,
31
+ });
32
+
33
+ if (result.error) {
34
+ console.error(`omx api build: failed to launch cargo: ${result.error.message}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
39
+
40
+ if (!existsSync(releaseBinaryPath)) {
41
+ console.error(`omx api build: expected release binary at ${releaseBinaryPath}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ mkdirSync(packagedBinaryDir, { recursive: true });
46
+ copyFileSync(releaseBinaryPath, packagedBinaryPath);
47
+ if (platform() !== 'win32') chmodSync(packagedBinaryPath, 0o755);
48
+ console.log(`omx api build: staged native binary at ${packagedBinaryPath}`);
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { closeSync, existsSync, openSync, readFileSync, readSync } from "fs";
3
- import { appendFile, mkdir, readFile, readdir, writeFile } from "fs/promises";
3
+ import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
4
4
  import { extname, join, relative, resolve } from "path";
5
5
  import { pathToFileURL } from "url";
6
6
  import { readModeState, readModeStateForActiveDecision, readModeStateForSession, updateModeState } from "../modes/base.js";
@@ -128,6 +128,7 @@ const TEAM_STOP_BLOCKING_TASK_STATUSES = new Set(["pending", "in_progress", "blo
128
128
  const TEAM_WORKER_TERMINAL_RUN_STATES = new Set(["done", "complete", "completed", "failed", "stopped", "cancelled"]);
129
129
  const NATIVE_STOP_STATE_FILE = "native-stop-state.json";
130
130
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_MAX_REPEATS = 8;
131
+ const RALPH_ORPHANED_STARTING_STALE_MS = 15 * 60_000;
131
132
  const ORDINARY_STOP_NO_PROGRESS_DEFAULT_IDLE_MS = 10 * 60_000;
132
133
  const ORDINARY_STOP_NO_PROGRESS_MAX_MESSAGE_LENGTH = 240;
133
134
  const STABLE_FINAL_RECOMMENDATION_PATTERNS = [
@@ -513,6 +514,52 @@ function isRalphStartingPhase(state: Record<string, unknown>): boolean {
513
514
  return safeString(state.current_phase ?? state.currentPhase).trim().toLowerCase() === "starting";
514
515
  }
515
516
 
517
+
518
+ function parseTimestampMs(value: unknown): number | null {
519
+ const text = safeString(value).trim();
520
+ if (!text) return null;
521
+ const ms = Date.parse(text);
522
+ return Number.isFinite(ms) ? ms : null;
523
+ }
524
+
525
+ function numericValue(value: unknown): number | null {
526
+ if (typeof value === "number" && Number.isFinite(value)) return value;
527
+ if (typeof value === "string" && value.trim()) {
528
+ const parsed = Number(value);
529
+ return Number.isFinite(parsed) ? parsed : null;
530
+ }
531
+ return null;
532
+ }
533
+
534
+ function hasRalphOwnerHint(state: Record<string, unknown>): boolean {
535
+ return [
536
+ state.owner_omx_session_id,
537
+ state.owner_codex_session_id,
538
+ state.owner_codex_thread_id,
539
+ state.thread_id,
540
+ state.tmux_pane_id,
541
+ state.task_slug,
542
+ ].some((value) => safeString(value).trim() !== "");
543
+ }
544
+
545
+ async function isStaleOrphanedRalphStartingState(
546
+ state: Record<string, unknown>,
547
+ path: string,
548
+ nowMs = Date.now(),
549
+ ): Promise<boolean> {
550
+ if (!isRalphStartingPhase(state)) return false;
551
+ if (numericValue(state.iteration) !== 0) return false;
552
+ if (hasRalphOwnerHint(state)) return false;
553
+
554
+ const timestampMs = parseTimestampMs(state.updated_at)
555
+ ?? parseTimestampMs(state.started_at)
556
+ ?? parseTimestampMs(state.created_at)
557
+ ?? await stat(path).then((info) => info.mtimeMs, () => null);
558
+ if (timestampMs === null) return false;
559
+
560
+ return nowMs - timestampMs > RALPH_ORPHANED_STARTING_STALE_MS;
561
+ }
562
+
516
563
  function hasValue(values: string[], value: string): boolean {
517
564
  return value !== "" && values.some((candidate) => candidate === value);
518
565
  }
@@ -597,6 +644,99 @@ async function hasConsistentRalphSkillActivation(stateDir: string, sessionId: st
597
644
  return true;
598
645
  }
599
646
 
647
+ function isShadowableRalphStartingSeed(state: Record<string, unknown>): boolean {
648
+ if (state.active !== true) return false;
649
+ if (!isRalphStartingPhase(state)) return false;
650
+ if (state.completion_audit || state.completionAudit) return false;
651
+ const iteration = numericValue(state.iteration);
652
+ return iteration === null || iteration <= 0;
653
+ }
654
+
655
+ function hasPassingCompletedRalphAudit(state: Record<string, unknown> | null, cwd: string): boolean {
656
+ if (!state) return false;
657
+ if (state.mode && safeString(state.mode) !== "ralph") return false;
658
+ if (!isRalphCompletePhase(state.current_phase ?? state.currentPhase)) return false;
659
+ if (state.active === true) return false;
660
+ return evaluateRalphCompletionAuditEvidence(state, cwd).complete === true;
661
+ }
662
+
663
+ function shouldRetireShadowedRalphStartingSeed(
664
+ seedState: Record<string, unknown>,
665
+ completedState: Record<string, unknown> | null,
666
+ cwd: string,
667
+ ownerContext?: {
668
+ completedSessionId?: string;
669
+ payloadSessionId?: string;
670
+ threadId?: string;
671
+ currentNativeSessionId?: string;
672
+ tmuxPaneId?: string;
673
+ },
674
+ ): boolean {
675
+ if (!isShadowableRalphStartingSeed(seedState)) return false;
676
+ if (!hasPassingCompletedRalphAudit(completedState, cwd)) return false;
677
+ if (!completedState) return false;
678
+
679
+ const completedSessionId = safeString(ownerContext?.completedSessionId ?? completedState.session_id).trim();
680
+ if (
681
+ completedSessionId
682
+ && !activeRalphStateMatchesStopOwner(completedState, {
683
+ sessionId: completedSessionId,
684
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
685
+ threadId: safeString(ownerContext?.threadId).trim(),
686
+ currentNativeSessionId: safeString(ownerContext?.currentNativeSessionId).trim(),
687
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
688
+ })
689
+ ) {
690
+ return false;
691
+ }
692
+
693
+ const seedThreadId = safeString(seedState.owner_codex_thread_id ?? seedState.thread_id).trim();
694
+ const completedThreadId = safeString(completedState?.owner_codex_thread_id ?? completedState?.thread_id).trim();
695
+ const stopThreadId = safeString(ownerContext?.threadId).trim();
696
+ if (seedThreadId && completedThreadId && seedThreadId !== completedThreadId) return false;
697
+ if (seedThreadId && stopThreadId && seedThreadId !== stopThreadId) return false;
698
+ if (completedThreadId && stopThreadId && completedThreadId !== stopThreadId) return false;
699
+
700
+ const seedPaneId = safeString(seedState.tmux_pane_id).trim();
701
+ const completedPaneId = safeString(completedState?.tmux_pane_id).trim();
702
+ const stopPaneId = safeString(ownerContext?.tmuxPaneId).trim();
703
+ if (seedPaneId && completedPaneId && seedPaneId !== completedPaneId) return false;
704
+ if (seedPaneId && stopPaneId && seedPaneId !== stopPaneId) return false;
705
+ if (completedPaneId && stopPaneId && completedPaneId !== stopPaneId) return false;
706
+
707
+ const seedStartedAt = parseTimestampMs(seedState.started_at ?? seedState.startedAt);
708
+ const completedAt = parseTimestampMs(completedState?.completed_at ?? completedState?.completedAt);
709
+ if (completedAt === null) return false;
710
+ if (seedStartedAt !== null && seedStartedAt > completedAt) return false;
711
+
712
+ return true;
713
+ }
714
+
715
+ async function retireShadowedRalphStartingSeed(
716
+ path: string,
717
+ seedState: Record<string, unknown>,
718
+ completedSessionId: string,
719
+ completedPath: string,
720
+ completedState: Record<string, unknown>,
721
+ ): Promise<void> {
722
+ const nowIso = new Date().toISOString();
723
+ const completedAt = safeString(completedState.completed_at ?? completedState.completedAt).trim() || nowIso;
724
+ const next: Record<string, unknown> = {
725
+ ...seedState,
726
+ active: false,
727
+ current_phase: "complete",
728
+ completed_at: completedAt,
729
+ stop_reason: "shadowed_by_completed_canonical_ralph",
730
+ shadowed_by_completed_canonical_ralph: {
731
+ session_id: completedSessionId,
732
+ state_path: completedPath,
733
+ completed_at: completedAt,
734
+ reconciled_at: nowIso,
735
+ },
736
+ };
737
+ await writeFile(path, JSON.stringify(next, null, 2));
738
+ }
739
+
600
740
 
601
741
  async function readRalphCompletionAuditBlockState(
602
742
  cwd: string,
@@ -686,6 +826,12 @@ async function readActiveRalphState(
686
826
  safeString(preferredSessionId).trim(),
687
827
  currentOmxSessionId,
688
828
  ].filter(Boolean))];
829
+ const completedCanonicalPath = currentOmxSessionId
830
+ ? getStateFilePath("ralph-state.json", cwd, currentOmxSessionId)
831
+ : "";
832
+ const completedCanonicalState = completedCanonicalPath
833
+ ? await readJsonIfExists(completedCanonicalPath)
834
+ : null;
689
835
 
690
836
  // Ralph Stop stays authoritative-scope-only once the Stop payload is session-bound.
691
837
  // That is intentionally stricter than generic state MCP reads: do not scan sibling
@@ -699,12 +845,37 @@ async function readActiveRalphState(
699
845
  }
700
846
  const sessionScopedPath = getStateFilePath("ralph-state.json", cwd, sessionId);
701
847
  const sessionScoped = await readJsonIfExists(sessionScopedPath);
702
- if (
703
- sessionScoped?.active === true
704
- && isRalphStartingPhase(sessionScoped)
705
- && !(await isVisibleRalphActiveForSession(stateDir, sessionId))
706
- ) {
707
- continue;
848
+ if (sessionScoped?.active === true) {
849
+ if (
850
+ currentOmxSessionId
851
+ && sessionId !== currentOmxSessionId
852
+ && completedCanonicalState
853
+ && shouldRetireShadowedRalphStartingSeed(sessionScoped, completedCanonicalState, cwd, {
854
+ completedSessionId: currentOmxSessionId,
855
+ payloadSessionId: safeString(ownerContext?.payloadSessionId).trim(),
856
+ threadId: safeString(ownerContext?.threadId).trim(),
857
+ currentNativeSessionId,
858
+ tmuxPaneId: safeString(ownerContext?.tmuxPaneId).trim(),
859
+ })
860
+ ) {
861
+ await retireShadowedRalphStartingSeed(
862
+ sessionScopedPath,
863
+ sessionScoped,
864
+ currentOmxSessionId,
865
+ completedCanonicalPath,
866
+ completedCanonicalState,
867
+ );
868
+ continue;
869
+ }
870
+ if (await isStaleOrphanedRalphStartingState(sessionScoped, sessionScopedPath)) {
871
+ continue;
872
+ }
873
+ if (
874
+ isRalphStartingPhase(sessionScoped)
875
+ && !(await isVisibleRalphActiveForSession(stateDir, sessionId))
876
+ ) {
877
+ continue;
878
+ }
708
879
  }
709
880
  if (
710
881
  sessionScoped?.active === true
@@ -1699,11 +1870,14 @@ async function buildModeBasedStopOutput(
1699
1870
  const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
1700
1871
  if (!state || !shouldContinueRun(state)) return null;
1701
1872
  const phase = formatPhase(state.current_phase);
1873
+ const systemMessage = mode === "autopilot" && phase.toLowerCase().replace(/_/g, "-") === "code-review"
1874
+ ? "OMX autopilot is still active (phase: code-review). Run the required $code-review step before completing or clearing Autopilot state."
1875
+ : `OMX ${mode} is still active (phase: ${phase}).`;
1702
1876
  return {
1703
1877
  decision: "block",
1704
1878
  reason: `OMX ${mode} is still active (phase: ${phase}); continue the task and gather fresh verification evidence before stopping.`,
1705
1879
  stopReason: `${mode}_${phase}`,
1706
- systemMessage: `OMX ${mode} is still active (phase: ${phase}).`,
1880
+ systemMessage,
1707
1881
  };
1708
1882
  }
1709
1883
 
@@ -1715,6 +1889,12 @@ export function looksLikeGoalCompletionPrompt(text: string): boolean {
1715
1889
  || /(?:^|[.!?]\s+)(?:the\s+)?goal\s+(?:is\s+|now\s+|has\s+been\s+)?(?:complete|completed|finished|closed)(?:\s*(?:[.!?]|$)|\s*[:;]\s*\S|\s*[—–-]\s*\S)/i.test(text);
1716
1890
  }
1717
1891
 
1892
+ function reportsAutoresearchGoalObjectiveMismatch(text: string): boolean {
1893
+ return /\bautoresearch[-\s]goal\b/i.test(text)
1894
+ && /\b(?:complete|completion|reconciliation)\b/i.test(text)
1895
+ && /objective mismatch/i.test(text);
1896
+ }
1897
+
1718
1898
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
1719
1899
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
1720
1900
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -1758,10 +1938,19 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
1758
1938
  const completion = await readJsonIfExists(join(autoresearchRoot, entry.name, "completion.json"));
1759
1939
  const completionVerdict = safeString(completion?.verdict);
1760
1940
  const completionPassed = completion?.passed === true || completionVerdict === "pass";
1761
- if (mission?.workflow === "autoresearch-goal" && status && status !== "complete" && completionPassed) {
1941
+ if (
1942
+ mission?.workflow === "autoresearch-goal"
1943
+ && status
1944
+ && status !== "complete"
1945
+ && completionPassed
1946
+ ) {
1762
1947
  return {
1763
1948
  workflow: "autoresearch-goal",
1764
1949
  command: `omx autoresearch-goal complete --slug ${safeString(mission.slug) || entry.name} --codex-goal-json '<get_goal JSON or path>'`,
1950
+ remediation: [
1951
+ "If that command fails with a Codex goal objective mismatch after a fresh get_goal snapshot, do not repeat the same complete command blindly in this thread.",
1952
+ "Either retry with a correct fresh snapshot or record an explicit blocked verdict for this autoresearch-goal and continue it from a fresh Codex thread.",
1953
+ ].join(" "),
1765
1954
  };
1766
1955
  }
1767
1956
  }
@@ -1789,6 +1978,9 @@ async function buildGoalWorkflowReconciliationStopOutput(
1789
1978
  if (!looksLikeGoalCompletionPrompt(lastAssistantMessage)) return null;
1790
1979
  const requirement = await findActiveGoalWorkflowReconciliationRequirement(cwd);
1791
1980
  if (!requirement) return null;
1981
+ if (requirement.workflow === "autoresearch-goal" && reportsAutoresearchGoalObjectiveMismatch(lastAssistantMessage)) {
1982
+ return null;
1983
+ }
1792
1984
  const systemMessage =
1793
1985
  [
1794
1986
  `OMX ${requirement.workflow} requires get_goal snapshot reconciliation before completion; call get_goal and pass --codex-goal-json to ${requirement.command}.`,
@@ -1914,7 +2106,7 @@ function readPayloadSessionId(payload: CodexHookPayload): string {
1914
2106
  }
1915
2107
 
1916
2108
  function readPayloadThreadId(payload: CodexHookPayload): string {
1917
- return safeString(payload.thread_id ?? payload.threadId).trim();
2109
+ return safeString(payload.owner_codex_thread_id ?? payload.thread_id ?? payload.threadId).trim();
1918
2110
  }
1919
2111
 
1920
2112
  function readPayloadTurnId(payload: CodexHookPayload): string {
@@ -2017,6 +2209,14 @@ async function readBlockingSkillForStop(
2017
2209
  const modeSnapshot = getRunContinuationSnapshot(modeState);
2018
2210
  if (modeSnapshot?.terminal === true) continue;
2019
2211
 
2212
+ if (await shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
2213
+ cwd,
2214
+ stateDir,
2215
+ skill,
2216
+ sessionId,
2217
+ threadId,
2218
+ )) continue;
2219
+
2020
2220
  const phase = formatPhase(
2021
2221
  modeState.current_phase,
2022
2222
  formatPhase(
@@ -2068,6 +2268,58 @@ function isTerminalOrInactiveModeState(state: Record<string, unknown> | null): b
2068
2268
  return phase !== "" && TERMINAL_MODE_PHASES.has(phase);
2069
2269
  }
2070
2270
 
2271
+ function rootSkillStateHasNoActiveSkillForStopContext(
2272
+ rootState: SkillActiveStateLike | null,
2273
+ skill: string,
2274
+ sessionId: string,
2275
+ threadId: string,
2276
+ ): boolean {
2277
+ if (!rootState) return false;
2278
+ return !listActiveSkills(rootState).some((entry) => (
2279
+ entry.skill === skill
2280
+ && matchesSkillStopContext(entry, rootState, sessionId, threadId)
2281
+ ));
2282
+ }
2283
+
2284
+ function rootModeStateIsCanonicalForStopContext(
2285
+ state: Record<string, unknown>,
2286
+ cwd: string,
2287
+ sessionId: string,
2288
+ threadId: string,
2289
+ ): boolean {
2290
+ if (!modeStateMatchesSkillStopContext(state, cwd, sessionId)) return false;
2291
+
2292
+ const stateSessionId = safeString(
2293
+ state.owner_omx_session_id
2294
+ ?? state.session_id
2295
+ ?? state.codex_session_id
2296
+ ?? state.owner_codex_session_id,
2297
+ ).trim();
2298
+ if (sessionId && stateSessionId !== sessionId) return false;
2299
+
2300
+ const stateThreadId = safeString(state.owner_codex_thread_id ?? state.thread_id).trim();
2301
+ if (threadId && stateThreadId && stateThreadId !== threadId) return false;
2302
+
2303
+ return true;
2304
+ }
2305
+
2306
+ async function shouldIgnoreSessionSkillBlockerForCanonicalInactiveRoot(
2307
+ cwd: string,
2308
+ stateDir: string,
2309
+ skill: string,
2310
+ sessionId: string,
2311
+ threadId: string,
2312
+ ): Promise<boolean> {
2313
+ const rootModeState = await readJsonIfExists(join(stateDir, `${skill}-state.json`));
2314
+ if (!rootModeState) return false;
2315
+ if (!rootModeStateIsCanonicalForStopContext(rootModeState, cwd, sessionId, threadId)) return false;
2316
+ if (!isTerminalOrInactiveModeState(rootModeState)) return false;
2317
+
2318
+ const { rootPath } = getSkillActiveStatePathsForStateDir(stateDir);
2319
+ const rootSkillState = await readSkillActiveState(rootPath);
2320
+ return rootSkillStateHasNoActiveSkillForStopContext(rootSkillState, skill, sessionId, threadId);
2321
+ }
2322
+
2071
2323
  async function readSessionScopedModeStateForRootSkill(
2072
2324
  cwd: string,
2073
2325
  stateDir: string,