oh-my-codex 0.11.0 → 0.11.2

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 (180) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/crates/omx-explore/src/main.rs +115 -9
  4. package/dist/cli/__tests__/ask.test.js +3 -3
  5. package/dist/cli/__tests__/ask.test.js.map +1 -1
  6. package/dist/cli/__tests__/doctor-team.test.js +12 -2
  7. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  8. package/dist/cli/__tests__/exec.test.js +4 -0
  9. package/dist/cli/__tests__/exec.test.js.map +1 -1
  10. package/dist/cli/__tests__/hooks.test.d.ts +2 -0
  11. package/dist/cli/__tests__/hooks.test.d.ts.map +1 -0
  12. package/dist/cli/__tests__/hooks.test.js +55 -0
  13. package/dist/cli/__tests__/hooks.test.js.map +1 -0
  14. package/dist/cli/__tests__/index.test.js +6 -2
  15. package/dist/cli/__tests__/index.test.js.map +1 -1
  16. package/dist/cli/__tests__/package-bin-contract.test.js +3 -10
  17. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  18. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -1
  19. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  20. package/dist/cli/__tests__/team.test.js +4 -4
  21. package/dist/cli/__tests__/team.test.js.map +1 -1
  22. package/dist/cli/ask.js +1 -1
  23. package/dist/cli/ask.js.map +1 -1
  24. package/dist/cli/hooks.d.ts.map +1 -1
  25. package/dist/cli/hooks.js +2 -2
  26. package/dist/cli/hooks.js.map +1 -1
  27. package/dist/cli/index.d.ts.map +1 -1
  28. package/dist/cli/index.js +26 -12
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/cli/tmux-hook.d.ts.map +1 -1
  31. package/dist/cli/tmux-hook.js +5 -4
  32. package/dist/cli/tmux-hook.js.map +1 -1
  33. package/dist/hooks/__tests__/codebase-map.test.js +1 -1
  34. package/dist/hooks/__tests__/codebase-map.test.js.map +1 -1
  35. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +46 -36
  36. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  37. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +6 -0
  38. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  39. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +24 -24
  40. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  41. package/dist/hooks/__tests__/notify-hook-linked-sync.test.js +45 -1
  42. package/dist/hooks/__tests__/notify-hook-linked-sync.test.js.map +1 -1
  43. package/dist/hooks/__tests__/notify-hook-modules.test.js +1 -1
  44. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  45. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js +5 -5
  46. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js.map +1 -1
  47. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +22 -1
  48. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  49. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +126 -1
  50. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  51. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +5 -4
  52. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  53. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +141 -3
  54. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  55. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +11 -8
  56. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  57. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +1 -1
  58. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  59. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js +1 -1
  60. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js.map +1 -1
  61. package/dist/hooks/__tests__/tmux-hook-engine.test.js +65 -1
  62. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
  63. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts +2 -0
  64. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts.map +1 -0
  65. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js +32 -0
  66. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js.map +1 -0
  67. package/dist/hooks/extensibility/__tests__/sdk.test.js +172 -0
  68. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  69. package/dist/hooks/extensibility/sdk/logging.d.ts +6 -0
  70. package/dist/hooks/extensibility/sdk/logging.d.ts.map +1 -0
  71. package/dist/hooks/extensibility/sdk/logging.js +32 -0
  72. package/dist/hooks/extensibility/sdk/logging.js.map +1 -0
  73. package/dist/hooks/extensibility/sdk/paths.d.ts +7 -0
  74. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -0
  75. package/dist/hooks/extensibility/sdk/paths.js +22 -0
  76. package/dist/hooks/extensibility/sdk/paths.js.map +1 -0
  77. package/dist/hooks/extensibility/sdk/plugin-state.d.ts +5 -0
  78. package/dist/hooks/extensibility/sdk/plugin-state.d.ts.map +1 -0
  79. package/dist/hooks/extensibility/sdk/plugin-state.js +66 -0
  80. package/dist/hooks/extensibility/sdk/plugin-state.js.map +1 -0
  81. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +3 -0
  82. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -0
  83. package/dist/hooks/extensibility/sdk/runtime-state.js +41 -0
  84. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -0
  85. package/dist/hooks/extensibility/sdk/tmux.d.ts +10 -0
  86. package/dist/hooks/extensibility/sdk/tmux.d.ts.map +1 -0
  87. package/dist/hooks/extensibility/sdk/tmux.js +178 -0
  88. package/dist/hooks/extensibility/sdk/tmux.js.map +1 -0
  89. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  90. package/dist/hooks/extensibility/sdk.js +14 -246
  91. package/dist/hooks/extensibility/sdk.js.map +1 -1
  92. package/dist/hooks/extensibility/types.d.ts +52 -0
  93. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  94. package/dist/hud/__tests__/authority.test.js +2 -2
  95. package/dist/hud/__tests__/authority.test.js.map +1 -1
  96. package/dist/hud/__tests__/index.test.js +9 -2
  97. package/dist/hud/__tests__/index.test.js.map +1 -1
  98. package/dist/mcp/__tests__/state-server-schema.test.js +10 -0
  99. package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
  100. package/dist/mcp/__tests__/state-server.test.js +152 -1
  101. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  102. package/dist/mcp/state-server.d.ts +4 -4
  103. package/dist/mcp/state-server.d.ts.map +1 -1
  104. package/dist/mcp/state-server.js +1 -0
  105. package/dist/mcp/state-server.js.map +1 -1
  106. package/dist/modes/__tests__/base-autoresearch-contract.test.js +17 -0
  107. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
  108. package/dist/modes/base.d.ts +2 -2
  109. package/dist/modes/base.d.ts.map +1 -1
  110. package/dist/modes/base.js +1 -1
  111. package/dist/notifications/__tests__/reply-listener.test.js +375 -8
  112. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  113. package/dist/notifications/reply-listener.d.ts +57 -0
  114. package/dist/notifications/reply-listener.d.ts.map +1 -1
  115. package/dist/notifications/reply-listener.js +85 -40
  116. package/dist/notifications/reply-listener.js.map +1 -1
  117. package/dist/scripts/__tests__/smoke-packed-install.test.js +8 -41
  118. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  119. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts +2 -0
  120. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts.map +1 -0
  121. package/dist/scripts/__tests__/test-reply-listener-live.test.js +82 -0
  122. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -0
  123. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  124. package/dist/scripts/notify-hook/team-dispatch.js +7 -3
  125. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  126. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  127. package/dist/scripts/notify-hook/team-leader-nudge.js +4 -3
  128. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  129. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  130. package/dist/scripts/notify-hook/team-worker.js +27 -8
  131. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  132. package/dist/scripts/notify-hook/tmux-injection.d.ts +0 -1
  133. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  134. package/dist/scripts/notify-hook/tmux-injection.js +123 -112
  135. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  136. package/dist/scripts/smoke-packed-install.d.ts +1 -3
  137. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  138. package/dist/scripts/smoke-packed-install.js +14 -155
  139. package/dist/scripts/smoke-packed-install.js.map +1 -1
  140. package/dist/scripts/test-reply-listener-live.d.ts +24 -0
  141. package/dist/scripts/test-reply-listener-live.d.ts.map +1 -0
  142. package/dist/scripts/test-reply-listener-live.js +138 -0
  143. package/dist/scripts/test-reply-listener-live.js.map +1 -0
  144. package/dist/scripts/tmux-hook-engine.d.ts +3 -3
  145. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  146. package/dist/scripts/tmux-hook-engine.js +17 -16
  147. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  148. package/dist/team/__tests__/hardening-e2e.test.js +11 -1
  149. package/dist/team/__tests__/hardening-e2e.test.js.map +1 -1
  150. package/dist/team/__tests__/mcp-comm.test.js +11 -1
  151. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  152. package/dist/team/__tests__/runtime.test.js +11 -1
  153. package/dist/team/__tests__/runtime.test.js.map +1 -1
  154. package/dist/team/__tests__/state.test.js +9 -1
  155. package/dist/team/__tests__/state.test.js.map +1 -1
  156. package/dist/team/__tests__/tmux-test-fixture.d.ts +9 -3
  157. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  158. package/dist/team/__tests__/tmux-test-fixture.js +69 -24
  159. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  160. package/dist/team/__tests__/tmux-test-fixture.test.js +80 -3
  161. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  162. package/dist/team/tmux-session.d.ts.map +1 -1
  163. package/dist/team/tmux-session.js +2 -7
  164. package/dist/team/tmux-session.js.map +1 -1
  165. package/dist/verification/__tests__/ci-rust-gates.test.js +13 -0
  166. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  167. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +15 -6
  168. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  169. package/dist/verification/__tests__/native-release-manifest.test.js +1 -1
  170. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  171. package/package.json +2 -1
  172. package/src/scripts/__tests__/smoke-packed-install.test.ts +9 -50
  173. package/src/scripts/__tests__/test-reply-listener-live.test.ts +101 -0
  174. package/src/scripts/notify-hook/team-dispatch.ts +8 -2
  175. package/src/scripts/notify-hook/team-leader-nudge.ts +4 -3
  176. package/src/scripts/notify-hook/team-worker.ts +26 -8
  177. package/src/scripts/notify-hook/tmux-injection.ts +118 -107
  178. package/src/scripts/smoke-packed-install.ts +17 -164
  179. package/src/scripts/test-reply-listener-live.ts +188 -0
  180. package/src/scripts/tmux-hook-engine.ts +18 -16
