hammoc 1.3.0 → 1.5.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 (336) hide show
  1. package/README.md +422 -403
  2. package/bin/hammoc.js +0 -6
  3. package/package.json +100 -93
  4. package/packages/client/dist/assets/agentExampleHighlight-BgwTm15v.js +1 -0
  5. package/packages/client/dist/assets/commandTokenHighlight-BljHwnrK.js +1 -0
  6. package/packages/client/dist/assets/index-CjyjnXB8.css +32 -0
  7. package/packages/client/dist/assets/index-D3LxqW3f.js +2 -0
  8. package/packages/client/dist/assets/index-NqJdhlek.js +1498 -0
  9. package/packages/client/dist/assets/snippetTokenHighlight-DWsaQXX0.js +1 -0
  10. package/packages/client/dist/index.html +2 -2
  11. package/packages/client/dist/sw.js +1 -1
  12. package/packages/server/dist/app.d.ts.map +1 -1
  13. package/packages/server/dist/app.js +24 -24
  14. package/packages/server/dist/app.js.map +1 -1
  15. package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
  16. package/packages/server/dist/controllers/boardController.js +0 -5
  17. package/packages/server/dist/controllers/boardController.js.map +1 -1
  18. package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
  19. package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
  20. package/packages/server/dist/controllers/claudeMdController.js +158 -0
  21. package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
  22. package/packages/server/dist/controllers/fileSystemController.d.ts +4 -0
  23. package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
  24. package/packages/server/dist/controllers/fileSystemController.js +20 -2
  25. package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
  26. package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
  27. package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
  28. package/packages/server/dist/controllers/harnessAgentController.js +339 -0
  29. package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
  30. package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
  31. package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
  32. package/packages/server/dist/controllers/harnessCommandController.js +382 -0
  33. package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
  34. package/packages/server/dist/controllers/harnessController.d.ts +21 -0
  35. package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
  36. package/packages/server/dist/controllers/harnessController.js +176 -0
  37. package/packages/server/dist/controllers/harnessController.js.map +1 -0
  38. package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
  39. package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
  40. package/packages/server/dist/controllers/harnessHookController.js +363 -0
  41. package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
  42. package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
  43. package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
  44. package/packages/server/dist/controllers/harnessLintController.js +72 -0
  45. package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
  46. package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
  47. package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
  48. package/packages/server/dist/controllers/harnessMcpController.js +310 -0
  49. package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
  50. package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
  51. package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
  52. package/packages/server/dist/controllers/harnessPluginController.js +115 -0
  53. package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
  54. package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
  55. package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
  56. package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
  57. package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
  58. package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
  59. package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
  60. package/packages/server/dist/controllers/harnessSkillController.js +453 -0
  61. package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
  62. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  63. package/packages/server/dist/controllers/projectController.js +11 -0
  64. package/packages/server/dist/controllers/projectController.js.map +1 -1
  65. package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
  66. package/packages/server/dist/controllers/serverController.js +84 -49
  67. package/packages/server/dist/controllers/serverController.js.map +1 -1
  68. package/packages/server/dist/controllers/snippetController.d.ts +35 -0
  69. package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
  70. package/packages/server/dist/controllers/snippetController.js +294 -0
  71. package/packages/server/dist/controllers/snippetController.js.map +1 -0
  72. package/packages/server/dist/handlers/websocket.d.ts +16 -0
  73. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  74. package/packages/server/dist/handlers/websocket.js +221 -8
  75. package/packages/server/dist/handlers/websocket.js.map +1 -1
  76. package/packages/server/dist/index.js +66 -0
  77. package/packages/server/dist/index.js.map +1 -1
  78. package/packages/server/dist/locales/en/server.json +41 -6
  79. package/packages/server/dist/locales/es/server.json +3 -5
  80. package/packages/server/dist/locales/ja/server.json +3 -5
  81. package/packages/server/dist/locales/ko/server.json +4 -6
  82. package/packages/server/dist/locales/pt/server.json +3 -5
  83. package/packages/server/dist/locales/zh-CN/server.json +3 -5
  84. package/packages/server/dist/routes/account.d.ts +7 -0
  85. package/packages/server/dist/routes/account.d.ts.map +1 -0
  86. package/packages/server/dist/routes/account.js +35 -0
  87. package/packages/server/dist/routes/account.js.map +1 -0
  88. package/packages/server/dist/routes/debug.d.ts +1 -1
  89. package/packages/server/dist/routes/debug.d.ts.map +1 -1
  90. package/packages/server/dist/routes/debug.js +60 -1
  91. package/packages/server/dist/routes/debug.js.map +1 -1
  92. package/packages/server/dist/routes/harness.d.ts +8 -0
  93. package/packages/server/dist/routes/harness.d.ts.map +1 -0
  94. package/packages/server/dist/routes/harness.js +92 -0
  95. package/packages/server/dist/routes/harness.js.map +1 -0
  96. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  97. package/packages/server/dist/routes/preferences.js +11 -2
  98. package/packages/server/dist/routes/preferences.js.map +1 -1
  99. package/packages/server/dist/routes/projects.d.ts.map +1 -1
  100. package/packages/server/dist/routes/projects.js +5 -60
  101. package/packages/server/dist/routes/projects.js.map +1 -1
  102. package/packages/server/dist/routes/snippets.d.ts +14 -0
  103. package/packages/server/dist/routes/snippets.d.ts.map +1 -0
  104. package/packages/server/dist/routes/snippets.js +27 -0
  105. package/packages/server/dist/routes/snippets.js.map +1 -0
  106. package/packages/server/dist/services/accountInfoService.d.ts +38 -0
  107. package/packages/server/dist/services/accountInfoService.d.ts.map +1 -0
  108. package/packages/server/dist/services/accountInfoService.js +118 -0
  109. package/packages/server/dist/services/accountInfoService.js.map +1 -0
  110. package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
  111. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  112. package/packages/server/dist/services/bmadStatusService.js +88 -32
  113. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  114. package/packages/server/dist/services/chatService.d.ts +3 -0
  115. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  116. package/packages/server/dist/services/chatService.js +36 -8
  117. package/packages/server/dist/services/chatService.js.map +1 -1
  118. package/packages/server/dist/services/claudeMdService.d.ts +48 -0
  119. package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
  120. package/packages/server/dist/services/claudeMdService.js +240 -0
  121. package/packages/server/dist/services/claudeMdService.js.map +1 -0
  122. package/packages/server/dist/services/commandService.d.ts +10 -0
  123. package/packages/server/dist/services/commandService.d.ts.map +1 -1
  124. package/packages/server/dist/services/commandService.js +129 -4
  125. package/packages/server/dist/services/commandService.js.map +1 -1
  126. package/packages/server/dist/services/fileSystemService.d.ts +7 -1
  127. package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
  128. package/packages/server/dist/services/fileSystemService.js +67 -8
  129. package/packages/server/dist/services/fileSystemService.js.map +1 -1
  130. package/packages/server/dist/services/fileWatcherService.d.ts +59 -0
  131. package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -0
  132. package/packages/server/dist/services/fileWatcherService.js +329 -0
  133. package/packages/server/dist/services/fileWatcherService.js.map +1 -0
  134. package/packages/server/dist/services/gitService.d.ts.map +1 -1
  135. package/packages/server/dist/services/gitService.js +67 -7
  136. package/packages/server/dist/services/gitService.js.map +1 -1
  137. package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
  138. package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
  139. package/packages/server/dist/services/harnessAgentService.js +933 -0
  140. package/packages/server/dist/services/harnessAgentService.js.map +1 -0
  141. package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
  142. package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
  143. package/packages/server/dist/services/harnessCommandService.js +853 -0
  144. package/packages/server/dist/services/harnessCommandService.js.map +1 -0
  145. package/packages/server/dist/services/harnessHookService.d.ts +55 -0
  146. package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
  147. package/packages/server/dist/services/harnessHookService.js +1060 -0
  148. package/packages/server/dist/services/harnessHookService.js.map +1 -0
  149. package/packages/server/dist/services/harnessLintService.d.ts +49 -0
  150. package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
  151. package/packages/server/dist/services/harnessLintService.js +628 -0
  152. package/packages/server/dist/services/harnessLintService.js.map +1 -0
  153. package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
  154. package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
  155. package/packages/server/dist/services/harnessMcpService.js +814 -0
  156. package/packages/server/dist/services/harnessMcpService.js.map +1 -0
  157. package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
  158. package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
  159. package/packages/server/dist/services/harnessPluginService.js +559 -0
  160. package/packages/server/dist/services/harnessPluginService.js.map +1 -0
  161. package/packages/server/dist/services/harnessService.d.ts +40 -0
  162. package/packages/server/dist/services/harnessService.d.ts.map +1 -0
  163. package/packages/server/dist/services/harnessService.js +222 -0
  164. package/packages/server/dist/services/harnessService.js.map +1 -0
  165. package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
  166. package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
  167. package/packages/server/dist/services/harnessShareScopeService.js +93 -0
  168. package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
  169. package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
  170. package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
  171. package/packages/server/dist/services/harnessSkillService.js +636 -0
  172. package/packages/server/dist/services/harnessSkillService.js.map +1 -0
  173. package/packages/server/dist/services/historyParser.d.ts +4 -14
  174. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  175. package/packages/server/dist/services/historyParser.js +60 -5
  176. package/packages/server/dist/services/historyParser.js.map +1 -1
  177. package/packages/server/dist/services/issueService.d.ts.map +1 -1
  178. package/packages/server/dist/services/issueService.js +10 -2
  179. package/packages/server/dist/services/issueService.js.map +1 -1
  180. package/packages/server/dist/services/manualSyncService.d.ts +19 -0
  181. package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
  182. package/packages/server/dist/services/manualSyncService.js +110 -0
  183. package/packages/server/dist/services/manualSyncService.js.map +1 -0
  184. package/packages/server/dist/services/notificationService.d.ts.map +1 -1
  185. package/packages/server/dist/services/notificationService.js +34 -9
  186. package/packages/server/dist/services/notificationService.js.map +1 -1
  187. package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
  188. package/packages/server/dist/services/preferencesService.js +8 -1
  189. package/packages/server/dist/services/preferencesService.js.map +1 -1
  190. package/packages/server/dist/services/projectService.d.ts +5 -0
  191. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  192. package/packages/server/dist/services/projectService.js +42 -2
  193. package/packages/server/dist/services/projectService.js.map +1 -1
  194. package/packages/server/dist/services/ptyService.d.ts +1 -0
  195. package/packages/server/dist/services/ptyService.d.ts.map +1 -1
  196. package/packages/server/dist/services/ptyService.js +36 -5
  197. package/packages/server/dist/services/ptyService.js.map +1 -1
  198. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  199. package/packages/server/dist/services/queueService.js +83 -14
  200. package/packages/server/dist/services/queueService.js.map +1 -1
  201. package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
  202. package/packages/server/dist/services/sessionBufferManager.js +26 -0
  203. package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
  204. package/packages/server/dist/services/sessionService.d.ts +4 -3
  205. package/packages/server/dist/services/sessionService.d.ts.map +1 -1
  206. package/packages/server/dist/services/sessionService.js +5 -4
  207. package/packages/server/dist/services/sessionService.js.map +1 -1
  208. package/packages/server/dist/services/snippetService.d.ts +54 -0
  209. package/packages/server/dist/services/snippetService.d.ts.map +1 -0
  210. package/packages/server/dist/services/snippetService.js +371 -0
  211. package/packages/server/dist/services/snippetService.js.map +1 -0
  212. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
  213. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
  214. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
  215. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
  216. package/packages/server/dist/services/webPushService.d.ts.map +1 -1
  217. package/packages/server/dist/services/webPushService.js +8 -1
  218. package/packages/server/dist/services/webPushService.js.map +1 -1
  219. package/packages/server/dist/snippets/split-commit +9 -0
  220. package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
  221. package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
  222. package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
  223. package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
  224. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
  225. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
  226. package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
  227. package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
  228. package/packages/server/dist/utils/effortUtils.d.ts +21 -0
  229. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -0
  230. package/packages/server/dist/utils/effortUtils.js +36 -0
  231. package/packages/server/dist/utils/effortUtils.js.map +1 -0
  232. package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
  233. package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
  234. package/packages/server/dist/utils/gitignoreFilter.js +42 -0
  235. package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
  236. package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
  237. package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
  238. package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
  239. package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
  240. package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
  241. package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
  242. package/packages/server/dist/utils/harnessPaths.js +124 -0
  243. package/packages/server/dist/utils/harnessPaths.js.map +1 -0
  244. package/packages/server/dist/utils/pathUtils.d.ts +3 -2
  245. package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
  246. package/packages/server/dist/utils/pathUtils.js +26 -2
  247. package/packages/server/dist/utils/pathUtils.js.map +1 -1
  248. package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
  249. package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
  250. package/packages/server/dist/utils/secretHeuristic.js +163 -0
  251. package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
  252. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
  253. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
  254. package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
  255. package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
  256. package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
  257. package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
  258. package/packages/server/dist/utils/serverPathResolver.js +59 -0
  259. package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
  260. package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
  261. package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
  262. package/packages/server/dist/utils/snippetPaths.js +123 -0
  263. package/packages/server/dist/utils/snippetPaths.js.map +1 -0
  264. package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
  265. package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
  266. package/packages/server/dist/utils/structuredEditor.js +111 -0
  267. package/packages/server/dist/utils/structuredEditor.js.map +1 -0
  268. package/packages/server/package.json +6 -2
  269. package/packages/server/resources/internals/INDEX.md +23 -0
  270. package/packages/server/resources/internals/harness-files.md +63 -0
  271. package/packages/server/resources/internals/image-storage.md +43 -0
  272. package/packages/server/resources/manual/01-getting-started.md +104 -0
  273. package/packages/server/resources/manual/02-chat.md +285 -0
  274. package/packages/server/resources/manual/03-sessions.md +48 -0
  275. package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
  276. package/packages/server/resources/manual/05-projects.md +74 -0
  277. package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
  278. package/packages/server/resources/manual/07-git.md +94 -0
  279. package/packages/server/resources/manual/08-terminal.md +59 -0
  280. package/packages/server/resources/manual/09-queue-runner.md +262 -0
  281. package/packages/server/resources/manual/10-project-board.md +193 -0
  282. package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
  283. package/packages/server/resources/manual/12-harness-workbench.md +175 -0
  284. package/packages/server/resources/manual/13-settings.md +241 -0
  285. package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
  286. package/packages/server/resources/manual/15-environment-variables.md +28 -0
  287. package/packages/server/resources/manual/16-troubleshooting.md +110 -0
  288. package/packages/server/resources/manual/INDEX.md +60 -0
  289. package/packages/shared/dist/index.d.ts +3 -0
  290. package/packages/shared/dist/index.d.ts.map +1 -1
  291. package/packages/shared/dist/index.js +6 -0
  292. package/packages/shared/dist/index.js.map +1 -1
  293. package/packages/shared/dist/types/command.d.ts +3 -3
  294. package/packages/shared/dist/types/command.d.ts.map +1 -1
  295. package/packages/shared/dist/types/fileSystem.d.ts +19 -0
  296. package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
  297. package/packages/shared/dist/types/fileSystem.js +5 -0
  298. package/packages/shared/dist/types/fileSystem.js.map +1 -1
  299. package/packages/shared/dist/types/git.d.ts +6 -1
  300. package/packages/shared/dist/types/git.d.ts.map +1 -1
  301. package/packages/shared/dist/types/git.js.map +1 -1
  302. package/packages/shared/dist/types/harness.d.ts +1211 -0
  303. package/packages/shared/dist/types/harness.d.ts.map +1 -0
  304. package/packages/shared/dist/types/harness.js +107 -0
  305. package/packages/shared/dist/types/harness.js.map +1 -0
  306. package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
  307. package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
  308. package/packages/shared/dist/types/harnessBundle.js +18 -0
  309. package/packages/shared/dist/types/harnessBundle.js.map +1 -0
  310. package/packages/shared/dist/types/history.d.ts +7 -0
  311. package/packages/shared/dist/types/history.d.ts.map +1 -1
  312. package/packages/shared/dist/types/preferences.d.ts +4 -1
  313. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  314. package/packages/shared/dist/types/preferences.js +1 -0
  315. package/packages/shared/dist/types/preferences.js.map +1 -1
  316. package/packages/shared/dist/types/queue.d.ts +9 -0
  317. package/packages/shared/dist/types/queue.d.ts.map +1 -1
  318. package/packages/shared/dist/types/sdk.d.ts +42 -1
  319. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  320. package/packages/shared/dist/types/sdk.js +26 -2
  321. package/packages/shared/dist/types/sdk.js.map +1 -1
  322. package/packages/shared/dist/types/websocket.d.ts +24 -0
  323. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  324. package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
  325. package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
  326. package/packages/shared/dist/utils/markdownSections.js +111 -0
  327. package/packages/shared/dist/utils/markdownSections.js.map +1 -0
  328. package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
  329. package/packages/shared/dist/utils/queueParser.js +104 -0
  330. package/packages/shared/dist/utils/queueParser.js.map +1 -1
  331. package/scripts/build-manual-shards.mjs +100 -0
  332. package/scripts/mock-telegram.mjs +172 -0
  333. package/scripts/run-integration-test.mjs +362 -0
  334. package/packages/client/dist/assets/index-Bf0D9oVJ.css +0 -32
  335. package/packages/client/dist/assets/index-CRmzoqHy.js +0 -2
  336. package/packages/client/dist/assets/index-CszGQ29O.js +0 -1432
