opencode-repos 0.1.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 (477) hide show
  1. package/AGENTS.md +180 -0
  2. package/README.md +103 -3
  3. package/TODO.md +3 -0
  4. package/bun.lock +0 -85
  5. package/index.ts +1585 -153
  6. package/oh-my-opencode/.github/FUNDING.yml +15 -0
  7. package/oh-my-opencode/.github/ISSUE_TEMPLATE/bug_report.yml +129 -0
  8. package/oh-my-opencode/.github/ISSUE_TEMPLATE/config.yml +8 -0
  9. package/oh-my-opencode/.github/ISSUE_TEMPLATE/feature_request.yml +100 -0
  10. package/oh-my-opencode/.github/ISSUE_TEMPLATE/general.yml +83 -0
  11. package/oh-my-opencode/.github/assets/google.jpg +0 -0
  12. package/oh-my-opencode/.github/assets/hero.jpg +0 -0
  13. package/oh-my-opencode/.github/assets/indent.jpg +0 -0
  14. package/oh-my-opencode/.github/assets/microsoft.jpg +0 -0
  15. package/oh-my-opencode/.github/assets/omo.png +0 -0
  16. package/oh-my-opencode/.github/assets/orchestrator-atlas.png +0 -0
  17. package/oh-my-opencode/.github/assets/sisyphus.png +0 -0
  18. package/oh-my-opencode/.github/assets/sisyphuslabs.png +0 -0
  19. package/oh-my-opencode/.github/pull_request_template.md +34 -0
  20. package/oh-my-opencode/.github/workflows/ci.yml +138 -0
  21. package/oh-my-opencode/.github/workflows/cla.yml +41 -0
  22. package/oh-my-opencode/.github/workflows/lint-workflows.yml +22 -0
  23. package/oh-my-opencode/.github/workflows/publish.yml +165 -0
  24. package/oh-my-opencode/.github/workflows/sisyphus-agent.yml +500 -0
  25. package/oh-my-opencode/.opencode/background-tasks.json +27 -0
  26. package/oh-my-opencode/.opencode/command/get-unpublished-changes.md +84 -0
  27. package/oh-my-opencode/.opencode/command/omomomo.md +37 -0
  28. package/oh-my-opencode/.opencode/command/publish.md +257 -0
  29. package/oh-my-opencode/AGENTS.md +179 -0
  30. package/oh-my-opencode/CLA.md +58 -0
  31. package/oh-my-opencode/CONTRIBUTING.md +268 -0
  32. package/oh-my-opencode/LICENSE.md +82 -0
  33. package/oh-my-opencode/README.ja.md +370 -0
  34. package/oh-my-opencode/README.md +376 -0
  35. package/oh-my-opencode/README.zh-cn.md +380 -0
  36. package/oh-my-opencode/assets/oh-my-opencode.schema.json +2171 -0
  37. package/oh-my-opencode/bin/oh-my-opencode.js +80 -0
  38. package/oh-my-opencode/bin/platform.js +38 -0
  39. package/oh-my-opencode/bin/platform.test.ts +148 -0
  40. package/oh-my-opencode/bun.lock +314 -0
  41. package/oh-my-opencode/bunfig.toml +2 -0
  42. package/oh-my-opencode/docs/category-skill-guide.md +200 -0
  43. package/oh-my-opencode/docs/cli-guide.md +272 -0
  44. package/oh-my-opencode/docs/configurations.md +654 -0
  45. package/oh-my-opencode/docs/features.md +550 -0
  46. package/oh-my-opencode/docs/guide/installation.md +288 -0
  47. package/oh-my-opencode/docs/guide/overview.md +97 -0
  48. package/oh-my-opencode/docs/guide/understanding-orchestration-system.md +445 -0
  49. package/oh-my-opencode/docs/orchestration-guide.md +152 -0
  50. package/oh-my-opencode/docs/ultrawork-manifesto.md +197 -0
  51. package/oh-my-opencode/package.json +89 -0
  52. package/oh-my-opencode/packages/darwin-arm64/bin/.gitkeep +0 -0
  53. package/oh-my-opencode/packages/darwin-arm64/package.json +22 -0
  54. package/oh-my-opencode/packages/darwin-x64/bin/.gitkeep +0 -0
  55. package/oh-my-opencode/packages/darwin-x64/package.json +22 -0
  56. package/oh-my-opencode/packages/linux-arm64/bin/.gitkeep +0 -0
  57. package/oh-my-opencode/packages/linux-arm64/package.json +25 -0
  58. package/oh-my-opencode/packages/linux-arm64-musl/bin/.gitkeep +0 -0
  59. package/oh-my-opencode/packages/linux-arm64-musl/package.json +25 -0
  60. package/oh-my-opencode/packages/linux-x64/bin/.gitkeep +0 -0
  61. package/oh-my-opencode/packages/linux-x64/package.json +25 -0
  62. package/oh-my-opencode/packages/linux-x64-musl/bin/.gitkeep +0 -0
  63. package/oh-my-opencode/packages/linux-x64-musl/package.json +25 -0
  64. package/oh-my-opencode/packages/windows-x64/bin/.gitkeep +0 -0
  65. package/oh-my-opencode/packages/windows-x64/package.json +22 -0
  66. package/oh-my-opencode/postinstall.mjs +43 -0
  67. package/oh-my-opencode/script/build-binaries.ts +103 -0
  68. package/oh-my-opencode/script/build-schema.ts +28 -0
  69. package/oh-my-opencode/script/generate-changelog.ts +92 -0
  70. package/oh-my-opencode/script/publish.ts +344 -0
  71. package/oh-my-opencode/signatures/cla.json +676 -0
  72. package/oh-my-opencode/src/agents/AGENTS.md +67 -0
  73. package/oh-my-opencode/src/agents/atlas.ts +1383 -0
  74. package/oh-my-opencode/src/agents/dynamic-agent-prompt-builder.ts +400 -0
  75. package/oh-my-opencode/src/agents/explore.ts +122 -0
  76. package/oh-my-opencode/src/agents/index.ts +13 -0
  77. package/oh-my-opencode/src/agents/librarian.ts +326 -0
  78. package/oh-my-opencode/src/agents/metis.ts +315 -0
  79. package/oh-my-opencode/src/agents/momus.test.ts +57 -0
  80. package/oh-my-opencode/src/agents/momus.ts +444 -0
  81. package/oh-my-opencode/src/agents/multimodal-looker.ts +56 -0
  82. package/oh-my-opencode/src/agents/oracle.ts +122 -0
  83. package/oh-my-opencode/src/agents/prometheus-prompt.test.ts +22 -0
  84. package/oh-my-opencode/src/agents/prometheus-prompt.ts +1196 -0
  85. package/oh-my-opencode/src/agents/sisyphus-junior.test.ts +232 -0
  86. package/oh-my-opencode/src/agents/sisyphus-junior.ts +134 -0
  87. package/oh-my-opencode/src/agents/sisyphus.ts +633 -0
  88. package/oh-my-opencode/src/agents/types.ts +80 -0
  89. package/oh-my-opencode/src/agents/utils.test.ts +311 -0
  90. package/oh-my-opencode/src/agents/utils.ts +240 -0
  91. package/oh-my-opencode/src/cli/AGENTS.md +91 -0
  92. package/oh-my-opencode/src/cli/config-manager.test.ts +364 -0
  93. package/oh-my-opencode/src/cli/config-manager.ts +641 -0
  94. package/oh-my-opencode/src/cli/doctor/checks/auth.test.ts +114 -0
  95. package/oh-my-opencode/src/cli/doctor/checks/auth.ts +115 -0
  96. package/oh-my-opencode/src/cli/doctor/checks/config.test.ts +103 -0
  97. package/oh-my-opencode/src/cli/doctor/checks/config.ts +123 -0
  98. package/oh-my-opencode/src/cli/doctor/checks/dependencies.test.ts +152 -0
  99. package/oh-my-opencode/src/cli/doctor/checks/dependencies.ts +163 -0
  100. package/oh-my-opencode/src/cli/doctor/checks/gh.test.ts +151 -0
  101. package/oh-my-opencode/src/cli/doctor/checks/gh.ts +171 -0
  102. package/oh-my-opencode/src/cli/doctor/checks/index.ts +34 -0
  103. package/oh-my-opencode/src/cli/doctor/checks/lsp.test.ts +134 -0
  104. package/oh-my-opencode/src/cli/doctor/checks/lsp.ts +77 -0
  105. package/oh-my-opencode/src/cli/doctor/checks/mcp.test.ts +115 -0
  106. package/oh-my-opencode/src/cli/doctor/checks/mcp.ts +128 -0
  107. package/oh-my-opencode/src/cli/doctor/checks/opencode.test.ts +227 -0
  108. package/oh-my-opencode/src/cli/doctor/checks/opencode.ts +178 -0
  109. package/oh-my-opencode/src/cli/doctor/checks/plugin.test.ts +109 -0
  110. package/oh-my-opencode/src/cli/doctor/checks/plugin.ts +124 -0
  111. package/oh-my-opencode/src/cli/doctor/checks/version.test.ts +148 -0
  112. package/oh-my-opencode/src/cli/doctor/checks/version.ts +135 -0
  113. package/oh-my-opencode/src/cli/doctor/constants.ts +72 -0
  114. package/oh-my-opencode/src/cli/doctor/formatter.test.ts +218 -0
  115. package/oh-my-opencode/src/cli/doctor/formatter.ts +140 -0
  116. package/oh-my-opencode/src/cli/doctor/index.ts +11 -0
  117. package/oh-my-opencode/src/cli/doctor/runner.test.ts +153 -0
  118. package/oh-my-opencode/src/cli/doctor/runner.ts +132 -0
  119. package/oh-my-opencode/src/cli/doctor/types.ts +113 -0
  120. package/oh-my-opencode/src/cli/get-local-version/formatter.ts +66 -0
  121. package/oh-my-opencode/src/cli/get-local-version/index.ts +106 -0
  122. package/oh-my-opencode/src/cli/get-local-version/types.ts +14 -0
  123. package/oh-my-opencode/src/cli/index.ts +153 -0
  124. package/oh-my-opencode/src/cli/install.ts +523 -0
  125. package/oh-my-opencode/src/cli/model-fallback.ts +246 -0
  126. package/oh-my-opencode/src/cli/run/completion.test.ts +170 -0
  127. package/oh-my-opencode/src/cli/run/completion.ts +79 -0
  128. package/oh-my-opencode/src/cli/run/events.test.ts +155 -0
  129. package/oh-my-opencode/src/cli/run/events.ts +325 -0
  130. package/oh-my-opencode/src/cli/run/index.ts +2 -0
  131. package/oh-my-opencode/src/cli/run/runner.ts +159 -0
  132. package/oh-my-opencode/src/cli/run/types.ts +76 -0
  133. package/oh-my-opencode/src/cli/types.ts +40 -0
  134. package/oh-my-opencode/src/config/index.ts +26 -0
  135. package/oh-my-opencode/src/config/schema.test.ts +444 -0
  136. package/oh-my-opencode/src/config/schema.ts +339 -0
  137. package/oh-my-opencode/src/features/AGENTS.md +77 -0
  138. package/oh-my-opencode/src/features/background-agent/concurrency.test.ts +418 -0
  139. package/oh-my-opencode/src/features/background-agent/concurrency.ts +137 -0
  140. package/oh-my-opencode/src/features/background-agent/index.ts +3 -0
  141. package/oh-my-opencode/src/features/background-agent/manager.test.ts +1928 -0
  142. package/oh-my-opencode/src/features/background-agent/manager.ts +1335 -0
  143. package/oh-my-opencode/src/features/background-agent/types.ts +66 -0
  144. package/oh-my-opencode/src/features/boulder-state/constants.ts +13 -0
  145. package/oh-my-opencode/src/features/boulder-state/index.ts +3 -0
  146. package/oh-my-opencode/src/features/boulder-state/storage.test.ts +250 -0
  147. package/oh-my-opencode/src/features/boulder-state/storage.ts +150 -0
  148. package/oh-my-opencode/src/features/boulder-state/types.ts +26 -0
  149. package/oh-my-opencode/src/features/builtin-commands/commands.ts +89 -0
  150. package/oh-my-opencode/src/features/builtin-commands/index.ts +2 -0
  151. package/oh-my-opencode/src/features/builtin-commands/templates/init-deep.ts +300 -0
  152. package/oh-my-opencode/src/features/builtin-commands/templates/ralph-loop.ts +38 -0
  153. package/oh-my-opencode/src/features/builtin-commands/templates/refactor.ts +619 -0
  154. package/oh-my-opencode/src/features/builtin-commands/templates/start-work.ts +72 -0
  155. package/oh-my-opencode/src/features/builtin-commands/types.ts +9 -0
  156. package/oh-my-opencode/src/features/builtin-skills/frontend-ui-ux/SKILL.md +78 -0
  157. package/oh-my-opencode/src/features/builtin-skills/git-master/SKILL.md +1105 -0
  158. package/oh-my-opencode/src/features/builtin-skills/index.ts +2 -0
  159. package/oh-my-opencode/src/features/builtin-skills/skills.ts +1203 -0
  160. package/oh-my-opencode/src/features/builtin-skills/types.ts +16 -0
  161. package/oh-my-opencode/src/features/claude-code-agent-loader/index.ts +2 -0
  162. package/oh-my-opencode/src/features/claude-code-agent-loader/loader.ts +90 -0
  163. package/oh-my-opencode/src/features/claude-code-agent-loader/types.ts +17 -0
  164. package/oh-my-opencode/src/features/claude-code-command-loader/index.ts +2 -0
  165. package/oh-my-opencode/src/features/claude-code-command-loader/loader.ts +144 -0
  166. package/oh-my-opencode/src/features/claude-code-command-loader/types.ts +46 -0
  167. package/oh-my-opencode/src/features/claude-code-mcp-loader/env-expander.ts +27 -0
  168. package/oh-my-opencode/src/features/claude-code-mcp-loader/index.ts +11 -0
  169. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.test.ts +162 -0
  170. package/oh-my-opencode/src/features/claude-code-mcp-loader/loader.ts +113 -0
  171. package/oh-my-opencode/src/features/claude-code-mcp-loader/transformer.ts +53 -0
  172. package/oh-my-opencode/src/features/claude-code-mcp-loader/types.ts +42 -0
  173. package/oh-my-opencode/src/features/claude-code-plugin-loader/index.ts +3 -0
  174. package/oh-my-opencode/src/features/claude-code-plugin-loader/loader.ts +486 -0
  175. package/oh-my-opencode/src/features/claude-code-plugin-loader/types.ts +210 -0
  176. package/oh-my-opencode/src/features/claude-code-session-state/index.ts +1 -0
  177. package/oh-my-opencode/src/features/claude-code-session-state/state.test.ts +126 -0
  178. package/oh-my-opencode/src/features/claude-code-session-state/state.ts +37 -0
  179. package/oh-my-opencode/src/features/context-injector/collector.test.ts +330 -0
  180. package/oh-my-opencode/src/features/context-injector/collector.ts +85 -0
  181. package/oh-my-opencode/src/features/context-injector/index.ts +14 -0
  182. package/oh-my-opencode/src/features/context-injector/injector.test.ts +122 -0
  183. package/oh-my-opencode/src/features/context-injector/injector.ts +167 -0
  184. package/oh-my-opencode/src/features/context-injector/types.ts +91 -0
  185. package/oh-my-opencode/src/features/hook-message-injector/constants.ts +6 -0
  186. package/oh-my-opencode/src/features/hook-message-injector/index.ts +4 -0
  187. package/oh-my-opencode/src/features/hook-message-injector/injector.ts +195 -0
  188. package/oh-my-opencode/src/features/hook-message-injector/types.ts +47 -0
  189. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.test.ts +448 -0
  190. package/oh-my-opencode/src/features/opencode-skill-loader/async-loader.ts +180 -0
  191. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.test.ts +210 -0
  192. package/oh-my-opencode/src/features/opencode-skill-loader/blocking.ts +62 -0
  193. package/oh-my-opencode/src/features/opencode-skill-loader/discover-worker.ts +59 -0
  194. package/oh-my-opencode/src/features/opencode-skill-loader/index.ts +4 -0
  195. package/oh-my-opencode/src/features/opencode-skill-loader/loader.test.ts +273 -0
  196. package/oh-my-opencode/src/features/opencode-skill-loader/loader.ts +259 -0
  197. package/oh-my-opencode/src/features/opencode-skill-loader/merger.ts +267 -0
  198. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.test.ts +267 -0
  199. package/oh-my-opencode/src/features/opencode-skill-loader/skill-content.ts +206 -0
  200. package/oh-my-opencode/src/features/opencode-skill-loader/types.ts +38 -0
  201. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.test.ts +201 -0
  202. package/oh-my-opencode/src/features/skill-mcp-manager/env-cleaner.ts +27 -0
  203. package/oh-my-opencode/src/features/skill-mcp-manager/index.ts +2 -0
  204. package/oh-my-opencode/src/features/skill-mcp-manager/manager.test.ts +611 -0
  205. package/oh-my-opencode/src/features/skill-mcp-manager/manager.ts +520 -0
  206. package/oh-my-opencode/src/features/skill-mcp-manager/types.ts +14 -0
  207. package/oh-my-opencode/src/features/task-toast-manager/index.ts +2 -0
  208. package/oh-my-opencode/src/features/task-toast-manager/manager.test.ts +249 -0
  209. package/oh-my-opencode/src/features/task-toast-manager/manager.ts +215 -0
  210. package/oh-my-opencode/src/features/task-toast-manager/types.ts +24 -0
  211. package/oh-my-opencode/src/hooks/AGENTS.md +73 -0
  212. package/oh-my-opencode/src/hooks/agent-usage-reminder/constants.ts +54 -0
  213. package/oh-my-opencode/src/hooks/agent-usage-reminder/index.ts +109 -0
  214. package/oh-my-opencode/src/hooks/agent-usage-reminder/storage.ts +42 -0
  215. package/oh-my-opencode/src/hooks/agent-usage-reminder/types.ts +6 -0
  216. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.test.ts +307 -0
  217. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/executor.ts +485 -0
  218. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/index.ts +151 -0
  219. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/parser.ts +201 -0
  220. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.test.ts +33 -0
  221. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-deduplication.ts +184 -0
  222. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/pruning-types.ts +44 -0
  223. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.test.ts +77 -0
  224. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/storage.ts +250 -0
  225. package/oh-my-opencode/src/hooks/anthropic-context-window-limit-recovery/types.ts +42 -0
  226. package/oh-my-opencode/src/hooks/atlas/index.test.ts +953 -0
  227. package/oh-my-opencode/src/hooks/atlas/index.ts +771 -0
  228. package/oh-my-opencode/src/hooks/auto-slash-command/constants.ts +12 -0
  229. package/oh-my-opencode/src/hooks/auto-slash-command/detector.test.ts +296 -0
  230. package/oh-my-opencode/src/hooks/auto-slash-command/detector.ts +65 -0
  231. package/oh-my-opencode/src/hooks/auto-slash-command/executor.ts +205 -0
  232. package/oh-my-opencode/src/hooks/auto-slash-command/index.test.ts +254 -0
  233. package/oh-my-opencode/src/hooks/auto-slash-command/index.ts +89 -0
  234. package/oh-my-opencode/src/hooks/auto-slash-command/types.ts +23 -0
  235. package/oh-my-opencode/src/hooks/auto-update-checker/cache.ts +93 -0
  236. package/oh-my-opencode/src/hooks/auto-update-checker/checker.test.ts +24 -0
  237. package/oh-my-opencode/src/hooks/auto-update-checker/checker.ts +284 -0
  238. package/oh-my-opencode/src/hooks/auto-update-checker/constants.ts +64 -0
  239. package/oh-my-opencode/src/hooks/auto-update-checker/index.test.ts +254 -0
  240. package/oh-my-opencode/src/hooks/auto-update-checker/index.ts +260 -0
  241. package/oh-my-opencode/src/hooks/auto-update-checker/types.ts +29 -0
  242. package/oh-my-opencode/src/hooks/background-compaction/index.ts +87 -0
  243. package/oh-my-opencode/src/hooks/background-notification/index.ts +28 -0
  244. package/oh-my-opencode/src/hooks/background-notification/types.ts +5 -0
  245. package/oh-my-opencode/src/hooks/claude-code-hooks/AGENTS.md +70 -0
  246. package/oh-my-opencode/src/hooks/claude-code-hooks/config-loader.ts +107 -0
  247. package/oh-my-opencode/src/hooks/claude-code-hooks/config.ts +103 -0
  248. package/oh-my-opencode/src/hooks/claude-code-hooks/index.ts +401 -0
  249. package/oh-my-opencode/src/hooks/claude-code-hooks/plugin-config.ts +12 -0
  250. package/oh-my-opencode/src/hooks/claude-code-hooks/post-tool-use.ts +199 -0
  251. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-compact.ts +109 -0
  252. package/oh-my-opencode/src/hooks/claude-code-hooks/pre-tool-use.ts +172 -0
  253. package/oh-my-opencode/src/hooks/claude-code-hooks/stop.ts +118 -0
  254. package/oh-my-opencode/src/hooks/claude-code-hooks/todo.ts +76 -0
  255. package/oh-my-opencode/src/hooks/claude-code-hooks/tool-input-cache.ts +47 -0
  256. package/oh-my-opencode/src/hooks/claude-code-hooks/transcript.ts +252 -0
  257. package/oh-my-opencode/src/hooks/claude-code-hooks/types.ts +204 -0
  258. package/oh-my-opencode/src/hooks/claude-code-hooks/user-prompt-submit.ts +117 -0
  259. package/oh-my-opencode/src/hooks/comment-checker/cli.test.ts +68 -0
  260. package/oh-my-opencode/src/hooks/comment-checker/cli.ts +221 -0
  261. package/oh-my-opencode/src/hooks/comment-checker/downloader.ts +196 -0
  262. package/oh-my-opencode/src/hooks/comment-checker/index.ts +171 -0
  263. package/oh-my-opencode/src/hooks/comment-checker/types.ts +33 -0
  264. package/oh-my-opencode/src/hooks/compaction-context-injector/index.ts +61 -0
  265. package/oh-my-opencode/src/hooks/context-window-monitor.ts +99 -0
  266. package/oh-my-opencode/src/hooks/delegate-task-retry/index.test.ts +119 -0
  267. package/oh-my-opencode/src/hooks/delegate-task-retry/index.ts +136 -0
  268. package/oh-my-opencode/src/hooks/directory-agents-injector/constants.ts +9 -0
  269. package/oh-my-opencode/src/hooks/directory-agents-injector/index.ts +182 -0
  270. package/oh-my-opencode/src/hooks/directory-agents-injector/storage.ts +48 -0
  271. package/oh-my-opencode/src/hooks/directory-agents-injector/types.ts +5 -0
  272. package/oh-my-opencode/src/hooks/directory-readme-injector/constants.ts +9 -0
  273. package/oh-my-opencode/src/hooks/directory-readme-injector/index.ts +177 -0
  274. package/oh-my-opencode/src/hooks/directory-readme-injector/storage.ts +48 -0
  275. package/oh-my-opencode/src/hooks/directory-readme-injector/types.ts +5 -0
  276. package/oh-my-opencode/src/hooks/edit-error-recovery/index.test.ts +126 -0
  277. package/oh-my-opencode/src/hooks/edit-error-recovery/index.ts +57 -0
  278. package/oh-my-opencode/src/hooks/empty-task-response-detector.ts +27 -0
  279. package/oh-my-opencode/src/hooks/index.ts +32 -0
  280. package/oh-my-opencode/src/hooks/interactive-bash-session/constants.ts +15 -0
  281. package/oh-my-opencode/src/hooks/interactive-bash-session/index.ts +262 -0
  282. package/oh-my-opencode/src/hooks/interactive-bash-session/storage.ts +59 -0
  283. package/oh-my-opencode/src/hooks/interactive-bash-session/types.ts +11 -0
  284. package/oh-my-opencode/src/hooks/keyword-detector/constants.ts +300 -0
  285. package/oh-my-opencode/src/hooks/keyword-detector/detector.ts +52 -0
  286. package/oh-my-opencode/src/hooks/keyword-detector/index.test.ts +529 -0
  287. package/oh-my-opencode/src/hooks/keyword-detector/index.ts +100 -0
  288. package/oh-my-opencode/src/hooks/keyword-detector/types.ts +4 -0
  289. package/oh-my-opencode/src/hooks/non-interactive-env/constants.ts +70 -0
  290. package/oh-my-opencode/src/hooks/non-interactive-env/detector.ts +19 -0
  291. package/oh-my-opencode/src/hooks/non-interactive-env/index.test.ts +323 -0
  292. package/oh-my-opencode/src/hooks/non-interactive-env/index.ts +63 -0
  293. package/oh-my-opencode/src/hooks/non-interactive-env/types.ts +3 -0
  294. package/oh-my-opencode/src/hooks/prometheus-md-only/constants.ts +32 -0
  295. package/oh-my-opencode/src/hooks/prometheus-md-only/index.test.ts +488 -0
  296. package/oh-my-opencode/src/hooks/prometheus-md-only/index.ts +136 -0
  297. package/oh-my-opencode/src/hooks/ralph-loop/constants.ts +5 -0
  298. package/oh-my-opencode/src/hooks/ralph-loop/index.test.ts +835 -0
  299. package/oh-my-opencode/src/hooks/ralph-loop/index.ts +417 -0
  300. package/oh-my-opencode/src/hooks/ralph-loop/storage.ts +115 -0
  301. package/oh-my-opencode/src/hooks/ralph-loop/types.ts +19 -0
  302. package/oh-my-opencode/src/hooks/rules-injector/constants.ts +30 -0
  303. package/oh-my-opencode/src/hooks/rules-injector/finder.test.ts +381 -0
  304. package/oh-my-opencode/src/hooks/rules-injector/finder.ts +263 -0
  305. package/oh-my-opencode/src/hooks/rules-injector/index.ts +223 -0
  306. package/oh-my-opencode/src/hooks/rules-injector/matcher.ts +63 -0
  307. package/oh-my-opencode/src/hooks/rules-injector/parser.test.ts +226 -0
  308. package/oh-my-opencode/src/hooks/rules-injector/parser.ts +211 -0
  309. package/oh-my-opencode/src/hooks/rules-injector/storage.ts +59 -0
  310. package/oh-my-opencode/src/hooks/rules-injector/types.ts +57 -0
  311. package/oh-my-opencode/src/hooks/session-notification-utils.ts +140 -0
  312. package/oh-my-opencode/src/hooks/session-notification.test.ts +361 -0
  313. package/oh-my-opencode/src/hooks/session-notification.ts +330 -0
  314. package/oh-my-opencode/src/hooks/session-recovery/constants.ts +10 -0
  315. package/oh-my-opencode/src/hooks/session-recovery/index.test.ts +223 -0
  316. package/oh-my-opencode/src/hooks/session-recovery/index.ts +435 -0
  317. package/oh-my-opencode/src/hooks/session-recovery/storage.ts +390 -0
  318. package/oh-my-opencode/src/hooks/session-recovery/types.ts +98 -0
  319. package/oh-my-opencode/src/hooks/start-work/index.test.ts +402 -0
  320. package/oh-my-opencode/src/hooks/start-work/index.ts +242 -0
  321. package/oh-my-opencode/src/hooks/task-resume-info/index.ts +36 -0
  322. package/oh-my-opencode/src/hooks/think-mode/detector.ts +57 -0
  323. package/oh-my-opencode/src/hooks/think-mode/index.test.ts +353 -0
  324. package/oh-my-opencode/src/hooks/think-mode/index.ts +89 -0
  325. package/oh-my-opencode/src/hooks/think-mode/switcher.test.ts +461 -0
  326. package/oh-my-opencode/src/hooks/think-mode/switcher.ts +222 -0
  327. package/oh-my-opencode/src/hooks/think-mode/types.ts +21 -0
  328. package/oh-my-opencode/src/hooks/thinking-block-validator/index.ts +171 -0
  329. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.test.ts +876 -0
  330. package/oh-my-opencode/src/hooks/todo-continuation-enforcer.ts +480 -0
  331. package/oh-my-opencode/src/hooks/tool-output-truncator.test.ts +168 -0
  332. package/oh-my-opencode/src/hooks/tool-output-truncator.ts +61 -0
  333. package/oh-my-opencode/src/index.ts +589 -0
  334. package/oh-my-opencode/src/mcp/AGENTS.md +70 -0
  335. package/oh-my-opencode/src/mcp/context7.ts +6 -0
  336. package/oh-my-opencode/src/mcp/grep-app.ts +6 -0
  337. package/oh-my-opencode/src/mcp/index.test.ts +86 -0
  338. package/oh-my-opencode/src/mcp/index.ts +32 -0
  339. package/oh-my-opencode/src/mcp/types.ts +9 -0
  340. package/oh-my-opencode/src/mcp/websearch.ts +10 -0
  341. package/oh-my-opencode/src/plugin-config.test.ts +119 -0
  342. package/oh-my-opencode/src/plugin-config.ts +135 -0
  343. package/oh-my-opencode/src/plugin-handlers/config-handler.test.ts +103 -0
  344. package/oh-my-opencode/src/plugin-handlers/config-handler.ts +399 -0
  345. package/oh-my-opencode/src/plugin-handlers/index.ts +1 -0
  346. package/oh-my-opencode/src/plugin-state.ts +30 -0
  347. package/oh-my-opencode/src/shared/AGENTS.md +63 -0
  348. package/oh-my-opencode/src/shared/agent-tool-restrictions.ts +44 -0
  349. package/oh-my-opencode/src/shared/agent-variant.test.ts +83 -0
  350. package/oh-my-opencode/src/shared/agent-variant.ts +40 -0
  351. package/oh-my-opencode/src/shared/claude-config-dir.test.ts +60 -0
  352. package/oh-my-opencode/src/shared/claude-config-dir.ts +11 -0
  353. package/oh-my-opencode/src/shared/command-executor.ts +225 -0
  354. package/oh-my-opencode/src/shared/config-errors.ts +18 -0
  355. package/oh-my-opencode/src/shared/config-path.ts +47 -0
  356. package/oh-my-opencode/src/shared/data-path.ts +22 -0
  357. package/oh-my-opencode/src/shared/deep-merge.test.ts +336 -0
  358. package/oh-my-opencode/src/shared/deep-merge.ts +53 -0
  359. package/oh-my-opencode/src/shared/dynamic-truncator.ts +193 -0
  360. package/oh-my-opencode/src/shared/external-plugin-detector.test.ts +133 -0
  361. package/oh-my-opencode/src/shared/external-plugin-detector.ts +132 -0
  362. package/oh-my-opencode/src/shared/file-reference-resolver.ts +85 -0
  363. package/oh-my-opencode/src/shared/file-utils.ts +40 -0
  364. package/oh-my-opencode/src/shared/first-message-variant.test.ts +32 -0
  365. package/oh-my-opencode/src/shared/first-message-variant.ts +28 -0
  366. package/oh-my-opencode/src/shared/frontmatter.test.ts +262 -0
  367. package/oh-my-opencode/src/shared/frontmatter.ts +31 -0
  368. package/oh-my-opencode/src/shared/hook-disabled.ts +22 -0
  369. package/oh-my-opencode/src/shared/index.ts +29 -0
  370. package/oh-my-opencode/src/shared/jsonc-parser.test.ts +266 -0
  371. package/oh-my-opencode/src/shared/jsonc-parser.ts +66 -0
  372. package/oh-my-opencode/src/shared/logger.ts +20 -0
  373. package/oh-my-opencode/src/shared/migration.test.ts +602 -0
  374. package/oh-my-opencode/src/shared/migration.ts +191 -0
  375. package/oh-my-opencode/src/shared/model-resolver.test.ts +101 -0
  376. package/oh-my-opencode/src/shared/model-resolver.ts +35 -0
  377. package/oh-my-opencode/src/shared/model-sanitizer.ts +12 -0
  378. package/oh-my-opencode/src/shared/opencode-config-dir.test.ts +318 -0
  379. package/oh-my-opencode/src/shared/opencode-config-dir.ts +142 -0
  380. package/oh-my-opencode/src/shared/opencode-version.test.ts +223 -0
  381. package/oh-my-opencode/src/shared/opencode-version.ts +72 -0
  382. package/oh-my-opencode/src/shared/pattern-matcher.ts +29 -0
  383. package/oh-my-opencode/src/shared/permission-compat.test.ts +134 -0
  384. package/oh-my-opencode/src/shared/permission-compat.ts +77 -0
  385. package/oh-my-opencode/src/shared/session-cursor.test.ts +66 -0
  386. package/oh-my-opencode/src/shared/session-cursor.ts +85 -0
  387. package/oh-my-opencode/src/shared/shell-env.test.ts +278 -0
  388. package/oh-my-opencode/src/shared/shell-env.ts +111 -0
  389. package/oh-my-opencode/src/shared/snake-case.ts +49 -0
  390. package/oh-my-opencode/src/shared/system-directive.ts +40 -0
  391. package/oh-my-opencode/src/shared/tool-name.ts +26 -0
  392. package/oh-my-opencode/src/shared/zip-extractor.ts +83 -0
  393. package/oh-my-opencode/src/tools/AGENTS.md +74 -0
  394. package/oh-my-opencode/src/tools/ast-grep/cli.ts +230 -0
  395. package/oh-my-opencode/src/tools/ast-grep/constants.ts +261 -0
  396. package/oh-my-opencode/src/tools/ast-grep/downloader.ts +128 -0
  397. package/oh-my-opencode/src/tools/ast-grep/index.ts +13 -0
  398. package/oh-my-opencode/src/tools/ast-grep/tools.ts +112 -0
  399. package/oh-my-opencode/src/tools/ast-grep/types.ts +61 -0
  400. package/oh-my-opencode/src/tools/ast-grep/utils.ts +102 -0
  401. package/oh-my-opencode/src/tools/background-task/constants.ts +7 -0
  402. package/oh-my-opencode/src/tools/background-task/index.ts +7 -0
  403. package/oh-my-opencode/src/tools/background-task/tools.ts +479 -0
  404. package/oh-my-opencode/src/tools/background-task/types.ts +16 -0
  405. package/oh-my-opencode/src/tools/call-omo-agent/constants.ts +7 -0
  406. package/oh-my-opencode/src/tools/call-omo-agent/index.ts +3 -0
  407. package/oh-my-opencode/src/tools/call-omo-agent/tools.ts +338 -0
  408. package/oh-my-opencode/src/tools/call-omo-agent/types.ts +27 -0
  409. package/oh-my-opencode/src/tools/delegate-task/constants.ts +205 -0
  410. package/oh-my-opencode/src/tools/delegate-task/index.ts +3 -0
  411. package/oh-my-opencode/src/tools/delegate-task/tools.test.ts +1575 -0
  412. package/oh-my-opencode/src/tools/delegate-task/tools.ts +885 -0
  413. package/oh-my-opencode/src/tools/delegate-task/types.ts +9 -0
  414. package/oh-my-opencode/src/tools/glob/cli.test.ts +158 -0
  415. package/oh-my-opencode/src/tools/glob/cli.ts +191 -0
  416. package/oh-my-opencode/src/tools/glob/constants.ts +12 -0
  417. package/oh-my-opencode/src/tools/glob/index.ts +3 -0
  418. package/oh-my-opencode/src/tools/glob/tools.ts +41 -0
  419. package/oh-my-opencode/src/tools/glob/types.ts +22 -0
  420. package/oh-my-opencode/src/tools/glob/utils.ts +26 -0
  421. package/oh-my-opencode/src/tools/grep/cli.ts +229 -0
  422. package/oh-my-opencode/src/tools/grep/constants.ts +127 -0
  423. package/oh-my-opencode/src/tools/grep/downloader.test.ts +103 -0
  424. package/oh-my-opencode/src/tools/grep/downloader.ts +145 -0
  425. package/oh-my-opencode/src/tools/grep/index.ts +3 -0
  426. package/oh-my-opencode/src/tools/grep/tools.ts +40 -0
  427. package/oh-my-opencode/src/tools/grep/types.ts +39 -0
  428. package/oh-my-opencode/src/tools/grep/utils.ts +53 -0
  429. package/oh-my-opencode/src/tools/index.ts +72 -0
  430. package/oh-my-opencode/src/tools/interactive-bash/constants.ts +18 -0
  431. package/oh-my-opencode/src/tools/interactive-bash/index.ts +4 -0
  432. package/oh-my-opencode/src/tools/interactive-bash/tools.ts +126 -0
  433. package/oh-my-opencode/src/tools/interactive-bash/utils.ts +71 -0
  434. package/oh-my-opencode/src/tools/look-at/constants.ts +3 -0
  435. package/oh-my-opencode/src/tools/look-at/index.ts +3 -0
  436. package/oh-my-opencode/src/tools/look-at/tools.test.ts +73 -0
  437. package/oh-my-opencode/src/tools/look-at/tools.ts +173 -0
  438. package/oh-my-opencode/src/tools/look-at/types.ts +4 -0
  439. package/oh-my-opencode/src/tools/lsp/client.ts +596 -0
  440. package/oh-my-opencode/src/tools/lsp/config.test.ts +130 -0
  441. package/oh-my-opencode/src/tools/lsp/config.ts +285 -0
  442. package/oh-my-opencode/src/tools/lsp/constants.ts +390 -0
  443. package/oh-my-opencode/src/tools/lsp/index.ts +7 -0
  444. package/oh-my-opencode/src/tools/lsp/tools.ts +261 -0
  445. package/oh-my-opencode/src/tools/lsp/types.ts +124 -0
  446. package/oh-my-opencode/src/tools/lsp/utils.ts +406 -0
  447. package/oh-my-opencode/src/tools/session-manager/constants.ts +97 -0
  448. package/oh-my-opencode/src/tools/session-manager/index.ts +3 -0
  449. package/oh-my-opencode/src/tools/session-manager/storage.test.ts +315 -0
  450. package/oh-my-opencode/src/tools/session-manager/storage.ts +238 -0
  451. package/oh-my-opencode/src/tools/session-manager/tools.test.ts +124 -0
  452. package/oh-my-opencode/src/tools/session-manager/tools.ts +146 -0
  453. package/oh-my-opencode/src/tools/session-manager/types.ts +99 -0
  454. package/oh-my-opencode/src/tools/session-manager/utils.test.ts +160 -0
  455. package/oh-my-opencode/src/tools/session-manager/utils.ts +199 -0
  456. package/oh-my-opencode/src/tools/skill/constants.ts +8 -0
  457. package/oh-my-opencode/src/tools/skill/index.ts +3 -0
  458. package/oh-my-opencode/src/tools/skill/tools.test.ts +239 -0
  459. package/oh-my-opencode/src/tools/skill/tools.ts +200 -0
  460. package/oh-my-opencode/src/tools/skill/types.ts +31 -0
  461. package/oh-my-opencode/src/tools/skill-mcp/constants.ts +3 -0
  462. package/oh-my-opencode/src/tools/skill-mcp/index.ts +3 -0
  463. package/oh-my-opencode/src/tools/skill-mcp/tools.test.ts +215 -0
  464. package/oh-my-opencode/src/tools/skill-mcp/tools.ts +172 -0
  465. package/oh-my-opencode/src/tools/skill-mcp/types.ts +8 -0
  466. package/oh-my-opencode/src/tools/slashcommand/index.ts +2 -0
  467. package/oh-my-opencode/src/tools/slashcommand/tools.ts +252 -0
  468. package/oh-my-opencode/src/tools/slashcommand/types.ts +28 -0
  469. package/oh-my-opencode/test-setup.ts +6 -0
  470. package/oh-my-opencode/tsconfig.json +20 -0
  471. package/package.json +1 -4
  472. package/src/__tests__/git.test.ts +7 -2
  473. package/src/__tests__/manifest.test.ts +5 -5
  474. package/src/agents/repo-explorer.ts +2 -1
  475. package/src/git.ts +18 -3
  476. package/src/manifest.ts +22 -15
  477. package/src/scanner.ts +41 -32