@@ -1,16 +1,11 @@
1
- import { createServer } from 'node:http';
2
- import type { AddressInfo } from 'node:net';
3
1
  import {
4
- cpSync,
5
2
  existsSync,
6
3
  lstatSync,
7
4
  mkdtempSync,
8
- readFileSync,
9
5
  rmSync,
10
6
  symlinkSync,
11
- writeFileSync,
12
7
  } from 'node:fs';
13
- import { chmodSync, mkdirSync } from 'node:fs';
8
+ import { mkdirSync } from 'node:fs';
14
9
  import { tmpdir } from 'node:os';
15
10
  import { basename, dirname, join, resolve } from 'node:path';
16
11
  import { spawnSync } from 'node:child_process';
@@ -23,12 +18,17 @@ const REQUIRED_NODE_MODULE_MARKERS = [
23
18
  join('zod', 'package.json'),
24
19
  ];
25
20
 
21
+ export const PACKED_INSTALL_SMOKE_CORE_COMMANDS = [
22
+ ['--help'],
23
+ ['version'],
24
+ ] as const;
25
+
26
26
  function usage(): string {
27
27
  return [
28
- 'Usage: node scripts/smoke-packed-install.mjs [--release-assets-dir <dir>] [--require-no-fallback]',
28
+ 'Usage: node scripts/smoke-packed-install.mjs',
29
29
  '',
30
30
  'Creates an npm tarball, installs it into an isolated prefix, and smoke tests the installed omx CLI.',
31
- 'When --release-assets-dir is provided, native hydration is also exercised using a local HTTP server.',
31
+ 'Release smoke stays intentionally minimal: install + boot + 1-2 core commands only.',
32
32
  ].join('\n');
33
33
  }
