oh-my-codex 0.18.0 → 0.18.1

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 (293) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +43 -19
  4. package/crates/omx-api/src/lib.rs +66 -9
  5. package/crates/omx-sparkshell/src/exec.rs +125 -3
  6. package/crates/omx-sparkshell/src/main.rs +126 -36
  7. package/crates/omx-sparkshell/tests/execution.rs +225 -1
  8. package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
  9. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
  11. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  12. package/dist/cli/__tests__/index.test.js +49 -1
  13. package/dist/cli/__tests__/index.test.js.map +1 -1
  14. package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
  15. package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
  16. package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
  17. package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
  18. package/dist/cli/__tests__/launch-fallback.test.js +115 -0
  19. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  20. package/dist/cli/__tests__/question.test.js +27 -41
  21. package/dist/cli/__tests__/question.test.js.map +1 -1
  22. package/dist/cli/__tests__/setup-install-mode.test.js +94 -35
  23. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  24. package/dist/cli/__tests__/sparkshell-cli.test.js +20 -1
  25. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  26. package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
  27. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
  28. package/dist/cli/__tests__/ultragoal.test.js +227 -4
  29. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  30. package/dist/cli/__tests__/update.test.js +72 -1
  31. package/dist/cli/__tests__/update.test.js.map +1 -1
  32. package/dist/cli/codex-feature-probe.d.ts +5 -0
  33. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  34. package/dist/cli/codex-feature-probe.js +13 -7
  35. package/dist/cli/codex-feature-probe.js.map +1 -1
  36. package/dist/cli/doctor.d.ts +7 -0
  37. package/dist/cli/doctor.d.ts.map +1 -1
  38. package/dist/cli/doctor.js +119 -10
  39. package/dist/cli/doctor.js.map +1 -1
  40. package/dist/cli/index.d.ts +3 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +345 -90
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/plugin-marketplace.d.ts +2 -0
  45. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  46. package/dist/cli/plugin-marketplace.js +15 -1
  47. package/dist/cli/plugin-marketplace.js.map +1 -1
  48. package/dist/cli/setup.d.ts.map +1 -1
  49. package/dist/cli/setup.js +71 -11
  50. package/dist/cli/setup.js.map +1 -1
  51. package/dist/cli/sparkshell.d.ts +7 -1
  52. package/dist/cli/sparkshell.d.ts.map +1 -1
  53. package/dist/cli/sparkshell.js +13 -3
  54. package/dist/cli/sparkshell.js.map +1 -1
  55. package/dist/cli/ultragoal.d.ts +1 -1
  56. package/dist/cli/ultragoal.d.ts.map +1 -1
  57. package/dist/cli/ultragoal.js +184 -10
  58. package/dist/cli/ultragoal.js.map +1 -1
  59. package/dist/cli/update.d.ts +2 -0
  60. package/dist/cli/update.d.ts.map +1 -1
  61. package/dist/cli/update.js +14 -3
  62. package/dist/cli/update.js.map +1 -1
  63. package/dist/compat/__tests__/doctor-contract.test.js +3 -0
  64. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  65. package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
  66. package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
  67. package/dist/config/__tests__/codex-hooks.test.js +19 -8
  68. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  69. package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
  70. package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
  71. package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
  72. package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
  73. package/dist/config/codex-feature-flags.d.ts +4 -0
  74. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  75. package/dist/config/codex-feature-flags.js +4 -0
  76. package/dist/config/codex-feature-flags.js.map +1 -1
  77. package/dist/config/codex-hooks.js +6 -6
  78. package/dist/config/codex-hooks.js.map +1 -1
  79. package/dist/config/commit-lore-guard.d.ts +1 -0
  80. package/dist/config/commit-lore-guard.d.ts.map +1 -1
  81. package/dist/config/commit-lore-guard.js +29 -3
  82. package/dist/config/commit-lore-guard.js.map +1 -1
  83. package/dist/config/generator.d.ts +3 -1
  84. package/dist/config/generator.d.ts.map +1 -1
  85. package/dist/config/generator.js +24 -10
  86. package/dist/config/generator.js.map +1 -1
  87. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
  88. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  89. package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
  90. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  91. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
  92. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  93. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
  94. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
  95. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  96. package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
  97. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  98. package/dist/hooks/__tests__/keyword-detector.test.js +4 -3
  99. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  100. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
  101. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  102. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
  103. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  104. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  105. package/dist/hooks/extensibility/dispatcher.js +29 -14
  106. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  107. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  108. package/dist/hooks/keyword-detector.js +8 -3
  109. package/dist/hooks/keyword-detector.js.map +1 -1
  110. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  111. package/dist/hooks/prompt-guidance-contract.js +3 -2
  112. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  113. package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
  114. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  115. package/dist/hud/__tests__/reconcile.test.js +2 -2
  116. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  117. package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
  118. package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  119. package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
  120. package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
  121. package/dist/hud/index.d.ts +1 -1
  122. package/dist/hud/index.d.ts.map +1 -1
  123. package/dist/hud/index.js +10 -4
  124. package/dist/hud/index.js.map +1 -1
  125. package/dist/hud/tmux.js +2 -2
  126. package/dist/hud/tmux.js.map +1 -1
  127. package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
  128. package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
  129. package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
  130. package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
  131. package/dist/notifications/__tests__/verbosity.test.js +20 -0
  132. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  133. package/dist/notifications/config.d.ts.map +1 -1
  134. package/dist/notifications/config.js +6 -3
  135. package/dist/notifications/config.js.map +1 -1
  136. package/dist/notifications/http-client.d.ts.map +1 -1
  137. package/dist/notifications/http-client.js +78 -27
  138. package/dist/notifications/http-client.js.map +1 -1
  139. package/dist/notifications/types.d.ts +2 -0
  140. package/dist/notifications/types.d.ts.map +1 -1
  141. package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
  142. package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
  143. package/dist/openclaw/dispatcher.d.ts +7 -4
  144. package/dist/openclaw/dispatcher.d.ts.map +1 -1
  145. package/dist/openclaw/dispatcher.js +32 -69
  146. package/dist/openclaw/dispatcher.js.map +1 -1
  147. package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
  148. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  149. package/dist/pipeline/__tests__/stages.test.js +50 -5
  150. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  151. package/dist/pipeline/index.d.ts +8 -2
  152. package/dist/pipeline/index.d.ts.map +1 -1
  153. package/dist/pipeline/index.js +5 -2
  154. package/dist/pipeline/index.js.map +1 -1
  155. package/dist/pipeline/orchestrator.d.ts +5 -4
  156. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  157. package/dist/pipeline/orchestrator.js +56 -15
  158. package/dist/pipeline/orchestrator.js.map +1 -1
  159. package/dist/pipeline/stages/code-review.d.ts +2 -2
  160. package/dist/pipeline/stages/code-review.d.ts.map +1 -1
  161. package/dist/pipeline/stages/code-review.js +5 -3
  162. package/dist/pipeline/stages/code-review.js.map +1 -1
  163. package/dist/pipeline/stages/deep-interview.d.ts +15 -0
  164. package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
  165. package/dist/pipeline/stages/deep-interview.js +32 -0
  166. package/dist/pipeline/stages/deep-interview.js.map +1 -0
  167. package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
  168. package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
  169. package/dist/pipeline/stages/ralph-verify.js +2 -2
  170. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  171. package/dist/pipeline/stages/ultragoal.d.ts +19 -0
  172. package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
  173. package/dist/pipeline/stages/ultragoal.js +38 -0
  174. package/dist/pipeline/stages/ultragoal.js.map +1 -0
  175. package/dist/pipeline/stages/ultraqa.d.ts +30 -0
  176. package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
  177. package/dist/pipeline/stages/ultraqa.js +46 -0
  178. package/dist/pipeline/stages/ultraqa.js.map +1 -0
  179. package/dist/pipeline/types.d.ts +8 -6
  180. package/dist/pipeline/types.d.ts.map +1 -1
  181. package/dist/pipeline/types.js +2 -2
  182. package/dist/scripts/__tests__/codex-native-hook.test.js +705 -45
  183. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  184. package/dist/scripts/__tests__/smoke-packed-install.test.js +23 -1
  185. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  186. package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
  187. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  188. package/dist/scripts/cleanup-explore-harness.js +1 -0
  189. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  190. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  191. package/dist/scripts/codex-native-hook.js +158 -10
  192. package/dist/scripts/codex-native-hook.js.map +1 -1
  193. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  194. package/dist/scripts/codex-native-pre-post.js +9 -1
  195. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  196. package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
  197. package/dist/scripts/notify-hook/process-runner.js +39 -17
  198. package/dist/scripts/notify-hook/process-runner.js.map +1 -1
  199. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  200. package/dist/scripts/notify-hook/team-dispatch.js +9 -5
  201. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  202. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
  203. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/team-tmux-guard.js +7 -1
  205. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  206. package/dist/scripts/smoke-packed-install.d.ts +3 -0
  207. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  208. package/dist/scripts/smoke-packed-install.js +99 -1
  209. package/dist/scripts/smoke-packed-install.js.map +1 -1
  210. package/dist/scripts/sync-plugin-mirror.js +2 -2
  211. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  212. package/dist/scripts/verify-native-agents.js +2 -2
  213. package/dist/scripts/verify-native-agents.js.map +1 -1
  214. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
  215. package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
  216. package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
  217. package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
  218. package/dist/sidecar/index.d.ts +1 -1
  219. package/dist/sidecar/index.d.ts.map +1 -1
  220. package/dist/sidecar/index.js +29 -12
  221. package/dist/sidecar/index.js.map +1 -1
  222. package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
  223. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  224. package/dist/state/operations.d.ts.map +1 -1
  225. package/dist/state/operations.js +11 -0
  226. package/dist/state/operations.js.map +1 -1
  227. package/dist/team/__tests__/tmux-session.test.js +111 -3
  228. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  229. package/dist/team/tmux-session.d.ts.map +1 -1
  230. package/dist/team/tmux-session.js +39 -18
  231. package/dist/team/tmux-session.js.map +1 -1
  232. package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
  233. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  234. package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
  235. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  236. package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
  237. package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
  238. package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
  239. package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
  240. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
  241. package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
  242. package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
  243. package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
  244. package/dist/ultragoal/artifacts.d.ts +97 -2
  245. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  246. package/dist/ultragoal/artifacts.js +811 -256
  247. package/dist/ultragoal/artifacts.js.map +1 -1
  248. package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
  249. package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
  250. package/dist/utils/__tests__/sleep-resource.test.js +39 -0
  251. package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
  252. package/dist/utils/sleep.d.ts.map +1 -1
  253. package/dist/utils/sleep.js +17 -6
  254. package/dist/utils/sleep.js.map +1 -1
  255. package/package.json +2 -1
  256. package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
  257. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
  258. package/plugins/oh-my-codex/hooks/hooks.json +77 -0
  259. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
  260. package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
  261. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +8 -8
  262. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  263. package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
  264. package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
  265. package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
  266. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +4 -4
  267. package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
  268. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
  269. package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
  270. package/prompts/planner.md +1 -1
  271. package/skills/autopilot/SKILL.md +77 -47
  272. package/skills/cancel/SKILL.md +2 -2
  273. package/skills/deep-interview/SKILL.md +8 -8
  274. package/skills/omx-setup/SKILL.md +1 -1
  275. package/skills/pipeline/SKILL.md +22 -11
  276. package/skills/plan/SKILL.md +8 -8
  277. package/skills/ralph/SKILL.md +7 -0
  278. package/skills/ralplan/SKILL.md +4 -4
  279. package/skills/team/SKILL.md +1 -1
  280. package/skills/ultragoal/SKILL.md +38 -4
  281. package/skills/ultrawork/SKILL.md +1 -1
  282. package/src/scripts/__tests__/codex-native-hook.test.ts +867 -81
  283. package/src/scripts/__tests__/smoke-packed-install.test.ts +31 -0
  284. package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
  285. package/src/scripts/cleanup-explore-harness.ts +1 -0
  286. package/src/scripts/codex-native-hook.ts +156 -10
  287. package/src/scripts/codex-native-pre-post.ts +16 -1
  288. package/src/scripts/notify-hook/process-runner.ts +40 -16
  289. package/src/scripts/notify-hook/team-dispatch.ts +9 -5
  290. package/src/scripts/notify-hook/team-tmux-guard.ts +7 -0
  291. package/src/scripts/smoke-packed-install.ts +105 -0
  292. package/src/scripts/sync-plugin-mirror.ts +3 -3
  293. package/src/scripts/verify-native-agents.ts +2 -2
