oh-my-codex 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/Cargo.lock +5 -5
  2. package/Cargo.toml +1 -1
  3. package/dist/autoresearch/__tests__/skill-validation.test.d.ts +2 -0
  4. package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +1 -0
  5. package/dist/autoresearch/__tests__/skill-validation.test.js +91 -0
  6. package/dist/autoresearch/__tests__/skill-validation.test.js.map +1 -0
  7. package/dist/autoresearch/skill-validation.d.ts +13 -0
  8. package/dist/autoresearch/skill-validation.d.ts.map +1 -0
  9. package/dist/autoresearch/skill-validation.js +165 -0
  10. package/dist/autoresearch/skill-validation.js.map +1 -0
  11. package/dist/catalog/__tests__/schema.test.js +6 -0
  12. package/dist/catalog/__tests__/schema.test.js.map +1 -1
  13. package/dist/cli/__tests__/autoresearch-guided.test.js +236 -273
  14. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  15. package/dist/cli/__tests__/autoresearch.test.js +64 -653
  16. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  17. package/dist/cli/__tests__/index.test.js +7 -0
  18. package/dist/cli/__tests__/index.test.js.map +1 -1
  19. package/dist/cli/__tests__/nested-help-routing.test.js +2 -1
  20. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  21. package/dist/cli/__tests__/question.test.d.ts +2 -0
  22. package/dist/cli/__tests__/question.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/question.test.js +113 -0
  24. package/dist/cli/__tests__/question.test.js.map +1 -0
  25. package/dist/cli/__tests__/session-search-help.test.js +1 -1
  26. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  27. package/dist/cli/__tests__/setup-skills-overwrite.test.js +2 -0
  28. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
  29. package/dist/cli/autoresearch-guided.d.ts +24 -7
  30. package/dist/cli/autoresearch-guided.d.ts.map +1 -1
  31. package/dist/cli/autoresearch-guided.js +189 -130
  32. package/dist/cli/autoresearch-guided.js.map +1 -1
  33. package/dist/cli/autoresearch.d.ts +3 -2
  34. package/dist/cli/autoresearch.d.ts.map +1 -1
  35. package/dist/cli/autoresearch.js +29 -305
  36. package/dist/cli/autoresearch.js.map +1 -1
  37. package/dist/cli/doctor.d.ts.map +1 -1
  38. package/dist/cli/doctor.js +43 -0
  39. package/dist/cli/doctor.js.map +1 -1
  40. package/dist/cli/index.d.ts +1 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +8 -1
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/cli/question.d.ts +3 -0
  45. package/dist/cli/question.d.ts.map +1 -0
  46. package/dist/cli/question.js +182 -0
  47. package/dist/cli/question.js.map +1 -0
  48. package/dist/hooks/__tests__/analyze-routing-contract.test.js +22 -13
  49. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  50. package/dist/hooks/__tests__/anti-slop-workflow.test.js +3 -3
  51. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  52. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +2 -2
  53. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +1 -1
  54. package/dist/hooks/__tests__/deep-interview-contract.test.js +22 -5
  55. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  56. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +2 -2
  57. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +1 -1
  58. package/dist/hooks/__tests__/keyword-detector.test.js +308 -17
  59. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  60. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +570 -2
  61. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  62. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +717 -16
  63. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  64. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +25 -0
  65. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  66. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +894 -1
  67. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  68. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +34 -0
  69. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  70. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +132 -0
  71. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  72. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +22 -4
  73. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  74. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +4 -2
  75. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  76. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +1 -0
  77. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +1 -1
  78. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +4 -1
  79. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  80. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +28 -0
  81. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
  82. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +5 -4
  83. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +1 -1
  84. package/dist/hooks/__tests__/prompt-team-routing.test.js +2 -2
  85. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +1 -1
  86. package/dist/hooks/__tests__/triage-config.test.d.ts +2 -0
  87. package/dist/hooks/__tests__/triage-config.test.d.ts.map +1 -0
  88. package/dist/hooks/__tests__/triage-config.test.js +211 -0
  89. package/dist/hooks/__tests__/triage-config.test.js.map +1 -0
  90. package/dist/hooks/__tests__/triage-heuristic.test.d.ts +2 -0
  91. package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +1 -0
  92. package/dist/hooks/__tests__/triage-heuristic.test.js +230 -0
  93. package/dist/hooks/__tests__/triage-heuristic.test.js.map +1 -0
  94. package/dist/hooks/__tests__/triage-state.test.d.ts +2 -0
  95. package/dist/hooks/__tests__/triage-state.test.d.ts.map +1 -0
  96. package/dist/hooks/__tests__/triage-state.test.js +426 -0
  97. package/dist/hooks/__tests__/triage-state.test.js.map +1 -0
  98. package/dist/hooks/keyword-detector.d.ts +26 -7
  99. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  100. package/dist/hooks/keyword-detector.js +97 -26
  101. package/dist/hooks/keyword-detector.js.map +1 -1
  102. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  103. package/dist/hooks/keyword-registry.js +16 -9
  104. package/dist/hooks/keyword-registry.js.map +1 -1
  105. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  106. package/dist/hooks/prompt-guidance-contract.js +28 -1
  107. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  108. package/dist/hooks/triage-config.d.ts +33 -0
  109. package/dist/hooks/triage-config.d.ts.map +1 -0
  110. package/dist/hooks/triage-config.js +87 -0
  111. package/dist/hooks/triage-config.js.map +1 -0
  112. package/dist/hooks/triage-heuristic.d.ts +20 -0
  113. package/dist/hooks/triage-heuristic.d.ts.map +1 -0
  114. package/dist/hooks/triage-heuristic.js +210 -0
  115. package/dist/hooks/triage-heuristic.js.map +1 -0
  116. package/dist/hooks/triage-state.d.ts +63 -0
  117. package/dist/hooks/triage-state.d.ts.map +1 -0
  118. package/dist/hooks/triage-state.js +138 -0
  119. package/dist/hooks/triage-state.js.map +1 -0
  120. package/dist/hud/__tests__/reconcile.test.js +20 -0
  121. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  122. package/dist/hud/reconcile.d.ts +1 -0
  123. package/dist/hud/reconcile.d.ts.map +1 -1
  124. package/dist/hud/reconcile.js +2 -1
  125. package/dist/hud/reconcile.js.map +1 -1
  126. package/dist/mcp/__tests__/state-server.test.js +1 -0
  127. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  128. package/dist/mcp/state-server.d.ts +8 -0
  129. package/dist/mcp/state-server.d.ts.map +1 -1
  130. package/dist/mcp/state-server.js +4 -0
  131. package/dist/mcp/state-server.js.map +1 -1
  132. package/dist/modes/__tests__/base-ralph-contract.test.js +15 -0
  133. package/dist/modes/__tests__/base-ralph-contract.test.js.map +1 -1
  134. package/dist/modes/base.d.ts +1 -0
  135. package/dist/modes/base.d.ts.map +1 -1
  136. package/dist/modes/base.js +22 -6
  137. package/dist/modes/base.js.map +1 -1
  138. package/dist/notifications/__tests__/index.test.js +78 -0
  139. package/dist/notifications/__tests__/index.test.js.map +1 -1
  140. package/dist/notifications/index.d.ts.map +1 -1
  141. package/dist/notifications/index.js +39 -22
  142. package/dist/notifications/index.js.map +1 -1
  143. package/dist/openclaw/index.d.ts +5 -3
  144. package/dist/openclaw/index.d.ts.map +1 -1
  145. package/dist/openclaw/index.js +5 -3
  146. package/dist/openclaw/index.js.map +1 -1
  147. package/dist/question/__tests__/client.test.d.ts +2 -0
  148. package/dist/question/__tests__/client.test.d.ts.map +1 -0
  149. package/dist/question/__tests__/client.test.js +70 -0
  150. package/dist/question/__tests__/client.test.js.map +1 -0
  151. package/dist/question/__tests__/deep-interview.test.d.ts +2 -0
  152. package/dist/question/__tests__/deep-interview.test.d.ts.map +1 -0
  153. package/dist/question/__tests__/deep-interview.test.js +108 -0
  154. package/dist/question/__tests__/deep-interview.test.js.map +1 -0
  155. package/dist/question/__tests__/policy.test.d.ts +2 -0
  156. package/dist/question/__tests__/policy.test.d.ts.map +1 -0
  157. package/dist/question/__tests__/policy.test.js +107 -0
  158. package/dist/question/__tests__/policy.test.js.map +1 -0
  159. package/dist/question/__tests__/renderer.test.d.ts +2 -0
  160. package/dist/question/__tests__/renderer.test.d.ts.map +1 -0
  161. package/dist/question/__tests__/renderer.test.js +88 -0
  162. package/dist/question/__tests__/renderer.test.js.map +1 -0
  163. package/dist/question/__tests__/state.test.d.ts +2 -0
  164. package/dist/question/__tests__/state.test.d.ts.map +1 -0
  165. package/dist/question/__tests__/state.test.js +55 -0
  166. package/dist/question/__tests__/state.test.js.map +1 -0
  167. package/dist/question/__tests__/types.test.d.ts +2 -0
  168. package/dist/question/__tests__/types.test.d.ts.map +1 -0
  169. package/dist/question/__tests__/types.test.js +44 -0
  170. package/dist/question/__tests__/types.test.js.map +1 -0
  171. package/dist/question/__tests__/ui.test.d.ts +2 -0
  172. package/dist/question/__tests__/ui.test.d.ts.map +1 -0
  173. package/dist/question/__tests__/ui.test.js +169 -0
  174. package/dist/question/__tests__/ui.test.js.map +1 -0
  175. package/dist/question/client.d.ts +54 -0
  176. package/dist/question/client.d.ts.map +1 -0
  177. package/dist/question/client.js +77 -0
  178. package/dist/question/client.js.map +1 -0
  179. package/dist/question/deep-interview.d.ts +27 -0
  180. package/dist/question/deep-interview.d.ts.map +1 -0
  181. package/dist/question/deep-interview.js +101 -0
  182. package/dist/question/deep-interview.js.map +1 -0
  183. package/dist/question/policy.d.ts +18 -0
  184. package/dist/question/policy.d.ts.map +1 -0
  185. package/dist/question/policy.js +77 -0
  186. package/dist/question/policy.js.map +1 -0
  187. package/dist/question/renderer.d.ts +18 -0
  188. package/dist/question/renderer.d.ts.map +1 -0
  189. package/dist/question/renderer.js +128 -0
  190. package/dist/question/renderer.js.map +1 -0
  191. package/dist/question/state.d.ts +19 -0
  192. package/dist/question/state.d.ts.map +1 -0
  193. package/dist/question/state.js +108 -0
  194. package/dist/question/state.js.map +1 -0
  195. package/dist/question/types.d.ts +66 -0
  196. package/dist/question/types.d.ts.map +1 -0
  197. package/dist/question/types.js +82 -0
  198. package/dist/question/types.js.map +1 -0
  199. package/dist/question/ui.d.ts +38 -0
  200. package/dist/question/ui.d.ts.map +1 -0
  201. package/dist/question/ui.js +321 -0
  202. package/dist/question/ui.js.map +1 -0
  203. package/dist/ralph/contract.d.ts +1 -1
  204. package/dist/ralph/contract.d.ts.map +1 -1
  205. package/dist/ralph/contract.js +4 -1
  206. package/dist/ralph/contract.js.map +1 -1
  207. package/dist/ralplan/runtime.js +1 -1
  208. package/dist/ralplan/runtime.js.map +1 -1
  209. package/dist/runtime/__tests__/run-loop.test.d.ts +2 -0
  210. package/dist/runtime/__tests__/run-loop.test.d.ts.map +1 -0
  211. package/dist/runtime/__tests__/run-loop.test.js +35 -0
  212. package/dist/runtime/__tests__/run-loop.test.js.map +1 -0
  213. package/dist/runtime/__tests__/run-outcome.test.d.ts +2 -0
  214. package/dist/runtime/__tests__/run-outcome.test.d.ts.map +1 -0
  215. package/dist/runtime/__tests__/run-outcome.test.js +64 -0
  216. package/dist/runtime/__tests__/run-outcome.test.js.map +1 -0
  217. package/dist/runtime/run-loop.d.ts +41 -0
  218. package/dist/runtime/run-loop.d.ts.map +1 -0
  219. package/dist/runtime/run-loop.js +46 -0
  220. package/dist/runtime/run-loop.js.map +1 -0
  221. package/dist/runtime/run-outcome.d.ts +28 -0
  222. package/dist/runtime/run-outcome.d.ts.map +1 -0
  223. package/dist/runtime/run-outcome.js +136 -0
  224. package/dist/runtime/run-outcome.js.map +1 -0
  225. package/dist/runtime/run-state.d.ts +36 -0
  226. package/dist/runtime/run-state.d.ts.map +1 -0
  227. package/dist/runtime/run-state.js +110 -0
  228. package/dist/runtime/run-state.js.map +1 -0
  229. package/dist/scripts/__tests__/codex-native-hook.test.js +1128 -85
  230. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  231. package/dist/scripts/codex-native-hook.d.ts +2 -0
  232. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  233. package/dist/scripts/codex-native-hook.js +199 -11
  234. package/dist/scripts/codex-native-hook.js.map +1 -1
  235. package/dist/scripts/notify-fallback-watcher.js +81 -2
  236. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  237. package/dist/scripts/notify-hook/auto-nudge.d.ts +27 -0
  238. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  239. package/dist/scripts/notify-hook/auto-nudge.js +83 -20
  240. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  241. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  242. package/dist/scripts/notify-hook/managed-tmux.js +64 -38
  243. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  244. package/dist/scripts/notify-hook/ralph-session-resume.js +1 -1
  245. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  246. package/dist/scripts/notify-hook.js +15 -5
  247. package/dist/scripts/notify-hook.js.map +1 -1
  248. package/dist/scripts/sync-prompt-guidance-fragments.js +5 -0
  249. package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
  250. package/dist/state/__tests__/operations-ralph-phase.test.js +21 -0
  251. package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
  252. package/dist/state/__tests__/workflow-transition.test.js +11 -0
  253. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  254. package/dist/state/operations.d.ts.map +1 -1
  255. package/dist/state/operations.js +15 -0
  256. package/dist/state/operations.js.map +1 -1
  257. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  258. package/dist/state/workflow-transition-reconcile.js +14 -1
  259. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  260. package/dist/state/workflow-transition.d.ts.map +1 -1
  261. package/dist/state/workflow-transition.js +3 -1
  262. package/dist/state/workflow-transition.js.map +1 -1
  263. package/dist/team/__tests__/followup-planner.test.js +15 -0
  264. package/dist/team/__tests__/followup-planner.test.js.map +1 -1
  265. package/dist/team/__tests__/role-router.test.js +41 -0
  266. package/dist/team/__tests__/role-router.test.js.map +1 -1
  267. package/dist/team/followup-planner.d.ts.map +1 -1
  268. package/dist/team/followup-planner.js +31 -9
  269. package/dist/team/followup-planner.js.map +1 -1
  270. package/dist/team/role-router.d.ts.map +1 -1
  271. package/dist/team/role-router.js +73 -0
  272. package/dist/team/role-router.js.map +1 -1
  273. package/package.json +3 -2
  274. package/prompts/dependency-expert.md +3 -0
  275. package/prompts/executor.md +5 -0
  276. package/prompts/explore.md +2 -0
  277. package/prompts/planner.md +5 -0
  278. package/prompts/product-analyst.md +8 -8
  279. package/prompts/researcher.md +78 -30
  280. package/prompts/verifier.md +4 -0
  281. package/skills/autoresearch/SKILL.md +68 -0
  282. package/skills/deep-interview/SKILL.md +10 -9
  283. package/skills/help/SKILL.md +3 -1
  284. package/skills/ralplan/SKILL.md +1 -0
  285. package/skills/team/SKILL.md +1 -0
  286. package/skills/ultrawork/SKILL.md +1 -0
  287. package/src/scripts/__tests__/codex-native-hook.test.ts +1495 -188
  288. package/src/scripts/codex-native-hook.ts +235 -19
  289. package/src/scripts/notify-fallback-watcher.ts +92 -2
  290. package/src/scripts/notify-hook/auto-nudge.ts +89 -20
  291. package/src/scripts/notify-hook/managed-tmux.ts +70 -31
  292. package/src/scripts/notify-hook/ralph-session-resume.ts +1 -1
  293. package/src/scripts/notify-hook.ts +23 -5
  294. package/src/scripts/sync-prompt-guidance-fragments.ts +4 -0
  295. package/templates/AGENTS.md +48 -37
  296. package/templates/catalog-manifest.json +7 -0
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { join } from 'node:path';
7
- import { detectKeywords, detectPrimaryKeyword, recordSkillActivation, DEEP_INTERVIEW_STATE_FILE, DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS, DEEP_INTERVIEW_INPUT_LOCK_MESSAGE, } from '../keyword-detector.js';
7
+ import { detectKeywords, detectPrimaryKeyword, recordSkillActivation, DEEP_INTERVIEW_STATE_FILE, DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS, DEEP_INTERVIEW_INPUT_LOCK_MESSAGE, persistDeepInterviewModeState, } from '../keyword-detector.js';
8
8
  import { SKILL_ACTIVE_STATE_FILE } from '../../state/skill-active.js';