34
34
 
@@ -151,43 +151,24 @@ export function ensureRepoDependencies(repoRoot: string, options: EnsureRepoDeps
151
151
  };
152
152
  }
153
153
 
154
- export function hasSparkShellFallbackBanner(stderr: string | undefined): boolean {
155
- return /GLIBC-incompatible native sidecar detected/i.test(String(stderr || ''));
156
- }
157
-
158
- function parseArgs(argv: string[]): { releaseAssetsDir: string | undefined; requireNoFallback: boolean } {
159
- let releaseAssetsDir: string | undefined;
160
- let requireNoFallback = false;
161
- for (let index = 0; index < argv.length; index += 1) {
162
- const token = argv[index];
154
+ function parseArgs(argv: string[]): void {
155
+ for (const token of argv) {
163
156
  if (token === '--help' || token === '-h') {
164
157
  console.log(usage());
165
158
  process.exit(0);
166
159
  }
167
- if (token === '--release-assets-dir') {
168
- const value = argv[index + 1];
169
- if (!value) throw new Error('Missing value after --release-assets-dir');
170
- releaseAssetsDir = resolve(value);
171
- index += 1;
172
- continue;
173
- }
174
- if (token === '--require-no-fallback') {
175
- requireNoFallback = true;
176
- continue;
177
- }
178
160
  throw new Error(`Unknown argument: ${token}\n${usage()}`);
179
161
  }
180
- return { releaseAssetsDir, requireNoFallback };
181
162
  }
182
163
 
183
- function run(cmd: string, args: string[], options: Record<string, unknown> = {}): ReturnType<typeof spawnSync> {
184
- const result = spawnSync(cmd, args, {
164
+ function run(cmd: string, args: readonly string[], options: Record<string, unknown> = {}): ReturnType<typeof spawnSync> {
165
+ const result = spawnSync(cmd, [...args], {
185
166
  encoding: 'utf-8',
186
167
  stdio: 'pipe',
187
168
  ...options,
188
169
  });
189
170
  if (result.status !== 0) {
190
- throw new Error(formatCommandFailure(cmd, args, result));
171
+ throw new Error(formatCommandFailure(cmd, [...args], result));
191
172
  }
192
173
  return result;
193
174
  }
@@ -196,115 +177,14 @@ function npmBinName(name: string): string {
196
177
  return process.platform === 'win32' ? `${name}.cmd` : name;
197
178
  }
198
179
 
199
- function startStaticServer(root: string): Promise<{ baseUrl: string; close: () => Promise<void> }> {
200
- return new Promise((resolveServer, reject) => {
201
- const server = createServer((req, res) => {
202
- try {
203
- const url = new URL(req.url || '/', 'http://127.0.0.1');
204
- const requested = resolve(root, url.pathname.replace(/^\//, ''));
205
- if (!requested.startsWith(root) || !existsSync(requested)) {
206
- res.writeHead(404);
207
- res.end('missing');
208
- return;
209
- }
210
- res.writeHead(200);
211
- res.end(readFileSync(requested));
212
- } catch (error) {
213
- res.writeHead(500);
214
- res.end(String(error));
215
- }
216
- });
217
- server.on('error', reject);
218
- server.listen(0, '127.0.0.1', () => {
219
- const address = server.address() as AddressInfo | null;
220
- if (!address || typeof address === 'string') {
221
- reject(new Error('Failed to bind local asset server'));
222
- return;
223
- }
224
- resolveServer({
225
- baseUrl: `http://127.0.0.1:${address.port}`,
226
- close: () => new Promise<void>((done, fail) => server.close((error) => error ? fail(error) : done())),
227
- });
228
- });
229
- });
230
- }
231
-
232
- function writeCodexStub(binDir: string): string {
233
- const stubPath = join(binDir, process.platform === 'win32' ? 'codex.cmd' : 'codex');
234
- if (process.platform === 'win32') {
235
- writeFileSync(stubPath, [
236
- '@echo off',
237
- 'setlocal enabledelayedexpansion',
238
- 'set output_path=',
239
- ':loop',
240
- 'if "%~1"=="" goto done',
241
- 'if "%~1"=="-o" (',
242
- ' set output_path=%~2',
243
- ' shift',
244
- ')',
245
- 'shift',
246
- 'goto loop',
247
- ':done',
248
- 'if "%output_path%"=="" exit /b 1',
249
- '> "%output_path%" echo # Answer',
250
- '>> "%output_path%" echo - packed install smoke harness',
251
- 'exit /b 0',
252
- '',
253
- ].join('\r\n'));
254
- } else {
255
- writeFileSync(stubPath, [
256
- '#!/bin/sh',
257
- 'set -eu',
258
- "output_path=''",
259
- 'while [ "$#" -gt 0 ]; do',
260
- ' if [ "$1" = "-o" ] && [ "$#" -ge 2 ]; then',
261
- ' output_path="$2"',
262
- ' shift 2',
263
- ' continue',
264
- ' fi',
265
- ' shift',
266
- 'done',
267
- 'if [ -z "$output_path" ]; then',
268
- " printf 'missing -o output path\\n' >&2",
269
- ' exit 1',
270
- 'fi',
271
- "printf '# Answer\\n- packed install smoke harness\\n' > \"$output_path\"",
272
- '',
273
- ].join('\n'));
274
- chmodSync(stubPath, 0o755);
275
- }
276
- return stubPath;
277
- }
278
-
279
- export function prepareLocalHydrationAssetDirectory(sourceDir: string, tempRoot: string): string {
280
- const localDir = join(tempRoot, 'hydration-assets');
281
- cpSync(sourceDir, localDir, { recursive: true });
282
- return localDir;
283
- }
284
-
285
- export function rewriteManifestDownloadUrls(manifestPath: string, baseUrl: string): void {
286
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { assets: Array<{ archive: string; download_url: string }> };
287
- manifest.assets = Array.isArray(manifest.assets)
288
- ? manifest.assets.map((asset) => ({
289
- ...asset,
290
- download_url: new URL(asset.archive, `${baseUrl}/`).toString(),
291
- }))
292
- : [];
293
- writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
294
- }
295
-
296
180
  async function main(): Promise<void> {
297
- const { releaseAssetsDir, requireNoFallback } = parseArgs(process.argv.slice(2));
181
+ parseArgs(process.argv.slice(2));
182
+
298
183
  const repoRoot = process.cwd();
299
184
  const tempRoot = mkdtempSync(join(tmpdir(), 'omx-packed-install-'));
300
185
  const prefixDir = join(tempRoot, 'prefix');
301
- const cacheDir = join(tempRoot, 'cache');
302
- const helperBinDir = join(tempRoot, 'bin');
303
186
  mkdirSync(prefixDir, { recursive: true });
304
- mkdirSync(cacheDir, { recursive: true });
305
- mkdirSync(helperBinDir, { recursive: true });
306
187
 
307
- let server: { baseUrl: string; close: () => Promise<void> } | undefined;
308
188
  let tarballPath: string | undefined;
309
189
  try {
310
190
  ensureRepoDependencies(repoRoot, {
@@ -320,40 +200,13 @@ async function main(): Promise<void> {
320
200
  run('npm', ['install', '-g', tarballPath, '--prefix', prefixDir], { cwd: repoRoot });
321
201
 
322
202
  const omxPath = join(prefixDir, process.platform === 'win32' ? '' : 'bin', npmBinName('omx'));
323
- run(omxPath, ['version'], { cwd: repoRoot });
324
- run(omxPath, ['--help'], { cwd: repoRoot });
325
-
326
- if (releaseAssetsDir) {
327
- const hydrationAssetsDir = prepareLocalHydrationAssetDirectory(releaseAssetsDir, tempRoot);
328
- server = await startStaticServer(hydrationAssetsDir);
329
- rewriteManifestDownloadUrls(join(hydrationAssetsDir, 'native-release-manifest.json'), server.baseUrl);
330
- const codexStub = writeCodexStub(helperBinDir);
331
- const env = {
332
- ...process.env,
333
- PATH: `${helperBinDir}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH || ''}`,
334
- OMX_NATIVE_MANIFEST_URL: `${server.baseUrl}/native-release-manifest.json`,
335
- OMX_NATIVE_CACHE_DIR: cacheDir,
336
- OMX_EXPLORE_CODEX_BIN: codexStub,
337
- };
338
-
339
- const sparkshell = run(omxPath, ['sparkshell', 'node', '--version'], { cwd: repoRoot, env });
340
- if (requireNoFallback && hasSparkShellFallbackBanner(sparkshell.stderr as string)) {
341
- throw new Error(`Unexpected sparkshell fallback stderr:\n${sparkshell.stderr}`);
342
- }
343
- if (!/v\d+\./.test(sparkshell.stdout as string)) {
344
- throw new Error(`Unexpected sparkshell stdout:\n${sparkshell.stdout}`);
345
- }
346
-
347
- const explore = run(omxPath, ['explore', '--prompt', 'where is buildExploreRoutingGuidance defined'], { cwd: repoRoot, env });
348
- if (!(explore.stdout as string).includes('# Answer') || !(explore.stdout as string).includes('packed install smoke harness')) {
349
- throw new Error(`Unexpected explore stdout:\n${explore.stdout}`);
350
- }
203
+ for (const argv of PACKED_INSTALL_SMOKE_CORE_COMMANDS) {
204
+ run(omxPath, argv, { cwd: repoRoot });
351
205
  }
352
206
 
353
207
  console.log('packed install smoke: PASS');
354
208
  } finally {
355
209
  if (tarballPath) rmSync(tarballPath, { force: true });
356
- if (server) await server.close();
357
210
  rmSync(tempRoot, { recursive: true, force: true });
358
211
  }
359
212
  }
@@ -0,0 +1,188 @@
1
+ interface ReplyListenerLiveConfig {
2
+ discordBotToken: string;
3
+ discordChannelId: string;
4
+ telegramBotToken: string;
5
+ telegramChatId: string;
6
+ }
7
+
8
+ interface ReplyListenerLiveEnvResolution {
9
+ enabled: boolean;
10
+ missing: string[];
11
+ config: ReplyListenerLiveConfig | null;
12
+ }
13
+
14
+ interface ReplyListenerLiveSmokeResult {
15
+ discordMessageId: string;
16
+ telegramMessageId: string;
17
+ }
18
+
19
+ interface ReplyListenerLiveSmokeDeps {
20
+ fetchImpl?: typeof fetch;
21
+ log?: (message: string) => void;
22
+ }
23
+
24
+ const LIVE_ENABLE_ENV = 'OMX_REPLY_LISTENER_LIVE';
25
+ const REQUIRED_ENV_KEYS = [
26
+ 'OMX_DISCORD_NOTIFIER_BOT_TOKEN',
27
+ 'OMX_DISCORD_NOTIFIER_CHANNEL',
28
+ 'OMX_TELEGRAM_BOT_TOKEN',
29
+ 'OMX_TELEGRAM_CHAT_ID',
30
+ ] as const;
31
+
32
+ function requireJsonObject(value: unknown, label: string): Record<string, unknown> {
33
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
34
+ throw new Error(`${label} returned a non-object JSON payload`);
35
+ }
36
+ return value as Record<string, unknown>;
37
+ }
38
+
39
+ async function parseResponseJson(response: Response, label: string): Promise<Record<string, unknown>> {
40
+ const body = await response.json() as unknown;
41
+ return requireJsonObject(body, label);
42
+ }
43
+
44
+ export function resolveReplyListenerLiveEnv(env: NodeJS.ProcessEnv = process.env): ReplyListenerLiveEnvResolution {
45
+ const enabled = env[LIVE_ENABLE_ENV] === '1';
46
+ if (!enabled) {
47
+ return { enabled: false, missing: [], config: null };
48
+ }
49
+
50
+ const missing = REQUIRED_ENV_KEYS.filter((key) => {
51
+ const value = env[key];
52
+ return typeof value !== 'string' || value.trim().length === 0;
53
+ });
54
+ if (missing.length > 0) {
55
+ return { enabled: true, missing: [...missing], config: null };
56
+ }
57
+
58
+ return {
59
+ enabled: true,
60
+ missing: [],
61
+ config: {
62
+ discordBotToken: env.OMX_DISCORD_NOTIFIER_BOT_TOKEN!.trim(),
63
+ discordChannelId: env.OMX_DISCORD_NOTIFIER_CHANNEL!.trim(),
64
+ telegramBotToken: env.OMX_TELEGRAM_BOT_TOKEN!.trim(),
65
+ telegramChatId: env.OMX_TELEGRAM_CHAT_ID!.trim(),
66
+ },
67
+ };
68
+ }
69
+
70
+ export async function runReplyListenerLiveSmoke(
71
+ config: ReplyListenerLiveConfig,
72
+ deps: ReplyListenerLiveSmokeDeps = {},
73
+ ): Promise<ReplyListenerLiveSmokeResult> {
74
+ const fetchImpl = deps.fetchImpl ?? fetch;
75
+ const log = deps.log ?? console.log;
76
+ const stamp = new Date().toISOString();
77
+
78
+ const discordSendResponse = await fetchImpl(
79
+ `https://discord.com/api/v10/channels/${config.discordChannelId}/messages`,
80
+ {
81
+ method: 'POST',
82
+ headers: {
83
+ Authorization: `Bot ${config.discordBotToken}`,
84
+ 'Content-Type': 'application/json',
85
+ },
86
+ body: JSON.stringify({
87
+ content: `[omx live smoke ${stamp}] reply-listener Discord connectivity probe`,
88
+ }),
89
+ signal: AbortSignal.timeout(10_000),
90
+ },
91
+ );
92
+ if (!discordSendResponse.ok) {
93
+ throw new Error(`Discord live smoke failed: HTTP ${discordSendResponse.status}`);
94
+ }
95
+ const discordPayload = await parseResponseJson(discordSendResponse, 'Discord sendMessage');
96
+ const discordMessageId = typeof discordPayload.id === 'string' && discordPayload.id.trim()
97
+ ? discordPayload.id
98
+ : null;
99
+ if (!discordMessageId) {
100
+ throw new Error('Discord live smoke failed: missing message id');
101
+ }
102
+ log(`Discord probe message sent: ${discordMessageId}`);
103
+
104
+ try {
105
+ await fetchImpl(
106
+ `https://discord.com/api/v10/channels/${config.discordChannelId}/messages/${discordMessageId}`,
107
+ {
108
+ method: 'DELETE',
109
+ headers: { Authorization: `Bot ${config.discordBotToken}` },
110
+ signal: AbortSignal.timeout(10_000),
111
+ },
112
+ );
113
+ } catch {
114
+ log(`Discord probe cleanup skipped for ${discordMessageId}`);
115
+ }
116
+
117
+ const telegramSendResponse = await fetchImpl(
118
+ `https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`,
119
+ {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({
123
+ chat_id: config.telegramChatId,
124
+ text: `[omx live smoke ${stamp}] reply-listener Telegram connectivity probe`,
125
+ }),
126
+ signal: AbortSignal.timeout(10_000),
127
+ },
128
+ );
129
+ if (!telegramSendResponse.ok) {
130
+ throw new Error(`Telegram live smoke failed: HTTP ${telegramSendResponse.status}`);
131
+ }
132
+ const telegramPayload = await parseResponseJson(telegramSendResponse, 'Telegram sendMessage');
133
+ const telegramResult = requireJsonObject(telegramPayload.result, 'Telegram sendMessage.result');
134
+ const telegramMessageId = typeof telegramResult.message_id === 'number' || typeof telegramResult.message_id === 'string'
135
+ ? String(telegramResult.message_id)
136
+ : null;
137
+ if (!telegramMessageId) {
138
+ throw new Error('Telegram live smoke failed: missing message id');
139
+ }
140
+ log(`Telegram probe message sent: ${telegramMessageId}`);
141
+
142
+ try {
143
+ await fetchImpl(
144
+ `https://api.telegram.org/bot${config.telegramBotToken}/deleteMessage`,
145
+ {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({
149
+ chat_id: config.telegramChatId,
150
+ message_id: Number(telegramMessageId),
151
+ }),
152
+ signal: AbortSignal.timeout(10_000),
153
+ },
154
+ );
155
+ } catch {
156
+ log(`Telegram probe cleanup skipped for ${telegramMessageId}`);
157
+ }
158
+
159
+ return { discordMessageId, telegramMessageId };
160
+ }
161
+
162
+ export async function main(): Promise<void> {
163
+ const resolution = resolveReplyListenerLiveEnv();
164
+ if (!resolution.enabled) {
165
+ console.log(`reply-listener live smoke: SKIP (${LIVE_ENABLE_ENV}=1 to enable)`);
166
+ return;
167
+ }
168
+ if (!resolution.config) {
169
+ console.log(`reply-listener live smoke: SKIP (missing env: ${resolution.missing.join(', ')})`);
170
+ return;
171
+ }
172
+
173
+ const result = await runReplyListenerLiveSmoke(resolution.config);
174
+ console.log('reply-listener live smoke: PASS');
175
+ console.log(`discord_message_id=${result.discordMessageId}`);
176
+ console.log(`telegram_message_id=${result.telegramMessageId}`);
177
+ }
178
+
179
+ const isMain = process.argv[1]
180
+ ? import.meta.url === new URL(`file://${process.argv[1]}`).href
181
+ : false;
182
+
183
+ if (isMain) {
184
+ main().catch((error) => {
185
+ console.error(`reply-listener live smoke: FAIL\n${error instanceof Error ? error.message : String(error)}`);
186
+ process.exit(1);
187
+ });
188
+ }
@@ -182,13 +182,17 @@ export function isPaneRunningShell(paneCurrentCommand: any): boolean {
182
182
  // Codex agent commands — do NOT include 'claude' (that's Claude Code CLI, a different tool)
183
183
  const AGENT_COMMANDS = new Set(['node', 'codex', 'npx']);
184
184
 
185
+ function isHudStartCommand(startCommand: string): boolean {
186
+ return /\bomx\b.*\bhud\b.*--watch/i.test(startCommand);
187
+ }
188
+
185
189
  /**
186
190
  * Canonical codex pane resolver. Finds the tmux pane running a codex/claude agent.
187
191
  *
188
192
  * Resolution order:
189
- * 1. TMUX_PANE env var — but only if the pane is running an agent (not a shell)
190
- * 2. Scan all panes in the same tmux session for one running an agent command
191
- * 3. Fall back to TMUX_PANE even if it's a shell (best effort)
193
+ * 1. TMUX_PANE env var — but only if the pane looks like a real agent pane, not HUD
194
+ * 2. Scan all panes in the same tmux session for one started with codex
195
+ * 3. Fail closed instead of guessing a shell or HUD pane
192
196
  *
193
197
  * All callers (auto-nudge, ralph steer, team dispatch, tmux injection) should
194
198
  * use this instead of raw `process.env.TMUX_PANE`.
@@ -197,43 +201,42 @@ export function resolveCodexPane(): string {
197
201
  const envPane = (process.env.TMUX_PANE || '').trim();
198
202
  if (!envPane) return '';
199
203
 
200
- // Check if TMUX_PANE is actually running an agent
201
204
  try {
202
205
  const cmd = execFileSync('tmux', ['display-message', '-t', envPane, '-p', '#{pane_current_command}'], {
203
206
  encoding: 'utf-8', timeout: 2000,
204
207
  }).trim().toLowerCase();
208
+ const startCmd = execFileSync('tmux', ['display-message', '-t', envPane, '-p', '#{pane_start_command}'], {
209
+ encoding: 'utf-8', timeout: 2000,
210
+ }).trim().toLowerCase();
205
211
  const base = cmd.split('/').pop()?.replace(/^-/, '') || '';
206
- if (AGENT_COMMANDS.has(base)) {
207
- return envPane; // TMUX_PANE is running a codex agent — use it
212
+ if (AGENT_COMMANDS.has(base) && !isHudStartCommand(startCmd)) {
213
+ return envPane;
208
214
  }
209
215
  if (!SHELL_COMMANDS.has(base)) {
210
- // Not a shell and not a known agent (e.g. 'claude' = Claude Code CLI)
211
- // Fall through to session scan to find the actual codex pane
216
+ // Not a shell and not a known agent (e.g. claude CLI) fall through to
217
+ // session scan so we can still reject HUD or locate a codex pane.
212
218
  }
213
219
  } catch {
214
- return envPane; // Can't check use it as-is
220
+ // Fall through to session scan instead of guessing.
215
221
  }
216
222
 
217
- // TMUX_PANE is a shell — find the actual agent pane in the same session
218
223
  try {
219
- const sessionName = execFileSync('tmux', ['display-message', '-t', envPane, '-p', '#{session_name}'], {
224
+ const sessionName = execFileSync('tmux', ['display-message', '-t', envPane, '-p', '#S'], {
220
225
  encoding: 'utf-8', timeout: 2000,
221
226
  }).trim();
222
- if (!sessionName) return envPane;
227
+ if (!sessionName) return '';
223
228
 
224
229
  const panes = execFileSync('tmux', [
225
230
  'list-panes', '-s', '-t', sessionName,
226
231
  '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}',
227
232
  ], { encoding: 'utf-8', timeout: 2000 }).trim().split('\n');
228
233
 
229
- // Find the pane that was STARTED with codex (not the HUD or shell)
230
234
  for (const line of panes) {
231
235
  const parts = line.split('\t');
232
236
  const paneId = parts[0];
233
237
  const startCmd = (parts[2] || '').toLowerCase();
234
238
  if (!paneId) continue;
235
- // The codex pane has 'codex' in its start command
236
- if (startCmd.includes('codex') && !startCmd.includes('hud')) {
239
+ if (startCmd.includes('codex') && !isHudStartCommand(startCmd)) {
237
240
  return paneId;
238
241
  }
239
242
  }
@@ -241,7 +244,6 @@ export function resolveCodexPane(): string {
241
244
  // Fall through
242
245
  }
243
246
 
244
- // No agent pane found in the session — don't fall back to a shell pane
245
247
  return '';
246
248
  }
247
249