@@ -7,10 +7,13 @@ import { test } from 'node:test';
7
7
  import {
8
8
  ensureRepoDependencies,
9
9
  hasUsableNodeModules,
10
+ buildNativeHookSmokePayload,
11
+ PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS,
10
12
  PACKED_INSTALL_SMOKE_CORE_COMMANDS,
11
13
  parseNpmPackJsonOutput,
12
14
  resolveGitCommonDir,
13
15
  resolveReusableNodeModulesSource,
16
+ validateHookStdout,
14
17
  } from '../smoke-packed-install.js';
15
18
 
16
19
  test('packed install smoke stays limited to boot + core commands', () => {
@@ -30,6 +33,34 @@ test('packed install smoke stays limited to boot + core commands', () => {
30
33
  );
31
34
  });
32
35
 
36
+ test('packed install smoke covers every installed native hook event with minimal payloads', () => {
37
+ assert.deepEqual(PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS, [
38
+ 'SessionStart',
39
+ 'PreToolUse',
40
+ 'PostToolUse',
41
+ 'UserPromptSubmit',
42
+ 'PreCompact',
43
+ 'PostCompact',
44
+ 'Stop',
45
+ ]);
46
+
47
+ for (const eventName of PACKED_INSTALL_NATIVE_HOOK_SMOKE_EVENTS) {
48
+ const payload = buildNativeHookSmokePayload(eventName, '/tmp/omx-packed-hook-smoke');
49
+ assert.equal(payload.hook_event_name, eventName);
50
+ assert.equal(typeof payload.session_id, 'string');
51
+ assert.equal(payload.cwd, '/tmp/omx-packed-hook-smoke');
52
+ }
53
+ });
54
+
55
+ test('packed install native hook stdout validation allows empty or JSON output only', () => {
56
+ assert.doesNotThrow(() => validateHookStdout('PostCompact', ''));
57
+ assert.doesNotThrow(() => validateHookStdout('Stop', '{}\n'));
58
+ assert.throws(
59
+ () => validateHookStdout('UserPromptSubmit', '{not json'),
60
+ /native hook UserPromptSubmit emitted invalid JSON stdout/,
61
+ );
62
+ });
63
+
33
64
  test('parseNpmPackJsonOutput ignores prepack logs before npm pack JSON', () => {
34
65
  const parsed = parseNpmPackJsonOutput([
35
66
  '[sync-plugin-mirror] synced 29 canonical skill directories and plugin metadata',
@@ -53,7 +53,7 @@ describe("verify-native-agents", () => {
53
53
  ]),
54
54
  definitions: { executor: definition, "style-reviewer": { ...definition, name: "style-reviewer" } },
55
55
  promptNames: new Set(["executor", "style-reviewer", "explore-harness"]),
56
- pluginManifest: { skills: "./skills/" },
56
+ pluginManifest: { skills: "./skills/", hooks: "./hooks/hooks.json" },
57
57
  });
58
58
 
59
59
  assert.deepEqual(result.installableAgentNames, ["executor"]);
@@ -183,6 +183,26 @@ describe("verify-native-agents", () => {
183
183
  pluginManifest: { agents: "./agents/" },
184
184
  }),
185
185
  );
186
+
187
+ await rejectsWithCode("native_agent_plugin_boundary_violation", () =>
188
+ verifyNativeAgents({
189
+ manifest: manifest([{ name: "executor", category: "build", status: "active" }]),
190
+ definitions: { executor: definition },
191
+ promptNames: new Set(["executor"]),
192
+ pluginManifest: { prompts: "./prompts/" },
193
+ }),
194
+ );
195
+ });
196
+
197
+ it("allows plugin-scoped lifecycle hooks without treating them as native agents", async () => {
198
+ const result = await verifyNativeAgents({
199
+ manifest: manifest([{ name: "executor", category: "build", status: "active" }]),
200
+ definitions: { executor: definition },
201
+ promptNames: new Set(["executor"]),
202
+ pluginManifest: { hooks: "./hooks/hooks.json" },
203
+ });
204
+
205
+ assert.deepEqual(result.installableAgentNames, ["executor"]);
186
206
  });