9
9
  import { isUnderspecifiedForExecution, applyRalplanGate } from '../keyword-detector.js';
10
10
  import { KEYWORD_TRIGGER_DEFINITIONS } from '../keyword-registry.js';
@@ -43,15 +43,17 @@ describe('keyword detector swarm/team compatibility', () => {
43
43
  const primary = detectPrimaryKeyword('/prompts:architect, analyze this issue');
44
44
  assert.equal(primary, null);
45
45
  });
46
- it('maps analyze keyword to analyze skill', () => {
47
- const match = detectPrimaryKeyword('please analyze this workflow');
46
+ it('maps explicit $analyze invocation to analyze skill', () => {
47
+ const match = detectPrimaryKeyword('please run $analyze on this workflow');
48
48
  assert.ok(match);
49
49
  assert.equal(match.skill, 'analyze');
50
+ assert.equal(match.keyword.toLowerCase(), '$analyze');
50
51
  });
51
52
  it('maps code-review keyword variants to code-review skill', () => {
52
- const hyphen = detectPrimaryKeyword('run code-review before merge');
53
+ const hyphen = detectPrimaryKeyword('run $code-review before merge');
53
54
  assert.ok(hyphen);
54
55
  assert.equal(hyphen.skill, 'code-review');
56
+ assert.equal(hyphen.keyword.toLowerCase(), '$code-review');
55
57
  const spaced = detectPrimaryKeyword('please do a code review');
56
58
  assert.ok(spaced);
57
59
  assert.equal(spaced.skill, 'code-review');
@@ -79,8 +81,8 @@ describe('keyword detector swarm/team compatibility', () => {
79
81
  assert.equal(match.skill, 'team');
80
82
  assert.match(match.keyword.toLowerCase(), /swarm/);
81
83
  });
82
- it('keeps swarm trigger priority aligned with team trigger', () => {
83
- const teamMatch = detectKeywords('use team agents for this').find((entry) => entry.skill === 'team');
84
+ it('keeps swarm trigger priority aligned with explicit team invocation', () => {
85
+ const teamMatch = detectKeywords('use $team agents for this').find((entry) => entry.skill === 'team');
84
86
  const swarmMatch = detectKeywords('use swarm for this').find((entry) => entry.skill === 'team');
85
87
  assert.ok(teamMatch);
86
88
  assert.ok(swarmMatch);
@@ -94,6 +96,10 @@ describe('keyword detector swarm/team compatibility', () => {
94
96
  const match = detectPrimaryKeyword('the team reviewed the document and shared feedback');
95
97
  assert.equal(match, null);
96
98
  });
99
+ it('does not trigger team from bare skill-name phrasing without $ invocation', () => {
100
+ const match = detectPrimaryKeyword('please use team agents for this');
101
+ assert.equal(match, null);
102
+ });
97
103
  it('still triggers team for explicit $team invocation', () => {
98
104
  const match = detectPrimaryKeyword('please run $team now');
99
105
  assert.ok(match);
@@ -113,8 +119,8 @@ describe('keyword detector swarm/team compatibility', () => {
113
119
  assert.equal(match.skill, 'ralph');
114
120
  assert.equal(match.keyword.toLowerCase(), '$ralph');
115
121
  });
116
- it('prefers ralplan over ralph when both keywords are present', () => {
117
- const match = detectPrimaryKeyword('use ralph mode but do ralplan first');
122
+ it('prefers ralplan over ralph follow-up language when both implicit routes are present', () => {
123
+ const match = detectPrimaryKeyword('keep going but do consensus plan first');
118
124
  assert.ok(match);
119
125
  assert.equal(match.skill, 'ralplan');
120
126
  });
@@ -181,14 +187,44 @@ describe('keyword detector swarm/team compatibility', () => {
181
187
  assert.equal(detectPrimaryKeyword('running 8 tests in parallel across 4 workers'), null);
182
188
  });
183
189
  });
190
+ describe('autoresearch keyword detection', () => {
191
+ it('detects explicit $autoresearch invocation', () => {
192
+ const match = detectPrimaryKeyword('please run $autoresearch now');
193
+ assert.ok(match);
194
+ assert.equal(match.skill, 'autoresearch');
195
+ assert.equal(match.keyword.toLowerCase(), '$autoresearch');
196
+ });
197
+ it('does not detect bare autoresearch phrasing without explicit $ invocation', () => {
198
+ const match = detectPrimaryKeyword('please use autoresearch workflow for this mission');
199
+ assert.equal(match, null);
200
+ });
201
+ it('does not trigger autoresearch from incidental prose', () => {
202
+ const match = detectPrimaryKeyword('Karpathy did autoresearch before native hooks existed');
203
+ assert.equal(match, null);
204
+ });
205
+ });
206
+ describe('explicit skill-name invocation requirement', () => {
207
+ it('does not trigger analyze from bare skill-name usage', () => {
208
+ assert.equal(detectPrimaryKeyword('please analyze this workflow'), null);
209
+ });
210
+ it('does not trigger autoresearch from bare skill-name usage', () => {
211
+ assert.equal(detectPrimaryKeyword('please run autoresearch now'), null);
212
+ });
213
+ it('does not trigger ralph from bare skill-name usage', () => {
214
+ assert.equal(detectPrimaryKeyword('please use ralph for this task'), null);
215
+ });
216
+ it('does not trigger ralplan from bare skill-name usage', () => {
217
+ assert.equal(detectPrimaryKeyword('please do ralplan first'), null);
218
+ });
219
+ });
184
220
  describe('keyword registry coverage', () => {
185
221
  it('includes key team/swarm aliases in runtime keyword registry', () => {
186
222
  const registryKeywords = new Set(KEYWORD_TRIGGER_DEFINITIONS.map((v) => v.keyword.toLowerCase()));
187
- assert.ok(registryKeywords.has('ultraqa'));
188
- assert.ok(registryKeywords.has('analyze'));
223
+ assert.ok(registryKeywords.has('$ultraqa'));
224
+ assert.ok(registryKeywords.has('$analyze'));
189
225
  assert.ok(registryKeywords.has('investigate'));
190
226
  assert.ok(registryKeywords.has('code review'));
191
- assert.ok(registryKeywords.has('code-review'));
227
+ assert.ok(registryKeywords.has('$code-review'));
192
228
  assert.ok(registryKeywords.has('coordinated team'));
193
229
  assert.ok(registryKeywords.has('swarm'));
194
230
  assert.ok(registryKeywords.has('coordinated swarm'));
@@ -198,6 +234,7 @@ describe('keyword registry coverage', () => {
198
234
  assert.ok(registryKeywords.has('wiki query'));
199
235
  assert.ok(registryKeywords.has('wiki add'));
200
236
  assert.ok(registryKeywords.has('wiki lint'));
237
+ assert.ok(registryKeywords.has('$autoresearch'));
201
238
  });
202
239
  });
203
240
  describe('keyword detector skill-active-state lifecycle', () => {
@@ -208,7 +245,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
208
245
  await mkdir(stateDir, { recursive: true });
209
246
  const result = await recordSkillActivation({
210
247
  stateDir,
211
- text: 'please run autopilot and keep going',
248
+ text: 'please run $autopilot and keep going',
212
249
  sessionId: 'sess-1',
213
250
  threadId: 'thread-1',
214
251
  turnId: 'turn-1',
@@ -389,6 +426,53 @@ describe('keyword detector skill-active-state lifecycle', () => {
389
426
  await rm(cwd, { recursive: true, force: true });
390
427
  }
391
428
  });
429
+ it('seeds executing state for autoresearch prompt-submit activation', async () => {
430
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autoresearch-'));
431
+ const stateDir = join(cwd, '.omx', 'state');
432
+ try {
433
+ await mkdir(stateDir, { recursive: true });
434
+ const result = await recordSkillActivation({
435
+ stateDir,
436
+ text: '$autoresearch continue the mission',
437
+ sessionId: 'sess-autoresearch',
438
+ nowIso: '2026-04-17T00:00:00.000Z',
439
+ });
440
+ assert.ok(result);
441
+ assert.equal(result.skill, 'autoresearch');
442
+ assert.equal(result.phase, 'executing');
443
+ assert.equal(result.initialized_mode, 'autoresearch');
444
+ assert.equal(result.initialized_state_path, '.omx/state/sessions/sess-autoresearch/autoresearch-state.json');
445
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autoresearch', 'autoresearch-state.json'), 'utf-8'));
446
+ assert.equal(modeState.mode, 'autoresearch');
447
+ assert.equal(modeState.active, true);
448
+ assert.equal(modeState.current_phase, 'executing');
449
+ }
450
+ finally {
451
+ await rm(cwd, { recursive: true, force: true });
452
+ }
453
+ });
454
+ it('preserves the planning skill when ralplan and autoresearch are invoked together', async () => {
455
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-autoresearch-planning-precedence-'));
456
+ const stateDir = join(cwd, '.omx', 'state');
457
+ try {
458
+ await mkdir(stateDir, { recursive: true });
459
+ const result = await recordSkillActivation({
460
+ stateDir,
461
+ text: '$ralplan $autoresearch wire the mission loop',
462
+ sessionId: 'sess-autoresearch-precedence',
463
+ nowIso: '2026-04-17T00:05:00.000Z',
464
+ });
465
+ assert.equal(result?.transition_error, undefined);
466
+ assert.equal(result?.skill, 'ralplan');
467
+ assert.deepEqual(result?.active_skills?.map((entry) => entry.skill), ['ralplan']);
468
+ assert.deepEqual(result?.deferred_skills, ['autoresearch']);
469
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-autoresearch-precedence', 'ralplan-state.json')), true);
470
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-autoresearch-precedence', 'autoresearch-state.json')), false);
471
+ }
472
+ finally {
473
+ await rm(cwd, { recursive: true, force: true });
474
+ }
475
+ });
392
476
  it('seeds first-class state for ralplan prompt-submit activation', async () => {
393
477
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ralplan-'));
394
478
  const stateDir = join(cwd, '.omx', 'state');
@@ -426,7 +510,17 @@ describe('keyword detector skill-active-state lifecycle', () => {
426
510
  session_id: 'sess-handoff',
427
511
  active_skills: [{ skill: 'deep-interview', phase: 'planning', active: true, session_id: 'sess-handoff' }],
428
512
  }, null, 2));
429
- await writeFile(join(stateDir, 'sessions', 'sess-handoff', 'deep-interview-state.json'), JSON.stringify({ active: true, mode: 'deep-interview', current_phase: 'intent-first' }, null, 2));
513
+ await writeFile(join(stateDir, 'sessions', 'sess-handoff', 'deep-interview-state.json'), JSON.stringify({
514
+ active: true,
515
+ mode: 'deep-interview',
516
+ current_phase: 'intent-first',
517
+ question_enforcement: {
518
+ obligation_id: 'obligation-handoff',
519
+ source: 'omx-question',
520
+ status: 'pending',
521
+ requested_at: '2026-04-09T23:59:00.000Z',
522
+ },
523
+ }, null, 2));
430
524
  const result = await recordSkillActivation({
431
525
  stateDir,
432
526
  text: '$ralplan implement the approved contract',
@@ -438,6 +532,9 @@ describe('keyword detector skill-active-state lifecycle', () => {
438
532
  const completed = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-handoff', 'deep-interview-state.json'), 'utf-8'));
439
533
  assert.equal(completed.active, false);
440
534
  assert.equal(completed.current_phase, 'completed');
535
+ assert.equal(completed.question_enforcement?.status, 'cleared');
536
+ assert.equal(completed.question_enforcement?.clear_reason, 'handoff');
537
+ assert.ok(completed.question_enforcement?.cleared_at);
441
538
  }
442
539
  finally {
443
540
  await rm(cwd, { recursive: true, force: true });
@@ -647,6 +744,84 @@ describe('keyword detector skill-active-state lifecycle', () => {
647
744
  await rm(cwd, { recursive: true, force: true });
648
745
  }
649
746
  });
747
+ it('creates the session-scoped deep-interview state directory before persisting mode state', async () => {
748
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-deep-interview-session-dir-'));
749
+ const stateDir = join(cwd, '.omx', 'state');
750
+ try {
751
+ await mkdir(stateDir, { recursive: true });
752
+ await persistDeepInterviewModeState(stateDir, {
753
+ version: 1,
754
+ active: true,
755
+ skill: 'deep-interview',
756
+ keyword: 'deep interview',
757
+ phase: 'planning',
758
+ activated_at: '2026-02-25T00:00:00.000Z',
759
+ updated_at: '2026-02-25T00:00:00.000Z',
760
+ source: 'keyword-detector',
761
+ session_id: 'sess-sync',
762
+ input_lock: {
763
+ active: true,
764
+ scope: 'deep-interview-auto-approval',
765
+ acquired_at: '2026-02-25T00:00:00.000Z',
766
+ blocked_inputs: [...DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS],
767
+ message: DEEP_INTERVIEW_INPUT_LOCK_MESSAGE,
768
+ },
769
+ }, '2026-02-25T00:00:00.000Z', null, { sessionId: 'sess-sync' });
770
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-sync', DEEP_INTERVIEW_STATE_FILE), 'utf-8'));
771
+ assert.equal(modeState.active, true);
772
+ assert.equal(modeState.mode, 'deep-interview');
773
+ }
774
+ finally {
775
+ await rm(cwd, { recursive: true, force: true });
776
+ }
777
+ });
778
+ it('clears stale pending deep-interview question enforcement when deep-interview is reactivated', async () => {
779
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-deep-interview-reactivation-'));
780
+ const stateDir = join(cwd, '.omx', 'state');
781
+ try {
782
+ await mkdir(join(stateDir, 'sessions', 'sess-reactivate'), { recursive: true });
783
+ await writeFile(join(stateDir, 'sessions', 'sess-reactivate', DEEP_INTERVIEW_STATE_FILE), JSON.stringify({
784
+ active: false,
785
+ mode: 'deep-interview',
786
+ current_phase: 'completed',
787
+ started_at: '2026-04-10T00:00:00.000Z',
788
+ updated_at: '2026-04-10T00:10:00.000Z',
789
+ completed_at: '2026-04-10T00:10:00.000Z',
790
+ question_enforcement: {
791
+ obligation_id: 'obligation-reactivate',
792
+ source: 'omx-question',
793
+ status: 'pending',
794
+ requested_at: '2026-04-10T00:05:00.000Z',
795
+ },
796
+ }, null, 2));
797
+ await persistDeepInterviewModeState(stateDir, {
798
+ version: 1,
799
+ active: true,
800
+ skill: 'deep-interview',
801
+ keyword: 'deep interview',
802
+ phase: 'planning',
803
+ activated_at: '2026-04-10T00:11:00.000Z',
804
+ updated_at: '2026-04-10T00:11:00.000Z',
805
+ source: 'keyword-detector',
806
+ session_id: 'sess-reactivate',
807
+ input_lock: {
808
+ active: true,
809
+ scope: 'deep-interview-auto-approval',
810
+ acquired_at: '2026-04-10T00:11:00.000Z',
811
+ blocked_inputs: [...DEEP_INTERVIEW_BLOCKED_APPROVAL_INPUTS],
812
+ message: DEEP_INTERVIEW_INPUT_LOCK_MESSAGE,
813
+ },
814
+ }, '2026-04-10T00:11:00.000Z', null, { sessionId: 'sess-reactivate' });
815
+ const reactivated = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-reactivate', DEEP_INTERVIEW_STATE_FILE), 'utf-8'));
816
+ assert.equal(reactivated.active, true);
817
+ assert.equal(reactivated.question_enforcement?.status, 'cleared');
818
+ assert.equal(reactivated.question_enforcement?.clear_reason, 'handoff');
819
+ assert.ok(reactivated.question_enforcement?.cleared_at);
820
+ }
821
+ finally {
822
+ await rm(cwd, { recursive: true, force: true });
823
+ }
824
+ });
650
825
  it('releases the deep-interview input lock on abort via cancel keyword', async () => {
651
826
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-deep-interview-abort-'));
652
827
  const stateDir = join(cwd, '.omx', 'state');
@@ -654,7 +829,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
654
829
  await mkdir(stateDir, { recursive: true });
655
830
  await recordSkillActivation({
656
831
  stateDir,
657
- text: 'please run deep interview',
832
+ text: 'please run $deep-interview',
658
833
  nowIso: '2026-02-25T00:00:00.000Z',
659
834
  });
660
835
  const result = await recordSkillActivation({
@@ -719,7 +894,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
719
894
  });
720
895
  const result = await recordSkillActivation({
721
896
  stateDir: join('/definitely-missing', 'nested', 'state-dir'),
722
- text: 'please run autopilot',
897
+ text: 'please run $autopilot',
723
898
  nowIso: '2026-02-25T00:00:00.000Z',
724
899
  });
725
900
  assert.ok(result);
@@ -737,7 +912,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
737
912
  version: 1,
738
913
  active: true,
739
914
  skill: 'autopilot',
740
- keyword: 'autopilot',
915
+ keyword: '$autopilot',
741
916
  phase: 'planning',
742
917
  activated_at: '2026-02-25T00:00:00.000Z',
743
918
  updated_at: '2026-02-25T00:10:00.000Z',
@@ -749,6 +924,8 @@ describe('keyword detector skill-active-state lifecycle', () => {
749
924
  nowIso: '2026-02-26T00:00:00.000Z',
750
925
  });
751
926
  assert.ok(result);
927
+ assert.equal(result.skill, 'autopilot');
928
+ assert.equal(result.transition_error, undefined);
752
929
  assert.equal(result.activated_at, '2026-02-25T00:00:00.000Z');
753
930
  assert.equal(result.updated_at, '2026-02-26T00:00:00.000Z');
754
931
  }
@@ -790,6 +967,9 @@ describe('keyword detector skill-active-state lifecycle', () => {
790
967
  nowIso: '2026-02-26T00:00:00.000Z',
791
968
  });
792
969
  assert.ok(result);
970
+ assert.equal(result.skill, 'autopilot');
971
+ assert.equal(result.phase, 'planning');
972
+ assert.equal(result.transition_error, undefined);
793
973
  const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot', 'autopilot-state.json'), 'utf-8'));
794
974
  assert.equal(modeState.current_phase, 'execution');
795
975
  assert.equal(modeState.started_at, '2026-02-25T00:00:00.000Z');
@@ -852,6 +1032,8 @@ describe('keyword detector skill-active-state lifecycle', () => {
852
1032
  nowIso: '2026-02-26T00:00:00.000Z',
853
1033
  });
854
1034
  assert.ok(result);
1035
+ assert.equal(result.skill, 'ralph');
1036
+ assert.equal(result.transition_error, undefined);
855
1037
  const modeState = JSON.parse(await readFile(join(stateDir, 'ralph-state.json'), 'utf-8'));
856
1038
  assert.equal(modeState.current_phase, 'verifying');
857
1039
  assert.equal(modeState.iteration, 3);
@@ -861,6 +1043,115 @@ describe('keyword detector skill-active-state lifecycle', () => {
861
1043
  await rm(cwd, { recursive: true, force: true });
862
1044
  }
863
1045
  });
1046
+ it('routes bare keep-going continuation to the active autopilot skill instead of generic ralph continuation', async () => {
1047
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-autopilot-bare-continuation-'));
1048
+ const stateDir = join(cwd, '.omx', 'state');
1049
+ try {
1050
+ await mkdir(join(stateDir, 'sessions', 'sess-autopilot-bare'), { recursive: true });
1051
+ await writeFile(join(stateDir, 'sessions', 'sess-autopilot-bare', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1052
+ version: 1,
1053
+ active: true,
1054
+ skill: 'autopilot',
1055
+ keyword: '$autopilot',
1056
+ phase: 'planning',
1057
+ activated_at: '2026-04-19T00:00:00.000Z',
1058
+ updated_at: '2026-04-19T00:10:00.000Z',
1059
+ source: 'keyword-detector',
1060
+ session_id: 'sess-autopilot-bare',
1061
+ active_skills: [
1062
+ {
1063
+ skill: 'autopilot',
1064
+ phase: 'planning',
1065
+ active: true,
1066
+ activated_at: '2026-04-19T00:00:00.000Z',
1067
+ updated_at: '2026-04-19T00:10:00.000Z',
1068
+ session_id: 'sess-autopilot-bare',
1069
+ },
1070
+ ],
1071
+ }, null, 2));
1072
+ await writeFile(join(stateDir, 'sessions', 'sess-autopilot-bare', 'autopilot-state.json'), JSON.stringify({
1073
+ active: true,
1074
+ mode: 'autopilot',
1075
+ current_phase: 'execution',
1076
+ started_at: '2026-04-19T00:00:00.000Z',
1077
+ updated_at: '2026-04-19T00:10:00.000Z',
1078
+ session_id: 'sess-autopilot-bare',
1079
+ state: { context_snapshot_path: '.omx/context/autopilot.md' },
1080
+ }, null, 2));
1081
+ const result = await recordSkillActivation({
1082
+ stateDir,
1083
+ text: '\\ keep going now',
1084
+ sessionId: 'sess-autopilot-bare',
1085
+ nowIso: '2026-04-19T00:15:00.000Z',
1086
+ });
1087
+ assert.ok(result);
1088
+ assert.equal(result.skill, 'autopilot');
1089
+ assert.equal(result.keyword, '$autopilot');
1090
+ assert.equal(result.transition_error, undefined);
1091
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-autopilot-bare', 'autopilot-state.json'), 'utf-8'));
1092
+ assert.equal(modeState.current_phase, 'execution');
1093
+ assert.equal(modeState.state?.context_snapshot_path, '.omx/context/autopilot.md');
1094
+ assert.equal(existsSync(join(stateDir, 'sessions', 'sess-autopilot-bare', 'ralph-state.json')), false);
1095
+ }
1096
+ finally {
1097
+ await rm(cwd, { recursive: true, force: true });
1098
+ }
1099
+ });
1100
+ it('routes bare keep-going continuation to the active ralph skill instead of resetting through generic keep-going detection', async () => {
1101
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-ralph-bare-continuation-'));
1102
+ const stateDir = join(cwd, '.omx', 'state');
1103
+ try {
1104
+ await mkdir(join(stateDir, 'sessions', 'sess-ralph-bare'), { recursive: true });
1105
+ await writeFile(join(stateDir, 'sessions', 'sess-ralph-bare', SKILL_ACTIVE_STATE_FILE), JSON.stringify({
1106
+ version: 1,
1107
+ active: true,
1108
+ skill: 'ralph',
1109
+ keyword: '$ralph',
1110
+ phase: 'executing',
1111
+ activated_at: '2026-04-19T00:00:00.000Z',
1112
+ updated_at: '2026-04-19T00:10:00.000Z',
1113
+ source: 'keyword-detector',
1114
+ session_id: 'sess-ralph-bare',
1115
+ active_skills: [
1116
+ {
1117
+ skill: 'ralph',
1118
+ phase: 'executing',
1119
+ active: true,
1120
+ activated_at: '2026-04-19T00:00:00.000Z',
1121
+ updated_at: '2026-04-19T00:10:00.000Z',
1122
+ session_id: 'sess-ralph-bare',
1123
+ },
1124
+ ],
1125
+ }, null, 2));
1126
+ await writeFile(join(stateDir, 'sessions', 'sess-ralph-bare', 'ralph-state.json'), JSON.stringify({
1127
+ active: true,
1128
+ mode: 'ralph',
1129
+ current_phase: 'verifying',
1130
+ started_at: '2026-04-19T00:00:00.000Z',
1131
+ updated_at: '2026-04-19T00:10:00.000Z',
1132
+ iteration: 7,
1133
+ max_iterations: 50,
1134
+ session_id: 'sess-ralph-bare',
1135
+ }, null, 2));
1136
+ const result = await recordSkillActivation({
1137
+ stateDir,
1138
+ text: 'keep going now',
1139
+ sessionId: 'sess-ralph-bare',
1140
+ nowIso: '2026-04-19T00:15:00.000Z',
1141
+ });
1142
+ assert.ok(result);
1143
+ assert.equal(result.skill, 'ralph');
1144
+ assert.equal(result.keyword, '$ralph');
1145
+ assert.equal(result.transition_error, undefined);
1146
+ const modeState = JSON.parse(await readFile(join(stateDir, 'sessions', 'sess-ralph-bare', 'ralph-state.json'), 'utf-8'));
1147
+ assert.equal(modeState.current_phase, 'verifying');
1148
+ assert.equal(modeState.iteration, 7);
1149
+ assert.equal(modeState.max_iterations, 50);
1150
+ }
1151
+ finally {
1152
+ await rm(cwd, { recursive: true, force: true });
1153
+ }
1154
+ });
864
1155
  it('denies switching away from a standalone workflow without explicit clear', async () => {
865
1156
  const cwd = await mkdtemp(join(tmpdir(), 'omx-keyword-state-skill-switch-deny-'));
866
1157
  const stateDir = join(cwd, '.omx', 'state');
@@ -879,7 +1170,7 @@ describe('keyword detector skill-active-state lifecycle', () => {
879
1170
  }));
880
1171
  const result = await recordSkillActivation({
881
1172
  stateDir,
882
- text: 'please run ralph now',
1173
+ text: 'please run $ralph now',
883
1174
  nowIso: '2026-02-26T00:00:00.000Z',
884
1175
  });
885
1176
  assert.ok(result);