opencode-repos 0.2.0 → 0.3.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 (475) hide show
  1. package/AGENTS.md +180 -0
  2. package/README.md +103 -3
  3. package/TODO.md +3 -0
  4. package/index.ts +1590 -158
  5. package/oh-my-opencode/.github/FUNDING.yml +15 -0
  6. package/oh-my-opencode/.github/ISSUE_TEMPLATE/bug_report.yml +129 -0
  7. package/oh-my-opencode/.github/ISSUE_TEMPLATE/config.yml +8 -0
  8. package/oh-my-opencode/.github/ISSUE_TEMPLATE/feature_request.yml +100 -0
  9. package/oh-my-opencode/.github/ISSUE_TEMPLATE/general.yml +83 -0
  10. package/oh-my-opencode/.github/assets/google.jpg +0 -0
  11. package/oh-my-opencode/.github/assets/hero.jpg +0 -0
  12. package/oh-my-opencode/.github/assets/indent.jpg +0 -0
  13. package/oh-my-opencode/.github/assets/microsoft.jpg +0 -0
  14. package/oh-my-opencode/.github/assets/omo.png +0 -0
  15. package/oh-my-opencode/.github/assets/orchestrator-atlas.png +0 -0
  16. package/oh-my-opencode/.github/assets/sisyphus.png +0 -0
  17. package/oh-my-opencode/.github/assets/sisyphuslabs.png +0 -0
  18. package/oh-my-opencode/.github/pull_request_template.md +34 -0
  19. package/oh-my-opencode/.github/workflows/ci.yml +138 -0
  20. package/oh-my-opencode/.github/workflows/cla.yml +41 -0
  21. package/oh-my-opencode/.github/workflows/lint-workflows.yml +22 -0
  22. package/oh-my-opencode/.github/workflows/publish.yml +165 -0
  23. package/oh-my-opencode/.github/workflows/sisyphus-agent.yml +500 -0
  24. package/oh-my-opencode/.opencode/background-tasks.json +27 -0
  25. package/oh-my-opencode/.opencode/command/get-unpublished-changes.md +84 -0
  26. package/oh-my-opencode/.opencode/command/omomomo.md +37 -0
  27. package/oh-my-opencode/.opencode/command/publish.md +257 -0
  28. package/oh-my-opencode/AGENTS.md +179 -0
  29. package/oh-my-opencode/CLA.md +58 -0
  30. package/oh-my-opencode/CONTRIBUTING.md +268 -0
  31. package/oh-my-opencode/LICENSE.md +82 -0
  32. package/oh-my-opencode/README.ja.md +370 -0
  33. package/oh-my-opencode/README.md +376 -0
  34. package/oh-my-opencode/README.zh-cn.md +380 -0
  35. package/oh-my-opencode/assets/oh-my-opencode.schema.json +2171 -0
  36. package/oh-my-opencode/bin/oh-my-opencode.js +80 -0
  37. package/oh-my-opencode/bin/platform.js +38 -0
  38. package/oh-my-opencode/bin/platform.test.ts +148 -0
  39. package/oh-my-opencode/bun.lock +314 -0
  40. package/oh-my-opencode/bunfig.toml +2 -0
  41. package/oh-my-opencode/docs/category-skill-guide.md +200 -0
  42. package/oh-my-opencode/docs/cli-guide.md +272 -0
  43. package/oh-my-opencode/docs/configurations.md +654 -0
  44. package/oh-my-opencode/docs/features.md +550 -0
  45. package/oh-my-opencode/docs/guide/installation.md +288 -0
  46. package/oh-my-opencode/docs/guide/overview.md +97 -0
  47. package/oh-my-opencode/docs/guide/understanding-orchestration-system.md +445 -0
  48. package/oh-my-opencode/docs/orchestration-guide.md +152 -0
  49. package/oh-my-opencode/docs/ultrawork-manifesto.md +197 -0
  50. package/oh-my-opencode/package.json +89 -0
  51. package/oh-my-opencode/packages/darwin-arm64/bin/.gitkeep +0 -0
  52. package/oh-my-opencode/packages/darwin-arm64/package.json +22 -0
  53. package/oh-my-opencode/packages/darwin-x64/bin/.gitkeep +0 -0
  54. package/oh-my-opencode/packages/darwin-x64/package.json +22 -0
  55. package/oh-my-opencode/packages/linux-arm64/bin/.gitkeep +0 -0
  56. package/oh-my-opencode/packages/linux-arm64/package.json +25 -0
  57. package/oh-my-opencode/packages/linux-arm64-musl/bin/.gitkeep +0 -0
  58. package/oh-my-opencode/packages/linux-arm64-musl/package.json +25 -0
  59. package/oh-my-opencode/packages/linux-x64/bin/.gitkeep +0 -0
  60. package/oh-my-opencode/packages/linux-x64/package.json +25 -0
  61. package/oh-my-opencode/packages/linux-x64-musl/bin/.gitkeep +0 -0
  62. package/oh-my-opencode/packages/linux-x64-musl/package.json +25 -0
  63. package/oh-my-opencode/packages/windows-x64/bin/.gitkeep +0 -0
  64. package/oh-my-opencode/packages/windows-x64/package.json +22 -0
  65. package/oh-my-opencode/postinstall.mjs +43 -0
  66. package/oh-my-opencode/script/build-binaries.ts +103 -0
  67. package/oh-my-opencode/script/build-schema.ts +28 -0
  68. package/oh-my-opencode/script/generate-changelog.ts +92 -0
  69. package/oh-my-opencode/script/publish.ts +344 -0
  70. package/oh-my-opencode/signatures/cla.json +676 -0
  71. package/oh-my-opencode/src/agents/AGENTS.md +67 -0
  72. package/oh-my-opencode/src/agents/atlas.ts +1383 -0
  73. package/oh-my-opencode/src/agents/dynamic-agent-prompt-builder.ts +400 -0
  74. package/oh-my-opencode/src/agents/explore.ts +122 -0
  75. package/oh-my-opencode/src/agents/index.ts +13 -0
  76. package/oh-my-opencode/src/agents/librarian.ts +326 -0
  77. package/oh-my-opencode/src/agents/metis.ts +315 -0
  78. package/oh-my-opencode/src/agents/momus.test.ts +57 -0
  79. package/oh-my-opencode/src/agents/momus.ts +444 -0
  80. package/oh-my-opencode/src/agents/multimodal-looker.ts +56 -0
  81. package/oh-my-opencode/src/agents/oracle.ts +122 -0
  82. package/oh-my-opencode/src/agents/prometheus-prompt.test.ts +22 -0
  83. package/oh-my-opencode/src/agents/prometheus-prompt.ts +1196 -0
  84. package/oh-my-opencode/src/agents/sisyphus-junior.test.ts +232 -0
  85. package/oh-my-opencode/src/agents/sisyphus-junior.ts +134 -0
  86. package/oh-my-opencode/src/agents/sisyphus.ts +633 -0
  87. package/oh-my-opencode/src/agents/types.ts +80 -0
  88. package/oh-my-opencode/src/agents/utils.test.ts +311 -0
  89. package/oh-my-opencode/src/agents/utils.ts +240 -0
  90. package/oh-my-opencode/src/cli/AGENTS.md +91 -0
  91. package/oh-my-opencode/src/cli/config-manager.test.ts +364 -0
  92. package/oh-my-opencode/src/cli/config-manager.ts +641 -0
  93. package/oh-my-opencode/src/cli/doctor/checks/auth.test.ts +114 -0
  94. package/oh-my-opencode/src/cli/doctor/checks/auth.ts +115 -0
  95. package/oh-my-opencode/src/cli/doctor/checks/config.test.ts +103 -0
  96. package/oh-my-opencode/src/cli/doctor/checks/config.ts +123 -0
  97. package/oh-my-opencode/src/cli/doctor/checks/dependencies.test.ts +152 -0
  98. package/oh-my-opencode/src/cli/doctor/checks/dependencies.ts +163 -0
  99. package/oh-my-opencode/src/cli/doctor/checks/gh.test.ts +151 -0
  100. package/oh-my-opencode/src/cli/doctor/checks/gh.ts +171 -0
  101. package/oh-my-opencode/src/cli/doctor/checks/index.ts +34 -0
  102. package/oh-my-opencode/src/cli/doctor/checks/lsp.test.ts +134 -0
  103. package/oh-my-opencode/src/cli/doctor/checks/lsp.ts +77 -0
  104. package/oh-my-opencode/src/cli/doctor/checks/mcp.test.ts +115 -0
  105. package/oh-my-opencode/src/cli/doctor/checks/mcp.ts +128 -0
  106. package/oh-my-opencode/src/cli/doctor/checks/opencode.test.ts +227 -0
  107. package/oh-my-opencode/src/cli/doctor/checks/opencode.ts +178 -0
  108. package/oh-my-opencode/src/cli/doctor/checks/plugin.test.ts +109 -0
  109. package/oh-my-opencode/src/cli/doctor/checks/plugin.ts +124 -0
  110. package/oh-my-opencode/src/cli/doctor/checks/version.test.ts +148 -0
  111. package/oh-my-opencode/src/cli/doctor/checks/version.ts +135 -0
  112. package/oh-my-opencode/src/cli/doctor/constants.ts +72 -0
  113. package/oh-my-opencode/src/cli/doctor/formatter.test.ts +218 -0
  114. package/oh-my-opencode/src/cli/doctor/formatter.ts +140 -0
  115. package/oh-my-opencode/src/cli/doctor/index.ts +11 -0
  116. package/oh-my-opencode/src/cli/doctor/runner.test.ts +153 -0
  117. package/oh-my-opencode/src/cli/doctor/runner.ts +132 -0
  118. package/oh-my-opencode/src/cli/doctor/types.ts +113 -0
  119. package/oh-my-opencode/src/cli/get-local-version/formatter.ts +66 -0
  120. package/oh-my-opencode/src/cli/get-local-version/index.ts +106 -0
  121. package/oh-my-opencode/src/cli/get-local-version/types.ts +14 -0
  122. package/oh-my-opencode/src/cli/index.ts +153 -0
  123. package/oh-my-opencode/src/cli/install.ts +523 -0
  124. package/oh-my-opencode/src/cli/model-fallback.ts +246 -0
  125. package/oh-my-opencode/src/cli/run/completion.test.ts +170 -0
  126. package/oh-my-opencode/src/cli/run/completion.ts +79 -0
  127. package/oh-my-opencode/src/cli/run/events.test.ts +155 -0
  128. package/oh-my-opencode/src/cli/run/events.ts +325 -0
  129. package/oh-my-opencode/src/cli/run/index.ts +2 -0
  130. package/oh-my-opencode/src/cli/run/runner.ts +159 -0
  131. package/oh-my-opencode/src/cli/run/types.ts +76 -0
  132. package/oh-my-opencode/src/cli/types.ts +40 -0
  133. package/oh-my-opencode/src/config/index.ts +26 -0
  134. package/oh-my-opencode/src/config/schema.test.ts +444 -0
  135. package/oh-my-opencode/src/config/schema.ts +339 -0
  136. package/oh-my-opencode/src/features/AGENTS.md +77 -0
  137. package/oh-my-opencode/src/features/background-agent/concurrency.test.ts +418 -0
  138. package/oh-my-opencode/src/features/background-agent/concurrency.ts +137 -0
  139. package/oh-my-opencode/src/features/background-agent/index.ts +3 -0
  140. package/oh-my-opencode/src/features/background-agent/manager.test.ts +1928 -0
  141. package/oh-my-opencode/src/features/background-agent/manager.ts +1335 -0
  142. package/oh-my-opencode/src/features/background-agent/types.ts +66 -0
  143. package/oh-my-opencode/src/features/boulder-state/constants.ts +13 -0
  144. package/oh-my-opencode/src/features/boulder-state/index.ts +3 -0
  145. package/oh-my-opencode/src/features/boulder-state/storage.test.ts +250 -0
  146. package/oh-my-opencode/src/features/boulder-state/storage.ts +150 -0
  147. package/oh-my-opencode/src/features/boulder-state/types.ts +26 -0
  148. package/oh-my-opencode/src/features/builtin-commands/commands.ts +89 -0
  149. package/oh-my-opencode/src/features/builtin-commands/index.ts +2 -0
  150. package/oh-my-opencode/src/features/builtin-commands/templates/init-deep.ts +300 -0
  151. package/oh-my-opencode/src/features/builtin-commands/templates/ralph-loop.ts +38 -0
  152. package/oh-my-opencode/src/features/builtin-commands/templates/refactor.ts +619 -0
  153. package/oh-my-opencode/src/features/builtin-commands/templates/start-work.ts +72 -0
  154. package/oh-my-opencode/src/features/builtin-commands/types.ts +9 -0
  155. package/oh-my-opencode/src/features/builtin-skills/frontend-ui-ux/SKILL.md +78 -0
  156. package/oh-my-opencode/src/features/builtin-skills/git-master/SKILL.md +1105 -0
  157. package/oh-my-opencode/src/features/builtin-skills/index.ts +2 -0
  158. package/oh-my-opencode/src/features/builtin-skills/skills.ts +1203 -0
  159. package/oh-my-opencode/src/features/builtin-skills/types.ts +16 -0
  160. package/oh-my-opencode/src/features/claude-code-agent-loader/index.ts +2 -0
  161. package/oh-my-opencode/src/features/claude-code-agent-loader/loader.ts +90 -0
  162. package/oh-my-opencode/src/features/claude-code-agent-loader/types.ts +17 -0
  163. package/oh-my-opencode/src/features/claude-code-command-loader/index.ts +2 -0
  164. package/oh-my-opencode/src/features/claude-code-command-loader/loader.ts +144 -0
  165. package/oh-my-opencode/src/features/claude-code-command-loader/types.ts +46 -0
  166. package/oh-my-opencode/src/features/claude-code-mcp-loader/env-expander.ts +27 -0
  167. package/oh-my-opencode/src/features/claude-code-mcp-loader/index.ts +11 -0
  168. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.test.ts +162 -0
  169. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.ts +113 -0
  170. package/oh-my-opencode/src/features/claude-code-mcp-loader/transformer.ts +53 -0
  171. package/oh-my-opencode/src/features/claude-code-mcp-loader/types.ts +42 -0
  172. package/oh-my-opencode/src/features/claude-code-plugin-loader/index.ts +3 -0
  173. package/oh-my-opencode/src/features/claude-code-plugin-loader/loader.ts +486 -0
  174. package/oh-my-opencode/src/features/claude-code-plugin-loader/types.ts +210 -0
  175. package/oh-my-opencode/src/features/claude-code-session-state/index.ts +1 -0
  176. package/oh-my-opencode/src/features/claude-code-session-state/state.test.ts +126 -0
  177. package/oh-my-opencode/src/features/claude-code-session-state/state.ts +37 -0
  178. package/oh-my-opencode/src/features/context-injector/collector.test.ts +330 -0
  179. package/oh-my-opencode/src/features/context-injector/collector.ts +85 -0
  180. package/oh-my-opencode/src/features/context-injector/index.ts +14 -0
  181. package/oh-my-opencode/src/features/context-injector/injector.test.ts +122 -0
  182. package/oh-my-opencode/src/features/context-injector/injector.ts +167 -0
  183. package/oh-my-opencode/src/features/context-injector/types.ts +91 -0
  184. package/oh-my-opencode/src/features/hook-message-injector/constants.ts +6 -0
  185. package/oh-my-opencode/src/features/hook-message-injector/index.ts +4 -0
  186. package/oh-my-opencode/src/features/hook-message-injector/injector.ts +195 -0
  187. package/oh-my-opencode/src/features/hook-message-injector/types.ts +47 -0
  188. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.test.ts +448 -0
  189. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.ts +180 -0
  190. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.test.ts +210 -0
  191. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.ts +62 -0
  192. package/oh-my-opencode/src/features/opencode-skill-loader/discover-worker.ts +59 -0
  193. package/oh-my-opencode/src/features/opencode-skill-loader/index.ts +4 -0
  194. package/oh-my-opencode/src/features/opencode-skill-loader/loader.test.ts +273 -0
  195. package/oh-my-opencode/src/features/opencode-skill-loader/loader.ts +259 -0
  196. package/oh-my-opencode/src/features/opencode-skill-loader/merger.ts +267 -0
  197. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.test.ts +267 -0
  198. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.ts +206 -0
  199. package/oh-my-opencode/src/features/opencode-skill-loader/types.ts +38 -0
  200. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.test.ts +201 -0
  201. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.ts +27 -0
  202. package/oh-my-opencode/src/features/skill-mcp-manager/index.ts +2 -0
  203. package/oh-my-opencode/src/features/skill-mcp-manager/manager.test.ts +611 -0
  204. package/oh-my-opencode/src/features/skill-mcp-manager/manager.ts +520 -0
  205. package/oh-my-opencode/src/features/skill-mcp-manager/types.ts +14 -0
  206. package/oh-my-opencode/src/features/task-toast-manager/index.ts +2 -0
  207. package/oh-my-opencode/src/features/task-toast-manager/manager.test.ts +249 -0
  208. package/oh-my-opencode/src/features/task-toast-manager/manager.ts +215 -0
  209. package/oh-my-opencode/src/features/task-toast-manager/types.ts +24 -0
  210. package/oh-my-opencode/src/hooks/AGENTS.md +73 -0
  211. package/oh-my-opencode/src/hooks/agent-usage-reminder/constants.ts +54 -0
  212. package/oh-my-opencode/src/hooks/agent-usage-reminder/index.ts +109 -0
  213. package/oh-my-opencode/src/hooks/agent-usage-reminder/storage.ts +42 -0
  214. package/oh-my-opencode/src/hooks/agent-usage-reminder/types.ts +6 -0
  215. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +307 -0
  216. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.ts +485 -0
  217. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/index.ts +151 -0
  218. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/parser.ts +201 -0
  219. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts +33 -0
  220. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +184 -0
  221. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-types.ts +44 -0
  222. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +77 -0
  223. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.ts +250 -0
  224. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/types.ts +42 -0
  225. package/oh-my-opencode/src/hooks/atlas/index.test.ts +953 -0
  226. package/oh-my-opencode/src/hooks/atlas/index.ts +771 -0
  227. package/oh-my-opencode/src/hooks/auto-slash-command/constants.ts +12 -0
  228. package/oh-my-opencode/src/hooks/auto-slash-command/detector.test.ts +296 -0
  229. package/oh-my-opencode/src/hooks/auto-slash-command/detector.ts +65 -0
  230. package/oh-my-opencode/src/hooks/auto-slash-command/executor.ts +205 -0
  231. package/oh-my-opencode/src/hooks/auto-slash-command/index.test.ts +254 -0
  232. package/oh-my-opencode/src/hooks/auto-slash-command/index.ts +89 -0
  233. package/oh-my-opencode/src/hooks/auto-slash-command/types.ts +23 -0
  234. package/oh-my-opencode/src/hooks/auto-update-checker/cache.ts +93 -0
  235. package/oh-my-opencode/src/hooks/auto-update-checker/checker.test.ts +24 -0
  236. package/oh-my-opencode/src/hooks/auto-update-checker/checker.ts +284 -0
  237. package/oh-my-opencode/src/hooks/auto-update-checker/constants.ts +64 -0
  238. package/oh-my-opencode/src/hooks/auto-update-checker/index.test.ts +254 -0
  239. package/oh-my-opencode/src/hooks/auto-update-checker/index.ts +260 -0
  240. package/oh-my-opencode/src/hooks/auto-update-checker/types.ts +29 -0
  241. package/oh-my-opencode/src/hooks/background-compaction/index.ts +87 -0
  242. package/oh-my-opencode/src/hooks/background-notification/index.ts +28 -0
  243. package/oh-my-opencode/src/hooks/background-notification/types.ts +5 -0
  244. package/oh-my-opencode/src/hooks/claude-code-hooks/AGENTS.md +70 -0
  245. package/oh-my-opencode/src/hooks/claude-code-hooks/config-loader.ts +107 -0
  246. package/oh-my-opencode/src/hooks/claude-code-hooks/config.ts +103 -0
  247. package/oh-my-opencode/src/hooks/claude-code-hooks/index.ts +401 -0
  248. package/oh-my-opencode/src/hooks/claude-code-hooks/plugin-config.ts +12 -0
  249. package/oh-my-opencode/src/hooks/claude-code-hooks/post-tool-use.ts +199 -0
  250. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-compact.ts +109 -0
  251. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-tool-use.ts +172 -0
  252. package/oh-my-opencode/src/hooks/claude-code-hooks/stop.ts +118 -0
  253. package/oh-my-opencode/src/hooks/claude-code-hooks/todo.ts +76 -0
  254. package/oh-my-opencode/src/hooks/claude-code-hooks/tool-input-cache.ts +47 -0
  255. package/oh-my-opencode/src/hooks/claude-code-hooks/transcript.ts +252 -0
  256. package/oh-my-opencode/src/hooks/claude-code-hooks/types.ts +204 -0
  257. package/oh-my-opencode/src/hooks/claude-code-hooks/user-prompt-submit.ts +117 -0
  258. package/oh-my-opencode/src/hooks/comment-checker/cli.test.ts +68 -0
  259. package/oh-my-opencode/src/hooks/comment-checker/cli.ts +221 -0
  260. package/oh-my-opencode/src/hooks/comment-checker/downloader.ts +196 -0
  261. package/oh-my-opencode/src/hooks/comment-checker/index.ts +171 -0
  262. package/oh-my-opencode/src/hooks/comment-checker/types.ts +33 -0
  263. package/oh-my-opencode/src/hooks/compaction-context-injector/index.ts +61 -0
  264. package/oh-my-opencode/src/hooks/context-window-monitor.ts +99 -0
  265. package/oh-my-opencode/src/hooks/delegate-task-retry/index.test.ts +119 -0
  266. package/oh-my-opencode/src/hooks/delegate-task-retry/index.ts +136 -0
  267. package/oh-my-opencode/src/hooks/directory-agents-injector/constants.ts +9 -0
  268. package/oh-my-opencode/src/hooks/directory-agents-injector/index.ts +182 -0
  269. package/oh-my-opencode/src/hooks/directory-agents-injector/storage.ts +48 -0
  270. package/oh-my-opencode/src/hooks/directory-agents-injector/types.ts +5 -0
  271. package/oh-my-opencode/src/hooks/directory-readme-injector/constants.ts +9 -0
  272. package/oh-my-opencode/src/hooks/directory-readme-injector/index.ts +177 -0
  273. package/oh-my-opencode/src/hooks/directory-readme-injector/storage.ts +48 -0
  274. package/oh-my-opencode/src/hooks/directory-readme-injector/types.ts +5 -0
  275. package/oh-my-opencode/src/hooks/edit-error-recovery/index.test.ts +126 -0
  276. package/oh-my-opencode/src/hooks/edit-error-recovery/index.ts +57 -0
  277. package/oh-my-opencode/src/hooks/empty-task-response-detector.ts +27 -0
  278. package/oh-my-opencode/src/hooks/index.ts +32 -0
  279. package/oh-my-opencode/src/hooks/interactive-bash-session/constants.ts +15 -0
  280. package/oh-my-opencode/src/hooks/interactive-bash-session/index.ts +262 -0
  281. package/oh-my-opencode/src/hooks/interactive-bash-session/storage.ts +59 -0
  282. package/oh-my-opencode/src/hooks/interactive-bash-session/types.ts +11 -0
  283. package/oh-my-opencode/src/hooks/keyword-detector/constants.ts +300 -0
  284. package/oh-my-opencode/src/hooks/keyword-detector/detector.ts +52 -0
  285. package/oh-my-opencode/src/hooks/keyword-detector/index.test.ts +529 -0
  286. package/oh-my-opencode/src/hooks/keyword-detector/index.ts +100 -0
  287. package/oh-my-opencode/src/hooks/keyword-detector/types.ts +4 -0
  288. package/oh-my-opencode/src/hooks/non-interactive-env/constants.ts +70 -0
  289. package/oh-my-opencode/src/hooks/non-interactive-env/detector.ts +19 -0
  290. package/oh-my-opencode/src/hooks/non-interactive-env/index.test.ts +323 -0
  291. package/oh-my-opencode/src/hooks/non-interactive-env/index.ts +63 -0
  292. package/oh-my-opencode/src/hooks/non-interactive-env/types.ts +3 -0
  293. package/oh-my-opencode/src/hooks/prometheus-md-only/constants.ts +32 -0
  294. package/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts +488 -0
  295. package/oh-my-opencode/src/hooks/prometheus-md-only/index.ts +136 -0
  296. package/oh-my-opencode/src/hooks/ralph-loop/constants.ts +5 -0
  297. package/oh-my-opencode/src/hooks/ralph-loop/index.test.ts +835 -0
  298. package/oh-my-opencode/src/hooks/ralph-loop/index.ts +417 -0
  299. package/oh-my-opencode/src/hooks/ralph-loop/storage.ts +115 -0
  300. package/oh-my-opencode/src/hooks/ralph-loop/types.ts +19 -0
  301. package/oh-my-opencode/src/hooks/rules-injector/constants.ts +30 -0
  302. package/oh-my-opencode/src/hooks/rules-injector/finder.test.ts +381 -0
  303. package/oh-my-opencode/src/hooks/rules-injector/finder.ts +263 -0
  304. package/oh-my-opencode/src/hooks/rules-injector/index.ts +223 -0
  305. package/oh-my-opencode/src/hooks/rules-injector/matcher.ts +63 -0
  306. package/oh-my-opencode/src/hooks/rules-injector/parser.test.ts +226 -0
  307. package/oh-my-opencode/src/hooks/rules-injector/parser.ts +211 -0
  308. package/oh-my-opencode/src/hooks/rules-injector/storage.ts +59 -0
  309. package/oh-my-opencode/src/hooks/rules-injector/types.ts +57 -0
  310. package/oh-my-opencode/src/hooks/session-notification-utils.ts +140 -0
  311. package/oh-my-opencode/src/hooks/session-notification.test.ts +361 -0
  312. package/oh-my-opencode/src/hooks/session-notification.ts +330 -0
  313. package/oh-my-opencode/src/hooks/session-recovery/constants.ts +10 -0
  314. package/oh-my-opencode/src/hooks/session-recovery/index.test.ts +223 -0
  315. package/oh-my-opencode/src/hooks/session-recovery/index.ts +435 -0
  316. package/oh-my-opencode/src/hooks/session-recovery/storage.ts +390 -0
  317. package/oh-my-opencode/src/hooks/session-recovery/types.ts +98 -0
  318. package/oh-my-opencode/src/hooks/start-work/index.test.ts +402 -0
  319. package/oh-my-opencode/src/hooks/start-work/index.ts +242 -0
  320. package/oh-my-opencode/src/hooks/task-resume-info/index.ts +36 -0
  321. package/oh-my-opencode/src/hooks/think-mode/detector.ts +57 -0
  322. package/oh-my-opencode/src/hooks/think-mode/index.test.ts +353 -0
  323. package/oh-my-opencode/src/hooks/think-mode/index.ts +89 -0
  324. package/oh-my-opencode/src/hooks/think-mode/switcher.test.ts +461 -0
  325. package/oh-my-opencode/src/hooks/think-mode/switcher.ts +222 -0
  326. package/oh-my-opencode/src/hooks/think-mode/types.ts +21 -0
  327. package/oh-my-opencode/src/hooks/thinking-block-validator/index.ts +171 -0
  328. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.test.ts +876 -0
  329. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.ts +480 -0
  330. package/oh-my-opencode/src/hooks/tool-output-truncator.test.ts +168 -0
  331. package/oh-my-opencode/src/hooks/tool-output-truncator.ts +61 -0
  332. package/oh-my-opencode/src/index.ts +589 -0
  333. package/oh-my-opencode/src/mcp/AGENTS.md +70 -0
  334. package/oh-my-opencode/src/mcp/context7.ts +6 -0
  335. package/oh-my-opencode/src/mcp/grep-app.ts +6 -0
  336. package/oh-my-opencode/src/mcp/index.test.ts +86 -0
  337. package/oh-my-opencode/src/mcp/index.ts +32 -0
  338. package/oh-my-opencode/src/mcp/types.ts +9 -0
  339. package/oh-my-opencode/src/mcp/websearch.ts +10 -0
  340. package/oh-my-opencode/src/plugin-config.test.ts +119 -0
  341. package/oh-my-opencode/src/plugin-config.ts +135 -0
  342. package/oh-my-opencode/src/plugin-handlers/config-handler.test.ts +103 -0
  343. package/oh-my-opencode/src/plugin-handlers/config-handler.ts +399 -0
  344. package/oh-my-opencode/src/plugin-handlers/index.ts +1 -0
  345. package/oh-my-opencode/src/plugin-state.ts +30 -0
  346. package/oh-my-opencode/src/shared/AGENTS.md +63 -0
  347. package/oh-my-opencode/src/shared/agent-tool-restrictions.ts +44 -0
  348. package/oh-my-opencode/src/shared/agent-variant.test.ts +83 -0
  349. package/oh-my-opencode/src/shared/agent-variant.ts +40 -0
  350. package/oh-my-opencode/src/shared/claude-config-dir.test.ts +60 -0
  351. package/oh-my-opencode/src/shared/claude-config-dir.ts +11 -0
  352. package/oh-my-opencode/src/shared/command-executor.ts +225 -0
  353. package/oh-my-opencode/src/shared/config-errors.ts +18 -0
  354. package/oh-my-opencode/src/shared/config-path.ts +47 -0
  355. package/oh-my-opencode/src/shared/data-path.ts +22 -0
  356. package/oh-my-opencode/src/shared/deep-merge.test.ts +336 -0
  357. package/oh-my-opencode/src/shared/deep-merge.ts +53 -0
  358. package/oh-my-opencode/src/shared/dynamic-truncator.ts +193 -0
  359. package/oh-my-opencode/src/shared/external-plugin-detector.test.ts +133 -0
  360. package/oh-my-opencode/src/shared/external-plugin-detector.ts +132 -0
  361. package/oh-my-opencode/src/shared/file-reference-resolver.ts +85 -0
  362. package/oh-my-opencode/src/shared/file-utils.ts +40 -0
  363. package/oh-my-opencode/src/shared/first-message-variant.test.ts +32 -0
  364. package/oh-my-opencode/src/shared/first-message-variant.ts +28 -0
  365. package/oh-my-opencode/src/shared/frontmatter.test.ts +262 -0
  366. package/oh-my-opencode/src/shared/frontmatter.ts +31 -0
  367. package/oh-my-opencode/src/shared/hook-disabled.ts +22 -0
  368. package/oh-my-opencode/src/shared/index.ts +29 -0
  369. package/oh-my-opencode/src/shared/jsonc-parser.test.ts +266 -0
  370. package/oh-my-opencode/src/shared/jsonc-parser.ts +66 -0
  371. package/oh-my-opencode/src/shared/logger.ts +20 -0
  372. package/oh-my-opencode/src/shared/migration.test.ts +602 -0
  373. package/oh-my-opencode/src/shared/migration.ts +191 -0
  374. package/oh-my-opencode/src/shared/model-resolver.test.ts +101 -0
  375. package/oh-my-opencode/src/shared/model-resolver.ts +35 -0
  376. package/oh-my-opencode/src/shared/model-sanitizer.ts +12 -0
  377. package/oh-my-opencode/src/shared/opencode-config-dir.test.ts +318 -0
  378. package/oh-my-opencode/src/shared/opencode-config-dir.ts +142 -0
  379. package/oh-my-opencode/src/shared/opencode-version.test.ts +223 -0
  380. package/oh-my-opencode/src/shared/opencode-version.ts +72 -0
  381. package/oh-my-opencode/src/shared/pattern-matcher.ts +29 -0
  382. package/oh-my-opencode/src/shared/permission-compat.test.ts +134 -0
  383. package/oh-my-opencode/src/shared/permission-compat.ts +77 -0
  384. package/oh-my-opencode/src/shared/session-cursor.test.ts +66 -0
  385. package/oh-my-opencode/src/shared/session-cursor.ts +85 -0
  386. package/oh-my-opencode/src/shared/shell-env.test.ts +278 -0
  387. package/oh-my-opencode/src/shared/shell-env.ts +111 -0
  388. package/oh-my-opencode/src/shared/snake-case.ts +49 -0
  389. package/oh-my-opencode/src/shared/system-directive.ts +40 -0
  390. package/oh-my-opencode/src/shared/tool-name.ts +26 -0
  391. package/oh-my-opencode/src/shared/zip-extractor.ts +83 -0
  392. package/oh-my-opencode/src/tools/AGENTS.md +74 -0
  393. package/oh-my-opencode/src/tools/ast-grep/cli.ts +230 -0
  394. package/oh-my-opencode/src/tools/ast-grep/constants.ts +261 -0
  395. package/oh-my-opencode/src/tools/ast-grep/downloader.ts +128 -0
  396. package/oh-my-opencode/src/tools/ast-grep/index.ts +13 -0
  397. package/oh-my-opencode/src/tools/ast-grep/tools.ts +112 -0
  398. package/oh-my-opencode/src/tools/ast-grep/types.ts +61 -0
  399. package/oh-my-opencode/src/tools/ast-grep/utils.ts +102 -0
  400. package/oh-my-opencode/src/tools/background-task/constants.ts +7 -0
  401. package/oh-my-opencode/src/tools/background-task/index.ts +7 -0
  402. package/oh-my-opencode/src/tools/background-task/tools.ts +479 -0
  403. package/oh-my-opencode/src/tools/background-task/types.ts +16 -0
  404. package/oh-my-opencode/src/tools/call-omo-agent/constants.ts +7 -0
  405. package/oh-my-opencode/src/tools/call-omo-agent/index.ts +3 -0
  406. package/oh-my-opencode/src/tools/call-omo-agent/tools.ts +338 -0
  407. package/oh-my-opencode/src/tools/call-omo-agent/types.ts +27 -0
  408. package/oh-my-opencode/src/tools/delegate-task/constants.ts +205 -0
  409. package/oh-my-opencode/src/tools/delegate-task/index.ts +3 -0
  410. package/oh-my-opencode/src/tools/delegate-task/tools.test.ts +1575 -0
  411. package/oh-my-opencode/src/tools/delegate-task/tools.ts +885 -0
  412. package/oh-my-opencode/src/tools/delegate-task/types.ts +9 -0
  413. package/oh-my-opencode/src/tools/glob/cli.test.ts +158 -0
  414. package/oh-my-opencode/src/tools/glob/cli.ts +191 -0
  415. package/oh-my-opencode/src/tools/glob/constants.ts +12 -0
  416. package/oh-my-opencode/src/tools/glob/index.ts +3 -0
  417. package/oh-my-opencode/src/tools/glob/tools.ts +41 -0
  418. package/oh-my-opencode/src/tools/glob/types.ts +22 -0
  419. package/oh-my-opencode/src/tools/glob/utils.ts +26 -0
  420. package/oh-my-opencode/src/tools/grep/cli.ts +229 -0
  421. package/oh-my-opencode/src/tools/grep/constants.ts +127 -0
  422. package/oh-my-opencode/src/tools/grep/downloader.test.ts +103 -0
  423. package/oh-my-opencode/src/tools/grep/downloader.ts +145 -0
  424. package/oh-my-opencode/src/tools/grep/index.ts +3 -0
  425. package/oh-my-opencode/src/tools/grep/tools.ts +40 -0
  426. package/oh-my-opencode/src/tools/grep/types.ts +39 -0
  427. package/oh-my-opencode/src/tools/grep/utils.ts +53 -0
  428. package/oh-my-opencode/src/tools/index.ts +72 -0
  429. package/oh-my-opencode/src/tools/interactive-bash/constants.ts +18 -0
  430. package/oh-my-opencode/src/tools/interactive-bash/index.ts +4 -0
  431. package/oh-my-opencode/src/tools/interactive-bash/tools.ts +126 -0
  432. package/oh-my-opencode/src/tools/interactive-bash/utils.ts +71 -0
  433. package/oh-my-opencode/src/tools/look-at/constants.ts +3 -0
  434. package/oh-my-opencode/src/tools/look-at/index.ts +3 -0
  435. package/oh-my-opencode/src/tools/look-at/tools.test.ts +73 -0
  436. package/oh-my-opencode/src/tools/look-at/tools.ts +173 -0
  437. package/oh-my-opencode/src/tools/look-at/types.ts +4 -0
  438. package/oh-my-opencode/src/tools/lsp/client.ts +596 -0
  439. package/oh-my-opencode/src/tools/lsp/config.test.ts +130 -0
  440. package/oh-my-opencode/src/tools/lsp/config.ts +285 -0
  441. package/oh-my-opencode/src/tools/lsp/constants.ts +390 -0
  442. package/oh-my-opencode/src/tools/lsp/index.ts +7 -0
  443. package/oh-my-opencode/src/tools/lsp/tools.ts +261 -0
  444. package/oh-my-opencode/src/tools/lsp/types.ts +124 -0
  445. package/oh-my-opencode/src/tools/lsp/utils.ts +406 -0
  446. package/oh-my-opencode/src/tools/session-manager/constants.ts +97 -0
  447. package/oh-my-opencode/src/tools/session-manager/index.ts +3 -0
  448. package/oh-my-opencode/src/tools/session-manager/storage.test.ts +315 -0
  449. package/oh-my-opencode/src/tools/session-manager/storage.ts +238 -0
  450. package/oh-my-opencode/src/tools/session-manager/tools.test.ts +124 -0
  451. package/oh-my-opencode/src/tools/session-manager/tools.ts +146 -0
  452. package/oh-my-opencode/src/tools/session-manager/types.ts +99 -0
  453. package/oh-my-opencode/src/tools/session-manager/utils.test.ts +160 -0
  454. package/oh-my-opencode/src/tools/session-manager/utils.ts +199 -0
  455. package/oh-my-opencode/src/tools/skill/constants.ts +8 -0
  456. package/oh-my-opencode/src/tools/skill/index.ts +3 -0
  457. package/oh-my-opencode/src/tools/skill/tools.test.ts +239 -0
  458. package/oh-my-opencode/src/tools/skill/tools.ts +200 -0
  459. package/oh-my-opencode/src/tools/skill/types.ts +31 -0
  460. package/oh-my-opencode/src/tools/skill-mcp/constants.ts +3 -0
  461. package/oh-my-opencode/src/tools/skill-mcp/index.ts +3 -0
  462. package/oh-my-opencode/src/tools/skill-mcp/tools.test.ts +215 -0
  463. package/oh-my-opencode/src/tools/skill-mcp/tools.ts +172 -0
  464. package/oh-my-opencode/src/tools/skill-mcp/types.ts +8 -0
  465. package/oh-my-opencode/src/tools/slashcommand/index.ts +2 -0
  466. package/oh-my-opencode/src/tools/slashcommand/tools.ts +252 -0
  467. package/oh-my-opencode/src/tools/slashcommand/types.ts +28 -0
  468. package/oh-my-opencode/test-setup.ts +6 -0
  469. package/oh-my-opencode/tsconfig.json +20 -0
  470. package/package.json +1 -1
  471. package/src/__tests__/git.test.ts +7 -2
  472. package/src/__tests__/manifest.test.ts +5 -5
  473. package/src/agents/repo-explorer.ts +2 -1
  474. package/src/git.ts +18 -3
  475. package/src/manifest.ts +22 -15