@@ -0,0 +1,1060 @@
1
+ /**
2
+ * Story 28.4: Harness Hook service.
3
+ *
4
+ * Combines three sources of hook definitions into a single 9-event card list:
5
+ * - <projectRoot>/.claude/settings.json (hooks field)
6
+ * - ~/.claude/settings.json (hooks field)
7
+ * - <pluginInstallPath>/hooks/hooks.json (hooks field, read-only)
8
+ *
9
+ * Disable-toggle mechanism follows Story 28.3 backup-file pattern (path 2):
10
+ * - <projectRoot>/.claude/hooks.disabled.json (project scope)
11
+ * - ~/.claude/hooks.disabled.json (user scope)
12
+ *
13
+ * `prompt`-type hook GA status is cached as `promptTypeSupport` on every list
14
+ * response — the spike result toggles a single constant here without touching
15
+ * the rest of the service. Default is `'unsupported'` (per the static evidence
16
+ * dossier — 5/5 official plugin bundles use `command` only).
17
+ */
18
+ import path from 'path';
19
+ import fs from 'fs/promises';
20
+ import { HARNESS_ERRORS, HARNESS_HOOK_EVENTS, } from '@hammoc/shared';
21
+ import { harnessService } from './harnessService.js';
22
+ import { projectService } from './projectService.js';
23
+ import { getUserHarnessRoot } from '../utils/harnessPaths.js';
24
+ import { applyJsoncPatch } from '../utils/structuredEditor.js';
25
+ import { createLogger } from '../utils/logger.js';
26
+ import { detectSecretsInText as detectSecretsInTextCanonical } from '../utils/secretHeuristic.js';
27
+ import { assertNoSecretOnShared } from '../utils/assertNoSecretOnShared.js';
28
+ const log = createLogger('harnessHookService');
29
+ /**
30
+ * Spike result cache — flip to `'supported'` once the prompt-type hook spike
31
+ * confirms Claude Code actually executes prompt-type entries. Default
32
+ * `'unsupported'` matches the static evidence (5/5 official bundles use
33
+ * command-type only).
34
+ */
35
+ const PROMPT_TYPE_SUPPORT = 'unsupported';
36
+ const SCOPE_PRIORITY = {
37
+ project: 0,
38
+ user: 1,
39
+ plugin: 2,
40
+ };
41
+ function throwMapped(code, message, extras) {
42
+ const err = new Error(message);
43
+ err.code = code;
44
+ if (extras)
45
+ Object.assign(err, extras);
46
+ throw err;
47
+ }
48
+ function isFileNotFound(err) {
49
+ const code = err?.code;
50
+ return code === 'ENOENT' || code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code;
51
+ }
52
+ function isStaleWrite(err) {
53
+ return err?.code === HARNESS_ERRORS.HARNESS_STALE_WRITE.code;
54
+ }
55
+ export function detectSecretsInHook(config) {
56
+ const paths = [];
57
+ const fields = [
58
+ ['command', config.command],
59
+ ['prompt', config.prompt],
60
+ ];
61
+ for (const [name, value] of fields) {
62
+ if (typeof value !== 'string' || value.length === 0)
63
+ continue;
64
+ if (detectSecretsInTextCanonical(value).matched) {
65
+ paths.push(name);
66
+ }
67
+ }
68
+ return { matched: paths.length > 0, paths };
69
+ }
70
+ const PLUGIN_ROOT_TOKEN = '${CLAUDE_PLUGIN_ROOT}';
71
+ function containsPluginRootToken(config) {
72
+ return ((typeof config.command === 'string' && config.command.includes(PLUGIN_ROOT_TOKEN)) ||
73
+ (typeof config.prompt === 'string' && config.prompt.includes(PLUGIN_ROOT_TOKEN)));
74
+ }
75
+ function safeParseJsonc(text) {
76
+ try {
77
+ return JSON.parse(text);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ function emptyEventMap() {
84
+ const out = {};
85
+ for (const event of HARNESS_HOOK_EVENTS)
86
+ out[event] = [];
87
+ return out;
88
+ }
89
+ function extractGroups(parsed) {
90
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
91
+ return null;
92
+ const wrapper = parsed.hooks;
93
+ const result = emptyEventMap();
94
+ if (wrapper === undefined || wrapper === null)
95
+ return result;
96
+ if (typeof wrapper !== 'object' || Array.isArray(wrapper))
97
+ return null;
98
+ for (const event of HARNESS_HOOK_EVENTS) {
99
+ const list = wrapper[event];
100
+ if (list === undefined)
101
+ continue;
102
+ if (!Array.isArray(list)) {
103
+ // Single malformed event — keep the rest, drop this one.
104
+ continue;
105
+ }
106
+ const groups = [];
107
+ for (const raw of list) {
108
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
109
+ continue;
110
+ const group = raw;
111
+ if (!Array.isArray(group.hooks))
112
+ continue;
113
+ const hooks = [];
114
+ for (const h of group.hooks) {
115
+ if (!h || typeof h !== 'object' || Array.isArray(h))
116
+ continue;
117
+ const item = h;
118
+ const t = item.type;
119
+ if (t !== 'command' && t !== 'prompt')
120
+ continue;
121
+ const config = { type: t };
122
+ if (typeof item.command === 'string')
123
+ config.command = item.command;
124
+ if (typeof item.prompt === 'string')
125
+ config.prompt = item.prompt;
126
+ if (typeof item.timeout === 'number' && Number.isFinite(item.timeout)) {
127
+ config.timeout = item.timeout;
128
+ }
129
+ hooks.push(config);
130
+ }
131
+ const matcher = typeof group.matcher === 'string' ? group.matcher : undefined;
132
+ groups.push({ matcher, hooks });
133
+ }
134
+ result[event] = groups;
135
+ }
136
+ return result;
137
+ }
138
+ async function readHarnessRefFile(ref) {
139
+ try {
140
+ const res = await harnessService.read(ref);
141
+ const text = res.content ?? '';
142
+ const trimmed = text.trim();
143
+ if (!trimmed) {
144
+ return { groups: emptyEventMap(), mtime: res.mtime, rawText: text, present: true };
145
+ }
146
+ const parsed = safeParseJsonc(trimmed);
147
+ if (parsed === null) {
148
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse ${ref.relativePath}`);
149
+ }
150
+ const groups = extractGroups(parsed);
151
+ return {
152
+ groups: groups ?? emptyEventMap(),
153
+ mtime: res.mtime,
154
+ rawText: text,
155
+ present: true,
156
+ };
157
+ }
158
+ catch (err) {
159
+ if (isFileNotFound(err)) {
160
+ return { groups: emptyEventMap(), mtime: '', rawText: '', present: false };
161
+ }
162
+ throw err;
163
+ }
164
+ }
165
+ async function readPluginHooksFile(absoluteFile) {
166
+ let stat;
167
+ try {
168
+ stat = await fs.stat(absoluteFile);
169
+ }
170
+ catch {
171
+ return { groups: emptyEventMap(), mtime: '', rawText: '', present: false };
172
+ }
173
+ if (!stat.isFile()) {
174
+ return { groups: emptyEventMap(), mtime: '', rawText: '', present: false };
175
+ }
176
+ let text;
177
+ try {
178
+ text = await fs.readFile(absoluteFile, 'utf-8');
179
+ }
180
+ catch {
181
+ return { groups: emptyEventMap(), mtime: '', rawText: '', present: false };
182
+ }
183
+ const trimmed = text.trim();
184
+ if (!trimmed) {
185
+ return {
186
+ groups: emptyEventMap(),
187
+ mtime: stat.mtime.toISOString(),
188
+ rawText: text,
189
+ present: true,
190
+ };
191
+ }
192
+ const parsed = safeParseJsonc(trimmed);
193
+ if (parsed === null) {
194
+ return {
195
+ groups: emptyEventMap(),
196
+ mtime: stat.mtime.toISOString(),
197
+ rawText: text,
198
+ present: false,
199
+ };
200
+ }
201
+ const groups = extractGroups(parsed);
202
+ return {
203
+ groups: groups ?? emptyEventMap(),
204
+ mtime: stat.mtime.toISOString(),
205
+ rawText: text,
206
+ present: true,
207
+ };
208
+ }
209
+ function buildSettingsRef(scope, projectSlug) {
210
+ if (scope === 'user') {
211
+ return { scope: 'user', relativePath: 'settings.json' };
212
+ }
213
+ if (!projectSlug) {
214
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
215
+ }
216
+ return { scope: 'project', projectSlug, relativePath: 'settings.json' };
217
+ }
218
+ function buildBackupRef(scope, projectSlug) {
219
+ if (scope === 'user') {
220
+ return { scope: 'user', relativePath: 'hooks.disabled.json' };
221
+ }
222
+ if (!projectSlug) {
223
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
224
+ }
225
+ return { scope: 'project', projectSlug, relativePath: 'hooks.disabled.json' };
226
+ }
227
+ async function getSettingsAbsolutePath(scope, projectSlug) {
228
+ if (scope === 'user') {
229
+ return path.join(getUserHarnessRoot(), 'settings.json');
230
+ }
231
+ if (!projectSlug) {
232
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
233
+ }
234
+ const projectRoot = await projectService.resolveOriginalPath(projectSlug);
235
+ return path.join(projectRoot, '.claude', 'settings.json');
236
+ }
237
+ async function getBackupAbsolutePath(scope, projectSlug) {
238
+ if (scope === 'user') {
239
+ return path.join(getUserHarnessRoot(), 'hooks.disabled.json');
240
+ }
241
+ if (!projectSlug) {
242
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for project scope');
243
+ }
244
+ const projectRoot = await projectService.resolveOriginalPath(projectSlug);
245
+ return path.join(projectRoot, '.claude', 'hooks.disabled.json');
246
+ }
247
+ function buildHooksPatch(target, ops) {
248
+ const sourceText = target.source.trim().length === 0 ? '{ "hooks": {} }' : target.source;
249
+ // Ensure the `hooks` wrapper exists before applying nested patches; jsonc-parser's
250
+ // `modify` does set intermediate objects, but explicitly seeding `hooks: {}` keeps
251
+ // the diff deterministic when the file existed without a hooks field.
252
+ const seeded = ensureHooksWrapper(sourceText);
253
+ const patchOps = ops.map((op) => ({ path: ['hooks', ...op.path], value: op.value }));
254
+ return applyJsoncPatch(seeded, patchOps);
255
+ }
256
+ function ensureHooksWrapper(source) {
257
+ const parsed = safeParseJsonc(source);
258
+ if (parsed &&
259
+ typeof parsed === 'object' &&
260
+ !Array.isArray(parsed) &&
261
+ Object.prototype.hasOwnProperty.call(parsed, 'hooks')) {
262
+ return source;
263
+ }
264
+ // Insert `hooks: {}` so subsequent ops see a settled wrapper.
265
+ return applyJsoncPatch(source, [{ path: ['hooks'], value: {} }]);
266
+ }
267
+ async function writePatched(target, patched) {
268
+ // Story 30.1 (AC4.b): block writes that would land plaintext secrets in
269
+ // a git-tracked `settings.json`. The harness path-ref carries
270
+ // `relativePath: 'settings.json'` (relative to `.claude/`); the share-scope
271
+ // service expects the project-relative form, so we prefix.
272
+ if (target.ref.scope === 'project' && target.ref.projectSlug) {
273
+ const secrets = detectSecretsInTextCanonical(patched);
274
+ await assertNoSecretOnShared({
275
+ scope: 'project',
276
+ projectSlug: target.ref.projectSlug,
277
+ relativePath: `.claude/${target.ref.relativePath ?? 'settings.json'}`,
278
+ secretDetected: secrets.matched,
279
+ detectedAt: { lines: secrets.lines },
280
+ });
281
+ }
282
+ // expectedMtime '' (file missing) → omit so harnessService.write does not refuse the create path.
283
+ const expectedMtime = target.expectedMtime || undefined;
284
+ const written = await harnessService.write(target.ref, {
285
+ content: patched,
286
+ expectedMtime,
287
+ });
288
+ return { mtime: written.mtime };
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // Service class
292
+ // ---------------------------------------------------------------------------
293
+ class HarnessHookService {
294
+ async listCards(currentProjectSlug) {
295
+ const cardsByEvent = emptyEventMap();
296
+ for (const e of HARNESS_HOOK_EVENTS)
297
+ cardsByEvent[e] = [];
298
+ const malformed = [];
299
+ if (currentProjectSlug) {
300
+ try {
301
+ await this.enumerateProjectHooks(currentProjectSlug, cardsByEvent, malformed);
302
+ }
303
+ catch (err) {
304
+ if (err?.code !== HARNESS_ERRORS.HARNESS_ROOT_MISSING.code) {
305
+ throw err;
306
+ }
307
+ }
308
+ }
309
+ await this.enumerateUserHooks(cardsByEvent, malformed);
310
+ await this.enumeratePluginHooks(cardsByEvent, malformed);
311
+ for (const e of HARNESS_HOOK_EVENTS) {
312
+ cardsByEvent[e].sort((a, b) => {
313
+ const sd = SCOPE_PRIORITY[a.scope] - SCOPE_PRIORITY[b.scope];
314
+ if (sd !== 0)
315
+ return sd;
316
+ if (a.groupIndex !== b.groupIndex)
317
+ return a.groupIndex - b.groupIndex;
318
+ return a.hookIndex - b.hookIndex;
319
+ });
320
+ }
321
+ const backupMtimeByScope = await this.collectBackupMtimes(currentProjectSlug);
322
+ return {
323
+ cardsByEvent,
324
+ malformed,
325
+ promptTypeSupport: PROMPT_TYPE_SUPPORT,
326
+ backupMtimeByScope,
327
+ };
328
+ }
329
+ async readHook(loc) {
330
+ const file = await readFileForLocation(loc);
331
+ const groups = file.groups[loc.event];
332
+ const group = groups?.[loc.groupIndex];
333
+ if (!group) {
334
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, `group not found at ${loc.event}[${loc.groupIndex}]`);
335
+ }
336
+ const config = group.hooks[loc.hookIndex];
337
+ if (!config) {
338
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, `hook not found at ${loc.event}[${loc.groupIndex}].hooks[${loc.hookIndex}]`);
339
+ }
340
+ const raw = JSON.stringify(group.matcher !== undefined ? { matcher: group.matcher, hooks: [config] } : { hooks: [config] }, null, 2);
341
+ return {
342
+ source: loc,
343
+ matcher: group.matcher,
344
+ config,
345
+ raw,
346
+ mtime: file.mtime,
347
+ disabledByBackup: loc.disabledByBackup,
348
+ };
349
+ }
350
+ async createHook(req) {
351
+ const ref = buildSettingsRef(req.scope, req.projectSlug);
352
+ const file = await readHarnessRefFile(ref);
353
+ const expectedMtime = req.expectedMtime ?? file.mtime;
354
+ if (file.present && req.expectedMtime !== undefined && req.expectedMtime !== file.mtime) {
355
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
356
+ currentMtime: file.mtime,
357
+ staleFile: 'main',
358
+ });
359
+ }
360
+ validateConfigShape(req.config);
361
+ const newGroup = req.matcher
362
+ ? { matcher: req.matcher, hooks: [req.config] }
363
+ : { hooks: [req.config] };
364
+ const existing = file.groups[req.event] ?? [];
365
+ const newGroupIndex = existing.length;
366
+ const patched = buildHooksPatch({ ref, expectedMtime, source: file.rawText }, [{ path: [req.event, newGroupIndex], value: newGroup }]);
367
+ const result = await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
368
+ return { success: true, mtime: result.mtime, newGroupIndex, newHookIndex: 0 };
369
+ }
370
+ async updateHook(loc, body) {
371
+ if (loc.scope === 'plugin') {
372
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope hooks are read-only');
373
+ }
374
+ const editableScope = loc.scope;
375
+ const provided = [body.config, body.matcher !== undefined ? 1 : undefined, body.raw, body.enabled].filter((x) => x !== undefined);
376
+ if (provided.length !== 1) {
377
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'exactly one of config / matcher / raw / enabled is required');
378
+ }
379
+ if (body.enabled !== undefined) {
380
+ return this.toggleEnabled(loc, body.enabled, body.expectedMtime, body.expectedBackupMtime);
381
+ }
382
+ // matcher / config / raw paths all target the same main settings.json file.
383
+ const ref = loc.disabledByBackup
384
+ ? buildBackupRef(editableScope, loc.projectSlug)
385
+ : buildSettingsRef(editableScope, loc.projectSlug);
386
+ const file = await readHarnessRefFile(ref);
387
+ if (body.expectedMtime !== undefined && body.expectedMtime !== file.mtime) {
388
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
389
+ currentMtime: file.mtime,
390
+ staleFile: 'main',
391
+ });
392
+ }
393
+ const groups = file.groups[loc.event] ?? [];
394
+ const group = groups[loc.groupIndex];
395
+ if (!group) {
396
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, `group not found at ${loc.event}[${loc.groupIndex}]`);
397
+ }
398
+ if (!group.hooks[loc.hookIndex]) {
399
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, `hook not found at ${loc.event}[${loc.groupIndex}].hooks[${loc.hookIndex}]`);
400
+ }
401
+ if (body.config !== undefined) {
402
+ validateConfigShape(body.config);
403
+ const patched = buildHooksPatch({ ref, expectedMtime: file.mtime, source: file.rawText }, [{ path: [loc.event, loc.groupIndex, 'hooks', loc.hookIndex], value: body.config }]);
404
+ const result = await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
405
+ return { success: true, mtime: result.mtime };
406
+ }
407
+ if (body.raw !== undefined) {
408
+ let parsed;
409
+ try {
410
+ parsed = JSON.parse(body.raw);
411
+ }
412
+ catch (cause) {
413
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `raw payload is not valid JSON: ${cause.message}`);
414
+ }
415
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
416
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'raw payload must be an object');
417
+ }
418
+ const obj = parsed;
419
+ if (!Array.isArray(obj.hooks) || obj.hooks.length !== 1) {
420
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'raw payload must contain a single-hook hooks array');
421
+ }
422
+ const nextConfig = obj.hooks[0];
423
+ validateConfigShape(nextConfig);
424
+ const ops = [
425
+ { path: [loc.event, loc.groupIndex, 'hooks', loc.hookIndex], value: nextConfig },
426
+ ];
427
+ if (typeof obj.matcher === 'string') {
428
+ ops.push({ path: [loc.event, loc.groupIndex, 'matcher'], value: obj.matcher });
429
+ }
430
+ else if (obj.matcher === undefined || obj.matcher === null) {
431
+ ops.push({ path: [loc.event, loc.groupIndex, 'matcher'], value: undefined });
432
+ }
433
+ const patched = buildHooksPatch({ ref, expectedMtime: file.mtime, source: file.rawText }, ops);
434
+ const result = await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
435
+ return { success: true, mtime: result.mtime };
436
+ }
437
+ // matcher path
438
+ const newMatcher = body.matcher;
439
+ const splitFromGroup = body.splitFromGroup === true;
440
+ const groupHookCount = group.hooks.length;
441
+ if (splitFromGroup && groupHookCount <= 1) {
442
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'splitFromGroup is a no-op for single-hook groups', {
443
+ cause: 'split-noop',
444
+ });
445
+ }
446
+ if (splitFromGroup) {
447
+ // Extract the hook into a new group, leaving siblings under the original matcher.
448
+ const extracted = group.hooks[loc.hookIndex];
449
+ const newGroupIndex = groups.length;
450
+ const ops = [
451
+ // Remove the extracted hook from its original group's hooks[].
452
+ { path: [loc.event, loc.groupIndex, 'hooks', loc.hookIndex], value: undefined },
453
+ // Append the extracted hook as a new single-hook group with the new matcher.
454
+ {
455
+ path: [loc.event, newGroupIndex],
456
+ value: newMatcher !== null && newMatcher !== undefined && newMatcher !== ''
457
+ ? { matcher: newMatcher, hooks: [extracted] }
458
+ : { hooks: [extracted] },
459
+ },
460
+ ];
461
+ const patched = buildHooksPatch({ ref, expectedMtime: file.mtime, source: file.rawText }, ops);
462
+ const result = await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
463
+ return {
464
+ success: true,
465
+ mtime: result.mtime,
466
+ newGroupIndex,
467
+ newHookIndex: 0,
468
+ };
469
+ }
470
+ // Default — update the parent group's matcher field; siblings inherit.
471
+ const ops = [];
472
+ if (newMatcher === null || newMatcher === '' || newMatcher === undefined) {
473
+ ops.push({ path: [loc.event, loc.groupIndex, 'matcher'], value: undefined });
474
+ }
475
+ else {
476
+ ops.push({ path: [loc.event, loc.groupIndex, 'matcher'], value: newMatcher });
477
+ }
478
+ const patched = buildHooksPatch({ ref, expectedMtime: file.mtime, source: file.rawText }, ops);
479
+ const result = await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
480
+ const response = { success: true, mtime: result.mtime };
481
+ if (groupHookCount >= 2) {
482
+ response.affectedSiblings = groupHookCount - 1;
483
+ }
484
+ return response;
485
+ }
486
+ async copyHook(req) {
487
+ const sourceLoc = await this.resolveCopySource(req);
488
+ const sourceFile = await readFileForLocation(sourceLoc);
489
+ const sourceGroup = sourceFile.groups[req.sourceEvent]?.[req.sourceGroupIndex];
490
+ if (!sourceGroup) {
491
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, `source group not found at ${req.sourceEvent}[${req.sourceGroupIndex}]`);
492
+ }
493
+ const sourceConfig = sourceGroup.hooks[req.sourceHookIndex];
494
+ if (!sourceConfig) {
495
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, 'source hook not found');
496
+ }
497
+ if (req.acknowledgedWarning !== true) {
498
+ const secrets = detectSecretsInHook(sourceConfig);
499
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the type-warning modal and echo acknowledgedWarning', {
500
+ cause: 'type-warning-not-acknowledged',
501
+ details: {
502
+ hookType: sourceConfig.type,
503
+ ...(secrets.matched ? { secretPaths: secrets.paths } : {}),
504
+ },
505
+ });
506
+ }
507
+ if (req.targetScope === 'plugin') {
508
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin destinations are forbidden');
509
+ }
510
+ const targetRef = buildSettingsRef(req.targetScope, req.targetProjectSlug);
511
+ const targetFile = await readHarnessRefFile(targetRef);
512
+ const targetGroups = targetFile.groups[req.sourceEvent] ?? [];
513
+ const matcher = sourceGroup.matcher;
514
+ // Conflict = matcher + body equality.
515
+ const conflictIndex = targetGroups.findIndex((g) => {
516
+ if ((g.matcher ?? '') !== (matcher ?? ''))
517
+ return false;
518
+ if (g.hooks.length !== 1)
519
+ return false;
520
+ return configsEqual(g.hooks[0], sourceConfig);
521
+ });
522
+ if (conflictIndex >= 0) {
523
+ switch (req.onConflict) {
524
+ case 'skip':
525
+ return {
526
+ success: true,
527
+ newGroupIndex: conflictIndex,
528
+ newHookIndex: 0,
529
+ skipped: true,
530
+ };
531
+ case 'overwrite': {
532
+ // Overwrite the existing matcher group's single hook in-place.
533
+ const ops = [
534
+ { path: [req.sourceEvent, conflictIndex, 'hooks', 0], value: sourceConfig },
535
+ ];
536
+ if (matcher !== undefined) {
537
+ ops.push({ path: [req.sourceEvent, conflictIndex, 'matcher'], value: matcher });
538
+ }
539
+ const patched = buildHooksPatch({ ref: targetRef, expectedMtime: targetFile.mtime, source: targetFile.rawText }, ops);
540
+ await writePatched({ ref: targetRef, expectedMtime: targetFile.mtime, source: targetFile.rawText }, patched);
541
+ const warnings = collectCopyWarnings(req, sourceConfig);
542
+ return {
543
+ success: true,
544
+ newGroupIndex: conflictIndex,
545
+ newHookIndex: 0,
546
+ skipped: false,
547
+ ...(warnings.length > 0 ? { warnings } : {}),
548
+ };
549
+ }
550
+ case 'duplicate':
551
+ // Fall through to append a new group below.
552
+ break;
553
+ }
554
+ }
555
+ const newGroupIndex = targetGroups.length;
556
+ const newGroup = matcher
557
+ ? { matcher, hooks: [sourceConfig] }
558
+ : { hooks: [sourceConfig] };
559
+ const patched = buildHooksPatch({ ref: targetRef, expectedMtime: targetFile.mtime, source: targetFile.rawText }, [{ path: [req.sourceEvent, newGroupIndex], value: newGroup }]);
560
+ await writePatched({ ref: targetRef, expectedMtime: targetFile.mtime, source: targetFile.rawText }, patched);
561
+ const warnings = collectCopyWarnings(req, sourceConfig);
562
+ return {
563
+ success: true,
564
+ newGroupIndex,
565
+ newHookIndex: 0,
566
+ skipped: false,
567
+ ...(warnings.length > 0 ? { warnings } : {}),
568
+ };
569
+ }
570
+ async deleteHook(req) {
571
+ const ref = buildSettingsRef(req.scope, req.projectSlug);
572
+ const file = await readHarnessRefFile(ref);
573
+ if (req.expectedMtime !== undefined && req.expectedMtime !== file.mtime) {
574
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
575
+ currentMtime: file.mtime,
576
+ staleFile: 'main',
577
+ });
578
+ }
579
+ const groups = file.groups[req.event] ?? [];
580
+ const group = groups[req.groupIndex];
581
+ if (!group || !group.hooks[req.hookIndex]) {
582
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, 'hook not found');
583
+ }
584
+ const ops = [
585
+ { path: [req.event, req.groupIndex, 'hooks', req.hookIndex], value: undefined },
586
+ ];
587
+ // If this removal empties the group, drop the group too.
588
+ if (group.hooks.length === 1) {
589
+ ops.push({ path: [req.event, req.groupIndex], value: undefined });
590
+ }
591
+ const patched = buildHooksPatch({ ref, expectedMtime: file.mtime, source: file.rawText }, ops);
592
+ await writePatched({ ref, expectedMtime: file.mtime, source: file.rawText }, patched);
593
+ return { success: true };
594
+ }
595
+ // ---- enumeration -------------------------------------------------------
596
+ async enumerateProjectHooks(projectSlug, cards, malformed) {
597
+ const settingsRef = { scope: 'project', projectSlug, relativePath: 'settings.json' };
598
+ const projectRoot = await projectService.resolveOriginalPath(projectSlug);
599
+ const settingsAbs = path.join(projectRoot, '.claude', 'settings.json');
600
+ await this.collectFromHarnessRef('project', settingsRef, settingsAbs, cards, malformed, false, projectSlug);
601
+ const backupRef = { scope: 'project', projectSlug, relativePath: 'hooks.disabled.json' };
602
+ const backupAbs = path.join(projectRoot, '.claude', 'hooks.disabled.json');
603
+ await this.collectFromHarnessRef('project', backupRef, backupAbs, cards, malformed, true, projectSlug);
604
+ }
605
+ async enumerateUserHooks(cards, malformed) {
606
+ const settingsRef = { scope: 'user', relativePath: 'settings.json' };
607
+ const settingsAbs = path.join(getUserHarnessRoot(), 'settings.json');
608
+ await this.collectFromHarnessRef('user', settingsRef, settingsAbs, cards, malformed, false);
609
+ const backupRef = { scope: 'user', relativePath: 'hooks.disabled.json' };
610
+ const backupAbs = path.join(getUserHarnessRoot(), 'hooks.disabled.json');
611
+ await this.collectFromHarnessRef('user', backupRef, backupAbs, cards, malformed, true);
612
+ }
613
+ async enumeratePluginHooks(cards, malformed) {
614
+ let installed = {};
615
+ try {
616
+ const res = await harnessService.read({
617
+ scope: 'user',
618
+ relativePath: 'plugins/installed_plugins.json',
619
+ });
620
+ const trimmed = (res.content ?? '').trim();
621
+ if (trimmed) {
622
+ try {
623
+ installed = JSON.parse(trimmed);
624
+ }
625
+ catch {
626
+ return;
627
+ }
628
+ }
629
+ }
630
+ catch (err) {
631
+ if (isFileNotFound(err))
632
+ return;
633
+ throw err;
634
+ }
635
+ const plugins = installed.plugins ?? {};
636
+ for (const [pluginKey, value] of Object.entries(plugins)) {
637
+ const entries = Array.isArray(value) ? value : [value];
638
+ for (const entry of entries) {
639
+ if (!entry?.installPath || typeof entry.installPath !== 'string')
640
+ continue;
641
+ const installRoot = path.resolve(entry.installPath);
642
+ const hooksFile = path.join(entry.installPath, 'hooks', 'hooks.json');
643
+ const resolved = path.resolve(hooksFile);
644
+ if (resolved !== installRoot && !resolved.startsWith(installRoot + path.sep))
645
+ continue;
646
+ const file = await readPluginHooksFile(hooksFile);
647
+ if (!file.present && file.mtime === '') {
648
+ continue;
649
+ }
650
+ if (!file.present && file.rawText.length > 0) {
651
+ malformed.push({
652
+ scope: 'plugin',
653
+ absoluteFile: hooksFile,
654
+ pluginKey,
655
+ reason: 'failed to parse JSON',
656
+ });
657
+ continue;
658
+ }
659
+ for (const event of HARNESS_HOOK_EVENTS) {
660
+ const groups = file.groups[event];
661
+ for (let gi = 0; gi < groups.length; gi += 1) {
662
+ const group = groups[gi];
663
+ for (let hi = 0; hi < group.hooks.length; hi += 1) {
664
+ cards[event].push({
665
+ scope: 'plugin',
666
+ absoluteFile: hooksFile,
667
+ pluginKey,
668
+ event,
669
+ groupIndex: gi,
670
+ hookIndex: hi,
671
+ disabledByBackup: false,
672
+ matcher: group.matcher,
673
+ config: group.hooks[hi],
674
+ mtime: file.mtime,
675
+ enabled: true,
676
+ });
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ async collectFromHarnessRef(scope, ref, absoluteFile, cards, malformed, disabledByBackup, projectSlug) {
684
+ let file;
685
+ try {
686
+ file = await readHarnessRefFile(ref);
687
+ }
688
+ catch (err) {
689
+ if (err?.code === HARNESS_ERRORS.HARNESS_PARSE_ERROR.code) {
690
+ malformed.push({
691
+ scope,
692
+ absoluteFile,
693
+ projectSlug,
694
+ reason: 'failed to parse JSON',
695
+ });
696
+ return;
697
+ }
698
+ throw err;
699
+ }
700
+ if (!file.present)
701
+ return;
702
+ for (const event of HARNESS_HOOK_EVENTS) {
703
+ const groups = file.groups[event];
704
+ for (let gi = 0; gi < groups.length; gi += 1) {
705
+ const group = groups[gi];
706
+ for (let hi = 0; hi < group.hooks.length; hi += 1) {
707
+ cards[event].push({
708
+ scope,
709
+ absoluteFile,
710
+ projectSlug,
711
+ event,
712
+ groupIndex: gi,
713
+ hookIndex: hi,
714
+ disabledByBackup,
715
+ matcher: group.matcher,
716
+ config: group.hooks[hi],
717
+ mtime: file.mtime,
718
+ enabled: !disabledByBackup,
719
+ });
720
+ }
721
+ }
722
+ }
723
+ }
724
+ async collectBackupMtimes(currentProjectSlug) {
725
+ const out = {};
726
+ try {
727
+ const userBackup = path.join(getUserHarnessRoot(), 'hooks.disabled.json');
728
+ const stat = await fs.stat(userBackup);
729
+ if (stat.isFile())
730
+ out.user = stat.mtime.toISOString();
731
+ }
732
+ catch {
733
+ // file absent — leave out.user undefined
734
+ }
735
+ if (currentProjectSlug) {
736
+ try {
737
+ const projectRoot = await projectService.resolveOriginalPath(currentProjectSlug);
738
+ const projBackup = path.join(projectRoot, '.claude', 'hooks.disabled.json');
739
+ const stat = await fs.stat(projBackup);
740
+ if (stat.isFile())
741
+ out.project = stat.mtime.toISOString();
742
+ }
743
+ catch {
744
+ // file absent — leave out.project undefined
745
+ }
746
+ }
747
+ return out;
748
+ }
749
+ async resolveCopySource(req) {
750
+ if (req.sourceScope === 'plugin') {
751
+ if (!req.sourcePluginKey) {
752
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'sourcePluginKey required for scope=plugin');
753
+ }
754
+ const installPath = await readPluginInstallPath(req.sourcePluginKey);
755
+ if (!installPath) {
756
+ throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${req.sourcePluginKey}`);
757
+ }
758
+ const hooksFile = path.join(installPath, 'hooks', 'hooks.json');
759
+ const installRoot = path.resolve(installPath);
760
+ const abs = path.resolve(hooksFile);
761
+ if (abs !== installRoot && !abs.startsWith(installRoot + path.sep)) {
762
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
763
+ }
764
+ return {
765
+ scope: 'plugin',
766
+ absoluteFile: hooksFile,
767
+ pluginKey: req.sourcePluginKey,
768
+ event: req.sourceEvent,
769
+ groupIndex: req.sourceGroupIndex,
770
+ hookIndex: req.sourceHookIndex,
771
+ disabledByBackup: false,
772
+ };
773
+ }
774
+ if (req.sourceScope === 'project') {
775
+ if (!req.sourceProjectSlug) {
776
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'sourceProjectSlug required for scope=project');
777
+ }
778
+ const projectRoot = await projectService.resolveOriginalPath(req.sourceProjectSlug);
779
+ return {
780
+ scope: 'project',
781
+ absoluteFile: path.join(projectRoot, '.claude', 'settings.json'),
782
+ projectSlug: req.sourceProjectSlug,
783
+ event: req.sourceEvent,
784
+ groupIndex: req.sourceGroupIndex,
785
+ hookIndex: req.sourceHookIndex,
786
+ disabledByBackup: false,
787
+ };
788
+ }
789
+ return {
790
+ scope: 'user',
791
+ absoluteFile: path.join(getUserHarnessRoot(), 'settings.json'),
792
+ event: req.sourceEvent,
793
+ groupIndex: req.sourceGroupIndex,
794
+ hookIndex: req.sourceHookIndex,
795
+ disabledByBackup: false,
796
+ };
797
+ }
798
+ // ---- enabled toggle (AC5) ---------------------------------------------
799
+ async toggleEnabled(loc, enabled, expectedMainMtime, expectedBackupMtime) {
800
+ const editableScope = loc.scope;
801
+ const mainRef = buildSettingsRef(editableScope, loc.projectSlug);
802
+ const backupRef = buildBackupRef(editableScope, loc.projectSlug);
803
+ if (enabled) {
804
+ // backup → main: source lives in the backup file currently.
805
+ if (!loc.disabledByBackup) {
806
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'enable can only run against a backup-resident hook');
807
+ }
808
+ const backupFile = await readHarnessRefFile(backupRef);
809
+ if (expectedBackupMtime !== undefined && expectedBackupMtime !== backupFile.mtime) {
810
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'backup changed on disk', {
811
+ currentMtime: backupFile.mtime,
812
+ staleFile: 'backup',
813
+ });
814
+ }
815
+ const backupGroup = backupFile.groups[loc.event]?.[loc.groupIndex];
816
+ const cfg = backupGroup?.hooks[loc.hookIndex];
817
+ if (!cfg) {
818
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, 'hook not in backup');
819
+ }
820
+ const matcher = backupGroup.matcher;
821
+ const mainFile = await readHarnessRefFile(mainRef);
822
+ if (expectedMainMtime !== undefined && expectedMainMtime !== mainFile.mtime) {
823
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'main changed on disk', {
824
+ currentMtime: mainFile.mtime,
825
+ staleFile: 'main',
826
+ });
827
+ }
828
+ const mainGroups = mainFile.groups[loc.event] ?? [];
829
+ const newGroup = matcher
830
+ ? { matcher, hooks: [cfg] }
831
+ : { hooks: [cfg] };
832
+ const newMainIndex = mainGroups.length;
833
+ const mainPatched = buildHooksPatch({ ref: mainRef, expectedMtime: mainFile.mtime, source: mainFile.rawText }, [{ path: [loc.event, newMainIndex], value: newGroup }]);
834
+ const mainWrite = await writePatched({ ref: mainRef, expectedMtime: mainFile.mtime, source: mainFile.rawText }, mainPatched);
835
+ // Now drop the group from the backup. If a sibling lives there, only
836
+ // remove the single hook entry; otherwise drop the group entirely.
837
+ const backupOps = [
838
+ { path: [loc.event, loc.groupIndex, 'hooks', loc.hookIndex], value: undefined },
839
+ ];
840
+ if (backupGroup.hooks.length === 1) {
841
+ backupOps.push({ path: [loc.event, loc.groupIndex], value: undefined });
842
+ }
843
+ try {
844
+ const backupPatched = buildHooksPatch({ ref: backupRef, expectedMtime: backupFile.mtime, source: backupFile.rawText }, backupOps);
845
+ const backupWrite = await writePatched({ ref: backupRef, expectedMtime: backupFile.mtime, source: backupFile.rawText }, backupPatched);
846
+ return {
847
+ success: true,
848
+ mtime: mainWrite.mtime,
849
+ backupMtime: backupWrite.mtime,
850
+ };
851
+ }
852
+ catch (err) {
853
+ // Rollback: drop the group we just appended to main.
854
+ await writePatched({ ref: mainRef, expectedMtime: mainWrite.mtime, source: '' }, buildHooksPatch({ ref: mainRef, expectedMtime: mainWrite.mtime, source: mainPatched }, [{ path: [loc.event, newMainIndex], value: undefined }])).catch((rollbackErr) => {
855
+ log.warn(`enable rollback failed for ${loc.event}[${loc.groupIndex}]: ${rollbackErr.message}`);
856
+ });
857
+ if (isStaleWrite(err)) {
858
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'backup changed on disk', {
859
+ currentMtime: err.currentMtime ?? '',
860
+ staleFile: 'backup',
861
+ });
862
+ }
863
+ throw err;
864
+ }
865
+ }
866
+ // enabled === false → main → backup move
867
+ if (loc.disabledByBackup) {
868
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'disable can only run against a main-resident hook');
869
+ }
870
+ const mainFile = await readHarnessRefFile(mainRef);
871
+ if (expectedMainMtime !== undefined && expectedMainMtime !== mainFile.mtime) {
872
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'main changed on disk', {
873
+ currentMtime: mainFile.mtime,
874
+ staleFile: 'main',
875
+ });
876
+ }
877
+ const mainGroup = mainFile.groups[loc.event]?.[loc.groupIndex];
878
+ const cfg = mainGroup?.hooks[loc.hookIndex];
879
+ if (!cfg) {
880
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_NOT_FOUND.code, 'hook not in main');
881
+ }
882
+ const matcher = mainGroup.matcher;
883
+ const backupFile = await readHarnessRefFile(backupRef);
884
+ if (backupFile.present &&
885
+ expectedBackupMtime !== undefined &&
886
+ expectedBackupMtime !== backupFile.mtime) {
887
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'backup changed on disk', {
888
+ currentMtime: backupFile.mtime,
889
+ staleFile: 'backup',
890
+ });
891
+ }
892
+ const backupGroups = backupFile.groups[loc.event] ?? [];
893
+ const newBackupIndex = backupGroups.length;
894
+ const newBackupGroup = matcher
895
+ ? { matcher, hooks: [cfg] }
896
+ : { hooks: [cfg] };
897
+ const backupPatched = buildHooksPatch({ ref: backupRef, expectedMtime: backupFile.mtime, source: backupFile.rawText }, [{ path: [loc.event, newBackupIndex], value: newBackupGroup }]);
898
+ const backupWrite = await writePatched({ ref: backupRef, expectedMtime: backupFile.mtime, source: backupFile.rawText }, backupPatched);
899
+ const mainOps = [
900
+ { path: [loc.event, loc.groupIndex, 'hooks', loc.hookIndex], value: undefined },
901
+ ];
902
+ if (mainGroup.hooks.length === 1) {
903
+ mainOps.push({ path: [loc.event, loc.groupIndex], value: undefined });
904
+ }
905
+ try {
906
+ const mainPatched = buildHooksPatch({ ref: mainRef, expectedMtime: mainFile.mtime, source: mainFile.rawText }, mainOps);
907
+ const mainWrite = await writePatched({ ref: mainRef, expectedMtime: mainFile.mtime, source: mainFile.rawText }, mainPatched);
908
+ return {
909
+ success: true,
910
+ mtime: mainWrite.mtime,
911
+ backupMtime: backupWrite.mtime,
912
+ };
913
+ }
914
+ catch (err) {
915
+ // Rollback: drop the group we just appended to backup.
916
+ const rollbackPatched = buildHooksPatch({ ref: backupRef, expectedMtime: backupWrite.mtime, source: backupPatched }, [{ path: [loc.event, newBackupIndex], value: undefined }]);
917
+ await writePatched({ ref: backupRef, expectedMtime: backupWrite.mtime, source: backupPatched }, rollbackPatched).catch((rollbackErr) => {
918
+ log.warn(`disable rollback failed for ${loc.event}[${loc.groupIndex}]: ${rollbackErr.message}`);
919
+ });
920
+ if (isStaleWrite(err)) {
921
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'main changed on disk', {
922
+ currentMtime: err.currentMtime ?? '',
923
+ staleFile: 'main',
924
+ });
925
+ }
926
+ throw err;
927
+ }
928
+ }
929
+ }
930
+ // ---- shared helpers --------------------------------------------------------
931
+ async function readFileForLocation(loc) {
932
+ if (loc.scope === 'plugin') {
933
+ return readPluginHooksFile(loc.absoluteFile);
934
+ }
935
+ const ref = loc.disabledByBackup
936
+ ? buildBackupRef(loc.scope, loc.projectSlug)
937
+ : buildSettingsRef(loc.scope, loc.projectSlug);
938
+ return readHarnessRefFile(ref);
939
+ }
940
+ async function readPluginInstallPath(pluginKey) {
941
+ try {
942
+ const res = await harnessService.read({
943
+ scope: 'user',
944
+ relativePath: 'plugins/installed_plugins.json',
945
+ });
946
+ const trimmed = (res.content ?? '').trim();
947
+ if (!trimmed)
948
+ return undefined;
949
+ const parsed = JSON.parse(trimmed);
950
+ const raw = parsed.plugins?.[pluginKey];
951
+ if (!raw)
952
+ return undefined;
953
+ const entries = Array.isArray(raw) ? raw : [raw];
954
+ const first = entries.find((e) => typeof e?.installPath === 'string');
955
+ return first?.installPath;
956
+ }
957
+ catch (err) {
958
+ if (isFileNotFound(err))
959
+ return undefined;
960
+ throw err;
961
+ }
962
+ }
963
+ function configsEqual(a, b) {
964
+ return (a.type === b.type &&
965
+ (a.command ?? '') === (b.command ?? '') &&
966
+ (a.prompt ?? '') === (b.prompt ?? '') &&
967
+ (a.timeout ?? null) === (b.timeout ?? null));
968
+ }
969
+ function collectCopyWarnings(req, config) {
970
+ const warnings = [];
971
+ if (req.sourceScope === 'plugin' && containsPluginRootToken(config)) {
972
+ warnings.push('plugin-root-reference');
973
+ }
974
+ return warnings;
975
+ }
976
+ function validateConfigShape(config) {
977
+ if (config.type !== 'command' && config.type !== 'prompt') {
978
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `unknown hook type: ${String(config.type)}`);
979
+ }
980
+ if (config.type === 'command') {
981
+ if (!config.command || typeof config.command !== 'string') {
982
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'command is required for type=command');
983
+ }
984
+ if (config.prompt) {
985
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'prompt forbidden for type=command');
986
+ }
987
+ }
988
+ else {
989
+ if (!config.prompt || typeof config.prompt !== 'string') {
990
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'prompt is required for type=prompt');
991
+ }
992
+ if (config.command) {
993
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'command forbidden for type=prompt');
994
+ }
995
+ }
996
+ }
997
+ export async function resolveSourceLocation(input) {
998
+ const { scope, event, groupIndex, hookIndex } = input;
999
+ if (!HARNESS_HOOK_EVENTS.includes(event)) {
1000
+ throwMapped(HARNESS_ERRORS.HARNESS_HOOK_INVALID_EVENT.code, `unknown event: ${event}`);
1001
+ }
1002
+ if (scope === 'project') {
1003
+ if (!input.projectSlug) {
1004
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
1005
+ }
1006
+ const projectRoot = await projectService.resolveOriginalPath(input.projectSlug);
1007
+ const filePath = input.disabledByBackup
1008
+ ? path.join(projectRoot, '.claude', 'hooks.disabled.json')
1009
+ : path.join(projectRoot, '.claude', 'settings.json');
1010
+ return {
1011
+ scope: 'project',
1012
+ absoluteFile: filePath,
1013
+ projectSlug: input.projectSlug,
1014
+ event,
1015
+ groupIndex,
1016
+ hookIndex,
1017
+ disabledByBackup: input.disabledByBackup === true,
1018
+ };
1019
+ }
1020
+ if (scope === 'user') {
1021
+ const filePath = input.disabledByBackup
1022
+ ? path.join(getUserHarnessRoot(), 'hooks.disabled.json')
1023
+ : path.join(getUserHarnessRoot(), 'settings.json');
1024
+ return {
1025
+ scope: 'user',
1026
+ absoluteFile: filePath,
1027
+ event,
1028
+ groupIndex,
1029
+ hookIndex,
1030
+ disabledByBackup: input.disabledByBackup === true,
1031
+ };
1032
+ }
1033
+ if (!input.pluginKey) {
1034
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'pluginKey required for scope=plugin');
1035
+ }
1036
+ const installPath = await readPluginInstallPath(input.pluginKey);
1037
+ if (!installPath) {
1038
+ throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${input.pluginKey}`);
1039
+ }
1040
+ const hooksFile = path.join(installPath, 'hooks', 'hooks.json');
1041
+ const installRoot = path.resolve(installPath);
1042
+ const abs = path.resolve(hooksFile);
1043
+ if (abs !== installRoot && !abs.startsWith(installRoot + path.sep)) {
1044
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
1045
+ }
1046
+ return {
1047
+ scope: 'plugin',
1048
+ absoluteFile: hooksFile,
1049
+ pluginKey: input.pluginKey,
1050
+ event,
1051
+ groupIndex,
1052
+ hookIndex,
1053
+ disabledByBackup: false,
1054
+ };
1055
+ }
1056
+ export const harnessHookService = new HarnessHookService();
1057
+ export const SPIKE_RESULTS = {
1058
+ promptTypeSupport: PROMPT_TYPE_SUPPORT,
1059
+ };
1060
+ //# sourceMappingURL=harnessHookService.js.map