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,67 +1,26 @@
1
1
  // @ts-nocheck
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { test } from 'node:test';
7
7
  import {
8
8
  ensureRepoDependencies,
9
- hasSparkShellFallbackBanner,
10
9
  hasUsableNodeModules,
11
- prepareLocalHydrationAssetDirectory,
10
+ PACKED_INSTALL_SMOKE_CORE_COMMANDS,
12
11
  resolveGitCommonDir,
13
12
  resolveReusableNodeModulesSource,
14
- rewriteManifestDownloadUrls,
15
13
  } from '../smoke-packed-install.js';
16
14
 
17
- test('detects the sparkshell GLIBC fallback banner', () => {
15
+ test('packed install smoke stays limited to boot + core commands', () => {
16
+ assert.deepEqual(PACKED_INSTALL_SMOKE_CORE_COMMANDS, [
17
+ ['--help'],
18
+ ['version'],
19
+ ]);
18
20
  assert.equal(
19
- hasSparkShellFallbackBanner('[sparkshell] GLIBC-incompatible native sidecar detected; falling back to raw command execution without summary support.\n'),
20
- true,
21
+ PACKED_INSTALL_SMOKE_CORE_COMMANDS.some((argv) => argv.includes('explore') || argv.includes('sparkshell')),
22
+ false,
21
23
  );
22
- assert.equal(hasSparkShellFallbackBanner('node v20.0.0\n'), false);
23
- });
24
-
25
- test('rewrites copied native manifest download urls to the local smoke server', async () => {
26
- const root = await mkdtemp(join(tmpdir(), 'omx-smoke-packed-install-'));
27
- try {
28
- const sourceDir = join(root, 'source-release-assets');
29
- await mkdir(sourceDir, { recursive: true });
30
- await writeFile(join(sourceDir, 'omx-explore-harness-x86_64-unknown-linux-musl.tar.xz'), 'explore');
31
- await writeFile(join(sourceDir, 'omx-sparkshell-x86_64-unknown-linux-musl.tar.xz'), 'sparkshell');
32
- await writeFile(join(sourceDir, 'native-release-manifest.json'), JSON.stringify({
33
- version: '0.9.0',
34
- assets: [
35
- {
36
- product: 'omx-explore-harness',
37
- archive: 'omx-explore-harness-x86_64-unknown-linux-musl.tar.xz',
38
- download_url: 'https://github.com/example/omx-explore-harness-x86_64-unknown-linux-musl.tar.xz',
39
- },
40
- {
41
- product: 'omx-sparkshell',
42
- archive: 'omx-sparkshell-x86_64-unknown-linux-musl.tar.xz',
43
- download_url: 'https://github.com/example/omx-sparkshell-x86_64-unknown-linux-musl.tar.xz',
44
- },
45
- ],
46
- }, null, 2));
47
-
48
- const copiedDir = prepareLocalHydrationAssetDirectory(sourceDir, root);
49
- rewriteManifestDownloadUrls(join(copiedDir, 'native-release-manifest.json'), 'http://127.0.0.1:43123');
50
-
51
- const originalManifest = JSON.parse(await readFile(join(sourceDir, 'native-release-manifest.json'), 'utf-8')) as { assets: Array<{ download_url: string }> };
52
- const copiedManifest = JSON.parse(await readFile(join(copiedDir, 'native-release-manifest.json'), 'utf-8')) as { assets: Array<{ download_url: string }> };
53
-
54
- assert.match(originalManifest.assets[0].download_url, /^https:\/\/github\.com\//);
55
- assert.deepEqual(
56
- copiedManifest.assets.map((asset) => asset.download_url),
57
- [
58
- 'http://127.0.0.1:43123/omx-explore-harness-x86_64-unknown-linux-musl.tar.xz',
59
- 'http://127.0.0.1:43123/omx-sparkshell-x86_64-unknown-linux-musl.tar.xz',
60
- ],
61
- );
62
- } finally {
63
- await rm(root, { recursive: true, force: true });
64
- }
65
24
  });
66
25
 
67
26
  test('resolveGitCommonDir resolves relative git common dir output against the repo root', () => {
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import {
4
+ resolveReplyListenerLiveEnv,
5
+ runReplyListenerLiveSmoke,
6
+ } from '../test-reply-listener-live.js';
7
+
8
+ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
9
+ const headers = new Headers(init.headers);
10
+ if (!headers.has('content-type')) {
11
+ headers.set('content-type', 'application/json');
12
+ }
13
+ return new Response(JSON.stringify(body), { ...init, headers });
14
+ }
15
+
16
+ test('resolveReplyListenerLiveEnv stays disabled until explicitly opted in', () => {
17
+ const result = resolveReplyListenerLiveEnv({
18
+ OMX_DISCORD_NOTIFIER_BOT_TOKEN: 'discord-token',
19
+ OMX_DISCORD_NOTIFIER_CHANNEL: 'channel-1',
20
+ OMX_TELEGRAM_BOT_TOKEN: 'telegram-token',
21
+ OMX_TELEGRAM_CHAT_ID: 'chat-1',
22
+ });
23
+
24
+ assert.deepEqual(result, {
25
+ enabled: false,
26
+ missing: [],
27
+ config: null,
28
+ });
29
+ });
30
+
31
+ test('resolveReplyListenerLiveEnv reports missing credentials when opted in', () => {
32
+ const result = resolveReplyListenerLiveEnv({
33
+ OMX_REPLY_LISTENER_LIVE: '1',
34
+ OMX_DISCORD_NOTIFIER_BOT_TOKEN: 'discord-token',
35
+ OMX_TELEGRAM_CHAT_ID: 'chat-1',
36
+ });
37
+
38
+ assert.equal(result.enabled, true);
39
+ assert.deepEqual(result.missing, [
40
+ 'OMX_DISCORD_NOTIFIER_CHANNEL',
41
+ 'OMX_TELEGRAM_BOT_TOKEN',
42
+ ]);
43
+ assert.equal(result.config, null);
44
+ });
45
+
46
+ test('runReplyListenerLiveSmoke exercises Discord and Telegram send + cleanup requests', async () => {
47
+ const calls: Array<{ url: string; init?: RequestInit }> = [];
48
+ const logs: string[] = [];
49
+
50
+ const fetchImpl: typeof fetch = async (input, init) => {
51
+ const url = String(input);
52
+ calls.push({ url, init });
53
+
54
+ if (url === 'https://discord.com/api/v10/channels/channel-1/messages') {
55
+ assert.equal(init?.method, 'POST');
56
+ assert.match(String(init?.body), /Discord connectivity probe/);
57
+ return jsonResponse({ id: 'discord-message-1' });
58
+ }
59
+
60
+ if (url === 'https://discord.com/api/v10/channels/channel-1/messages/discord-message-1') {
61
+ assert.equal(init?.method, 'DELETE');
62
+ return new Response(null, { status: 204 });
63
+ }
64
+
65
+ if (url === 'https://api.telegram.org/bottelegram-token/sendMessage') {
66
+ assert.equal(init?.method, 'POST');
67
+ assert.match(String(init?.body), /Telegram connectivity probe/);
68
+ return jsonResponse({ ok: true, result: { message_id: 42 } });
69
+ }
70
+
71
+ if (url === 'https://api.telegram.org/bottelegram-token/deleteMessage') {
72
+ assert.equal(init?.method, 'POST');
73
+ const payload = JSON.parse(String(init?.body)) as { chat_id: string; message_id: number };
74
+ assert.deepEqual(payload, { chat_id: 'chat-1', message_id: 42 });
75
+ return jsonResponse({ ok: true, result: true });
76
+ }
77
+
78
+ throw new Error(`Unexpected live smoke fetch url: ${url}`);
79
+ };
80
+
81
+ const result = await runReplyListenerLiveSmoke(
82
+ {
83
+ discordBotToken: 'discord-token',
84
+ discordChannelId: 'channel-1',
85
+ telegramBotToken: 'telegram-token',
86
+ telegramChatId: 'chat-1',
87
+ },
88
+ {
89
+ fetchImpl,
90
+ log: (message) => logs.push(message),
91
+ },
92
+ );
93
+
94
+ assert.deepEqual(result, {
95
+ discordMessageId: 'discord-message-1',
96
+ telegramMessageId: '42',
97
+ });
98
+ assert.equal(calls.length, 4);
99
+ assert.ok(logs.some((entry) => entry.includes('Discord probe message sent: discord-message-1')));
100
+ assert.ok(logs.some((entry) => entry.includes('Telegram probe message sent: 42')));
101
+ });
@@ -231,9 +231,15 @@ async function withMailboxLock(teamDirPath, workerName, fn) {
231
231
  }
232
232
  }
233
233
 
234
+ function resolveLeaderPaneId(config) {
235
+ return safeString(config?.leader_pane_id).trim();
236
+ }
237
+
238
+
234
239
  function defaultInjectTarget(request, config) {
235
240
  if (request.to_worker === 'leader-fixed') {
236
- if (config.leader_pane_id) return { type: 'pane', value: config.leader_pane_id };
241
+ const leaderPaneId = resolveLeaderPaneId(config);
242
+ if (leaderPaneId) return { type: 'pane', value: leaderPaneId };
237
243
  return null;
238
244
  }
239
245
  if (request.pane_id) return { type: 'pane', value: request.pane_id };
@@ -489,7 +495,7 @@ export async function drainPendingTeamDispatch({
489
495
  continue;
490
496
  }
491
497
 
492
- if (request.to_worker === 'leader-fixed' && !safeString(config?.leader_pane_id).trim()) {
498
+ if (request.to_worker === 'leader-fixed' && !resolveLeaderPaneId(config)) {
493
499
  const nowIso = new Date().toISOString();
494
500
  const alreadyDeferred = safeString(request.last_reason).trim() === LEADER_PANE_MISSING_DEFERRED_REASON;
495
501
  request.updated_at = nowIso;
@@ -11,7 +11,7 @@ import { readJsonIfExists, getScopedStateDirsForCurrentSession } from './state-i
11
11
  import { runProcess } from './process-runner.js';
12
12
  import { logTmuxHookEvent } from './log.js';
13
13
  import { evaluatePaneInjectionReadiness, sendPaneInput } from './team-tmux-guard.js';
14
- import { DEFAULT_MARKER } from '../tmux-hook-engine.js';
14
+ import { DEFAULT_MARKER, resolveCodexPane } from '../tmux-hook-engine.js';
15
15
  const LEADER_PANE_MISSING_NO_INJECTION_REASON = 'leader_pane_missing_no_injection';
16
16
  const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
17
17
  const LEADER_NOTIFICATION_DEFERRED_TYPE = 'leader_notification_deferred';
@@ -541,8 +541,9 @@ export async function maybeNudgeTeamLeader({ cwd, stateDir, logsDir, preComputed
541
541
  const workerPaneIds = Array.isArray(workers)
542
542
  ? workers.map((w) => safeString(w && w.pane_id ? w.pane_id : '')).filter(Boolean)
543
543
  : [];
544
- if (!tmuxSession && !leaderPaneId) continue;
545
- const tmuxTarget = leaderPaneId;
544
+ const canonicalLeaderPaneId = safeString(resolveCodexPane() || leaderPaneId).trim();
545
+ if (!tmuxSession && !canonicalLeaderPaneId) continue;
546
+ const tmuxTarget = canonicalLeaderPaneId;
546
547
  const paneStatus = tmuxSession
547
548
  ? await checkWorkerPanesAlive(tmuxSession, workerPaneIds)
548
549
  : { alive: false, paneCount: 0 };
@@ -10,6 +10,7 @@ import { asNumber, safeString, isTerminalPhase } from './utils.js';
10
10
  import { readJsonIfExists } from './state-io.js';
11
11
  import { logTmuxHookEvent } from './log.js';
12
12
  import { checkPaneReadyForTeamSendKeys, sendPaneInput } from './team-tmux-guard.js';
13
+ import { resolvePaneTarget } from './tmux-injection.js';
13
14
  import { DEFAULT_MARKER } from '../tmux-hook-engine.js';
14
15
  const LEADER_PANE_SHELL_NO_INJECTION_REASON = 'leader_pane_shell_no_injection';
15
16
 
@@ -257,6 +258,21 @@ export async function readTeamWorkersForIdleCheck(stateDir, teamName) {
257
258
  }
258
259
  }
259
260
 
261
+ async function resolveCanonicalLeaderPaneId(_tmuxSession, leaderPaneId) {
262
+ const normalizedLeaderPaneId = safeString(leaderPaneId).trim();
263
+ if (normalizedLeaderPaneId) {
264
+ try {
265
+ const resolved = await resolvePaneTarget({ type: 'pane', value: normalizedLeaderPaneId }, '', '', '');
266
+ const paneTarget = safeString(resolved?.paneTarget).trim();
267
+ if (paneTarget) return paneTarget;
268
+ } catch {
269
+ // fall through to tmux session scan
270
+ }
271
+ return normalizedLeaderPaneId;
272
+ }
273
+ return '';
274
+ }
275
+
260
276
  async function emitLeaderPaneMissingDeferred({
261
277
  stateDir,
262
278
  logsDir,
@@ -342,6 +358,7 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
342
358
  const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
343
359
  if (!teamInfo) return;
344
360
  const { workers, tmuxSession, leaderPaneId } = teamInfo;
361
+ const canonicalLeaderPaneId = await resolveCanonicalLeaderPaneId(tmuxSession, leaderPaneId);
345
362
 
346
363
  // Check cooldown to prevent notification spam
347
364
  const idleStatePath = join(stateDir, 'team', teamName, 'all-workers-idle.json');
@@ -364,7 +381,7 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
364
381
  );
365
382
  if (!allIdle) return;
366
383
 
367
- if (!leaderPaneId) {
384
+ if (!canonicalLeaderPaneId) {
368
385
  const nextIdleState = {
369
386
  ...idleState,
370
387
  last_notified_at_ms: nowMs,
@@ -380,7 +397,7 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
380
397
  workerName,
381
398
  sourceType: 'all_workers_idle',
382
399
  tmuxSession,
383
- leaderPaneId,
400
+ leaderPaneId: canonicalLeaderPaneId,
384
401
  });
385
402
  return;
386
403
  }
@@ -388,7 +405,7 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
388
405
  const N = workers.length;
389
406
  const nextAction = `Next: run omx team status ${teamName}, read unread worker messages, then decide whether to assign the next concrete task, reconcile results, or shut the team down.`;
390
407
  const message = `[OMX] All ${N} worker${N === 1 ? '' : 's'} idle. ${nextAction} ${DEFAULT_MARKER}`;
391
- const tmuxTarget = leaderPaneId;
408
+ const tmuxTarget = canonicalLeaderPaneId;
392
409
  const paneGuard = await checkPaneReadyForTeamSendKeys(tmuxTarget);
393
410
  if (!paneGuard.ok) {
394
411
  const nextIdleState = {
@@ -409,7 +426,7 @@ export async function maybeNotifyLeaderAllWorkersIdle({ cwd, stateDir, logsDir,
409
426
  paneCurrentCommand: paneGuard.paneCurrentCommand,
410
427
  sourceType: 'all_workers_idle',
411
428
  tmuxSession,
412
- leaderPaneId,
429
+ leaderPaneId: canonicalLeaderPaneId,
413
430
  });
414
431
  return;
415
432
  }
@@ -548,8 +565,9 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
548
565
  const teamInfo = await readTeamWorkersForIdleCheck(stateDir, teamName);
549
566
  if (!teamInfo) return;
550
567
  const { tmuxSession, leaderPaneId } = teamInfo;
568
+ const canonicalLeaderPaneId = await resolveCanonicalLeaderPaneId(tmuxSession, leaderPaneId);
551
569
 
552
- if (!leaderPaneId) {
570
+ if (!canonicalLeaderPaneId) {
553
571
  await emitLeaderPaneMissingDeferred({
554
572
  stateDir,
555
573
  logsDir,
@@ -557,11 +575,11 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
557
575
  workerName,
558
576
  sourceType: 'worker_idle',
559
577
  tmuxSession,
560
- leaderPaneId,
578
+ leaderPaneId: canonicalLeaderPaneId,
561
579
  });
562
580
  return;
563
581
  }
564
- const tmuxTarget = leaderPaneId;
582
+ const tmuxTarget = canonicalLeaderPaneId;
565
583
  const paneGuard = await checkPaneReadyForTeamSendKeys(tmuxTarget);
566
584
  if (!paneGuard.ok) {
567
585
  try {
@@ -584,7 +602,7 @@ export async function maybeNotifyLeaderWorkerIdle({ cwd, stateDir, logsDir, pars
584
602
  paneCurrentCommand: paneGuard.paneCurrentCommand,
585
603
  sourceType: 'worker_idle',
586
604
  tmuxSession,
587
- leaderPaneId,
605
+ leaderPaneId: canonicalLeaderPaneId,
588
606
  });
589
607
  return;
590
608
  }
@@ -23,75 +23,132 @@ import {
23
23
  pickActiveMode,
24
24
  evaluateInjectionGuards,
25
25
  buildSendKeysArgv,
26
+ resolveCodexPane,
26
27
  } from '../tmux-hook-engine.js';
27
28
 
28
- export async function resolveSessionToPane(sessionName: any): Promise<string | null> {
29
- const result = await runProcess('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_id} #{pane_active}']);
30
- const lines = result.stdout
31
- .split('\n')
32
- .map((line: string) => line.trim())
33
- .filter(Boolean);
34
- if (lines.length === 0) return null;
35
- const active = lines.find((line: string) => line.endsWith(' 1')) || lines[0];
36
- const paneId = active.split(' ')[0];
37
- return paneId || null;
29
+ function isHudPaneStartCommand(startCommand: any): boolean {
30
+ return /\bomx\b.*\bhud\b.*--watch/i.test(safeString(startCommand));
38
31
  }
39
32
 
40
- export async function resolvePaneByCwd(expectedCwd: any): Promise<any> {
33
+ async function resolvePaneCwdMismatch(paneId: string, expectedCwd: any): Promise<any | null> {
41
34
  if (!expectedCwd) return null;
42
- const result = await runProcess('tmux', ['list-panes', '-a', '-F', '#{pane_id}\t#{pane_current_path}\t#{pane_active}\t#{session_name}']);
43
- const lines = result.stdout
35
+ try {
36
+ const paneCwdResult = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#{pane_current_path}']);
37
+ const paneCwd = safeString(paneCwdResult.stdout).trim();
38
+ if (paneCwd && resolvePath(paneCwd) !== resolvePath(expectedCwd)) {
39
+ return {
40
+ paneTarget: null,
41
+ reason: 'pane_cwd_mismatch',
42
+ pane_cwd: paneCwd,
43
+ expected_cwd: expectedCwd,
44
+ };
45
+ }
46
+ } catch {
47
+ // Best effort only — if tmux cannot report cwd, keep the explicit pane target.
48
+ }
49
+ return null;
50
+ }
51
+
52
+ async function finalizeResolvedPane(paneId: string, reason: string, expectedCwd: any): Promise<any> {
53
+ const cwdMismatch = await resolvePaneCwdMismatch(paneId, expectedCwd);
54
+ if (cwdMismatch) return cwdMismatch;
55
+ let sessionName = '';
56
+ try {
57
+ const currentSession = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#S']);
58
+ sessionName = safeString(currentSession.stdout).trim();
59
+ } catch {
60
+ sessionName = '';
61
+ }
62
+ return {
63
+ paneTarget: paneId,
64
+ reason,
65
+ matched_session: sessionName || null,
66
+ };
67
+ }
68
+
69
+ async function resolveCanonicalPaneFromPaneTarget(paneTarget: any, expectedCwd: any): Promise<any> {
70
+ const paneResult = await runProcess('tmux', ['display-message', '-p', '-t', paneTarget, '#{pane_id}']);
71
+ const paneId = safeString(paneResult.stdout).trim();
72
+ if (!paneId) return { paneTarget: null, reason: 'target_not_found' };
73
+
74
+ let startCommand = '';
75
+ try {
76
+ const startResult = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#{pane_start_command}']);
77
+ startCommand = safeString(startResult.stdout).trim();
78
+ } catch {
79
+ startCommand = '';
80
+ }
81
+ if (!startCommand || !isHudPaneStartCommand(startCommand)) {
82
+ return finalizeResolvedPane(paneId, 'ok', expectedCwd);
83
+ }
84
+
85
+ let sessionName = '';
86
+ try {
87
+ const sessionResult = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#S']);
88
+ sessionName = safeString(sessionResult.stdout).trim();
89
+ } catch {
90
+ sessionName = '';
91
+ }
92
+ if (!sessionName) return { paneTarget: null, reason: 'target_is_hud_pane' };
93
+
94
+ const healedPaneId = await resolveSessionToPane(sessionName);
95
+ if (!healedPaneId) return { paneTarget: null, reason: 'target_is_hud_pane' };
96
+ return finalizeResolvedPane(healedPaneId, 'healed_hud_pane_target', expectedCwd);
97
+ }
98
+
99
+ export async function resolveSessionToPane(sessionName: any): Promise<string | null> {
100
+ const result = await runProcess('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_id}\t#{pane_active}\t#{pane_current_command}\t#{pane_start_command}']);
101
+ const rows = result.stdout
44
102
  .split('\n')
45
103
  .map((line: string) => line.trim())
46
- .filter(Boolean);
47
-
48
- const expected = resolvePath(expectedCwd);
49
- const candidates: any[] = [];
50
- for (const line of lines) {
51
- const parts = line.split('\t');
52
- if (parts.length < 4) continue;
53
- const [paneId, paneCwd, activeRaw, sessionName] = parts;
54
- if (!paneId || !paneCwd) continue;
55
- if (resolvePath(paneCwd) !== expected) continue;
56
- const active = activeRaw === '1';
57
- candidates.push({ paneId, paneCwd, active, sessionName: sessionName || null });
58
- }
59
- if (candidates.length === 0) return null;
104
+ .filter(Boolean)
105
+ .map((line: string) => {
106
+ const parts = line.includes('\t')
107
+ ? line.split('\t')
108
+ : line.split(/\s+/, 4);
109
+ const [paneId = '', activeRaw = '0', currentCommand = '', startCommand = ''] = parts;
110
+ return {
111
+ paneId,
112
+ active: activeRaw === '1',
113
+ currentCommand: safeString(currentCommand).trim().toLowerCase(),
114
+ startCommand: safeString(startCommand).trim(),
115
+ };
116
+ })
117
+ .filter((row: any) => row.paneId.startsWith('%'));
118
+ if (rows.length === 0) return null;
60
119
 
61
- const pick = candidates.find((c: any) => c.active) || candidates[0];
62
- return pick;
120
+ const nonHudRows = rows.filter((row: any) => !isHudPaneStartCommand(row.startCommand));
121
+ const canonicalRows = nonHudRows.filter((row: any) => /\bcodex\b/i.test(row.startCommand));
122
+ const activeCanonical = canonicalRows.find((row: any) => row.active);
123
+ if (activeCanonical) return activeCanonical.paneId;
124
+ if (canonicalRows[0]) return canonicalRows[0].paneId;
125
+
126
+ const activeNonHud = nonHudRows.find((row: any) => row.active);
127
+ if (activeNonHud) return activeNonHud.paneId;
128
+ return nonHudRows[0]?.paneId || null;
63
129
  }
64
130
 
65
131
  export async function resolvePaneTarget(target: any, fallbackPane: any, expectedCwd: any, modePane: any): Promise<any> {
132
+ const canonicalFallbackPane = safeString(fallbackPane).trim();
133
+ if (canonicalFallbackPane) {
134
+ try {
135
+ return await finalizeResolvedPane(canonicalFallbackPane, 'fallback_current_pane', expectedCwd);
136
+ } catch {
137
+ // Fall through to mode/config probes
138
+ }
139
+ }
140
+
66
141
  if (modePane) {
67
142
  try {
68
- const modePaneResult = await runProcess('tmux', ['display-message', '-p', '-t', modePane, '#{pane_id}']);
69
- const paneId = safeString(modePaneResult.stdout).trim();
70
- if (paneId) {
71
- if (expectedCwd) {
72
- const paneCwdResult = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#{pane_current_path}']);
73
- const paneCwd = safeString(paneCwdResult.stdout).trim();
74
- if (!paneCwd || resolvePath(paneCwd) === resolvePath(expectedCwd)) {
75
- const currentSession = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#S']);
76
- const sessionName = safeString(currentSession.stdout).trim();
77
- return {
78
- paneTarget: paneId,
79
- reason: 'fallback_mode_state_pane',
80
- matched_session: sessionName || null,
81
- };
82
- }
83
- } else {
84
- const currentSession = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#S']);
85
- const sessionName = safeString(currentSession.stdout).trim();
86
- return {
87
- paneTarget: paneId,
88
- reason: 'fallback_mode_state_pane',
89
- matched_session: sessionName || null,
90
- };
91
- }
143
+ const resolved = await resolveCanonicalPaneFromPaneTarget(modePane, expectedCwd);
144
+ if (resolved.paneTarget) {
145
+ return {
146
+ ...resolved,
147
+ reason: resolved.reason === 'ok' ? 'fallback_mode_state_pane' : resolved.reason,
148
+ };
92
149
  }
93
150
  } catch {
94
- // Fall through to config/fallback probes
151
+ // Fall through to config probes
95
152
  }
96
153
  }
97
154
 
@@ -99,65 +156,20 @@ export async function resolvePaneTarget(target: any, fallbackPane: any, expected
99
156
 
100
157
  if (target.type === 'pane') {
101
158
  try {
102
- const result = await runProcess('tmux', ['display-message', '-p', '-t', target.value, '#{pane_id}']);
103
- const paneId = safeString(result.stdout).trim();
104
- if (paneId) return { paneTarget: paneId, reason: 'ok' };
159
+ const resolved = await resolveCanonicalPaneFromPaneTarget(target.value, expectedCwd);
160
+ if (resolved.paneTarget) return resolved;
105
161
  } catch {
106
- // Fall through to fallback probe
162
+ // Fall through
107
163
  }
108
164
  } else {
109
165
  try {
110
166
  const paneId = await resolveSessionToPane(target.value);
111
- if (paneId) return { paneTarget: paneId, reason: 'ok' };
112
- } catch {
113
- // Fall through to fallback probe
114
- }
115
- }
116
-
117
- if (fallbackPane) {
118
- try {
119
- const currentPane = await runProcess('tmux', ['display-message', '-p', '-t', fallbackPane, '#{pane_id}']);
120
- const paneId = safeString(currentPane.stdout).trim();
121
- if (paneId) {
122
- if (expectedCwd) {
123
- const paneCwdResult = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#{pane_current_path}']);
124
- const paneCwd = safeString(paneCwdResult.stdout).trim();
125
- if (paneCwd && resolvePath(paneCwd) !== resolvePath(expectedCwd)) {
126
- return {
127
- paneTarget: null,
128
- reason: 'pane_cwd_mismatch',
129
- pane_cwd: paneCwd,
130
- expected_cwd: expectedCwd,
131
- };
132
- }
133
- }
134
-
135
- const currentSession = await runProcess('tmux', ['display-message', '-p', '-t', paneId, '#S']);
136
- const sessionName = safeString(currentSession.stdout).trim();
137
- return {
138
- paneTarget: paneId,
139
- reason: 'fallback_current_pane',
140
- matched_session: sessionName || null,
141
- };
142
- }
167
+ if (paneId) return await finalizeResolvedPane(paneId, 'ok', expectedCwd);
143
168
  } catch {
144
169
  // Fall through
145
170
  }
146
171
  }
147
172
 
148
- try {
149
- const match = await resolvePaneByCwd(expectedCwd);
150
- if (match && match.paneId) {
151
- return {
152
- paneTarget: match.paneId,
153
- reason: 'fallback_pane_by_cwd',
154
- matched_session: match.sessionName,
155
- };
156
- }
157
- } catch {
158
- // Fall through
159
- }
160
-
161
173
  return { paneTarget: null, reason: 'target_not_found' };
162
174
  }
163
175
 
@@ -268,8 +280,7 @@ export async function handleTmuxInjection({
268
280
  turnId,
269
281
  timestamp: nowIso,
270
282
  }), sourceText);
271
- const { resolveCodexPane } = await import('../tmux-hook-engine.js');
272
- const fallbackPane = resolveCodexPane() || safeString(process.env.TMUX_PANE || '');
283
+ const fallbackPane = resolveCodexPane();
273
284
  const resolution = await resolvePaneTarget(config.target, fallbackPane, cwd, modePane);
274
285
  if (!resolution.paneTarget) {
275
286
  state.last_reason = resolution.reason;
@@ -307,8 +318,8 @@ export async function handleTmuxInjection({
307
318
  return;
308
319
  }
309
320
 
310
- // Pane-canonical healing: persist resolved pane target so routing stops depending on session names.
311
- if (config.target && config.target.type !== 'pane') {
321
+ // Pane-canonical healing: persist resolved pane target so routing stops depending on session names or stale pane ids.
322
+ if (config.target && (config.target.type !== 'pane' || safeString(config.target.value).trim() !== paneTarget)) {
312
323
  try {
313
324
  const healed = {
314
325
  ...(rawConfig && typeof rawConfig === 'object' ? rawConfig : {}),