oh-my-codex 0.18.7 → 0.18.9

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 (307) hide show
  1. package/Cargo.lock +12 -12
  2. package/Cargo.toml +1 -1
  3. package/README.md +5 -5
  4. package/crates/omx-sparkshell/tests/execution.rs +1 -1
  5. package/dist/agents/__tests__/native-config.test.js +42 -1
  6. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  7. package/dist/agents/definitions.d.ts +8 -0
  8. package/dist/agents/definitions.d.ts.map +1 -1
  9. package/dist/agents/definitions.js +1 -0
  10. package/dist/agents/definitions.js.map +1 -1
  11. package/dist/agents/native-config.d.ts +5 -1
  12. package/dist/agents/native-config.d.ts.map +1 -1
  13. package/dist/agents/native-config.js +17 -2
  14. package/dist/agents/native-config.js.map +1 -1
  15. package/dist/autopilot/__tests__/fsm.test.js +3 -0
  16. package/dist/autopilot/__tests__/fsm.test.js.map +1 -1
  17. package/dist/autopilot/fsm.js +2 -2
  18. package/dist/autopilot/fsm.js.map +1 -1
  19. package/dist/cli/__tests__/auth.test.js +4 -2
  20. package/dist/cli/__tests__/auth.test.js.map +1 -1
  21. package/dist/cli/__tests__/codex-plugin-layout.test.js +512 -1
  22. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  23. package/dist/cli/__tests__/doctor-warning-copy.test.js +39 -0
  24. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  25. package/dist/cli/__tests__/index.test.js +98 -6
  26. package/dist/cli/__tests__/index.test.js.map +1 -1
  27. package/dist/cli/__tests__/package-bin-contract.test.js +28 -8
  28. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  29. package/dist/cli/__tests__/question.test.js +26 -9
  30. package/dist/cli/__tests__/question.test.js.map +1 -1
  31. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +13 -0
  32. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  33. package/dist/cli/__tests__/ralph.test.js +14 -0
  34. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  35. package/dist/cli/__tests__/resume.test.js +50 -1
  36. package/dist/cli/__tests__/resume.test.js.map +1 -1
  37. package/dist/cli/__tests__/setup-install-mode.test.js +89 -0
  38. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  39. package/dist/cli/__tests__/setup-refresh.test.js +65 -0
  40. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  41. package/dist/cli/__tests__/state.test.js +21 -0
  42. package/dist/cli/__tests__/state.test.js.map +1 -1
  43. package/dist/cli/__tests__/team.test.js +2 -2
  44. package/dist/cli/__tests__/update.test.js +323 -18
  45. package/dist/cli/__tests__/update.test.js.map +1 -1
  46. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +1 -1
  47. package/dist/cli/doctor.d.ts.map +1 -1
  48. package/dist/cli/doctor.js +8 -1
  49. package/dist/cli/doctor.js.map +1 -1
  50. package/dist/cli/index.d.ts +21 -4
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +143 -28
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/cli/plugin-marketplace.d.ts +14 -2
  55. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  56. package/dist/cli/plugin-marketplace.js +62 -15
  57. package/dist/cli/plugin-marketplace.js.map +1 -1
  58. package/dist/cli/ralph.d.ts.map +1 -1
  59. package/dist/cli/ralph.js +3 -1
  60. package/dist/cli/ralph.js.map +1 -1
  61. package/dist/cli/setup-preferences.d.ts +2 -0
  62. package/dist/cli/setup-preferences.d.ts.map +1 -1
  63. package/dist/cli/setup-preferences.js +4 -0
  64. package/dist/cli/setup-preferences.js.map +1 -1
  65. package/dist/cli/setup.d.ts +3 -0
  66. package/dist/cli/setup.d.ts.map +1 -1
  67. package/dist/cli/setup.js +166 -27
  68. package/dist/cli/setup.js.map +1 -1
  69. package/dist/cli/state.d.ts.map +1 -1
  70. package/dist/cli/state.js +8 -1
  71. package/dist/cli/state.js.map +1 -1
  72. package/dist/cli/tmux-hook.d.ts.map +1 -1
  73. package/dist/cli/tmux-hook.js +16 -0
  74. package/dist/cli/tmux-hook.js.map +1 -1
  75. package/dist/cli/update.d.ts +22 -3
  76. package/dist/cli/update.d.ts.map +1 -1
  77. package/dist/cli/update.js +312 -26
  78. package/dist/cli/update.js.map +1 -1
  79. package/dist/cli/version.d.ts.map +1 -1
  80. package/dist/cli/version.js +5 -9
  81. package/dist/cli/version.js.map +1 -1
  82. package/dist/compat/__tests__/doctor-contract.test.js +12 -1
  83. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  84. package/dist/config/__tests__/generator-notify.test.js +1 -0
  85. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  86. package/dist/config/generator.d.ts +2 -2
  87. package/dist/config/generator.d.ts.map +1 -1
  88. package/dist/config/generator.js +2 -2
  89. package/dist/config/generator.js.map +1 -1
  90. package/dist/config/team-mode.d.ts +12 -0
  91. package/dist/config/team-mode.d.ts.map +1 -0
  92. package/dist/config/team-mode.js +91 -0
  93. package/dist/config/team-mode.js.map +1 -0
  94. package/dist/hooks/__tests__/agents-overlay.test.js +88 -0
  95. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  96. package/dist/hooks/__tests__/code-review-skill-contract.test.js +12 -0
  97. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  98. package/dist/hooks/__tests__/deep-interview-contract.test.js +30 -1
  99. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  100. package/dist/hooks/__tests__/keyword-detector.test.js +423 -3
  101. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  102. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +1 -1
  103. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  104. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +189 -0
  105. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  106. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +35 -2
  107. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  108. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +3 -3
  109. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  110. package/dist/hooks/__tests__/skill-guidance-contract.test.js +21 -0
  111. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
  112. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  113. package/dist/hooks/agents-overlay.js +36 -50
  114. package/dist/hooks/agents-overlay.js.map +1 -1
  115. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +31 -0
  116. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +1 -1
  117. package/dist/hooks/extensibility/plugin-runner.js +17 -21
  118. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  119. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  120. package/dist/hooks/keyword-detector.js +258 -12
  121. package/dist/hooks/keyword-detector.js.map +1 -1
  122. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  123. package/dist/hooks/prompt-guidance-contract.js +6 -0
  124. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  125. package/dist/hooks/session.d.ts +1 -0
  126. package/dist/hooks/session.d.ts.map +1 -1
  127. package/dist/hooks/session.js.map +1 -1
  128. package/dist/hud/__tests__/authority.test.js +435 -32
  129. package/dist/hud/__tests__/authority.test.js.map +1 -1
  130. package/dist/hud/__tests__/hud-tmux-injection.test.js +2 -1
  131. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  132. package/dist/hud/__tests__/index.test.js +42 -0
  133. package/dist/hud/__tests__/index.test.js.map +1 -1
  134. package/dist/hud/__tests__/reconcile.test.js +642 -15
  135. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  136. package/dist/hud/__tests__/render.test.js +61 -0
  137. package/dist/hud/__tests__/render.test.js.map +1 -1
  138. package/dist/hud/__tests__/state.test.js +160 -4
  139. package/dist/hud/__tests__/state.test.js.map +1 -1
  140. package/dist/hud/__tests__/tmux.test.js +180 -21
  141. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  142. package/dist/hud/authority.d.ts +5 -0
  143. package/dist/hud/authority.d.ts.map +1 -1
  144. package/dist/hud/authority.js +324 -28
  145. package/dist/hud/authority.js.map +1 -1
  146. package/dist/hud/index.d.ts +3 -2
  147. package/dist/hud/index.d.ts.map +1 -1
  148. package/dist/hud/index.js +42 -19
  149. package/dist/hud/index.js.map +1 -1
  150. package/dist/hud/reconcile.d.ts +3 -3
  151. package/dist/hud/reconcile.d.ts.map +1 -1
  152. package/dist/hud/reconcile.js +128 -19
  153. package/dist/hud/reconcile.js.map +1 -1
  154. package/dist/hud/render.d.ts.map +1 -1
  155. package/dist/hud/render.js +35 -0
  156. package/dist/hud/render.js.map +1 -1
  157. package/dist/hud/state.d.ts.map +1 -1
  158. package/dist/hud/state.js +65 -80
  159. package/dist/hud/state.js.map +1 -1
  160. package/dist/hud/tmux.d.ts +24 -6
  161. package/dist/hud/tmux.d.ts.map +1 -1
  162. package/dist/hud/tmux.js +136 -38
  163. package/dist/hud/tmux.js.map +1 -1
  164. package/dist/hud/types.d.ts +11 -0
  165. package/dist/hud/types.d.ts.map +1 -1
  166. package/dist/hud/types.js.map +1 -1
  167. package/dist/mcp/__tests__/state-paths.test.js +71 -1
  168. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  169. package/dist/mcp/state-paths.d.ts +32 -0
  170. package/dist/mcp/state-paths.d.ts.map +1 -1
  171. package/dist/mcp/state-paths.js +113 -17
  172. package/dist/mcp/state-paths.js.map +1 -1
  173. package/dist/mcp/state-server.d.ts +4 -4
  174. package/dist/question/__tests__/renderer.test.js +566 -1
  175. package/dist/question/__tests__/renderer.test.js.map +1 -1
  176. package/dist/question/renderer.d.ts +9 -1
  177. package/dist/question/renderer.d.ts.map +1 -1
  178. package/dist/question/renderer.js +246 -70
  179. package/dist/question/renderer.js.map +1 -1
  180. package/dist/scripts/__tests__/codex-native-hook.test.js +837 -101
  181. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  182. package/dist/scripts/__tests__/notify-state-io.test.js +72 -1
  183. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  184. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts +2 -0
  185. package/dist/scripts/__tests__/notify-tmux-injection.test.d.ts.map +1 -0
  186. package/dist/scripts/__tests__/notify-tmux-injection.test.js +57 -0
  187. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -0
  188. package/dist/scripts/__tests__/run-test-files.test.js +74 -0
  189. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  190. package/dist/scripts/__tests__/verify-native-agents.test.js +65 -0
  191. package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
  192. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  193. package/dist/scripts/codex-native-hook.js +107 -39
  194. package/dist/scripts/codex-native-hook.js.map +1 -1
  195. package/dist/scripts/eval/eval-parity-smoke.js +1 -1
  196. package/dist/scripts/eval/eval-parity-smoke.js.map +1 -1
  197. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  198. package/dist/scripts/notify-hook/auto-nudge.js +3 -1
  199. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  200. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  201. package/dist/scripts/notify-hook/ralph-session-resume.js +3 -10
  202. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  203. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  204. package/dist/scripts/notify-hook/state-io.js +62 -38
  205. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  206. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  207. package/dist/scripts/notify-hook/team-leader-nudge.js +7 -0
  208. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  209. package/dist/scripts/notify-hook/tmux-injection.d.ts +7 -0
  210. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  211. package/dist/scripts/notify-hook/tmux-injection.js +24 -18
  212. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  213. package/dist/scripts/notify-hook.js +75 -11
  214. package/dist/scripts/notify-hook.js.map +1 -1
  215. package/dist/scripts/run-test-files.js +193 -22
  216. package/dist/scripts/run-test-files.js.map +1 -1
  217. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  218. package/dist/scripts/sync-plugin-mirror.js +61 -3
  219. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  220. package/dist/scripts/verify-native-agents.d.ts.map +1 -1
  221. package/dist/scripts/verify-native-agents.js +58 -1
  222. package/dist/scripts/verify-native-agents.js.map +1 -1
  223. package/dist/state/__tests__/operations.test.js +113 -0
  224. package/dist/state/__tests__/operations.test.js.map +1 -1
  225. package/dist/state/__tests__/skill-active.test.js +3 -16
  226. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  227. package/dist/state/__tests__/workflow-transition.test.js +25 -0
  228. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  229. package/dist/state/operations.d.ts.map +1 -1
  230. package/dist/state/operations.js +57 -2
  231. package/dist/state/operations.js.map +1 -1
  232. package/dist/state/skill-active.d.ts.map +1 -1
  233. package/dist/state/skill-active.js +7 -39
  234. package/dist/state/skill-active.js.map +1 -1
  235. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  236. package/dist/state/workflow-transition-reconcile.js +10 -14
  237. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  238. package/dist/team/__tests__/runtime.test.js +1 -1
  239. package/dist/team/__tests__/runtime.test.js.map +1 -1
  240. package/dist/team/__tests__/scaling.test.js +9 -4
  241. package/dist/team/__tests__/scaling.test.js.map +1 -1
  242. package/dist/team/__tests__/tmux-session.test.js +195 -2
  243. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  244. package/dist/team/__tests__/worker-runtime-identity.test.js +4 -2
  245. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  246. package/dist/team/scaling.d.ts.map +1 -1
  247. package/dist/team/scaling.js +3 -2
  248. package/dist/team/scaling.js.map +1 -1
  249. package/dist/team/tmux-session.d.ts +2 -0
  250. package/dist/team/tmux-session.d.ts.map +1 -1
  251. package/dist/team/tmux-session.js +142 -12
  252. package/dist/team/tmux-session.js.map +1 -1
  253. package/dist/utils/__tests__/platform-command.test.js +16 -1
  254. package/dist/utils/__tests__/platform-command.test.js.map +1 -1
  255. package/dist/utils/__tests__/version.test.d.ts +2 -0
  256. package/dist/utils/__tests__/version.test.d.ts.map +1 -0
  257. package/dist/utils/__tests__/version.test.js +51 -0
  258. package/dist/utils/__tests__/version.test.js.map +1 -0
  259. package/dist/utils/paths.d.ts +8 -1
  260. package/dist/utils/paths.d.ts.map +1 -1
  261. package/dist/utils/paths.js +16 -4
  262. package/dist/utils/paths.js.map +1 -1
  263. package/dist/utils/platform-command.d.ts +9 -0
  264. package/dist/utils/platform-command.d.ts.map +1 -1
  265. package/dist/utils/platform-command.js +15 -0
  266. package/dist/utils/platform-command.js.map +1 -1
  267. package/dist/utils/version.d.ts +7 -0
  268. package/dist/utils/version.d.ts.map +1 -0
  269. package/dist/utils/version.js +67 -0
  270. package/dist/utils/version.js.map +1 -0
  271. package/dist/verification/__tests__/ci-rust-gates.test.js +89 -1
  272. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  273. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +16 -2
  274. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  275. package/package.json +11 -10
  276. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  277. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +334 -21
  278. package/plugins/oh-my-codex/hooks/hooks.json +1 -2
  279. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +3 -1
  280. package/plugins/oh-my-codex/skills/code-review/SKILL.md +7 -7
  281. package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +51 -11
  282. package/plugins/oh-my-codex/skills/ralph/SKILL.md +22 -22
  283. package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +9 -0
  284. package/skills/autopilot/SKILL.md +3 -1
  285. package/skills/code-review/SKILL.md +7 -7
  286. package/skills/deep-interview/SKILL.md +51 -11
  287. package/skills/ralph/SKILL.md +22 -22
  288. package/skills/ultraqa/SKILL.md +9 -0
  289. package/src/scripts/__tests__/codex-native-hook.test.ts +946 -98
  290. package/src/scripts/__tests__/notify-state-io.test.ts +95 -0
  291. package/src/scripts/__tests__/notify-tmux-injection.test.ts +82 -0
  292. package/src/scripts/__tests__/run-test-files.test.ts +102 -0
  293. package/src/scripts/__tests__/verify-native-agents.test.ts +75 -0
  294. package/src/scripts/codex-native-hook.ts +123 -34
  295. package/src/scripts/demo-team-e2e.sh +10 -7
  296. package/src/scripts/eval/eval-parity-smoke.ts +1 -1
  297. package/src/scripts/notify-hook/auto-nudge.ts +3 -1
  298. package/src/scripts/notify-hook/ralph-session-resume.ts +2 -8
  299. package/src/scripts/notify-hook/state-io.ts +75 -37
  300. package/src/scripts/notify-hook/team-leader-nudge.ts +7 -0
  301. package/src/scripts/notify-hook/tmux-injection.ts +35 -19
  302. package/src/scripts/notify-hook.ts +91 -4
  303. package/src/scripts/prepare-build.js +83 -0
  304. package/src/scripts/run-test-files.ts +192 -22
  305. package/src/scripts/sync-plugin-mirror.ts +98 -9
  306. package/src/scripts/verify-native-agents.ts +65 -1
  307. package/src/scripts/postinstall-bootstrap.js +0 -23