187
207
 
188
208
  it("keeps merged prompt-backed agents out of the installable set", () => {
@@ -12,6 +12,7 @@ for (const path of [
12
12
  join(root, 'bin', 'omx-explore-harness.exe'),
13
13
  join(root, 'bin', 'omx-explore-harness.meta.json'),
14
14
  join(root, 'bin', 'native'),
15
+ join(root, 'crates', 'omx-sparkshell', '.omx'),
15
16
  ]) {
16
17
  await rm(path, { recursive: true, force: true });
17
18
  }
@@ -69,6 +69,7 @@ import {
69
69
  } from "../hooks/extensibility/events.js";
70
70
  import type { HookEventEnvelope } from "../hooks/extensibility/types.js";
71
71
  import { dispatchHookEventRuntime } from "../hooks/extensibility/runtime.js";
72
+ import { getNotificationConfig, getVerbosity } from "../notifications/config.js";
72
73
  import { reconcileHudForPromptSubmit } from "../hud/reconcile.js";
73
74
  import {
74
75
  onPreCompact as buildWikiPreCompactContext,
@@ -78,6 +79,11 @@ import { readAutoresearchCompletionStatus, readAutoresearchModeStateForActiveDec
78
79
  import { readRunState } from "../runtime/run-state.js";
79
80
  import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
80
81
  import { getRunContinuationSnapshot, shouldContinueRun } from "../runtime/run-loop.js";
82
+ import {
83
+ parseUltragoalSteeringDirective,
84
+ steerUltragoal,
85
+ type UltragoalSteeringProposal,
86
+ } from "../ultragoal/artifacts.js";
81
87
  import { triagePrompt } from "../hooks/triage-heuristic.js";
82
88
  import { readTriageConfig } from "../hooks/triage-config.js";
83
89
  import {
@@ -305,6 +311,13 @@ async function isNativeSubagentHook(
305
311
  return candidateIds.some((id) => summary.allSubagentThreadIds.includes(id));
306
312
  }
307
313
 
314
+ function shouldSuppressSubagentLifecycleHookDispatch(): boolean {
315
+ const config = getNotificationConfig();
316
+ if (config?.includeChildAgents === true) return false;
317
+ const verbosity = getVerbosity(config);
318
+ return verbosity !== "agent" && verbosity !== "verbose";
319
+ }
320
+
308
321
  async function recordIgnoredNativeSubagentSessionStart(
309
322
  cwd: string,
310
323
  canonicalSessionId: string,
@@ -419,6 +432,104 @@ function readPromptText(payload: CodexHookPayload): string {
419
432
  return "";
420
433
  }
421
434
 
435
+
436
+ function extractBalancedJsonObject(text: string, startIndex: number): string | null {
437
+ let depth = 0;
438
+ let inString = false;
439
+ let escaped = false;
440
+ for (let index = startIndex; index < text.length; index++) {
441
+ const char = text[index];
442
+ if (inString) {
443
+ if (escaped) escaped = false;
444
+ else if (char === "\\") escaped = true;
445
+ else if (char === '"') inString = false;
446
+ continue;
447
+ }
448
+ if (char === '"') {
449
+ inString = true;
450
+ continue;
451
+ }
452
+ if (char === "{") depth += 1;
453
+ else if (char === "}") {
454
+ depth -= 1;
455
+ if (depth === 0) return text.slice(startIndex, index + 1);
456
+ }
457
+ }
458
+ return null;
459
+ }
460
+
461
+ function normalizePromptSteeringProposal(raw: unknown, prompt: string): UltragoalSteeringProposal | null {
462
+ const candidate = safeObject(raw);
463
+ const nested = candidate.omx_ultragoal_steer ?? candidate.ultragoal_steer ?? candidate.steering ?? candidate;
464
+ const proposal = parseUltragoalSteeringDirective(JSON.stringify(nested));
465
+ if (!proposal) return null;
466
+ if (proposal.source !== "user_prompt_submit") return null;
467
+ const normalized = prompt.trim().toLowerCase();
468
+ return {
469
+ ...proposal,
470
+ directiveText: proposal.directiveText ?? safeContextSnippet(prompt, 600),
471
+ promptSignature: proposal.promptSignature ?? promptSignature(normalized),
472
+ idempotencyKey: proposal.idempotencyKey ?? `user_prompt_submit:${promptSignature(normalized)}`,
473
+ };
474
+ }
475
+
476
+ function parseUserPromptUltragoalSteeringDirective(prompt: string): UltragoalSteeringProposal | null {
477
+ const trimmed = prompt.trim();
478
+ if (!trimmed) return null;
479
+ const fenced = trimmed.match(/```(?:omx-ultragoal-steer|ultragoal-steer)\s*([\s\S]*?)```/i);
480
+ if (fenced?.[1]) {
481
+ try {
482
+ return normalizePromptSteeringProposal(JSON.parse(fenced[1]), prompt);
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+
488
+ const label = trimmed.match(/(?:^|\n)\s*(?:OMX_ULTRAGOAL_STEER|omx\.ultragoal\.steer|omx ultragoal steer)\s*:\s*{/i);
489
+ if (label?.index !== undefined) {
490
+ const brace = trimmed.indexOf("{", label.index);
491
+ const json = brace >= 0 ? extractBalancedJsonObject(trimmed, brace) : null;
492
+ if (json) {
493
+ try {
494
+ return normalizePromptSteeringProposal(JSON.parse(json), prompt);
495
+ } catch {
496
+ return null;
497
+ }
498
+ }
499
+ }
500
+
501
+ if (trimmed.startsWith("{")) {
502
+ try {
503
+ const parsed = JSON.parse(trimmed);
504
+ const object = safeObject(parsed);
505
+ if ("omx_ultragoal_steer" in object || "ultragoal_steer" in object) {
506
+ return normalizePromptSteeringProposal(parsed, prompt);
507
+ }
508
+ } catch {
509
+ return null;
510
+ }
511
+ }
512
+ return null;
513
+ }
514
+
515
+ async function applyUserPromptUltragoalSteering(cwd: string, prompt: string): Promise<string | null> {
516
+ const proposal = parseUserPromptUltragoalSteeringDirective(prompt);
517
+ if (!proposal) return null;
518
+ try {
519
+ const result = await steerUltragoal(cwd, proposal);
520
+ const status = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
521
+ const reasons = result.rejectedReasons.length > 0 ? ` rejectedReasons=${result.rejectedReasons.join("; ")}` : "";
522
+ return [
523
+ `OMX native UserPromptSubmit applied bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${status}.`,
524
+ `mutation=${result.audit.kind}; source=${result.audit.source}; targets=${result.audit.targetGoalIds.join(",") || "none"}; idempotencyKey=${result.audit.idempotencyKey ?? "none"}.${reasons}`,
525
+ "Only explicit structured steering directives are parsed; normal prose is ignored and cannot mutate .omx/ultragoal.",
526
+ ].join(" ");
527
+ } catch (error) {
528
+ const message = error instanceof Error ? error.message : String(error);
529
+ return `OMX native UserPromptSubmit rejected bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${message}`;
530
+ }
531
+ }
532
+
422
533
  function sanitizePayloadForHookContext(
423
534
  payload: CodexHookPayload,
424
535
  hookEventName: CodexHookEventName,
@@ -793,13 +904,12 @@ async function reopenRalphCompletionAuditBlock(block: RalphCompletionAuditBlockS
793
904
  const nowIso = new Date().toISOString();
794
905
  const next: Record<string, unknown> = {
795
906
  ...block.state,
796
- active: true,
797
- current_phase: "verifying",
907
+ active: false,
908
+ current_phase: "complete",
798
909
  completion_audit_gate: "blocked",
799
910
  completion_audit_missing_reason: block.reason,
800
911
  completion_audit_blocked_at: nowIso,
801
912
  };
802
- delete next.completed_at;
803
913
  await writeFile(block.path, JSON.stringify(next, null, 2));
804
914
  }
805
915
 
@@ -1604,7 +1714,7 @@ function buildAdditionalContextMessage(
1604
1714
  ? "Ultrawork protocol: ground the task before editing, define pass/fail acceptance criteria, keep shared-file work local, and use direct-tool plus background evidence lanes only for truly independent work. Direct ultrawork provides lightweight verification only; Ralph owns persistence and the full verified-completion promise."
1605
1715
  : null;
1606
1716
  const ultragoalPromptActivationNote = match.skill === "ultragoal"
1607
- ? "Ultragoal protocol: use `omx ultragoal create-goals` / `complete-goals` / `checkpoint` for `.omx/ultragoal` artifacts, then use Codex goal model tools only from the active agent handoff (`get_goal`, `create_goal`, `update_goal`) and never overwrite a different active Codex goal."
1717
+ ? "Ultragoal protocol: use `omx ultragoal create-goals` / `complete-goals` / `checkpoint` for `.omx/ultragoal` artifacts, then use Codex goal model tools only from the active agent handoff (`get_goal`, `create_goal`, `update_goal`) and never overwrite a different active Codex goal. Ultragoal does not call `/goal clear`; for multiple sequential ultragoal runs in one Codex session/thread, manually clear the completed Codex goal in the UI before creating the next aggregate goal."
1608
1718
  : null;
1609
1719
  const combinedTransitionMessage = (() => {
1610
1720
  if (!skillState?.transition_message) return null;
@@ -1867,6 +1977,9 @@ async function buildModeBasedStopOutput(
1867
1977
  cwd: string,
1868
1978
  sessionId?: string,
1869
1979
  ): Promise<Record<string, unknown> | null> {
1980
+ if (await readCanonicalTerminalRunStateForStop(cwd, sessionId, mode)) {
1981
+ return null;
1982
+ }
1870
1983
  const state = await readModeStateForActiveDecision(mode, sessionId?.trim() || undefined, cwd);
1871
1984
  if (!state || !shouldContinueRun(state)) return null;
1872
1985
  const phase = formatPhase(state.current_phase);
@@ -1895,6 +2008,22 @@ function reportsAutoresearchGoalObjectiveMismatch(text: string): boolean {
1895
2008
  && /objective mismatch/i.test(text);
1896
2009
  }
1897
2010
 
2011
+ function reportsBlockedPerformanceGoalObjectiveMismatch(state: unknown): boolean {
2012
+ const performanceState = safeObject(state);
2013
+ const lastValidation = safeObject(performanceState.lastValidation);
2014
+ if (safeString(performanceState.workflow) !== "performance-goal") return false;
2015
+ if (safeString(performanceState.status) !== "blocked") return false;
2016
+ if (safeString(lastValidation.status) !== "blocked") return false;
2017
+
2018
+ const evidence = [
2019
+ safeString(lastValidation.evidence),
2020
+ safeString(lastValidation.message),
2021
+ safeString(performanceState.evidence),
2022
+ safeString(performanceState.message),
2023
+ ].join(" ");
2024
+ return /objective mismatch/i.test(evidence);
2025
+ }
2026
+
1898
2027
  async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Promise<{ workflow: string; command: string; remediation?: string } | null> {
1899
2028
  const ultragoal = await readJsonIfExists(join(cwd, ".omx", "ultragoal", "goals.json"));
1900
2029
  const aggregateCompletion = safeObject(ultragoal?.aggregateCompletion);
@@ -1912,7 +2041,7 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
1912
2041
  `If get_goal returns a completed task-scoped objective for the same aggregate ultragoal plan, checkpoint ${goalId} with evidence naming ${goalId} plus .omx/ultragoal/goals.json or ledger.jsonl and pass final quality-gate JSON; OMX will reconcile the completed planned scope without mutating Codex goal state.`,
1913
2042
  `If get_goal instead returns a different completed legacy objective and complete checkpointing fails, do not repeat --status complete in this thread.`,
1914
2043
  `Record the non-terminal blocker with: omx ultragoal checkpoint --goal-id ${goalId} --status blocked --codex-goal-json '<different completed get_goal JSON or path>' --evidence '<completed legacy Codex goal blocks create_goal in this thread>'.`,
1915
- "Then continue this ultragoal from a fresh Codex thread in the same repo/worktree and create the intended goal there.",
2044
+ "Then continue only from a Codex goal context with no active/completed conflicting goal in the same repo/worktree and create the intended goal there.",
1916
2045
  ].join(" "),
1917
2046
  };
1918
2047
  }
@@ -1922,6 +2051,9 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
1922
2051
  if (!entry.isDirectory()) continue;
1923
2052
  const state = await readJsonIfExists(join(performanceRoot, entry.name, "state.json"));
1924
2053
  const status = safeString(state?.status);
2054
+ if (reportsBlockedPerformanceGoalObjectiveMismatch(state)) {
2055
+ continue;
2056
+ }
1925
2057
  if (state?.workflow === "performance-goal" && status && status !== "complete") {
1926
2058
  return {
1927
2059
  workflow: "performance-goal",
@@ -1948,8 +2080,8 @@ async function findActiveGoalWorkflowReconciliationRequirement(cwd: string): Pro
1948
2080
  workflow: "autoresearch-goal",
1949
2081
  command: `omx autoresearch-goal complete --slug ${safeString(mission.slug) || entry.name} --codex-goal-json '<get_goal JSON or path>'`,
1950
2082
  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.",
2083
+ "If that command fails with a Codex goal objective mismatch after a refreshed get_goal snapshot, do not repeat the same complete command blindly in this thread.",
2084
+ "Either retry with a correct refreshed snapshot or record an explicit blocked verdict for this autoresearch-goal and continue from the explicit blocker path.",
1953
2085
  ].join(" "),
1954
2086
  };
1955
2087
  }
@@ -3040,7 +3172,7 @@ async function buildStopHookOutput(
3040
3172
  `OMX Ralph completion audit is missing required evidence (${ralphCompletionAuditBlock.reason}; state: ${blockingPath}).`,
3041
3173
  "Continue verification and do not report complete yet.",
3042
3174
  "Record machine-readable completion evidence before stopping:",
3043
- "- either set state.completion_audit = { passed: true, prompt_to_artifact_checklist: [...], verification_evidence: [...] }",
3175
+ '- either set "completion_audit" on the Ralph state object, for example: omx state write --input \'{"mode":"ralph","active":false,"current_phase":"complete","completion_audit":{"passed":true,"prompt_to_artifact_checklist":["..."],"verification_evidence":["..."]}}\' --json',
3044
3176
  "- or set completion_audit_path / completion_audit_evidence_path to a repo-relative JSON file with those same fields.",
3045
3177
  "Markdown artifacts and flat top-level checklist/evidence fields are not accepted by the Ralph Stop gate.",
3046
3178
  ].join(" ");
@@ -3337,6 +3469,7 @@ export async function dispatchCodexNativeHook(
3337
3469
  let skillState: SkillActiveState | null = null;
3338
3470
  let triageAdditionalContext: string | null = null;
3339
3471
  let goalWorkflowAdditionalContext: string | null = null;
3472
+ let ultragoalSteeringAdditionalContext: string | null = null;
3340
3473
 
3341
3474
  const nativeSessionId = safeString(payload.session_id ?? payload.sessionId).trim();
3342
3475
  const threadId = safeString(payload.thread_id ?? payload.threadId).trim();
@@ -3345,11 +3478,13 @@ export async function dispatchCodexNativeHook(
3345
3478
  let canonicalSessionId = safeString(currentSessionState?.session_id).trim();
3346
3479
  let resolvedNativeSessionId = nativeSessionId;
3347
3480
  let skipCanonicalSessionStartContext = false;
3481
+ let isSubagentSessionStart = false;
3348
3482
 
3349
3483
  if (hookEventName === "SessionStart" && nativeSessionId) {
3350
3484
  const transcriptPath = safeString(payload.transcript_path ?? payload.transcriptPath).trim();
3351
3485
  const subagentSessionStart = readNativeSubagentSessionStartMetadata(transcriptPath);
3352
3486
  if (subagentSessionStart && canonicalSessionId) {
3487
+ isSubagentSessionStart = true;
3353
3488
  const belongsToCanonicalSession = await nativeSubagentSessionStartBelongsToCanonicalSession(
3354
3489
  cwd,
3355
3490
  canonicalSessionId,
@@ -3418,10 +3553,16 @@ export async function dispatchCodexNativeHook(
3418
3553
  .map((candidateSessionId) => isNativeSubagentHook(cwd, candidateSessionId, nativeSessionId, threadId)),
3419
3554
  )).some(Boolean)
3420
3555
  : false;
3556
+ const suppressNoisySubagentLifecycleDispatch =
3557
+ (isSubagentSessionStart || isSubagentStop)
3558
+ && shouldSuppressSubagentLifecycleHookDispatch();
3421
3559
 
3422
3560
  if (hookEventName === "UserPromptSubmit") {
3423
3561
  const prompt = readPromptText(payload);
3424
3562
  goalWorkflowAdditionalContext = await buildGoalWorkflowReconciliationPromptWarning(cwd, prompt).catch(() => null);
3563
+ ultragoalSteeringAdditionalContext = prompt && !isSubagentPromptSubmit
3564
+ ? await applyUserPromptUltragoalSteering(cwd, prompt).catch((error) => `OMX native UserPromptSubmit rejected bounded .omx/ultragoal steering for G002-cli-and-prompt-submit-bridge: ${error instanceof Error ? error.message : String(error)}`)
3565
+ : null;
3425
3566
  if (prompt && !isSubagentPromptSubmit) {
3426
3567
  skillState = buildNativeOutsideTmuxTeamPromptBlockState(
3427
3568
  prompt,
@@ -3513,7 +3654,7 @@ export async function dispatchCodexNativeHook(
3513
3654
  await reconcileHudForPromptSubmitFn(cwd, { sessionId: canonicalSessionId || sessionIdForState || undefined }).catch(() => {});
3514
3655
  }
3515
3656
 
3516
- if (omxEventName && !skipCanonicalSessionStartContext) {
3657
+ if (omxEventName && !skipCanonicalSessionStartContext && !suppressNoisySubagentLifecycleDispatch) {
3517
3658
  const baseContext = buildBaseContext(cwd, payload, hookEventName!, canonicalSessionId);
3518
3659
  if (resolvedNativeSessionId) {
3519
3660
  baseContext.native_session_id = resolvedNativeSessionId;
@@ -3554,7 +3695,12 @@ export async function dispatchCodexNativeHook(
3554
3695
  })
3555
3696
  : isSubagentPromptSubmit
3556
3697
  ? null
3557
- : (buildAdditionalContextMessage(readPromptText(payload), skillState, cwd, payload) ?? goalWorkflowAdditionalContext ?? triageAdditionalContext);
3698
+ : [
3699
+ buildAdditionalContextMessage(readPromptText(payload), skillState, cwd, payload),
3700
+ ultragoalSteeringAdditionalContext,
3701
+ goalWorkflowAdditionalContext,
3702
+ triageAdditionalContext,
3703
+ ].filter((entry): entry is string => Boolean(entry)).join("\n\n") || null;
3558
3704
  if (additionalContext) {
3559
3705
  outputJson = {
3560
3706
  hookSpecificOutput: {
@@ -2,7 +2,11 @@ import {
2
2
  buildDocumentRefreshAdvisoryOutput,
3
3
  evaluateStagedDocumentRefresh,
4
4
  } from "../document-refresh/enforcer.js";
5
- import { isLoreCommitGuardEnabled } from "../config/commit-lore-guard.js";
5
+ import {
6
+ OMX_LORE_COMMIT_GUARD_ENV,
7
+ isLoreCommitGuardEnabled,
8
+ readConfiguredLoreCommitGuardValue,
9
+ } from "../config/commit-lore-guard.js";
6
10
  import { resolveCodexExecutionSurface } from "./codex-execution-surface.js";
7
11
 
8
12
  type CodexHookPayload = Record<string, unknown>;
@@ -747,6 +751,17 @@ function buildEffectiveLoreCommitGuardEnv(parsed: GitCommitCommandParseResult):
747
751
  for (const [name, value] of Object.entries(parsed.inlineEnvironment)) {
748
752
  if (typeof value === "string") effectiveEnvironment[name] = value;
749
753
  }
754
+
755
+ if (
756
+ !parsed.environmentStartsClean
757
+ && !parsed.unsetEnvironmentNames.includes(OMX_LORE_COMMIT_GUARD_ENV)
758
+ && typeof effectiveEnvironment[OMX_LORE_COMMIT_GUARD_ENV] !== "string"
759
+ ) {
760
+ const configuredValue = readConfiguredLoreCommitGuardValue(effectiveEnvironment);
761
+ if (typeof configuredValue === "string") {
762
+ effectiveEnvironment[OMX_LORE_COMMIT_GUARD_ENV] = configuredValue;
763
+ }
764
+ }
750
765
  return effectiveEnvironment;
751
766
  }
752
767
 
@@ -17,35 +17,59 @@ export function runProcess(command: string, args: string[], timeoutMs = 3000): P
17
17
  let stdout = '';
18
18
  let stderr = '';
19
19
  let finished = false;
20
+ let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
20
21
 
21
- const timer = setTimeout(() => {
22
- if (finished) return;
23
- finished = true;
24
- child.kill('SIGTERM');
25
- reject(new Error(`timeout after ${effectiveTimeoutMs}ms`));
26
- }, effectiveTimeoutMs);
22
+ const cleanup = (clearPendingSigkill = true) => {
23
+ clearTimeout(timer);
24
+ if (clearPendingSigkill && sigkillTimer) {
25
+ clearTimeout(sigkillTimer);
26
+ sigkillTimer = undefined;
27
+ }
28
+ child.stdout.off('data', onStdout);
29
+ child.stderr.off('data', onStderr);
30
+ child.off('error', onError);
31
+ child.off('close', onClose);
32
+ };
27
33
 
28
- child.stdout.on('data', (chunk: Buffer) => {
34
+ const onStdout = (chunk: Buffer) => {
29
35
  stdout += chunk.toString();
30
- });
31
- child.stderr.on('data', (chunk: Buffer) => {
36
+ };
37
+ const onStderr = (chunk: Buffer) => {
32
38
  stderr += chunk.toString();
33
- });
34
- child.on('error', (err: Error) => {
39
+ };
40
+ const onError = (err: Error) => {
35
41
  if (finished) return;
36
42
  finished = true;
37
- clearTimeout(timer);
43
+ cleanup();
38
44
  reject(err);
39
- });
40
- child.on('close', (code: number | null) => {
45
+ };
46
+ const onClose = (code: number | null) => {
41
47
  if (finished) return;
42
48
  finished = true;
43
- clearTimeout(timer);
49
+ cleanup();
44
50
  if (code === 0) {
45
51
  resolve({ stdout, stderr, code });
46
52
  } else {
47
53
  reject(new Error(stderr.trim() || `${command} exited ${code}`));
48
54
  }
49
- });
55
+ };
56
+
57
+ const timer = setTimeout(() => {
58
+ if (finished) return;
59
+ finished = true;
60
+ child.kill('SIGTERM');
61
+ sigkillTimer = setTimeout(() => {
62
+ sigkillTimer = undefined;
63
+ child.kill('SIGKILL');
64
+ }, 250);
65
+ sigkillTimer.unref?.();
66
+ cleanup(false);
67
+ reject(new Error(`timeout after ${effectiveTimeoutMs}ms`));
68
+ }, effectiveTimeoutMs);
69
+
70
+ child.stdout.on('data', onStdout);
71
+ child.stderr.on('data', onStderr);
72
+ child.on('error', onError);
73
+ child.on('close', onClose);
50
74
  });
51
75
  }
@@ -822,6 +822,7 @@ async function injectDispatchRequest(request, config, cwd, stateDir) {
822
822
  prompt: request.trigger_message,
823
823
  submitKeyPresses,
824
824
  typePrompt: shouldTypePrompt,
825
+ queueFirstSubmit: leaderTargeted,
825
826
  });
826
827
  if (!sendResult.ok) {
827
828
  return {
@@ -876,11 +877,13 @@ async function injectDispatchRequest(request, config, cwd, stateDir) {
876
877
  tmux_injection_attempted: true,
877
878
  };
878
879
  }
879
- // Do not declare success while a *worker* pane is still bootstrapping / not
880
- // input-ready. Otherwise a pre-ready send can be marked "confirmed" and later
881
- // appear as a stuck unsent draft once the UI finishes loading.
882
- // Keep leader-fixed behavior unchanged to avoid regressing leader notification flow.
883
- if (request.to_worker !== 'leader-fixed' && !paneLooksReady(wideCap.stdout)) {
880
+ // Do not declare success while a pane is not input-ready. Otherwise a
881
+ // pre-ready send can be marked "confirmed" and later appear as a stuck
882
+ // unsent draft once the UI finishes loading. This includes leader-fixed:
883
+ // its Codex UI can show "tab to queue message" while busy, and marking
884
+ // delivered before queue/consumption confirmation loses the orchestration
885
+ // nudge until a human presses Tab manually.
886
+ if (!paneLooksReady(wideCap.stdout)) {
884
887
  continue;
885
888
  }
886
889
  if (!triggerInNarrow && !triggerNearTail) {
@@ -904,6 +907,7 @@ async function injectDispatchRequest(request, config, cwd, stateDir) {
904
907
  prompt: request.trigger_message,
905
908
  submitKeyPresses,
906
909
  typePrompt: false,
910
+ queueFirstSubmit: leaderTargeted,
907
911
  }).catch(() => {});
908
912
  }
909
913
 
@@ -134,6 +134,7 @@ export async function sendPaneInput({
134
134
  submitKeyPresses = 2,
135
135
  submitDelayMs = 0,
136
136
  typePrompt = true,
137
+ queueFirstSubmit = false,
137
138
  }: any): Promise<any> {
138
139
  const target = safeString(paneTarget).trim();
139
140
  if (!target) {
@@ -163,6 +164,12 @@ export async function sendPaneInput({
163
164
  if (typePrompt) {
164
165
  await runProcess('tmux', argv.typeArgv, 3000);
165
166
  }
167
+ if (queueFirstSubmit && argv.submitArgv.length > 0) {
168
+ await runProcess('tmux', ['send-keys', '-t', target, 'Tab'], 3000);
169
+ if (submitDelayMs > 0) {
170
+ await new Promise((resolve) => setTimeout(resolve, submitDelayMs));
171
+ }
172
+ }
166
173
  for (const submit of argv.submitArgv) {
167
174
  if (submitDelayMs > 0) {
168
175
  await new Promise((resolve) => setTimeout(resolve, submitDelayMs));