@@ -0,0 +1,953 @@
1
+ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import { createAtlasHook } from "./index"
6
+ import {
7
+ writeBoulderState,
8
+ clearBoulderState,
9
+ readBoulderState,
10
+ } from "../../features/boulder-state"
11
+ import type { BoulderState } from "../../features/boulder-state"
12
+
13
+ import { MESSAGE_STORAGE } from "../../features/hook-message-injector"
14
+
15
+ describe("atlas hook", () => {
16
+ const TEST_DIR = join(tmpdir(), "atlas-test-" + Date.now())
17
+ const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus")
18
+
19
+ function createMockPluginInput(overrides?: { promptMock?: ReturnType<typeof mock> }) {
20
+ const promptMock = overrides?.promptMock ?? mock(() => Promise.resolve())
21
+ return {
22
+ directory: TEST_DIR,
23
+ client: {
24
+ session: {
25
+ prompt: promptMock,
26
+ },
27
+ },
28
+ _promptMock: promptMock,
29
+ } as unknown as Parameters<typeof createAtlasHook>[0] & { _promptMock: ReturnType<typeof mock> }
30
+ }
31
+
32
+ function setupMessageStorage(sessionID: string, agent: string): void {
33
+ const messageDir = join(MESSAGE_STORAGE, sessionID)
34
+ if (!existsSync(messageDir)) {
35
+ mkdirSync(messageDir, { recursive: true })
36
+ }
37
+ const messageData = {
38
+ agent,
39
+ model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
40
+ }
41
+ writeFileSync(join(messageDir, "msg_test001.json"), JSON.stringify(messageData))
42
+ }
43
+
44
+ function cleanupMessageStorage(sessionID: string): void {
45
+ const messageDir = join(MESSAGE_STORAGE, sessionID)
46
+ if (existsSync(messageDir)) {
47
+ rmSync(messageDir, { recursive: true, force: true })
48
+ }
49
+ }
50
+
51
+ beforeEach(() => {
52
+ if (!existsSync(TEST_DIR)) {
53
+ mkdirSync(TEST_DIR, { recursive: true })
54
+ }
55
+ if (!existsSync(SISYPHUS_DIR)) {
56
+ mkdirSync(SISYPHUS_DIR, { recursive: true })
57
+ }
58
+ clearBoulderState(TEST_DIR)
59
+ })
60
+
61
+ afterEach(() => {
62
+ clearBoulderState(TEST_DIR)
63
+ if (existsSync(TEST_DIR)) {
64
+ rmSync(TEST_DIR, { recursive: true, force: true })
65
+ }
66
+ })
67
+
68
+ describe("tool.execute.after handler", () => {
69
+ test("should ignore non-delegate_task tools", async () => {
70
+ // #given - hook and non-delegate_task tool
71
+ const hook = createAtlasHook(createMockPluginInput())
72
+ const output = {
73
+ title: "Test Tool",
74
+ output: "Original output",
75
+ metadata: {},
76
+ }
77
+
78
+ // #when
79
+ await hook["tool.execute.after"](
80
+ { tool: "other_tool", sessionID: "session-123" },
81
+ output
82
+ )
83
+
84
+ // #then - output unchanged
85
+ expect(output.output).toBe("Original output")
86
+ })
87
+
88
+ test("should not transform when caller is not Atlas", async () => {
89
+ // #given - boulder state exists but caller agent in message storage is not Atlas
90
+ const sessionID = "session-non-orchestrator-test"
91
+ setupMessageStorage(sessionID, "other-agent")
92
+
93
+ const planPath = join(TEST_DIR, "test-plan.md")
94
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
95
+
96
+ const state: BoulderState = {
97
+ active_plan: planPath,
98
+ started_at: "2026-01-02T10:00:00Z",
99
+ session_ids: ["session-1"],
100
+ plan_name: "test-plan",
101
+ }
102
+ writeBoulderState(TEST_DIR, state)
103
+
104
+ const hook = createAtlasHook(createMockPluginInput())
105
+ const output = {
106
+ title: "Sisyphus Task",
107
+ output: "Task completed successfully",
108
+ metadata: {},
109
+ }
110
+
111
+ // #when
112
+ await hook["tool.execute.after"](
113
+ { tool: "delegate_task", sessionID },
114
+ output
115
+ )
116
+
117
+ // #then - output unchanged because caller is not orchestrator
118
+ expect(output.output).toBe("Task completed successfully")
119
+
120
+ cleanupMessageStorage(sessionID)
121
+ })
122
+
123
+ test("should append standalone verification when no boulder state but caller is Atlas", async () => {
124
+ // #given - no boulder state, but caller is Atlas
125
+ const sessionID = "session-no-boulder-test"
126
+ setupMessageStorage(sessionID, "Atlas")
127
+
128
+ const hook = createAtlasHook(createMockPluginInput())
129
+ const output = {
130
+ title: "Sisyphus Task",
131
+ output: "Task completed successfully",
132
+ metadata: {},
133
+ }
134
+
135
+ // #when
136
+ await hook["tool.execute.after"](
137
+ { tool: "delegate_task", sessionID },
138
+ output
139
+ )
140
+
141
+ // #then - standalone verification reminder appended
142
+ expect(output.output).toContain("Task completed successfully")
143
+ expect(output.output).toContain("MANDATORY:")
144
+ expect(output.output).toContain("delegate_task(resume=")
145
+
146
+ cleanupMessageStorage(sessionID)
147
+ })
148
+
149
+ test("should transform output when caller is Atlas with boulder state", async () => {
150
+ // #given - Atlas caller with boulder state
151
+ const sessionID = "session-transform-test"
152
+ setupMessageStorage(sessionID, "Atlas")
153
+
154
+ const planPath = join(TEST_DIR, "test-plan.md")
155
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2")
156
+
157
+ const state: BoulderState = {
158
+ active_plan: planPath,
159
+ started_at: "2026-01-02T10:00:00Z",
160
+ session_ids: ["session-1"],
161
+ plan_name: "test-plan",
162
+ }
163
+ writeBoulderState(TEST_DIR, state)
164
+
165
+ const hook = createAtlasHook(createMockPluginInput())
166
+ const output = {
167
+ title: "Sisyphus Task",
168
+ output: "Task completed successfully",
169
+ metadata: {},
170
+ }
171
+
172
+ // #when
173
+ await hook["tool.execute.after"](
174
+ { tool: "delegate_task", sessionID },
175
+ output
176
+ )
177
+
178
+ // #then - output should be transformed (original output preserved for debugging)
179
+ expect(output.output).toContain("Task completed successfully")
180
+ expect(output.output).toContain("SUBAGENT WORK COMPLETED")
181
+ expect(output.output).toContain("test-plan")
182
+ expect(output.output).toContain("LIE")
183
+ expect(output.output).toContain("delegate_task(resume=")
184
+
185
+ cleanupMessageStorage(sessionID)
186
+ })
187
+
188
+ test("should still transform when plan is complete (shows progress)", async () => {
189
+ // #given - boulder state with complete plan, Atlas caller
190
+ const sessionID = "session-complete-plan-test"
191
+ setupMessageStorage(sessionID, "Atlas")
192
+
193
+ const planPath = join(TEST_DIR, "complete-plan.md")
194
+ writeFileSync(planPath, "# Plan\n- [x] Task 1\n- [x] Task 2")
195
+
196
+ const state: BoulderState = {
197
+ active_plan: planPath,
198
+ started_at: "2026-01-02T10:00:00Z",
199
+ session_ids: ["session-1"],
200
+ plan_name: "complete-plan",
201
+ }
202
+ writeBoulderState(TEST_DIR, state)
203
+
204
+ const hook = createAtlasHook(createMockPluginInput())
205
+ const output = {
206
+ title: "Sisyphus Task",
207
+ output: "Original output",
208
+ metadata: {},
209
+ }
210
+
211
+ // #when
212
+ await hook["tool.execute.after"](
213
+ { tool: "delegate_task", sessionID },
214
+ output
215
+ )
216
+
217
+ // #then - output transformed even when complete (shows 2/2 done)
218
+ expect(output.output).toContain("SUBAGENT WORK COMPLETED")
219
+ expect(output.output).toContain("2/2 done")
220
+ expect(output.output).toContain("0 remaining")
221
+
222
+ cleanupMessageStorage(sessionID)
223
+ })
224
+
225
+ test("should append session ID to boulder state if not present", async () => {
226
+ // #given - boulder state without session-append-test, Atlas caller
227
+ const sessionID = "session-append-test"
228
+ setupMessageStorage(sessionID, "Atlas")
229
+
230
+ const planPath = join(TEST_DIR, "test-plan.md")
231
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
232
+
233
+ const state: BoulderState = {
234
+ active_plan: planPath,
235
+ started_at: "2026-01-02T10:00:00Z",
236
+ session_ids: ["session-1"],
237
+ plan_name: "test-plan",
238
+ }
239
+ writeBoulderState(TEST_DIR, state)
240
+
241
+ const hook = createAtlasHook(createMockPluginInput())
242
+ const output = {
243
+ title: "Sisyphus Task",
244
+ output: "Task output",
245
+ metadata: {},
246
+ }
247
+
248
+ // #when
249
+ await hook["tool.execute.after"](
250
+ { tool: "delegate_task", sessionID },
251
+ output
252
+ )
253
+
254
+ // #then - sessionID should be appended
255
+ const updatedState = readBoulderState(TEST_DIR)
256
+ expect(updatedState?.session_ids).toContain(sessionID)
257
+
258
+ cleanupMessageStorage(sessionID)
259
+ })
260
+
261
+ test("should not duplicate existing session ID", async () => {
262
+ // #given - boulder state already has session-dup-test, Atlas caller
263
+ const sessionID = "session-dup-test"
264
+ setupMessageStorage(sessionID, "Atlas")
265
+
266
+ const planPath = join(TEST_DIR, "test-plan.md")
267
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
268
+
269
+ const state: BoulderState = {
270
+ active_plan: planPath,
271
+ started_at: "2026-01-02T10:00:00Z",
272
+ session_ids: [sessionID],
273
+ plan_name: "test-plan",
274
+ }
275
+ writeBoulderState(TEST_DIR, state)
276
+
277
+ const hook = createAtlasHook(createMockPluginInput())
278
+ const output = {
279
+ title: "Sisyphus Task",
280
+ output: "Task output",
281
+ metadata: {},
282
+ }
283
+
284
+ // #when
285
+ await hook["tool.execute.after"](
286
+ { tool: "delegate_task", sessionID },
287
+ output
288
+ )
289
+
290
+ // #then - should still have only one sessionID
291
+ const updatedState = readBoulderState(TEST_DIR)
292
+ const count = updatedState?.session_ids.filter((id) => id === sessionID).length
293
+ expect(count).toBe(1)
294
+
295
+ cleanupMessageStorage(sessionID)
296
+ })
297
+
298
+ test("should include boulder.json path and notepad path in transformed output", async () => {
299
+ // #given - boulder state, Atlas caller
300
+ const sessionID = "session-path-test"
301
+ setupMessageStorage(sessionID, "Atlas")
302
+
303
+ const planPath = join(TEST_DIR, "my-feature.md")
304
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2\n- [x] Task 3")
305
+
306
+ const state: BoulderState = {
307
+ active_plan: planPath,
308
+ started_at: "2026-01-02T10:00:00Z",
309
+ session_ids: ["session-1"],
310
+ plan_name: "my-feature",
311
+ }
312
+ writeBoulderState(TEST_DIR, state)
313
+
314
+ const hook = createAtlasHook(createMockPluginInput())
315
+ const output = {
316
+ title: "Sisyphus Task",
317
+ output: "Task completed",
318
+ metadata: {},
319
+ }
320
+
321
+ // #when
322
+ await hook["tool.execute.after"](
323
+ { tool: "delegate_task", sessionID },
324
+ output
325
+ )
326
+
327
+ // #then - output should contain plan name and progress
328
+ expect(output.output).toContain("my-feature")
329
+ expect(output.output).toContain("1/3 done")
330
+ expect(output.output).toContain("2 remaining")
331
+
332
+ cleanupMessageStorage(sessionID)
333
+ })
334
+
335
+ test("should include resume and checkbox instructions in reminder", async () => {
336
+ // #given - boulder state, Atlas caller
337
+ const sessionID = "session-resume-test"
338
+ setupMessageStorage(sessionID, "Atlas")
339
+
340
+ const planPath = join(TEST_DIR, "test-plan.md")
341
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
342
+
343
+ const state: BoulderState = {
344
+ active_plan: planPath,
345
+ started_at: "2026-01-02T10:00:00Z",
346
+ session_ids: ["session-1"],
347
+ plan_name: "test-plan",
348
+ }
349
+ writeBoulderState(TEST_DIR, state)
350
+
351
+ const hook = createAtlasHook(createMockPluginInput())
352
+ const output = {
353
+ title: "Sisyphus Task",
354
+ output: "Task completed",
355
+ metadata: {},
356
+ }
357
+
358
+ // #when
359
+ await hook["tool.execute.after"](
360
+ { tool: "delegate_task", sessionID },
361
+ output
362
+ )
363
+
364
+ // #then - should include resume instructions and verification
365
+ expect(output.output).toContain("delegate_task(resume=")
366
+ expect(output.output).toContain("[x]")
367
+ expect(output.output).toContain("MANDATORY:")
368
+
369
+ cleanupMessageStorage(sessionID)
370
+ })
371
+
372
+ describe("Write/Edit tool direct work reminder", () => {
373
+ const ORCHESTRATOR_SESSION = "orchestrator-write-test"
374
+
375
+ beforeEach(() => {
376
+ setupMessageStorage(ORCHESTRATOR_SESSION, "Atlas")
377
+ })
378
+
379
+ afterEach(() => {
380
+ cleanupMessageStorage(ORCHESTRATOR_SESSION)
381
+ })
382
+
383
+ test("should append delegation reminder when orchestrator writes outside .sisyphus/", async () => {
384
+ // #given
385
+ const hook = createAtlasHook(createMockPluginInput())
386
+ const output = {
387
+ title: "Write",
388
+ output: "File written successfully",
389
+ metadata: { filePath: "/path/to/code.ts" },
390
+ }
391
+
392
+ // #when
393
+ await hook["tool.execute.after"](
394
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
395
+ output
396
+ )
397
+
398
+ // #then
399
+ expect(output.output).toContain("DELEGATION REQUIRED")
400
+ expect(output.output).toContain("ORCHESTRATOR, not an IMPLEMENTER")
401
+ expect(output.output).toContain("delegate_task")
402
+ })
403
+
404
+ test("should append delegation reminder when orchestrator edits outside .sisyphus/", async () => {
405
+ // #given
406
+ const hook = createAtlasHook(createMockPluginInput())
407
+ const output = {
408
+ title: "Edit",
409
+ output: "File edited successfully",
410
+ metadata: { filePath: "/src/components/button.tsx" },
411
+ }
412
+
413
+ // #when
414
+ await hook["tool.execute.after"](
415
+ { tool: "Edit", sessionID: ORCHESTRATOR_SESSION },
416
+ output
417
+ )
418
+
419
+ // #then
420
+ expect(output.output).toContain("DELEGATION REQUIRED")
421
+ })
422
+
423
+ test("should NOT append reminder when orchestrator writes inside .sisyphus/", async () => {
424
+ // #given
425
+ const hook = createAtlasHook(createMockPluginInput())
426
+ const originalOutput = "File written successfully"
427
+ const output = {
428
+ title: "Write",
429
+ output: originalOutput,
430
+ metadata: { filePath: "/project/.sisyphus/plans/work-plan.md" },
431
+ }
432
+
433
+ // #when
434
+ await hook["tool.execute.after"](
435
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
436
+ output
437
+ )
438
+
439
+ // #then
440
+ expect(output.output).toBe(originalOutput)
441
+ expect(output.output).not.toContain("DELEGATION REQUIRED")
442
+ })
443
+
444
+ test("should NOT append reminder when non-orchestrator writes outside .sisyphus/", async () => {
445
+ // #given
446
+ const nonOrchestratorSession = "non-orchestrator-session"
447
+ setupMessageStorage(nonOrchestratorSession, "Sisyphus-Junior")
448
+
449
+ const hook = createAtlasHook(createMockPluginInput())
450
+ const originalOutput = "File written successfully"
451
+ const output = {
452
+ title: "Write",
453
+ output: originalOutput,
454
+ metadata: { filePath: "/path/to/code.ts" },
455
+ }
456
+
457
+ // #when
458
+ await hook["tool.execute.after"](
459
+ { tool: "Write", sessionID: nonOrchestratorSession },
460
+ output
461
+ )
462
+
463
+ // #then
464
+ expect(output.output).toBe(originalOutput)
465
+ expect(output.output).not.toContain("DELEGATION REQUIRED")
466
+
467
+ cleanupMessageStorage(nonOrchestratorSession)
468
+ })
469
+
470
+ test("should NOT append reminder for read-only tools", async () => {
471
+ // #given
472
+ const hook = createAtlasHook(createMockPluginInput())
473
+ const originalOutput = "File content"
474
+ const output = {
475
+ title: "Read",
476
+ output: originalOutput,
477
+ metadata: { filePath: "/path/to/code.ts" },
478
+ }
479
+
480
+ // #when
481
+ await hook["tool.execute.after"](
482
+ { tool: "Read", sessionID: ORCHESTRATOR_SESSION },
483
+ output
484
+ )
485
+
486
+ // #then
487
+ expect(output.output).toBe(originalOutput)
488
+ })
489
+
490
+ test("should handle missing filePath gracefully", async () => {
491
+ // #given
492
+ const hook = createAtlasHook(createMockPluginInput())
493
+ const originalOutput = "File written successfully"
494
+ const output = {
495
+ title: "Write",
496
+ output: originalOutput,
497
+ metadata: {},
498
+ }
499
+
500
+ // #when
501
+ await hook["tool.execute.after"](
502
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
503
+ output
504
+ )
505
+
506
+ // #then
507
+ expect(output.output).toBe(originalOutput)
508
+ })
509
+
510
+ describe("cross-platform path validation (Windows support)", () => {
511
+ test("should NOT append reminder when orchestrator writes inside .sisyphus\\ (Windows backslash)", async () => {
512
+ // #given
513
+ const hook = createAtlasHook(createMockPluginInput())
514
+ const originalOutput = "File written successfully"
515
+ const output = {
516
+ title: "Write",
517
+ output: originalOutput,
518
+ metadata: { filePath: ".sisyphus\\plans\\work-plan.md" },
519
+ }
520
+
521
+ // #when
522
+ await hook["tool.execute.after"](
523
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
524
+ output
525
+ )
526
+
527
+ // #then
528
+ expect(output.output).toBe(originalOutput)
529
+ expect(output.output).not.toContain("DELEGATION REQUIRED")
530
+ })
531
+
532
+ test("should NOT append reminder when orchestrator writes inside .sisyphus with mixed separators", async () => {
533
+ // #given
534
+ const hook = createAtlasHook(createMockPluginInput())
535
+ const originalOutput = "File written successfully"
536
+ const output = {
537
+ title: "Write",
538
+ output: originalOutput,
539
+ metadata: { filePath: ".sisyphus\\plans/work-plan.md" },
540
+ }
541
+
542
+ // #when
543
+ await hook["tool.execute.after"](
544
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
545
+ output
546
+ )
547
+
548
+ // #then
549
+ expect(output.output).toBe(originalOutput)
550
+ expect(output.output).not.toContain("DELEGATION REQUIRED")
551
+ })
552
+
553
+ test("should NOT append reminder for absolute Windows path inside .sisyphus\\", async () => {
554
+ // #given
555
+ const hook = createAtlasHook(createMockPluginInput())
556
+ const originalOutput = "File written successfully"
557
+ const output = {
558
+ title: "Write",
559
+ output: originalOutput,
560
+ metadata: { filePath: "C:\\Users\\test\\project\\.sisyphus\\plans\\x.md" },
561
+ }
562
+
563
+ // #when
564
+ await hook["tool.execute.after"](
565
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
566
+ output
567
+ )
568
+
569
+ // #then
570
+ expect(output.output).toBe(originalOutput)
571
+ expect(output.output).not.toContain("DELEGATION REQUIRED")
572
+ })
573
+
574
+ test("should append reminder for Windows path outside .sisyphus\\", async () => {
575
+ // #given
576
+ const hook = createAtlasHook(createMockPluginInput())
577
+ const output = {
578
+ title: "Write",
579
+ output: "File written successfully",
580
+ metadata: { filePath: "C:\\Users\\test\\project\\src\\code.ts" },
581
+ }
582
+
583
+ // #when
584
+ await hook["tool.execute.after"](
585
+ { tool: "Write", sessionID: ORCHESTRATOR_SESSION },
586
+ output
587
+ )
588
+
589
+ // #then
590
+ expect(output.output).toContain("DELEGATION REQUIRED")
591
+ })
592
+ })
593
+ })
594
+ })
595
+
596
+ describe("session.idle handler (boulder continuation)", () => {
597
+ const MAIN_SESSION_ID = "main-session-123"
598
+
599
+ beforeEach(() => {
600
+ mock.module("../../features/claude-code-session-state", () => ({
601
+ getMainSessionID: () => MAIN_SESSION_ID,
602
+ subagentSessions: new Set<string>(),
603
+ }))
604
+ setupMessageStorage(MAIN_SESSION_ID, "Atlas")
605
+ })
606
+
607
+ afterEach(() => {
608
+ cleanupMessageStorage(MAIN_SESSION_ID)
609
+ })
610
+
611
+ test("should inject continuation when boulder has incomplete tasks", async () => {
612
+ // #given - boulder state with incomplete plan
613
+ const planPath = join(TEST_DIR, "test-plan.md")
614
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [x] Task 2\n- [ ] Task 3")
615
+
616
+ const state: BoulderState = {
617
+ active_plan: planPath,
618
+ started_at: "2026-01-02T10:00:00Z",
619
+ session_ids: [MAIN_SESSION_ID],
620
+ plan_name: "test-plan",
621
+ }
622
+ writeBoulderState(TEST_DIR, state)
623
+
624
+ const mockInput = createMockPluginInput()
625
+ const hook = createAtlasHook(mockInput)
626
+
627
+ // #when
628
+ await hook.handler({
629
+ event: {
630
+ type: "session.idle",
631
+ properties: { sessionID: MAIN_SESSION_ID },
632
+ },
633
+ })
634
+
635
+ // #then - should call prompt with continuation
636
+ expect(mockInput._promptMock).toHaveBeenCalled()
637
+ const callArgs = mockInput._promptMock.mock.calls[0][0]
638
+ expect(callArgs.path.id).toBe(MAIN_SESSION_ID)
639
+ expect(callArgs.body.parts[0].text).toContain("BOULDER CONTINUATION")
640
+ expect(callArgs.body.parts[0].text).toContain("2 remaining")
641
+ })
642
+
643
+ test("should not inject when no boulder state exists", async () => {
644
+ // #given - no boulder state
645
+ const mockInput = createMockPluginInput()
646
+ const hook = createAtlasHook(mockInput)
647
+
648
+ // #when
649
+ await hook.handler({
650
+ event: {
651
+ type: "session.idle",
652
+ properties: { sessionID: MAIN_SESSION_ID },
653
+ },
654
+ })
655
+
656
+ // #then - should not call prompt
657
+ expect(mockInput._promptMock).not.toHaveBeenCalled()
658
+ })
659
+
660
+ test("should not inject when boulder plan is complete", async () => {
661
+ // #given - boulder state with complete plan
662
+ const planPath = join(TEST_DIR, "complete-plan.md")
663
+ writeFileSync(planPath, "# Plan\n- [x] Task 1\n- [x] Task 2")
664
+
665
+ const state: BoulderState = {
666
+ active_plan: planPath,
667
+ started_at: "2026-01-02T10:00:00Z",
668
+ session_ids: [MAIN_SESSION_ID],
669
+ plan_name: "complete-plan",
670
+ }
671
+ writeBoulderState(TEST_DIR, state)
672
+
673
+ const mockInput = createMockPluginInput()
674
+ const hook = createAtlasHook(mockInput)
675
+
676
+ // #when
677
+ await hook.handler({
678
+ event: {
679
+ type: "session.idle",
680
+ properties: { sessionID: MAIN_SESSION_ID },
681
+ },
682
+ })
683
+
684
+ // #then - should not call prompt
685
+ expect(mockInput._promptMock).not.toHaveBeenCalled()
686
+ })
687
+
688
+ test("should skip when abort error occurred before idle", async () => {
689
+ // #given - boulder state with incomplete plan
690
+ const planPath = join(TEST_DIR, "test-plan.md")
691
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
692
+
693
+ const state: BoulderState = {
694
+ active_plan: planPath,
695
+ started_at: "2026-01-02T10:00:00Z",
696
+ session_ids: [MAIN_SESSION_ID],
697
+ plan_name: "test-plan",
698
+ }
699
+ writeBoulderState(TEST_DIR, state)
700
+
701
+ const mockInput = createMockPluginInput()
702
+ const hook = createAtlasHook(mockInput)
703
+
704
+ // #when - send abort error then idle
705
+ await hook.handler({
706
+ event: {
707
+ type: "session.error",
708
+ properties: {
709
+ sessionID: MAIN_SESSION_ID,
710
+ error: { name: "AbortError", message: "aborted" },
711
+ },
712
+ },
713
+ })
714
+ await hook.handler({
715
+ event: {
716
+ type: "session.idle",
717
+ properties: { sessionID: MAIN_SESSION_ID },
718
+ },
719
+ })
720
+
721
+ // #then - should not call prompt
722
+ expect(mockInput._promptMock).not.toHaveBeenCalled()
723
+ })
724
+
725
+ test("should skip when background tasks are running", async () => {
726
+ // #given - boulder state with incomplete plan
727
+ const planPath = join(TEST_DIR, "test-plan.md")
728
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
729
+
730
+ const state: BoulderState = {
731
+ active_plan: planPath,
732
+ started_at: "2026-01-02T10:00:00Z",
733
+ session_ids: [MAIN_SESSION_ID],
734
+ plan_name: "test-plan",
735
+ }
736
+ writeBoulderState(TEST_DIR, state)
737
+
738
+ const mockBackgroundManager = {
739
+ getTasksByParentSession: () => [{ status: "running" }],
740
+ }
741
+
742
+ const mockInput = createMockPluginInput()
743
+ const hook = createAtlasHook(mockInput, {
744
+ directory: TEST_DIR,
745
+ backgroundManager: mockBackgroundManager as any,
746
+ })
747
+
748
+ // #when
749
+ await hook.handler({
750
+ event: {
751
+ type: "session.idle",
752
+ properties: { sessionID: MAIN_SESSION_ID },
753
+ },
754
+ })
755
+
756
+ // #then - should not call prompt
757
+ expect(mockInput._promptMock).not.toHaveBeenCalled()
758
+ })
759
+
760
+ test("should clear abort state on message.updated", async () => {
761
+ // #given - boulder with incomplete plan
762
+ const planPath = join(TEST_DIR, "test-plan.md")
763
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
764
+
765
+ const state: BoulderState = {
766
+ active_plan: planPath,
767
+ started_at: "2026-01-02T10:00:00Z",
768
+ session_ids: [MAIN_SESSION_ID],
769
+ plan_name: "test-plan",
770
+ }
771
+ writeBoulderState(TEST_DIR, state)
772
+
773
+ const mockInput = createMockPluginInput()
774
+ const hook = createAtlasHook(mockInput)
775
+
776
+ // #when - abort error, then message update, then idle
777
+ await hook.handler({
778
+ event: {
779
+ type: "session.error",
780
+ properties: {
781
+ sessionID: MAIN_SESSION_ID,
782
+ error: { name: "AbortError" },
783
+ },
784
+ },
785
+ })
786
+ await hook.handler({
787
+ event: {
788
+ type: "message.updated",
789
+ properties: { info: { sessionID: MAIN_SESSION_ID, role: "user" } },
790
+ },
791
+ })
792
+ await hook.handler({
793
+ event: {
794
+ type: "session.idle",
795
+ properties: { sessionID: MAIN_SESSION_ID },
796
+ },
797
+ })
798
+
799
+ // #then - should call prompt because abort state was cleared
800
+ expect(mockInput._promptMock).toHaveBeenCalled()
801
+ })
802
+
803
+ test("should include plan progress in continuation prompt", async () => {
804
+ // #given - boulder state with specific progress
805
+ const planPath = join(TEST_DIR, "progress-plan.md")
806
+ writeFileSync(planPath, "# Plan\n- [x] Task 1\n- [x] Task 2\n- [ ] Task 3\n- [ ] Task 4")
807
+
808
+ const state: BoulderState = {
809
+ active_plan: planPath,
810
+ started_at: "2026-01-02T10:00:00Z",
811
+ session_ids: [MAIN_SESSION_ID],
812
+ plan_name: "progress-plan",
813
+ }
814
+ writeBoulderState(TEST_DIR, state)
815
+
816
+ const mockInput = createMockPluginInput()
817
+ const hook = createAtlasHook(mockInput)
818
+
819
+ // #when
820
+ await hook.handler({
821
+ event: {
822
+ type: "session.idle",
823
+ properties: { sessionID: MAIN_SESSION_ID },
824
+ },
825
+ })
826
+
827
+ // #then - should include progress
828
+ const callArgs = mockInput._promptMock.mock.calls[0][0]
829
+ expect(callArgs.body.parts[0].text).toContain("2/4 completed")
830
+ expect(callArgs.body.parts[0].text).toContain("2 remaining")
831
+ })
832
+
833
+ test("should not inject when last agent is not Atlas", async () => {
834
+ // #given - boulder state with incomplete plan, but last agent is NOT Atlas
835
+ const planPath = join(TEST_DIR, "test-plan.md")
836
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
837
+
838
+ const state: BoulderState = {
839
+ active_plan: planPath,
840
+ started_at: "2026-01-02T10:00:00Z",
841
+ session_ids: [MAIN_SESSION_ID],
842
+ plan_name: "test-plan",
843
+ }
844
+ writeBoulderState(TEST_DIR, state)
845
+
846
+ // #given - last agent is NOT Atlas
847
+ cleanupMessageStorage(MAIN_SESSION_ID)
848
+ setupMessageStorage(MAIN_SESSION_ID, "Sisyphus")
849
+
850
+ const mockInput = createMockPluginInput()
851
+ const hook = createAtlasHook(mockInput)
852
+
853
+ // #when
854
+ await hook.handler({
855
+ event: {
856
+ type: "session.idle",
857
+ properties: { sessionID: MAIN_SESSION_ID },
858
+ },
859
+ })
860
+
861
+ // #then - should NOT call prompt because agent is not Atlas
862
+ expect(mockInput._promptMock).not.toHaveBeenCalled()
863
+ })
864
+
865
+ test("should debounce rapid continuation injections (prevent infinite loop)", async () => {
866
+ // #given - boulder state with incomplete plan
867
+ const planPath = join(TEST_DIR, "test-plan.md")
868
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1\n- [ ] Task 2")
869
+
870
+ const state: BoulderState = {
871
+ active_plan: planPath,
872
+ started_at: "2026-01-02T10:00:00Z",
873
+ session_ids: [MAIN_SESSION_ID],
874
+ plan_name: "test-plan",
875
+ }
876
+ writeBoulderState(TEST_DIR, state)
877
+
878
+ const mockInput = createMockPluginInput()
879
+ const hook = createAtlasHook(mockInput)
880
+
881
+ // #when - fire multiple idle events in rapid succession (simulating infinite loop bug)
882
+ await hook.handler({
883
+ event: {
884
+ type: "session.idle",
885
+ properties: { sessionID: MAIN_SESSION_ID },
886
+ },
887
+ })
888
+ await hook.handler({
889
+ event: {
890
+ type: "session.idle",
891
+ properties: { sessionID: MAIN_SESSION_ID },
892
+ },
893
+ })
894
+ await hook.handler({
895
+ event: {
896
+ type: "session.idle",
897
+ properties: { sessionID: MAIN_SESSION_ID },
898
+ },
899
+ })
900
+
901
+ // #then - should only call prompt ONCE due to debouncing
902
+ expect(mockInput._promptMock).toHaveBeenCalledTimes(1)
903
+ })
904
+
905
+ test("should cleanup on session.deleted", async () => {
906
+ // #given - boulder state
907
+ const planPath = join(TEST_DIR, "test-plan.md")
908
+ writeFileSync(planPath, "# Plan\n- [ ] Task 1")
909
+
910
+ const state: BoulderState = {
911
+ active_plan: planPath,
912
+ started_at: "2026-01-02T10:00:00Z",
913
+ session_ids: [MAIN_SESSION_ID],
914
+ plan_name: "test-plan",
915
+ }
916
+ writeBoulderState(TEST_DIR, state)
917
+
918
+ const mockInput = createMockPluginInput()
919
+ const hook = createAtlasHook(mockInput)
920
+
921
+ // #when - create abort state then delete
922
+ await hook.handler({
923
+ event: {
924
+ type: "session.error",
925
+ properties: {
926
+ sessionID: MAIN_SESSION_ID,
927
+ error: { name: "AbortError" },
928
+ },
929
+ },
930
+ })
931
+ await hook.handler({
932
+ event: {
933
+ type: "session.deleted",
934
+ properties: { info: { id: MAIN_SESSION_ID } },
935
+ },
936
+ })
937
+
938
+ // Re-create boulder after deletion
939
+ writeBoulderState(TEST_DIR, state)
940
+
941
+ // Trigger idle - should inject because state was cleaned up
942
+ await hook.handler({
943
+ event: {
944
+ type: "session.idle",
945
+ properties: { sessionID: MAIN_SESSION_ID },
946
+ },
947
+ })
948
+
949
+ // #then - should call prompt because session state was cleaned
950
+ expect(mockInput._promptMock).toHaveBeenCalled()
951
+ })
952
+ })
953
+ })