hammoc 1.4.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 (252) hide show
  1. package/README.md +422 -405
  2. package/bin/hammoc.js +0 -6
  3. package/package.json +100 -94
  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 +13 -21
  14. package/packages/server/dist/app.js.map +1 -1
  15. package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
  16. package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
  17. package/packages/server/dist/controllers/claudeMdController.js +158 -0
  18. package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
  19. package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
  20. package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
  21. package/packages/server/dist/controllers/harnessAgentController.js +339 -0
  22. package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
  23. package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
  24. package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
  25. package/packages/server/dist/controllers/harnessCommandController.js +382 -0
  26. package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
  27. package/packages/server/dist/controllers/harnessController.d.ts +21 -0
  28. package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
  29. package/packages/server/dist/controllers/harnessController.js +176 -0
  30. package/packages/server/dist/controllers/harnessController.js.map +1 -0
  31. package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
  32. package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
  33. package/packages/server/dist/controllers/harnessHookController.js +363 -0
  34. package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
  35. package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
  36. package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
  37. package/packages/server/dist/controllers/harnessLintController.js +72 -0
  38. package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
  39. package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
  40. package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
  41. package/packages/server/dist/controllers/harnessMcpController.js +310 -0
  42. package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
  43. package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
  44. package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
  45. package/packages/server/dist/controllers/harnessPluginController.js +115 -0
  46. package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
  47. package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
  48. package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
  49. package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
  50. package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
  51. package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
  52. package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
  53. package/packages/server/dist/controllers/harnessSkillController.js +453 -0
  54. package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
  55. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  56. package/packages/server/dist/controllers/projectController.js +11 -0
  57. package/packages/server/dist/controllers/projectController.js.map +1 -1
  58. package/packages/server/dist/controllers/snippetController.d.ts +35 -0
  59. package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
  60. package/packages/server/dist/controllers/snippetController.js +294 -0
  61. package/packages/server/dist/controllers/snippetController.js.map +1 -0
  62. package/packages/server/dist/handlers/websocket.d.ts +15 -0
  63. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  64. package/packages/server/dist/handlers/websocket.js +79 -0
  65. package/packages/server/dist/handlers/websocket.js.map +1 -1
  66. package/packages/server/dist/index.js +5 -0
  67. package/packages/server/dist/index.js.map +1 -1
  68. package/packages/server/dist/locales/en/server.json +37 -4
  69. package/packages/server/dist/locales/es/server.json +0 -4
  70. package/packages/server/dist/locales/ja/server.json +0 -4
  71. package/packages/server/dist/locales/ko/server.json +0 -4
  72. package/packages/server/dist/locales/pt/server.json +0 -4
  73. package/packages/server/dist/locales/zh-CN/server.json +0 -4
  74. package/packages/server/dist/routes/harness.d.ts +8 -0
  75. package/packages/server/dist/routes/harness.d.ts.map +1 -0
  76. package/packages/server/dist/routes/harness.js +92 -0
  77. package/packages/server/dist/routes/harness.js.map +1 -0
  78. package/packages/server/dist/routes/projects.d.ts.map +1 -1
  79. package/packages/server/dist/routes/projects.js +5 -60
  80. package/packages/server/dist/routes/projects.js.map +1 -1
  81. package/packages/server/dist/routes/snippets.d.ts +14 -0
  82. package/packages/server/dist/routes/snippets.d.ts.map +1 -0
  83. package/packages/server/dist/routes/snippets.js +27 -0
  84. package/packages/server/dist/routes/snippets.js.map +1 -0
  85. package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
  86. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  87. package/packages/server/dist/services/bmadStatusService.js +88 -32
  88. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  89. package/packages/server/dist/services/chatService.d.ts +3 -0
  90. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  91. package/packages/server/dist/services/chatService.js +27 -6
  92. package/packages/server/dist/services/chatService.js.map +1 -1
  93. package/packages/server/dist/services/claudeMdService.d.ts +48 -0
  94. package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
  95. package/packages/server/dist/services/claudeMdService.js +240 -0
  96. package/packages/server/dist/services/claudeMdService.js.map +1 -0
  97. package/packages/server/dist/services/commandService.d.ts +10 -0
  98. package/packages/server/dist/services/commandService.d.ts.map +1 -1
  99. package/packages/server/dist/services/commandService.js +129 -4
  100. package/packages/server/dist/services/commandService.js.map +1 -1
  101. package/packages/server/dist/services/fileWatcherService.d.ts +24 -0
  102. package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -1
  103. package/packages/server/dist/services/fileWatcherService.js +192 -1
  104. package/packages/server/dist/services/fileWatcherService.js.map +1 -1
  105. package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
  106. package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
  107. package/packages/server/dist/services/harnessAgentService.js +933 -0
  108. package/packages/server/dist/services/harnessAgentService.js.map +1 -0
  109. package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
  110. package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
  111. package/packages/server/dist/services/harnessCommandService.js +853 -0
  112. package/packages/server/dist/services/harnessCommandService.js.map +1 -0
  113. package/packages/server/dist/services/harnessHookService.d.ts +55 -0
  114. package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
  115. package/packages/server/dist/services/harnessHookService.js +1060 -0
  116. package/packages/server/dist/services/harnessHookService.js.map +1 -0
  117. package/packages/server/dist/services/harnessLintService.d.ts +49 -0
  118. package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
  119. package/packages/server/dist/services/harnessLintService.js +628 -0
  120. package/packages/server/dist/services/harnessLintService.js.map +1 -0
  121. package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
  122. package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
  123. package/packages/server/dist/services/harnessMcpService.js +814 -0
  124. package/packages/server/dist/services/harnessMcpService.js.map +1 -0
  125. package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
  126. package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
  127. package/packages/server/dist/services/harnessPluginService.js +559 -0
  128. package/packages/server/dist/services/harnessPluginService.js.map +1 -0
  129. package/packages/server/dist/services/harnessService.d.ts +40 -0
  130. package/packages/server/dist/services/harnessService.d.ts.map +1 -0
  131. package/packages/server/dist/services/harnessService.js +222 -0
  132. package/packages/server/dist/services/harnessService.js.map +1 -0
  133. package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
  134. package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
  135. package/packages/server/dist/services/harnessShareScopeService.js +93 -0
  136. package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
  137. package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
  138. package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
  139. package/packages/server/dist/services/harnessSkillService.js +636 -0
  140. package/packages/server/dist/services/harnessSkillService.js.map +1 -0
  141. package/packages/server/dist/services/issueService.d.ts.map +1 -1
  142. package/packages/server/dist/services/issueService.js +2 -1
  143. package/packages/server/dist/services/issueService.js.map +1 -1
  144. package/packages/server/dist/services/manualSyncService.d.ts +19 -0
  145. package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
  146. package/packages/server/dist/services/manualSyncService.js +110 -0
  147. package/packages/server/dist/services/manualSyncService.js.map +1 -0
  148. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  149. package/packages/server/dist/services/queueService.js +45 -2
  150. package/packages/server/dist/services/queueService.js.map +1 -1
  151. package/packages/server/dist/services/snippetService.d.ts +54 -0
  152. package/packages/server/dist/services/snippetService.d.ts.map +1 -0
  153. package/packages/server/dist/services/snippetService.js +371 -0
  154. package/packages/server/dist/services/snippetService.js.map +1 -0
  155. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
  156. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
  157. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
  158. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
  159. package/packages/server/dist/snippets/split-commit +9 -0
  160. package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
  161. package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
  162. package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
  163. package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
  164. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
  165. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
  166. package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
  167. package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
  168. package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
  169. package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
  170. package/packages/server/dist/utils/gitignoreFilter.js +42 -0
  171. package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
  172. package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
  173. package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
  174. package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
  175. package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
  176. package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
  177. package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
  178. package/packages/server/dist/utils/harnessPaths.js +124 -0
  179. package/packages/server/dist/utils/harnessPaths.js.map +1 -0
  180. package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
  181. package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
  182. package/packages/server/dist/utils/secretHeuristic.js +163 -0
  183. package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
  184. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
  185. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
  186. package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
  187. package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
  188. package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
  189. package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
  190. package/packages/server/dist/utils/serverPathResolver.js +59 -0
  191. package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
  192. package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
  193. package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
  194. package/packages/server/dist/utils/snippetPaths.js +123 -0
  195. package/packages/server/dist/utils/snippetPaths.js.map +1 -0
  196. package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
  197. package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
  198. package/packages/server/dist/utils/structuredEditor.js +111 -0
  199. package/packages/server/dist/utils/structuredEditor.js.map +1 -0
  200. package/packages/server/package.json +4 -1
  201. package/packages/server/resources/internals/INDEX.md +23 -0
  202. package/packages/server/resources/internals/harness-files.md +63 -0
  203. package/packages/server/resources/internals/image-storage.md +43 -0
  204. package/packages/server/resources/manual/01-getting-started.md +104 -0
  205. package/packages/server/resources/manual/02-chat.md +285 -0
  206. package/packages/server/resources/manual/03-sessions.md +48 -0
  207. package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
  208. package/packages/server/resources/manual/05-projects.md +74 -0
  209. package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
  210. package/packages/server/resources/manual/07-git.md +94 -0
  211. package/packages/server/resources/manual/08-terminal.md +59 -0
  212. package/packages/server/resources/manual/09-queue-runner.md +262 -0
  213. package/packages/server/resources/manual/10-project-board.md +193 -0
  214. package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
  215. package/packages/server/resources/manual/12-harness-workbench.md +175 -0
  216. package/packages/server/resources/manual/13-settings.md +241 -0
  217. package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
  218. package/packages/server/resources/manual/15-environment-variables.md +28 -0
  219. package/packages/server/resources/manual/16-troubleshooting.md +110 -0
  220. package/packages/server/resources/manual/INDEX.md +60 -0
  221. package/packages/shared/dist/index.d.ts +3 -0
  222. package/packages/shared/dist/index.d.ts.map +1 -1
  223. package/packages/shared/dist/index.js +6 -0
  224. package/packages/shared/dist/index.js.map +1 -1
  225. package/packages/shared/dist/types/command.d.ts +3 -3
  226. package/packages/shared/dist/types/command.d.ts.map +1 -1
  227. package/packages/shared/dist/types/harness.d.ts +1211 -0
  228. package/packages/shared/dist/types/harness.d.ts.map +1 -0
  229. package/packages/shared/dist/types/harness.js +107 -0
  230. package/packages/shared/dist/types/harness.js.map +1 -0
  231. package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
  232. package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
  233. package/packages/shared/dist/types/harnessBundle.js +18 -0
  234. package/packages/shared/dist/types/harnessBundle.js.map +1 -0
  235. package/packages/shared/dist/types/preferences.d.ts +2 -0
  236. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  237. package/packages/shared/dist/types/preferences.js.map +1 -1
  238. package/packages/shared/dist/types/queue.d.ts +9 -0
  239. package/packages/shared/dist/types/queue.d.ts.map +1 -1
  240. package/packages/shared/dist/types/websocket.d.ts +10 -0
  241. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  242. package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
  243. package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
  244. package/packages/shared/dist/utils/markdownSections.js +111 -0
  245. package/packages/shared/dist/utils/markdownSections.js.map +1 -0
  246. package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
  247. package/packages/shared/dist/utils/queueParser.js +104 -0
  248. package/packages/shared/dist/utils/queueParser.js.map +1 -1
  249. package/scripts/build-manual-shards.mjs +100 -0
  250. package/packages/client/dist/assets/index-6jREnVYd.js +0 -2
  251. package/packages/client/dist/assets/index-BFF0iqyW.css +0 -32
  252. package/packages/client/dist/assets/index-BcI4y-fU.js +0 -1454
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Story 29.2: Snippet management controller.
3
+ *
4
+ * Mounts at `/api/snippets/*` (NOT `/api/harness/*`) — the snippet system is
5
+ * Hammoc-native (`%name%` chat-input expansion via `snippetResolver`), not a
6
+ * Claude Code harness primitive, so its API namespace is intentionally
7
+ * separate from `/api/harness/snippets` to keep intent boundaries crisp.
8
+ *
9
+ * Error envelope mirrors `claudeMdController` and `harnessController` so the
10
+ * client can route both response shapes through the same error-display logic:
11
+ * `{ error: { code, message, details? } }`
12
+ *
13
+ * Origin-socket broadcast (AC1.e): mutation endpoints (POST/PUT/DELETE/copy)
14
+ * read `X-Hammoc-Socket-Id` and `X-Hammoc-Working-Directory` headers and
15
+ * forward them to `broadcastSnippetList()` so the originating client's
16
+ * autocomplete surfaces (`SnippetPalette` via `useSnippets`) re-receive a
17
+ * fresh `snippets:list` payload without an explicit `refresh()` call.
18
+ * Phase-1 fan-out is single-socket; multi-tab sync is deferred (story AC1.e).
19
+ */
20
+ import { z } from 'zod';
21
+ import { HARNESS_ERRORS } from '@hammoc/shared';
22
+ import { snippetService } from '../services/snippetService.js';
23
+ import { broadcastSnippetList } from '../handlers/websocket.js';
24
+ // ---------------------------------------------------------------------------
25
+ // Schemas
26
+ // ---------------------------------------------------------------------------
27
+ const SCOPE_SCHEMA = z.enum(['project', 'user', 'bundled']);
28
+ const MUTABLE_SCOPE_SCHEMA = z.enum(['project', 'user']);
29
+ const refParamsSchema = z
30
+ .object({
31
+ scope: SCOPE_SCHEMA,
32
+ name: z.string().min(1),
33
+ });
34
+ const refQuerySchema = z.object({
35
+ projectSlug: z.string().optional(),
36
+ });
37
+ const listQuerySchema = z.object({
38
+ projectSlug: z.string().optional(),
39
+ });
40
+ const writeBodySchema = z.object({
41
+ content: z.string(),
42
+ expectedMtime: z.string().optional(),
43
+ projectSlug: z.string().optional(),
44
+ });
45
+ const deleteBodySchema = z.object({
46
+ expectedMtime: z.string().optional(),
47
+ projectSlug: z.string().optional(),
48
+ });
49
+ const copyBodySchema = z.object({
50
+ sourceScope: SCOPE_SCHEMA,
51
+ sourceName: z.string().min(1),
52
+ sourceProjectSlug: z.string().optional(),
53
+ targetScope: MUTABLE_SCOPE_SCHEMA,
54
+ targetName: z.string().min(1).optional(),
55
+ targetProjectSlug: z.string().optional(),
56
+ onConflict: z.enum(['abort', 'overwrite', 'rename']).optional(),
57
+ });
58
+ // ---------------------------------------------------------------------------
59
+ // Error mapping
60
+ // ---------------------------------------------------------------------------
61
+ const MAPPED_CODES = [
62
+ 'HARNESS_PATH_DENIED',
63
+ 'HARNESS_FORBIDDEN',
64
+ 'HARNESS_FILE_NOT_FOUND',
65
+ 'HARNESS_ROOT_MISSING',
66
+ 'HARNESS_STALE_WRITE',
67
+ 'HARNESS_FILE_EXISTS',
68
+ 'HARNESS_BUNDLED_READONLY',
69
+ 'HARNESS_PARSE_ERROR',
70
+ ];
71
+ const MESSAGE_KEY = {
72
+ HARNESS_PATH_DENIED: 'harness.error.pathDenied',
73
+ HARNESS_FORBIDDEN: 'harness.error.forbidden',
74
+ HARNESS_FILE_NOT_FOUND: 'harness.error.fileNotFound',
75
+ HARNESS_ROOT_MISSING: 'harness.error.rootMissing',
76
+ HARNESS_STALE_WRITE: 'harness.error.staleWrite',
77
+ HARNESS_FILE_EXISTS: 'harness.snippets.error.fileExists',
78
+ HARNESS_BUNDLED_READONLY: 'harness.snippets.error.bundledReadOnly',
79
+ HARNESS_PARSE_ERROR: 'harness.error.parseError',
80
+ };
81
+ function handleError(req, res, error) {
82
+ const nodeError = error;
83
+ for (const key of MAPPED_CODES) {
84
+ const entry = HARNESS_ERRORS[key];
85
+ if (nodeError.code === entry.code) {
86
+ const body = {
87
+ code: entry.code,
88
+ message: req.t(MESSAGE_KEY[key]),
89
+ };
90
+ if (key === 'HARNESS_STALE_WRITE') {
91
+ body.details = { currentMtime: nodeError.currentMtime ?? '' };
92
+ }
93
+ if (key === 'HARNESS_FILE_NOT_FOUND' && nodeError.absolutePath) {
94
+ body.details = { absolutePath: nodeError.absolutePath };
95
+ }
96
+ res.status(entry.httpStatus).json({ error: body });
97
+ return;
98
+ }
99
+ }
100
+ res.status(HARNESS_ERRORS.HARNESS_WRITE_ERROR.httpStatus).json({
101
+ error: {
102
+ code: HARNESS_ERRORS.HARNESS_WRITE_ERROR.code,
103
+ message: req.t('harness.error.writeError'),
104
+ },
105
+ });
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Helpers
109
+ // ---------------------------------------------------------------------------
110
+ function parseRefParams(req, res, projectSlug) {
111
+ const parsed = refParamsSchema.safeParse(req.params);
112
+ if (!parsed.success) {
113
+ res.status(400).json({
114
+ error: {
115
+ code: 'INVALID_REQUEST',
116
+ message: parsed.error.issues[0]?.message ?? 'invalid params',
117
+ },
118
+ });
119
+ return null;
120
+ }
121
+ if (parsed.data.scope === 'project' && !projectSlug) {
122
+ res.status(400).json({
123
+ error: {
124
+ code: 'INVALID_REQUEST',
125
+ message: 'projectSlug is required for project scope',
126
+ },
127
+ });
128
+ return null;
129
+ }
130
+ return {
131
+ scope: parsed.data.scope,
132
+ projectSlug: parsed.data.scope === 'project' ? projectSlug : undefined,
133
+ name: parsed.data.name,
134
+ };
135
+ }
136
+ function readBroadcastHeaders(req) {
137
+ const socketId = stringHeader(req.header('x-hammoc-socket-id'));
138
+ const workingDirectory = stringHeader(req.header('x-hammoc-working-directory'));
139
+ return { socketId, workingDirectory };
140
+ }
141
+ function stringHeader(value) {
142
+ if (!value)
143
+ return undefined;
144
+ const trimmed = value.trim();
145
+ return trimmed.length > 0 ? trimmed : undefined;
146
+ }
147
+ async function emitOriginRefresh(req) {
148
+ const { socketId, workingDirectory } = readBroadcastHeaders(req);
149
+ if (!socketId || !workingDirectory)
150
+ return;
151
+ await broadcastSnippetList(workingDirectory, socketId);
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Controller
155
+ // ---------------------------------------------------------------------------
156
+ export const snippetController = {
157
+ /** GET /api/snippets?projectSlug=<slug> */
158
+ async list(req, res) {
159
+ const parsed = listQuerySchema.safeParse(req.query);
160
+ if (!parsed.success) {
161
+ res.status(400).json({
162
+ error: {
163
+ code: 'INVALID_REQUEST',
164
+ message: parsed.error.issues[0]?.message ?? 'invalid query',
165
+ },
166
+ });
167
+ return;
168
+ }
169
+ try {
170
+ const result = await snippetService.list({ projectSlug: parsed.data.projectSlug });
171
+ res.json(result);
172
+ }
173
+ catch (error) {
174
+ handleError(req, res, error);
175
+ }
176
+ },
177
+ /** GET /api/snippets/:scope/:name?projectSlug=<slug> */
178
+ async read(req, res) {
179
+ const query = refQuerySchema.safeParse(req.query);
180
+ if (!query.success) {
181
+ res.status(400).json({ error: { code: 'INVALID_REQUEST', message: 'invalid query' } });
182
+ return;
183
+ }
184
+ const ref = parseRefParams(req, res, query.data.projectSlug);
185
+ if (!ref)
186
+ return;
187
+ try {
188
+ const result = await snippetService.read(ref);
189
+ res.json(result);
190
+ }
191
+ catch (error) {
192
+ handleError(req, res, error);
193
+ }
194
+ },
195
+ /** POST /api/snippets/:scope/:name body: { content, projectSlug? } */
196
+ async create(req, res) {
197
+ const body = writeBodySchema.safeParse(req.body ?? {});
198
+ if (!body.success) {
199
+ res.status(400).json({
200
+ error: {
201
+ code: 'INVALID_REQUEST',
202
+ message: body.error.issues[0]?.message ?? 'invalid body',
203
+ },
204
+ });
205
+ return;
206
+ }
207
+ const ref = parseRefParams(req, res, body.data.projectSlug);
208
+ if (!ref)
209
+ return;
210
+ try {
211
+ const result = await snippetService.create(ref, { content: body.data.content });
212
+ await emitOriginRefresh(req);
213
+ res.status(201).json(result);
214
+ }
215
+ catch (error) {
216
+ handleError(req, res, error);
217
+ }
218
+ },
219
+ /** PUT /api/snippets/:scope/:name body: { content, expectedMtime?, projectSlug? } */
220
+ async update(req, res) {
221
+ const body = writeBodySchema.safeParse(req.body ?? {});
222
+ if (!body.success) {
223
+ res.status(400).json({
224
+ error: {
225
+ code: 'INVALID_REQUEST',
226
+ message: body.error.issues[0]?.message ?? 'invalid body',
227
+ },
228
+ });
229
+ return;
230
+ }
231
+ const ref = parseRefParams(req, res, body.data.projectSlug);
232
+ if (!ref)
233
+ return;
234
+ try {
235
+ const result = await snippetService.update(ref, {
236
+ content: body.data.content,
237
+ expectedMtime: body.data.expectedMtime,
238
+ });
239
+ await emitOriginRefresh(req);
240
+ res.json(result);
241
+ }
242
+ catch (error) {
243
+ handleError(req, res, error);
244
+ }
245
+ },
246
+ /** DELETE /api/snippets/:scope/:name body: { expectedMtime?, projectSlug? } */
247
+ async delete(req, res) {
248
+ const body = deleteBodySchema.safeParse(req.body ?? {});
249
+ if (!body.success) {
250
+ res.status(400).json({
251
+ error: {
252
+ code: 'INVALID_REQUEST',
253
+ message: body.error.issues[0]?.message ?? 'invalid body',
254
+ },
255
+ });
256
+ return;
257
+ }
258
+ const ref = parseRefParams(req, res, body.data.projectSlug);
259
+ if (!ref)
260
+ return;
261
+ try {
262
+ const result = await snippetService.delete(ref, {
263
+ expectedMtime: body.data.expectedMtime,
264
+ });
265
+ await emitOriginRefresh(req);
266
+ res.json(result);
267
+ }
268
+ catch (error) {
269
+ handleError(req, res, error);
270
+ }
271
+ },
272
+ /** POST /api/snippets/copy body: SnippetCopyRequest */
273
+ async copy(req, res) {
274
+ const parsed = copyBodySchema.safeParse(req.body ?? {});
275
+ if (!parsed.success) {
276
+ res.status(400).json({
277
+ error: {
278
+ code: 'INVALID_REQUEST',
279
+ message: parsed.error.issues[0]?.message ?? 'invalid body',
280
+ },
281
+ });
282
+ return;
283
+ }
284
+ try {
285
+ const result = await snippetService.copy(parsed.data);
286
+ await emitOriginRefresh(req);
287
+ res.json(result);
288
+ }
289
+ catch (error) {
290
+ handleError(req, res, error);
291
+ }
292
+ },
293
+ };
294
+ //# sourceMappingURL=snippetController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snippetController.js","sourceRoot":"","sources":["../../src/controllers/snippetController.ts"],"names":[],"mappings":"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"}
@@ -82,6 +82,21 @@ export declare function initializeWebSocket(httpServer: HttpServer): Promise<Soc
82
82
  * @returns Socket.io server instance
83
83
  */
84
84
  export declare function getIO(): SocketIOServer<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
85
+ /**
86
+ * Story 29.2 (AC1.e Phase 1): emit a fresh `snippets:list` payload to the
87
+ * single socket that triggered a mutation, so the originating client's
88
+ * SnippetPalette / useSnippets cache picks up the new state without an
89
+ * explicit refresh round-trip.
90
+ *
91
+ * Phase-1 limitation: only the origin socket is notified. Other tabs / browsers
92
+ * connected to the same project see stale data until their next user action
93
+ * (panel open, page reload). The Phase-2 `project:${slug}` room fan-out is
94
+ * deferred until ≥ 3 user reports of multi-tab divergence.
95
+ *
96
+ * Failures are swallowed — broadcast is best-effort and must not affect the
97
+ * REST mutation's success/failure semantics.
98
+ */
99
+ export declare function broadcastSnippetList(workingDirectory: string | undefined, originSocketId: string | undefined): Promise<void>;
85
100
  /**
86
101
  * Get the current number of connected clients
87
102
  * @returns Number of connected clients
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/handlers/websocket.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAG7D,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,UAAU,EAMV,SAAS,EACV,MAAM,gBAAgB,CAAC;AAQxB,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAkGzD,KAAK,UAAU,GAAG,MAAM,CAAC,oBAAoB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,UAAU,CAAC,CAAC;AAEpG,UAAU,iBAAiB;IACzB,OAAO,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IACnH,eAAe,EAAE,YAAY,GAAG,UAAU,CAAC;CAC5C;AAED,UAAU,YAAY;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC;IACzB,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACnD,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mFAAmF;IACnF,aAAa,CAAC,EAAE,SAAS,CAAC;CAC3B;AA+BD,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpF;AAsSD,8DAA8D;AAC9D,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAIpD;AAED,sFAAsF;AACtF,wBAAgB,+BAA+B,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAUrE;AAED,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAU7E;AAsID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,eAAe,EAChC,WAAW,CAAC,EAAE,MAAM,GACnB;IACD,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9C,CAmCA;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAyB5E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BrE;AAuBD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAO9E;AAoBD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,cAAc,CAAC,oBAAoB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,UAAU,CAAC,CAAC,CA85CpG;AAED;;;;GAIG;AACH,wBAAgB,KAAK,IAAI,cAAc,CACrC,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,EACjB,UAAU,CACX,CAKA;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD"}
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/handlers/websocket.ts"],"names":[],"mappings":"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"}
@@ -95,6 +95,10 @@ const socketToSession = new Map();
95
95
  const socketSessionRoom = new Map();
96
96
  // Track which project room each socket joined (for leave on session switch)
97
97
  const socketProjectRoom = new Map();
98
+ // Story 28.0.5: Track harness rooms each socket has subscribed to, so we can
99
+ // release watcher ref counts on disconnect without leaking chokidar instances.
100
+ // Key: socket.id → Set of "harness:user" | "harness:project:<slug>" room strings.
101
+ const socketHarnessRooms = new Map();
98
102
  const chainState = new Map();
99
103
  // Per-session drain generation counter for race guard
100
104
  const chainDrainGeneration = new Map();
@@ -1790,6 +1794,10 @@ export async function initializeWebSocket(httpServer) {
1790
1794
  socket.emit('snippets:list', { snippets: [] });
1791
1795
  }
1792
1796
  });
1797
+ // Story 29.2: re-emit `snippets:list` after a snippet CRUD so the
1798
+ // originating client's autocomplete surfaces stay in sync without an
1799
+ // explicit refresh round-trip. This is the same channel the existing
1800
+ // listener above answers, so consumers don't need a new event handler.
1793
1801
  // Handle project:join/leave — room for queue event delivery (Story 15.2)
1794
1802
  socket.on('project:join', (projectSlug) => {
1795
1803
  socket.join(`project:${projectSlug}`);
@@ -1802,6 +1810,37 @@ export async function initializeWebSocket(httpServer) {
1802
1810
  socketProjectRoom.delete(socket.id);
1803
1811
  }
1804
1812
  });
1813
+ // Story 28.0.5: Harness workbench subscription — one room per scope.
1814
+ socket.on('harness:subscribe', async ({ scope, projectSlug }) => {
1815
+ const room = scope === 'user' ? 'harness:user' : `harness:project:${projectSlug ?? ''}`;
1816
+ await socket.join(room);
1817
+ let rooms = socketHarnessRooms.get(socket.id);
1818
+ if (!rooms) {
1819
+ rooms = new Set();
1820
+ socketHarnessRooms.set(socket.id, rooms);
1821
+ }
1822
+ // Idempotent subscribe: increment the watcher ref count only on the
1823
+ // first join so disconnect cleanup matches exactly.
1824
+ if (!rooms.has(room)) {
1825
+ rooms.add(room);
1826
+ try {
1827
+ await fileWatcherService.ensureHarnessWatcher({ scope, projectSlug });
1828
+ }
1829
+ catch (err) {
1830
+ log.warn(`harness:subscribe watcher failed for ${room}: ${String(err)}`);
1831
+ }
1832
+ }
1833
+ });
1834
+ socket.on('harness:unsubscribe', async ({ scope, projectSlug }) => {
1835
+ const room = scope === 'user' ? 'harness:user' : `harness:project:${projectSlug ?? ''}`;
1836
+ await socket.leave(room);
1837
+ const rooms = socketHarnessRooms.get(socket.id);
1838
+ if (rooms && rooms.delete(room)) {
1839
+ fileWatcherService.releaseHarnessWatcher({ scope, projectSlug });
1840
+ if (rooms.size === 0)
1841
+ socketHarnessRooms.delete(socket.id);
1842
+ }
1843
+ });
1805
1844
  // Handle queue events via WebSocket (Story 15.2)
1806
1845
  socket.on('queue:start', async (data) => {
1807
1846
  const { items, sessionId, projectSlug, permissionMode } = data;
@@ -2069,6 +2108,21 @@ export async function initializeWebSocket(httpServer) {
2069
2108
  }
2070
2109
  }
2071
2110
  socketProjectRoom.delete(socket.id);
2111
+ // Story 28.0.5: Release any harness watcher refs held by this socket so
2112
+ // chokidar instances don't leak when a client drops without unsubscribing.
2113
+ const harnessRooms = socketHarnessRooms.get(socket.id);
2114
+ if (harnessRooms) {
2115
+ for (const room of harnessRooms) {
2116
+ if (room === 'harness:user') {
2117
+ fileWatcherService.releaseHarnessWatcher({ scope: 'user' });
2118
+ }
2119
+ else if (room.startsWith('harness:project:')) {
2120
+ const slug = room.slice('harness:project:'.length);
2121
+ fileWatcherService.releaseHarnessWatcher({ scope: 'project', projectSlug: slug });
2122
+ }
2123
+ }
2124
+ socketHarnessRooms.delete(socket.id);
2125
+ }
2072
2126
  // Story 25.9: Cleanup summarizing state on disconnect
2073
2127
  const sumState = socketSummarizing.get(socket.id);
2074
2128
  if (sumState?.abortController) {
@@ -2104,6 +2158,31 @@ export function getIO() {
2104
2158
  }
2105
2159
  return io;
2106
2160
  }
2161
+ /**
2162
+ * Story 29.2 (AC1.e Phase 1): emit a fresh `snippets:list` payload to the
2163
+ * single socket that triggered a mutation, so the originating client's
2164
+ * SnippetPalette / useSnippets cache picks up the new state without an
2165
+ * explicit refresh round-trip.
2166
+ *
2167
+ * Phase-1 limitation: only the origin socket is notified. Other tabs / browsers
2168
+ * connected to the same project see stale data until their next user action
2169
+ * (panel open, page reload). The Phase-2 `project:${slug}` room fan-out is
2170
+ * deferred until ≥ 3 user reports of multi-tab divergence.
2171
+ *
2172
+ * Failures are swallowed — broadcast is best-effort and must not affect the
2173
+ * REST mutation's success/failure semantics.
2174
+ */
2175
+ export async function broadcastSnippetList(workingDirectory, originSocketId) {
2176
+ if (!workingDirectory || !originSocketId || !io)
2177
+ return;
2178
+ try {
2179
+ const snippets = await listSnippets(workingDirectory);
2180
+ io.to(originSocketId).emit('snippets:list', { snippets });
2181
+ }
2182
+ catch (err) {
2183
+ log.warn(`broadcastSnippetList failed for socket ${originSocketId}: ${String(err)}`);
2184
+ }
2185
+ }
2107
2186
  /**
2108
2187
  * Get the current number of connected clients
2109
2188
  * @returns Number of connected clients