@@ -0,0 +1,480 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin"
2
+ import { existsSync, readdirSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import type { BackgroundManager } from "../features/background-agent"
5
+ import { getMainSessionID, subagentSessions } from "../features/claude-code-session-state"
6
+ import {
7
+ findNearestMessageWithFields,
8
+ MESSAGE_STORAGE,
9
+ type ToolPermission,
10
+ } from "../features/hook-message-injector"
11
+ import { log } from "../shared/logger"
12
+ import { createSystemDirective, SystemDirectiveTypes } from "../shared/system-directive"
13
+
14
+ const HOOK_NAME = "todo-continuation-enforcer"
15
+
16
+ const DEFAULT_SKIP_AGENTS = ["Prometheus (Planner)"]
17
+
18
+ export interface TodoContinuationEnforcerOptions {
19
+ backgroundManager?: BackgroundManager
20
+ skipAgents?: string[]
21
+ }
22
+
23
+ export interface TodoContinuationEnforcer {
24
+ handler: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
25
+ markRecovering: (sessionID: string) => void
26
+ markRecoveryComplete: (sessionID: string) => void
27
+ }
28
+
29
+ interface Todo {
30
+ content: string
31
+ status: string
32
+ priority: string
33
+ id: string
34
+ }
35
+
36
+ interface SessionState {
37
+ countdownTimer?: ReturnType<typeof setTimeout>
38
+ countdownInterval?: ReturnType<typeof setInterval>
39
+ isRecovering?: boolean
40
+ countdownStartedAt?: number
41
+ abortDetectedAt?: number
42
+ }
43
+
44
+ const CONTINUATION_PROMPT = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)}
45
+
46
+ Incomplete tasks remain in your todo list. Continue working on the next pending task.
47
+
48
+ - Proceed without asking for permission
49
+ - Mark each task complete when finished
50
+ - Do not stop until all tasks are done`
51
+
52
+ const COUNTDOWN_SECONDS = 2
53
+ const TOAST_DURATION_MS = 900
54
+ const COUNTDOWN_GRACE_PERIOD_MS = 500
55
+
56
+ function getMessageDir(sessionID: string): string | null {
57
+ if (!existsSync(MESSAGE_STORAGE)) return null
58
+
59
+ const directPath = join(MESSAGE_STORAGE, sessionID)
60
+ if (existsSync(directPath)) return directPath
61
+
62
+ for (const dir of readdirSync(MESSAGE_STORAGE)) {
63
+ const sessionPath = join(MESSAGE_STORAGE, dir, sessionID)
64
+ if (existsSync(sessionPath)) return sessionPath
65
+ }
66
+
67
+ return null
68
+ }
69
+
70
+ function getIncompleteCount(todos: Todo[]): number {
71
+ return todos.filter(t => t.status !== "completed" && t.status !== "cancelled").length
72
+ }
73
+
74
+ interface MessageInfo {
75
+ id?: string
76
+ role?: string
77
+ error?: { name?: string; data?: unknown }
78
+ }
79
+
80
+ function isLastAssistantMessageAborted(messages: Array<{ info?: MessageInfo }>): boolean {
81
+ if (!messages || messages.length === 0) return false
82
+
83
+ const assistantMessages = messages.filter(m => m.info?.role === "assistant")
84
+ if (assistantMessages.length === 0) return false
85
+
86
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]
87
+ const errorName = lastAssistant.info?.error?.name
88
+
89
+ if (!errorName) return false
90
+
91
+ return errorName === "MessageAbortedError" || errorName === "AbortError"
92
+ }
93
+
94
+ export function createTodoContinuationEnforcer(
95
+ ctx: PluginInput,
96
+ options: TodoContinuationEnforcerOptions = {}
97
+ ): TodoContinuationEnforcer {
98
+ const { backgroundManager, skipAgents = DEFAULT_SKIP_AGENTS } = options
99
+ const sessions = new Map<string, SessionState>()
100
+
101
+ function getState(sessionID: string): SessionState {
102
+ let state = sessions.get(sessionID)
103
+ if (!state) {
104
+ state = {}
105
+ sessions.set(sessionID, state)
106
+ }
107
+ return state
108
+ }
109
+
110
+ function cancelCountdown(sessionID: string): void {
111
+ const state = sessions.get(sessionID)
112
+ if (!state) return
113
+
114
+ if (state.countdownTimer) {
115
+ clearTimeout(state.countdownTimer)
116
+ state.countdownTimer = undefined
117
+ }
118
+ if (state.countdownInterval) {
119
+ clearInterval(state.countdownInterval)
120
+ state.countdownInterval = undefined
121
+ }
122
+ state.countdownStartedAt = undefined
123
+ }
124
+
125
+ function cleanup(sessionID: string): void {
126
+ cancelCountdown(sessionID)
127
+ sessions.delete(sessionID)
128
+ }
129
+
130
+ const markRecovering = (sessionID: string): void => {
131
+ const state = getState(sessionID)
132
+ state.isRecovering = true
133
+ cancelCountdown(sessionID)
134
+ log(`[${HOOK_NAME}] Session marked as recovering`, { sessionID })
135
+ }
136
+
137
+ const markRecoveryComplete = (sessionID: string): void => {
138
+ const state = sessions.get(sessionID)
139
+ if (state) {
140
+ state.isRecovering = false
141
+ log(`[${HOOK_NAME}] Session recovery complete`, { sessionID })
142
+ }
143
+ }
144
+
145
+ async function showCountdownToast(seconds: number, incompleteCount: number): Promise<void> {
146
+ await ctx.client.tui.showToast({
147
+ body: {
148
+ title: "Todo Continuation",
149
+ message: `Resuming in ${seconds}s... (${incompleteCount} tasks remaining)`,
150
+ variant: "warning" as const,
151
+ duration: TOAST_DURATION_MS,
152
+ },
153
+ }).catch(() => {})
154
+ }
155
+
156
+ interface ResolvedMessageInfo {
157
+ agent?: string
158
+ model?: { providerID: string; modelID: string }
159
+ tools?: Record<string, ToolPermission>
160
+ }
161
+
162
+ async function injectContinuation(
163
+ sessionID: string,
164
+ incompleteCount: number,
165
+ total: number,
166
+ resolvedInfo?: ResolvedMessageInfo
167
+ ): Promise<void> {
168
+ const state = sessions.get(sessionID)
169
+
170
+ if (state?.isRecovering) {
171
+ log(`[${HOOK_NAME}] Skipped injection: in recovery`, { sessionID })
172
+ return
173
+ }
174
+
175
+ const hasRunningBgTasks = backgroundManager
176
+ ? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
177
+ : false
178
+
179
+ if (hasRunningBgTasks) {
180
+ log(`[${HOOK_NAME}] Skipped injection: background tasks running`, { sessionID })
181
+ return
182
+ }
183
+
184
+ let todos: Todo[] = []
185
+ try {
186
+ const response = await ctx.client.session.todo({ path: { id: sessionID } })
187
+ todos = (response.data ?? response) as Todo[]
188
+ } catch (err) {
189
+ log(`[${HOOK_NAME}] Failed to fetch todos`, { sessionID, error: String(err) })
190
+ return
191
+ }
192
+
193
+ const freshIncompleteCount = getIncompleteCount(todos)
194
+ if (freshIncompleteCount === 0) {
195
+ log(`[${HOOK_NAME}] Skipped injection: no incomplete todos`, { sessionID })
196
+ return
197
+ }
198
+
199
+ let agentName = resolvedInfo?.agent
200
+ let model = resolvedInfo?.model
201
+ let tools = resolvedInfo?.tools
202
+
203
+ if (!agentName || !model) {
204
+ const messageDir = getMessageDir(sessionID)
205
+ const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null
206
+ agentName = agentName ?? prevMessage?.agent
207
+ model = model ?? (prevMessage?.model?.providerID && prevMessage?.model?.modelID
208
+ ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }
209
+ : undefined)
210
+ tools = tools ?? prevMessage?.tools
211
+ }
212
+
213
+ if (agentName && skipAgents.includes(agentName)) {
214
+ log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: agentName })
215
+ return
216
+ }
217
+
218
+ const editPermission = tools?.edit
219
+ const writePermission = tools?.write
220
+ const hasWritePermission = !tools ||
221
+ ((editPermission !== false && editPermission !== "deny") &&
222
+ (writePermission !== false && writePermission !== "deny"))
223
+ if (!hasWritePermission) {
224
+ log(`[${HOOK_NAME}] Skipped: agent lacks write permission`, { sessionID, agent: agentName })
225
+ return
226
+ }
227
+
228
+ const prompt = `${CONTINUATION_PROMPT}\n\n[Status: ${todos.length - freshIncompleteCount}/${todos.length} completed, ${freshIncompleteCount} remaining]`
229
+
230
+ try {
231
+ log(`[${HOOK_NAME}] Injecting continuation`, { sessionID, agent: agentName, model, incompleteCount: freshIncompleteCount })
232
+
233
+ await ctx.client.session.prompt({
234
+ path: { id: sessionID },
235
+ body: {
236
+ agent: agentName,
237
+ ...(model !== undefined ? { model } : {}),
238
+ parts: [{ type: "text", text: prompt }],
239
+ },
240
+ query: { directory: ctx.directory },
241
+ })
242
+
243
+ log(`[${HOOK_NAME}] Injection successful`, { sessionID })
244
+ } catch (err) {
245
+ log(`[${HOOK_NAME}] Injection failed`, { sessionID, error: String(err) })
246
+ }
247
+ }
248
+
249
+ function startCountdown(
250
+ sessionID: string,
251
+ incompleteCount: number,
252
+ total: number,
253
+ resolvedInfo?: ResolvedMessageInfo
254
+ ): void {
255
+ const state = getState(sessionID)
256
+ cancelCountdown(sessionID)
257
+
258
+ let secondsRemaining = COUNTDOWN_SECONDS
259
+ showCountdownToast(secondsRemaining, incompleteCount)
260
+ state.countdownStartedAt = Date.now()
261
+
262
+ state.countdownInterval = setInterval(() => {
263
+ secondsRemaining--
264
+ if (secondsRemaining > 0) {
265
+ showCountdownToast(secondsRemaining, incompleteCount)
266
+ }
267
+ }, 1000)
268
+
269
+ state.countdownTimer = setTimeout(() => {
270
+ cancelCountdown(sessionID)
271
+ injectContinuation(sessionID, incompleteCount, total, resolvedInfo)
272
+ }, COUNTDOWN_SECONDS * 1000)
273
+
274
+ log(`[${HOOK_NAME}] Countdown started`, { sessionID, seconds: COUNTDOWN_SECONDS, incompleteCount })
275
+ }
276
+
277
+ const handler = async ({ event }: { event: { type: string; properties?: unknown } }): Promise<void> => {
278
+ const props = event.properties as Record<string, unknown> | undefined
279
+
280
+ if (event.type === "session.error") {
281
+ const sessionID = props?.sessionID as string | undefined
282
+ if (!sessionID) return
283
+
284
+ const error = props?.error as { name?: string } | undefined
285
+ if (error?.name === "MessageAbortedError" || error?.name === "AbortError") {
286
+ const state = getState(sessionID)
287
+ state.abortDetectedAt = Date.now()
288
+ log(`[${HOOK_NAME}] Abort detected via session.error`, { sessionID, errorName: error.name })
289
+ }
290
+
291
+ cancelCountdown(sessionID)
292
+ log(`[${HOOK_NAME}] session.error`, { sessionID })
293
+ return
294
+ }
295
+
296
+ if (event.type === "session.idle") {
297
+ const sessionID = props?.sessionID as string | undefined
298
+ if (!sessionID) return
299
+
300
+ log(`[${HOOK_NAME}] session.idle`, { sessionID })
301
+
302
+ const mainSessionID = getMainSessionID()
303
+ const isMainSession = sessionID === mainSessionID
304
+ const isBackgroundTaskSession = subagentSessions.has(sessionID)
305
+
306
+ if (mainSessionID && !isMainSession && !isBackgroundTaskSession) {
307
+ log(`[${HOOK_NAME}] Skipped: not main or background task session`, { sessionID })
308
+ return
309
+ }
310
+
311
+ const state = getState(sessionID)
312
+
313
+ if (state.isRecovering) {
314
+ log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID })
315
+ return
316
+ }
317
+
318
+ // Check 1: Event-based abort detection (primary, most reliable)
319
+ if (state.abortDetectedAt) {
320
+ const timeSinceAbort = Date.now() - state.abortDetectedAt
321
+ const ABORT_WINDOW_MS = 3000
322
+ if (timeSinceAbort < ABORT_WINDOW_MS) {
323
+ log(`[${HOOK_NAME}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID })
324
+ state.abortDetectedAt = undefined
325
+ return
326
+ }
327
+ state.abortDetectedAt = undefined
328
+ }
329
+
330
+ const hasRunningBgTasks = backgroundManager
331
+ ? backgroundManager.getTasksByParentSession(sessionID).some(t => t.status === "running")
332
+ : false
333
+
334
+ if (hasRunningBgTasks) {
335
+ log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID })
336
+ return
337
+ }
338
+
339
+ // Check 2: API-based abort detection (fallback, for cases where event was missed)
340
+ try {
341
+ const messagesResp = await ctx.client.session.messages({
342
+ path: { id: sessionID },
343
+ query: { directory: ctx.directory },
344
+ })
345
+ const messages = (messagesResp as { data?: Array<{ info?: MessageInfo }> }).data ?? []
346
+
347
+ if (isLastAssistantMessageAborted(messages)) {
348
+ log(`[${HOOK_NAME}] Skipped: last assistant message was aborted (API fallback)`, { sessionID })
349
+ return
350
+ }
351
+ } catch (err) {
352
+ log(`[${HOOK_NAME}] Messages fetch failed, continuing`, { sessionID, error: String(err) })
353
+ }
354
+
355
+ let todos: Todo[] = []
356
+ try {
357
+ const response = await ctx.client.session.todo({ path: { id: sessionID } })
358
+ todos = (response.data ?? response) as Todo[]
359
+ } catch (err) {
360
+ log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(err) })
361
+ return
362
+ }
363
+
364
+ if (!todos || todos.length === 0) {
365
+ log(`[${HOOK_NAME}] No todos`, { sessionID })
366
+ return
367
+ }
368
+
369
+ const incompleteCount = getIncompleteCount(todos)
370
+ if (incompleteCount === 0) {
371
+ log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length })
372
+ return
373
+ }
374
+
375
+ let resolvedInfo: ResolvedMessageInfo | undefined
376
+ try {
377
+ const messagesResp = await ctx.client.session.messages({
378
+ path: { id: sessionID },
379
+ })
380
+ const messages = (messagesResp.data ?? []) as Array<{
381
+ info?: {
382
+ agent?: string
383
+ model?: { providerID: string; modelID: string }
384
+ modelID?: string
385
+ providerID?: string
386
+ tools?: Record<string, ToolPermission>
387
+ }
388
+ }>
389
+ for (let i = messages.length - 1; i >= 0; i--) {
390
+ const info = messages[i].info
391
+ if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
392
+ resolvedInfo = {
393
+ agent: info.agent,
394
+ model: info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined),
395
+ tools: info.tools,
396
+ }
397
+ break
398
+ }
399
+ }
400
+ } catch (err) {
401
+ log(`[${HOOK_NAME}] Failed to fetch messages for agent check`, { sessionID, error: String(err) })
402
+ }
403
+
404
+ log(`[${HOOK_NAME}] Agent check`, { sessionID, agentName: resolvedInfo?.agent, skipAgents })
405
+ if (resolvedInfo?.agent && skipAgents.includes(resolvedInfo.agent)) {
406
+ log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedInfo.agent })
407
+ return
408
+ }
409
+
410
+ startCountdown(sessionID, incompleteCount, todos.length, resolvedInfo)
411
+ return
412
+ }
413
+
414
+ if (event.type === "message.updated") {
415
+ const info = props?.info as Record<string, unknown> | undefined
416
+ const sessionID = info?.sessionID as string | undefined
417
+ const role = info?.role as string | undefined
418
+
419
+ if (!sessionID) return
420
+
421
+ if (role === "user") {
422
+ const state = sessions.get(sessionID)
423
+ if (state?.countdownStartedAt) {
424
+ const elapsed = Date.now() - state.countdownStartedAt
425
+ if (elapsed < COUNTDOWN_GRACE_PERIOD_MS) {
426
+ log(`[${HOOK_NAME}] Ignoring user message in grace period`, { sessionID, elapsed })
427
+ return
428
+ }
429
+ }
430
+ if (state) state.abortDetectedAt = undefined
431
+ cancelCountdown(sessionID)
432
+ }
433
+
434
+ if (role === "assistant") {
435
+ const state = sessions.get(sessionID)
436
+ if (state) state.abortDetectedAt = undefined
437
+ cancelCountdown(sessionID)
438
+ }
439
+ return
440
+ }
441
+
442
+ if (event.type === "message.part.updated") {
443
+ const info = props?.info as Record<string, unknown> | undefined
444
+ const sessionID = info?.sessionID as string | undefined
445
+ const role = info?.role as string | undefined
446
+
447
+ if (sessionID && role === "assistant") {
448
+ const state = sessions.get(sessionID)
449
+ if (state) state.abortDetectedAt = undefined
450
+ cancelCountdown(sessionID)
451
+ }
452
+ return
453
+ }
454
+
455
+ if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
456
+ const sessionID = props?.sessionID as string | undefined
457
+ if (sessionID) {
458
+ const state = sessions.get(sessionID)
459
+ if (state) state.abortDetectedAt = undefined
460
+ cancelCountdown(sessionID)
461
+ }
462
+ return
463
+ }
464
+
465
+ if (event.type === "session.deleted") {
466
+ const sessionInfo = props?.info as { id?: string } | undefined
467
+ if (sessionInfo?.id) {
468
+ cleanup(sessionInfo.id)
469
+ log(`[${HOOK_NAME}] Session deleted: cleaned up`, { sessionID: sessionInfo.id })
470
+ }
471
+ return
472
+ }
473
+ }
474
+
475
+ return {
476
+ handler,
477
+ markRecovering,
478
+ markRecoveryComplete,
479
+ }
480
+ }
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test"
2
+ import { createToolOutputTruncatorHook } from "./tool-output-truncator"
3
+ import * as dynamicTruncator from "../shared/dynamic-truncator"
4
+
5
+ describe("createToolOutputTruncatorHook", () => {
6
+ let hook: ReturnType<typeof createToolOutputTruncatorHook>
7
+ let truncateSpy: ReturnType<typeof spyOn>
8
+
9
+ beforeEach(() => {
10
+ truncateSpy = spyOn(dynamicTruncator, "createDynamicTruncator").mockReturnValue({
11
+ truncate: mock(async (_sessionID: string, output: string, options?: { targetMaxTokens?: number }) => ({
12
+ result: output,
13
+ truncated: false,
14
+ targetMaxTokens: options?.targetMaxTokens,
15
+ })),
16
+ getUsage: mock(async () => null),
17
+ truncateSync: mock(() => ({ result: "", truncated: false })),
18
+ })
19
+ hook = createToolOutputTruncatorHook({} as never)
20
+ })
21
+
22
+ describe("tool.execute.after", () => {
23
+ const createInput = (tool: string) => ({
24
+ tool,
25
+ sessionID: "test-session",
26
+ callID: "test-call-id",
27
+ })
28
+
29
+ const createOutput = (outputText: string) => ({
30
+ title: "Result",
31
+ output: outputText,
32
+ metadata: {},
33
+ })
34
+
35
+ describe("#given webfetch tool", () => {
36
+ describe("#when output is processed", () => {
37
+ it("#then should use aggressive truncation limit (10k tokens)", async () => {
38
+ const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({
39
+ result: "truncated",
40
+ truncated: true,
41
+ targetMaxTokens: options?.targetMaxTokens,
42
+ }))
43
+ truncateSpy.mockReturnValue({
44
+ truncate: truncateMock,
45
+ getUsage: mock(async () => null),
46
+ truncateSync: mock(() => ({ result: "", truncated: false })),
47
+ })
48
+ hook = createToolOutputTruncatorHook({} as never)
49
+
50
+ const input = createInput("webfetch")
51
+ const output = createOutput("large content")
52
+
53
+ await hook["tool.execute.after"](input, output)
54
+
55
+ expect(truncateMock).toHaveBeenCalledWith(
56
+ "test-session",
57
+ "large content",
58
+ { targetMaxTokens: 10_000 }
59
+ )
60
+ })
61
+ })
62
+
63
+ describe("#when using WebFetch variant", () => {
64
+ it("#then should also use aggressive truncation limit", async () => {
65
+ const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({
66
+ result: "truncated",
67
+ truncated: true,
68
+ }))
69
+ truncateSpy.mockReturnValue({
70
+ truncate: truncateMock,
71
+ getUsage: mock(async () => null),
72
+ truncateSync: mock(() => ({ result: "", truncated: false })),
73
+ })
74
+ hook = createToolOutputTruncatorHook({} as never)
75
+
76
+ const input = createInput("WebFetch")
77
+ const output = createOutput("large content")
78
+
79
+ await hook["tool.execute.after"](input, output)
80
+
81
+ expect(truncateMock).toHaveBeenCalledWith(
82
+ "test-session",
83
+ "large content",
84
+ { targetMaxTokens: 10_000 }
85
+ )
86
+ })
87
+ })
88
+ })
89
+
90
+ describe("#given grep tool", () => {
91
+ describe("#when output is processed", () => {
92
+ it("#then should use default truncation limit (50k tokens)", async () => {
93
+ const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({
94
+ result: "truncated",
95
+ truncated: true,
96
+ }))
97
+ truncateSpy.mockReturnValue({
98
+ truncate: truncateMock,
99
+ getUsage: mock(async () => null),
100
+ truncateSync: mock(() => ({ result: "", truncated: false })),
101
+ })
102
+ hook = createToolOutputTruncatorHook({} as never)
103
+
104
+ const input = createInput("grep")
105
+ const output = createOutput("grep output")
106
+
107
+ await hook["tool.execute.after"](input, output)
108
+
109
+ expect(truncateMock).toHaveBeenCalledWith(
110
+ "test-session",
111
+ "grep output",
112
+ { targetMaxTokens: 50_000 }
113
+ )
114
+ })
115
+ })
116
+ })
117
+
118
+ describe("#given non-truncatable tool", () => {
119
+ describe("#when tool is not in TRUNCATABLE_TOOLS list", () => {
120
+ it("#then should not call truncator", async () => {
121
+ const truncateMock = mock(async () => ({
122
+ result: "truncated",
123
+ truncated: true,
124
+ }))
125
+ truncateSpy.mockReturnValue({
126
+ truncate: truncateMock,
127
+ getUsage: mock(async () => null),
128
+ truncateSync: mock(() => ({ result: "", truncated: false })),
129
+ })
130
+ hook = createToolOutputTruncatorHook({} as never)
131
+
132
+ const input = createInput("Read")
133
+ const output = createOutput("file content")
134
+
135
+ await hook["tool.execute.after"](input, output)
136
+
137
+ expect(truncateMock).not.toHaveBeenCalled()
138
+ })
139
+ })
140
+ })
141
+
142
+ describe("#given truncate_all_tool_outputs enabled", () => {
143
+ describe("#when any tool output is processed", () => {
144
+ it("#then should truncate non-listed tools too", async () => {
145
+ const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({
146
+ result: "truncated",
147
+ truncated: true,
148
+ }))
149
+ truncateSpy.mockReturnValue({
150
+ truncate: truncateMock,
151
+ getUsage: mock(async () => null),
152
+ truncateSync: mock(() => ({ result: "", truncated: false })),
153
+ })
154
+ hook = createToolOutputTruncatorHook({} as never, {
155
+ experimental: { truncate_all_tool_outputs: true },
156
+ })
157
+
158
+ const input = createInput("Read")
159
+ const output = createOutput("file content")
160
+
161
+ await hook["tool.execute.after"](input, output)
162
+
163
+ expect(truncateMock).toHaveBeenCalled()
164
+ })
165
+ })
166
+ })
167
+ })
168
+ })
@@ -0,0 +1,61 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin"
2
+ import type { ExperimentalConfig } from "../config/schema"
3
+ import { createDynamicTruncator } from "../shared/dynamic-truncator"
4
+
5
+ const DEFAULT_MAX_TOKENS = 50_000 // ~200k chars
6
+ const WEBFETCH_MAX_TOKENS = 10_000 // ~40k chars - web pages need aggressive truncation
7
+
8
+ const TRUNCATABLE_TOOLS = [
9
+ "grep",
10
+ "Grep",
11
+ "safe_grep",
12
+ "glob",
13
+ "Glob",
14
+ "safe_glob",
15
+ "lsp_diagnostics",
16
+ "ast_grep_search",
17
+ "interactive_bash",
18
+ "Interactive_bash",
19
+ "skill_mcp",
20
+ "webfetch",
21
+ "WebFetch",
22
+ ]
23
+
24
+ const TOOL_SPECIFIC_MAX_TOKENS: Record<string, number> = {
25
+ webfetch: WEBFETCH_MAX_TOKENS,
26
+ WebFetch: WEBFETCH_MAX_TOKENS,
27
+ }
28
+
29
+ interface ToolOutputTruncatorOptions {
30
+ experimental?: ExperimentalConfig
31
+ }
32
+
33
+ export function createToolOutputTruncatorHook(ctx: PluginInput, options?: ToolOutputTruncatorOptions) {
34
+ const truncator = createDynamicTruncator(ctx)
35
+ const truncateAll = options?.experimental?.truncate_all_tool_outputs ?? false
36
+
37
+ const toolExecuteAfter = async (
38
+ input: { tool: string; sessionID: string; callID: string },
39
+ output: { title: string; output: string; metadata: unknown }
40
+ ) => {
41
+ if (!truncateAll && !TRUNCATABLE_TOOLS.includes(input.tool)) return
42
+
43
+ try {
44
+ const targetMaxTokens = TOOL_SPECIFIC_MAX_TOKENS[input.tool] ?? DEFAULT_MAX_TOKENS
45
+ const { result, truncated } = await truncator.truncate(
46
+ input.sessionID,
47
+ output.output,
48
+ { targetMaxTokens }
49
+ )
50
+ if (truncated) {
51
+ output.output = result
52
+ }
53
+ } catch {
54
+ // Graceful degradation - don't break tool execution
55
+ }
56
+ }
57
+
58
+ return {
59
+ "tool.execute.after": toolExecuteAfter,
60
+ }
61
+ }