@@ -21,7 +21,7 @@ import { runProcess } from './process-runner.js';
21
21
  import { logTmuxHookEvent } from './log.js';
22
22
  import { resolveInvocationSessionId, resolveManagedCurrentPane, resolveManagedSessionContext, verifyManagedPaneTarget } from './managed-tmux.js';
23
23
  import { evaluatePaneInjectionReadiness, mapPaneInjectionReadinessReason, sendPaneInput } from './team-tmux-guard.js';
24
- import { listActiveSkills, readVisibleSkillActiveState } from '../../state/skill-active.js';
24
+ import { listActiveSkills, readVisibleSkillActiveStateForStateDir } from '../../state/skill-active.js';
25
25
  import {
26
26
  normalizeTmuxHookConfig,
27
27
  pickActiveMode,
@@ -99,12 +99,14 @@ async function resolveCanonicalPaneFromPaneTarget(paneTarget: any, expectedCwd:
99
99
  return finalizeResolvedPane(healedPaneId, 'healed_hud_pane_target', expectedCwd);
100
100
  }
101
101
 
102
- async function resolvePreferredModePane(stateDir: string, allowedModes: string[]): Promise<{ mode: string; state: any; pane: string; stateDir: string } | null> {
103
- const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir).catch(() => [stateDir]);
104
- const dirs = [...scopedDirs];
105
- if (!dirs.map((dir) => resolvePath(dir)).includes(resolvePath(stateDir))) {
106
- dirs.push(stateDir);
107
- }
102
+ async function resolvePreferredModePane(
103
+ stateDir: string,
104
+ allowedModes: string[],
105
+ options: { includeRootFallback?: boolean } = {},
106
+ ): Promise<{ mode: string; state: any; pane: string; stateDir: string } | null> {
107
+ const dirs = await getScopedStateDirsForCurrentSession(stateDir, undefined, {
108
+ includeRootFallback: options.includeRootFallback !== false,
109
+ }).catch(() => [stateDir]);
108
110
  for (const dir of dirs) {
109
111
  for (const mode of allowedModes || []) {
110
112
  const path = join(dir, `${mode}-state.json`);
@@ -189,12 +191,18 @@ async function validateResolvedInjectionOwnership({
189
191
  return { ok: true };
190
192
  }
191
193
 
192
- async function readVisibleAllowedModes(
194
+ export async function readVisibleAllowedModes(
193
195
  cwd: string,
194
196
  stateDir: string,
195
197
  payload: any,
196
198
  allowedModes: string[],
197
- ): Promise<{ canonicalPresent: boolean; allowedSet: Set<string> | null; preferredMode: string | null }> {
199
+ ): Promise<{
200
+ canonicalPresent: boolean;
201
+ activeSkillCount: number;
202
+ allowedSet: Set<string> | null;
203
+ preferredMode: string | null;
204
+ sessionScoped: boolean;
205
+ }> {
198
206
  const candidateSessionIds = [
199
207
  await readCurrentSessionId(stateDir).catch(() => undefined),
200
208
  resolveInvocationSessionId(payload),
@@ -203,41 +211,49 @@ async function readVisibleAllowedModes(
203
211
  .filter(Boolean);
204
212
 
205
213
  for (const sessionId of candidateSessionIds) {
206
- const canonicalState = await readVisibleSkillActiveState(cwd, sessionId);
214
+ const canonicalState = await readVisibleSkillActiveStateForStateDir(stateDir, sessionId);
207
215
  if (!canonicalState) continue;
208
216
 
217
+ const activeSkills = listActiveSkills(canonicalState);
209
218
  const allowedSet = new Set(
210
- listActiveSkills(canonicalState)
219
+ activeSkills
211
220
  .map((entry) => entry.skill)
212
221
  .filter((skill) => allowedModes.includes(skill)),
213
222
  );
214
223
  return {
215
224
  canonicalPresent: true,
225
+ activeSkillCount: activeSkills.length,
216
226
  allowedSet,
217
227
  preferredMode: pickActiveMode([...allowedSet], allowedModes),
228
+ sessionScoped: true,
218
229
  };
219
230
  }
220
231
 
221
232
  if (candidateSessionIds.length === 0) {
222
- const rootCanonicalState = await readVisibleSkillActiveState(cwd).catch(() => null);
233
+ const rootCanonicalState = await readVisibleSkillActiveStateForStateDir(stateDir).catch(() => null);
223
234
  if (rootCanonicalState) {
235
+ const activeSkills = listActiveSkills(rootCanonicalState);
224
236
  const allowedSet = new Set(
225
- listActiveSkills(rootCanonicalState)
237
+ activeSkills
226
238
  .map((entry) => entry.skill)
227
239
  .filter((skill) => allowedModes.includes(skill)),
228
240
  );
229
241
  return {
230
242
  canonicalPresent: true,
243
+ activeSkillCount: activeSkills.length,
231
244
  allowedSet,
232
245
  preferredMode: pickActiveMode([...allowedSet], allowedModes),
246
+ sessionScoped: false,
233
247
  };
234
248
  }
235
249
  }
236
250
 
237
251
  return {
238
252
  canonicalPresent: false,
253
+ activeSkillCount: 0,
239
254
  allowedSet: null,
240
255
  preferredMode: null,
256
+ sessionScoped: candidateSessionIds.length > 0,
241
257
  };
242
258
  }
243
259
 
@@ -415,10 +431,11 @@ export async function handleTmuxInjection({
415
431
  state.recent_keys = pruneRecentKeys(state.recent_keys, now);
416
432
  const canonicalModeState = await readVisibleAllowedModes(cwd, stateDir, payload, config.allowed_modes).catch(() => ({
417
433
  canonicalPresent: false,
434
+ activeSkillCount: 0,
418
435
  allowedSet: null,
419
436
  preferredMode: null,
420
437
  }));
421
- if (canonicalModeState.canonicalPresent && !canonicalModeState.preferredMode) {
438
+ if (canonicalModeState.canonicalPresent && canonicalModeState.activeSkillCount > 0 && !canonicalModeState.preferredMode) {
422
439
  const nextState = {
423
440
  ...state,
424
441
  last_reason: 'mode_not_allowed',
@@ -468,12 +485,10 @@ export async function handleTmuxInjection({
468
485
  }
469
486
  };
470
487
  try {
471
- const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir);
488
+ const scopedDirs = await getScopedStateDirsForCurrentSession(stateDir, undefined, {
489
+ includeRootFallback: !canonicalModeState.canonicalPresent && !canonicalModeState.sessionScoped,
490
+ });
472
491
  await scanActiveModeStateDirs(scopedDirs);
473
-
474
- if (!pickActiveMode(activeModes, config.allowed_modes) && !scannedStateDirs.has(resolvePath(stateDir))) {
475
- await scanActiveModeStateDirs([stateDir], true);
476
- }
477
492
  } catch {
478
493
  // Non-fatal
479
494
  }
@@ -483,6 +498,7 @@ export async function handleTmuxInjection({
483
498
  canonicalModeState.canonicalPresent
484
499
  ? (canonicalModeState.preferredMode ? [canonicalModeState.preferredMode] : [])
485
500
  : config.allowed_modes,
501
+ { includeRootFallback: !canonicalModeState.sessionScoped },
486
502
  ).catch(() => null);
487
503
  const mode = canonicalModeState.canonicalPresent
488
504
  ? canonicalModeState.preferredMode
@@ -262,6 +262,78 @@ function classifyIdleNotificationPhase(message: unknown): 'idle' | 'progress' |
262
262
  return 'idle';
263
263
  }
264
264
 
265
+
266
+ function isExplicitAutopilotActivationText(text: string): boolean {
267
+ return /(?:^|[^\w])\$autopilot\b/i.test(text)
268
+ || /^\s*\/autopilot\b/i.test(text)
269
+ || /^\s*(?:please\s+)?autopilot(?:\s+(?:this|mode|workflow|skill|loop|now))?\s*[.!]?\s*$/i.test(text)
270
+ || /\b(?:use|run|start|enable|launch|invoke|activate|resume|continue)\s+(?:the\s+)?autopilot(?:\s+(?:mode|workflow|skill|loop|now))?\s*[.!]?\s*$/i.test(text)
271
+ || /\bautopilot\s+(?:mode|workflow|skill|loop)\b/i.test(text);
272
+ }
273
+
274
+ function looksLikeAutopilotTerminalHandoff(text: string): boolean {
275
+ return /\bAutopilot complete\b/i.test(text)
276
+ || /\btask_complete\b/i.test(text)
277
+ || /\bautopilot\b[\s\S]{0,120}\b(?:complete|completed|finished)\b/i.test(text);
278
+ }
279
+
280
+ function isTerminalModeStateObject(value: unknown, mode: string): boolean {
281
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
282
+ const state = value as Record<string, unknown>;
283
+ if (safeString(state.mode).trim() !== mode) return false;
284
+ if (state.active === true) return false;
285
+ const phase = safeString(state.current_phase || state.currentPhase).trim().toLowerCase().replace(/_/g, '-');
286
+ if (['complete', 'completed', 'failed', 'cancelled', 'canceled', 'stopped', 'user-stopped'].includes(phase)) return true;
287
+ const outcome = safeString(state.run_outcome || state.outcome || state.lifecycle_outcome || state.terminal_outcome).trim().toLowerCase();
288
+ return ['finish', 'finished', 'complete', 'completed', 'failed', 'cancelled', 'canceled'].includes(outcome)
289
+ || safeString(state.completed_at || state.completedAt).trim() !== '';
290
+ }
291
+
292
+ function terminalStateMatchesNotifyTurn(state: Record<string, unknown>, payload: Record<string, unknown>): boolean {
293
+ const payloadTurnId = safeString(payload['turn-id'] || payload.turn_id || '').trim();
294
+ const stateTurnId = safeString(state.turn_id || state.turnId || '').trim();
295
+ const payloadThreadId = safeString(payload['thread-id'] || payload.thread_id || '').trim();
296
+ const stateThreadId = safeString(state.thread_id || state.threadId || '').trim();
297
+
298
+ if (payloadTurnId || stateTurnId) {
299
+ if (!payloadTurnId || !stateTurnId || payloadTurnId !== stateTurnId) return false;
300
+ return !payloadThreadId || !stateThreadId || payloadThreadId === stateThreadId;
301
+ }
302
+
303
+ return Boolean(payloadThreadId && stateThreadId && payloadThreadId === stateThreadId);
304
+ }
305
+
306
+ async function hasTerminalAutopilotStateForNotifyTurn(
307
+ stateDir: string,
308
+ sessionId: string,
309
+ payload: Record<string, unknown>,
310
+ ): Promise<boolean> {
311
+ const state = await readScopedJsonIfExists(
312
+ stateDir,
313
+ 'autopilot-state.json',
314
+ sessionId || undefined,
315
+ null,
316
+ { includeRootFallback: true },
317
+ );
318
+ return isTerminalModeStateObject(state, 'autopilot')
319
+ && terminalStateMatchesNotifyTurn(state as Record<string, unknown>, payload);
320
+ }
321
+
322
+ async function shouldSuppressAutopilotTerminalReplayActivation(
323
+ stateDir: string,
324
+ payload: Record<string, unknown>,
325
+ isAutopilotActivation: boolean,
326
+ sessionId: string,
327
+ ): Promise<boolean> {
328
+ if (!isTurnCompletePayload(payload) && !isNotifyFallbackTaskCompletePayload(payload)) return false;
329
+ if (!isAutopilotActivation) return false;
330
+
331
+ const lastAssistantMessage = safeString(payload['last-assistant-message'] || payload.last_assistant_message || '');
332
+ if (!looksLikeAutopilotTerminalHandoff(lastAssistantMessage) && !isNotifyFallbackTaskCompletePayload(payload)) return false;
333
+
334
+ return hasTerminalAutopilotStateForNotifyTurn(stateDir, sessionId, payload);
335
+ }
336
+
265
337
  function buildIdleNotificationFingerprint(payload: Record<string, unknown>): string {
266
338
  const lastAssistantMessage = safeString(payload['last-assistant-message'] || payload.last_assistant_message || '');
267
339
  const summary = summarizeIdleNotificationMessage(lastAssistantMessage);
@@ -641,16 +713,28 @@ async function main() {
641
713
 
642
714
  // 4.45. Skill activation tracking: update skill-active-state.json before any nudge logic.
643
715
  try {
644
- const { recordSkillActivation } = await import('../hooks/keyword-detector.js');
716
+ const { detectKeywords, recordSkillActivation } = await import('../hooks/keyword-detector.js');
645
717
  if (latestUserInput) {
718
+ const activationSessionId = getEffectiveSessionId();
719
+ const isAutopilotActivation = detectKeywords(latestUserInput)
720
+ .some((match) => match.skill === 'autopilot')
721
+ || isExplicitAutopilotActivationText(latestUserInput);
722
+ const suppressTerminalReplay = await shouldSuppressAutopilotTerminalReplayActivation(
723
+ stateDir,
724
+ payload,
725
+ isAutopilotActivation,
726
+ activationSessionId,
727
+ );
728
+ if (!suppressTerminalReplay) {
646
729
  await recordSkillActivation({
647
730
  stateDir,
648
731
  sourceCwd: cwd,
649
732
  text: latestUserInput,
650
- sessionId: getEffectiveSessionId(),
733
+ sessionId: activationSessionId,
651
734
  threadId: payloadThreadId,
652
735
  turnId: safeString(payload['turn-id'] || payload.turn_id || ''),
653
736
  });
737
+ }
654
738
  }
655
739
  } catch {
656
740
  // Non-fatal: keyword detector module may not be built yet
@@ -662,8 +746,11 @@ async function main() {
662
746
  // Non-fatal: lifecycle sync should not block the hook
663
747
  }
664
748
 
665
- const deepInterviewStateActive = await isDeepInterviewStateActive(stateDir, getEffectiveSessionId());
666
- const deepInterviewInputLockActive = await isDeepInterviewInputLockActive(stateDir, getEffectiveSessionId());
749
+ const effectiveSessionId = getEffectiveSessionId();
750
+ const deepInterviewStateActive = effectiveSessionId
751
+ ? await isDeepInterviewStateActive(stateDir, effectiveSessionId)
752
+ : await isDeepInterviewStateActive(stateDir, undefined);
753
+ const deepInterviewInputLockActive = await isDeepInterviewInputLockActive(stateDir, effectiveSessionId);
667
754
 
668
755
  // 4.55. Notify leader when individual worker transitions to idle (worker session only)
669
756
  if (isTeamWorker && parsedTeamWorker && !deepInterviewStateActive) {
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, rmSync } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const requiredDistFiles = [
7
+ join(process.cwd(), 'dist', 'cli', 'omx.js'),
8
+ join(process.cwd(), 'dist', 'scripts', 'postinstall.js'),
9
+ ];
10
+
11
+ if (requiredDistFiles.every((file) => existsSync(file))) {
12
+ process.exit(0);
13
+ }
14
+
15
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
16
+ const tscBin = process.platform === 'win32'
17
+ ? join(process.cwd(), 'node_modules', '.bin', 'tsc.cmd')
18
+ : join(process.cwd(), 'node_modules', '.bin', 'tsc');
19
+ const nodeModulesDir = join(process.cwd(), 'node_modules');
20
+
21
+ function runNpm(args, env = process.env) {
22
+ return spawnSync(npmBin, args, {
23
+ cwd: process.cwd(),
24
+ stdio: process.env.npm_config_json === 'true' ? ['inherit', 'ignore', 'inherit'] : 'inherit',
25
+ env,
26
+ });
27
+ }
28
+
29
+ function exitOnFailure(result, label) {
30
+ if (result.error) {
31
+ console.error(`omx prepare: failed to launch ${label}: ${result.error.message}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ if (result.status !== 0) {
36
+ process.exit(typeof result.status === 'number' ? result.status : 1);
37
+ }
38
+ }
39
+
40
+ let shouldCleanupBootstrappedDependencies = false;
41
+
42
+ if (!existsSync(tscBin)) {
43
+ const hadNodeModules = existsSync(nodeModulesDir);
44
+ const installResult = runNpm(
45
+ [
46
+ 'install',
47
+ '--global=false',
48
+ '--location=project',
49
+ '--include=dev',
50
+ '--ignore-scripts',
51
+ '--no-audit',
52
+ '--no-progress',
53
+ ],
54
+ {
55
+ ...process.env,
56
+ npm_config_global: 'false',
57
+ npm_config_location: 'project',
58
+ },
59
+ );
60
+ exitOnFailure(installResult, 'npm dependency bootstrap');
61
+ shouldCleanupBootstrappedDependencies = !hadNodeModules;
62
+ }
63
+
64
+ const pathWithLocalBins = [
65
+ join(process.cwd(), 'node_modules', '.bin'),
66
+ process.env.PATH ?? '',
67
+ ].filter(Boolean).join(delimiter);
68
+
69
+ const buildResult = spawnSync(npmBin, ['run', 'build'], {
70
+ cwd: process.cwd(),
71
+ stdio: process.env.npm_config_json === 'true' ? ['inherit', 'ignore', 'inherit'] : 'inherit',
72
+ env: { ...process.env, PATH: pathWithLocalBins },
73
+ });
74
+ exitOnFailure(buildResult, 'npm build');
75
+
76
+ if (shouldCleanupBootstrappedDependencies) {
77
+ try {
78
+ rmSync(nodeModulesDir, { recursive: true, force: true });
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.warn(`[omx:prepare] Warning: could not remove bootstrapped node_modules: ${message}`);
82
+ }
83
+ }
@@ -1,9 +1,10 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
2
2
  import { readdirSync, statSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
 
5
5
  const DEFAULT_TEST_TIMEOUT_MS = 0;
6
6
  const DEFAULT_RUNNER_TIMEOUT_MS = 30 * 60 * 1_000;
7
+ const DEFAULT_FORCE_EXIT_GRACE_MS = 30_000;
7
8
  const DEFAULT_CI_TEST_CONCURRENCY = 1;
8
9
  const RUNTIME_STATE_ENV_KEYS = [
9
10
  'OMX_ROOT',
@@ -47,6 +48,10 @@ function parseTimeoutMs(value: string | undefined, defaultTimeoutMs: number): nu
47
48
  return Math.floor(parsed);
48
49
  }
49
50
 
51
+ function isWindows(): boolean {
52
+ return process.platform === 'win32';
53
+ }
54
+
50
55
  function parseTestConcurrency(env: NodeJS.ProcessEnv): number | undefined {
51
56
  const rawValue = env.OMX_NODE_TEST_CONCURRENCY;
52
57
  if (rawValue) {
@@ -74,6 +79,7 @@ if (files.length === 0) {
74
79
 
75
80
  const testTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_TIMEOUT_MS, DEFAULT_TEST_TIMEOUT_MS);
76
81
  const runnerTimeoutMs = parseTimeoutMs(process.env.OMX_NODE_TEST_RUNNER_TIMEOUT_MS, DEFAULT_RUNNER_TIMEOUT_MS);
82
+ const forceExitGraceMs = parseTimeoutMs(process.env.OMX_NODE_TEST_FORCE_EXIT_GRACE_MS, DEFAULT_FORCE_EXIT_GRACE_MS);
77
83
  const testConcurrency = parseTestConcurrency(process.env);
78
84
  const forceExit = parseBooleanEnv(process.env.OMX_NODE_TEST_FORCE_EXIT);
79
85
  const testArgs = ['--test'];
@@ -84,7 +90,7 @@ if (testConcurrency) {
84
90
  testArgs.push(`--test-concurrency=${testConcurrency}`);
85
91
  }
86
92
  if (forceExit) {
87
- testArgs.push('--test-force-exit');
93
+ testArgs.push('--test-force-exit', '--test-reporter=tap');
88
94
  }
89
95
  testArgs.push(...files);
90
96
 
@@ -92,7 +98,7 @@ console.error(
92
98
  `[run-test-files] running ${files.length} test file(s) from ${targets.join(', ')}${
93
99
  testTimeoutMs > 0 ? ` with per-test timeout ${testTimeoutMs}ms` : ' with per-test timeout disabled'
94
100
  }${testConcurrency ? `, test concurrency ${testConcurrency}` : ', default test concurrency'}${
95
- forceExit ? ', force exit enabled' : ', force exit disabled'
101
+ forceExit ? `, force exit enabled with ${forceExitGraceMs}ms completion grace` : ', force exit disabled'
96
102
  }${runnerTimeoutMs > 0 ? `, and runner timeout ${runnerTimeoutMs}ms` : ', and runner timeout disabled'}`,
97
103
  );
98
104
 
@@ -105,26 +111,190 @@ if (!parseBooleanEnv(process.env.OMX_NODE_TEST_PRESERVE_RUNTIME_ENV)) {
105
111
  }
106
112
  }
107
113
 
108
- const result = spawnSync(process.execPath, testArgs, {
109
- stdio: 'inherit',
110
- env: childEnv,
111
- timeout: runnerTimeoutMs > 0 ? runnerTimeoutMs : undefined,
112
- killSignal: 'SIGTERM',
113
- });
114
+ function reportAbnormalExit(signal: NodeJS.Signals | null, errorMessage?: string): void {
115
+ if (errorMessage) {
116
+ console.error(`[run-test-files] node --test error: ${errorMessage}`);
117
+ }
118
+ console.error(
119
+ `[run-test-files] node --test did not exit normally${signal ? ` (signal: ${signal})` : ''}. `
120
+ + `Roots: ${targets.join(', ')}. Test files: ${files.length}. `
121
+ + `Per-test timeout: ${testTimeoutMs > 0 ? `${testTimeoutMs}ms` : 'disabled'}. `
122
+ + `Test concurrency: ${testConcurrency ?? 'default'}. `
123
+ + `Force exit: ${forceExit ? 'enabled' : 'disabled'}. `
124
+ + `Runner timeout: ${runnerTimeoutMs > 0 ? `${runnerTimeoutMs}ms` : 'disabled'}.`,
125
+ );
126
+ }
114
127
 
115
- if (typeof result.status === 'number') {
116
- process.exit(result.status);
128
+ function signalChild(child: ChildProcess, signal: NodeJS.Signals): void {
129
+ try {
130
+ if (!isWindows() && child.pid) {
131
+ process.kill(-child.pid, signal);
132
+ } else {
133
+ child.kill(signal);
134
+ }
135
+ } catch {
136
+ try {
137
+ child.kill(signal);
138
+ } catch {
139
+ // Ignore kill races. The child might have exited between detection and termination.
140
+ }
141
+ }
117
142
  }
118
143
 
119
- if (result.error) {
120
- console.error(`[run-test-files] node --test error: ${result.error.message}`);
144
+ function terminateChild(child: ChildProcess): void {
145
+ signalChild(child, 'SIGTERM');
146
+ signalChild(child, 'SIGKILL');
147
+ }
148
+
149
+ function runWithCompletionForceExit(): void {
150
+ let finished = false;
151
+ let sawFailure = false;
152
+ let lastTapOk = 0;
153
+ let tapTests: number | undefined;
154
+ let tapPass: number | undefined;
155
+ let tapFail = 0;
156
+ let tapCancelled = 0;
157
+ let completedFromSummary = false;
158
+ let completionTimer: NodeJS.Timeout | undefined;
159
+ let runnerTimer: NodeJS.Timeout | undefined;
160
+ let stdoutRemainder = '';
161
+ let stderrRemainder = '';
162
+
163
+ const child = spawn(process.execPath, testArgs, {
164
+ stdio: ['ignore', 'pipe', 'pipe'],
165
+ env: childEnv,
166
+ detached: !isWindows(),
167
+ });
168
+
169
+ function finish(exitCode: number, reason: string): void {
170
+ if (finished) return;
171
+ finished = true;
172
+ if (completionTimer) clearTimeout(completionTimer);
173
+ if (runnerTimer) clearTimeout(runnerTimer);
174
+ console.error(`[run-test-files] ${reason}; exiting with status ${exitCode}`);
175
+ terminateChild(child);
176
+ child.stdout?.destroy();
177
+ child.stderr?.destroy();
178
+ child.unref();
179
+ process.exit(exitCode);
180
+ }
181
+
182
+ function markFailure(): void {
183
+ sawFailure = true;
184
+ if (completionTimer) {
185
+ clearTimeout(completionTimer);
186
+ completionTimer = undefined;
187
+ }
188
+ }
189
+
190
+ function armCompletionTimer(reason: string): void {
191
+ if (sawFailure) return;
192
+ if (completionTimer) clearTimeout(completionTimer);
193
+ completionTimer = setTimeout(() => {
194
+ if (sawFailure) return;
195
+ finish(0, reason);
196
+ }, forceExitGraceMs);
197
+ }
198
+
199
+ function sawCleanTapSummary(): boolean {
200
+ if (tapTests === undefined || tapPass === undefined) return false;
201
+ return tapTests === tapPass && tapFail === 0 && tapCancelled === 0;
202
+ }
203
+
204
+ function parseTapLine(line: string): void {
205
+ if (/^(?:not ok|Bail out!)/.test(line)) {
206
+ markFailure();
207
+ return;
208
+ }
209
+
210
+ const summary = line.match(/^# (tests|pass|fail|cancelled) (\d+)$/);
211
+ if (summary) {
212
+ const count = Number(summary[2]);
213
+ if (summary[1] === 'tests') tapTests = count;
214
+ if (summary[1] === 'pass') tapPass = count;
215
+ if (summary[1] === 'fail') tapFail = count;
216
+ if (summary[1] === 'cancelled') tapCancelled = count;
217
+ if ((summary[1] === 'fail' || summary[1] === 'cancelled') && count > 0) markFailure();
218
+ return;
219
+ }
220
+
221
+ const ok = line.match(/^ok (\d+)\b/);
222
+ if (ok) {
223
+ lastTapOk = Number(ok[1]);
224
+ if (lastTapOk >= files.length) {
225
+ armCompletionTimer(`force-exit completion grace elapsed after TAP ok ${lastTapOk} with no later failures`);
226
+ }
227
+ return;
228
+ }
229
+
230
+ const plan = line.match(/^1\.\.(\d+)$/);
231
+ if (plan && Number(plan[1]) === lastTapOk && !sawFailure) {
232
+ armCompletionTimer(`force-exit completion grace elapsed after TAP plan ${line}`);
233
+ return;
234
+ }
235
+
236
+ if (/^# duration_ms /.test(line) && sawCleanTapSummary()) {
237
+ completedFromSummary = true;
238
+ armCompletionTimer('force-exit completion grace elapsed after clean TAP summary');
239
+ }
240
+ }
241
+
242
+ function handleOutput(chunk: Buffer, stream: NodeJS.WriteStream, isStdout: boolean): void {
243
+ stream.write(chunk);
244
+ const text = chunk.toString('utf8');
245
+ let combined = (isStdout ? stdoutRemainder : stderrRemainder) + text;
246
+ const lines = combined.split(/\r?\n/);
247
+ combined = lines.pop() ?? '';
248
+ if (isStdout) {
249
+ stdoutRemainder = combined;
250
+ } else {
251
+ stderrRemainder = combined;
252
+ }
253
+ for (const line of lines) parseTapLine(line);
254
+ }
255
+
256
+ child.stdout?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stdout, true));
257
+ child.stderr?.on('data', (chunk: Buffer) => handleOutput(chunk, process.stderr, false));
258
+
259
+ child.on('error', (error) => {
260
+ reportAbnormalExit(null, error.message);
261
+ finish(1, 'node --test failed to spawn');
262
+ });
263
+
264
+ child.on('exit', (status, signal) => {
265
+ if (finished) return;
266
+ if (stdoutRemainder) parseTapLine(stdoutRemainder);
267
+ if (stderrRemainder) parseTapLine(stderrRemainder);
268
+ if (typeof status === 'number') {
269
+ finish(status, `node --test exited normally${completedFromSummary ? ' after clean TAP summary' : ''}`);
270
+ return;
271
+ }
272
+ reportAbnormalExit(signal);
273
+ finish(1, 'node --test exited without a numeric status');
274
+ });
275
+
276
+ if (runnerTimeoutMs > 0) {
277
+ runnerTimer = setTimeout(() => {
278
+ reportAbnormalExit(null);
279
+ finish(1, `runner timeout ${runnerTimeoutMs}ms elapsed`);
280
+ }, runnerTimeoutMs);
281
+ }
282
+ }
283
+
284
+ if (forceExit) {
285
+ runWithCompletionForceExit();
286
+ } else {
287
+ const result = spawnSync(process.execPath, testArgs, {
288
+ stdio: 'inherit',
289
+ env: childEnv,
290
+ timeout: runnerTimeoutMs > 0 ? runnerTimeoutMs : undefined,
291
+ killSignal: 'SIGTERM',
292
+ });
293
+
294
+ if (typeof result.status === 'number') {
295
+ process.exit(result.status);
296
+ }
297
+
298
+ reportAbnormalExit(result.signal, result.error?.message);
299
+ process.exit(1);
121
300
  }
122
- console.error(
123
- `[run-test-files] node --test did not exit normally${result.signal ? ` (signal: ${result.signal})` : ''}. `
124
- + `Roots: ${targets.join(', ')}. Test files: ${files.length}. `
125
- + `Per-test timeout: ${testTimeoutMs > 0 ? `${testTimeoutMs}ms` : 'disabled'}. `
126
- + `Test concurrency: ${testConcurrency ?? 'default'}. `
127
- + `Force exit: ${forceExit ? 'enabled' : 'disabled'}. `
128
- + `Runner timeout: ${runnerTimeoutMs > 0 ? `${runnerTimeoutMs}ms` : 'disabled'}.`,
129
- );
130
- process.exit(1);