@winspan/claude-forge 8.50.6 → 8.51.1

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 (367) hide show
  1. package/CLAUDE.md +7 -7
  2. package/dist/claudemd/claudemd-generator.d.ts.map +1 -1
  3. package/dist/claudemd/claudemd-generator.js +27 -237
  4. package/dist/claudemd/claudemd-generator.js.map +1 -1
  5. package/dist/claudemd/resume-manager.js +1 -1
  6. package/dist/claudemd/resume-manager.js.map +1 -1
  7. package/dist/claudemd/templates/swarm-protocol.md +222 -0
  8. package/dist/cli/commands/daemon.js +6 -6
  9. package/dist/cli/commands/daemon.js.map +1 -1
  10. package/dist/cli/commands/executions.d.ts.map +1 -1
  11. package/dist/cli/commands/executions.js +4 -3
  12. package/dist/cli/commands/executions.js.map +1 -1
  13. package/dist/cli/commands/init.js +2 -2
  14. package/dist/cli/commands/init.js.map +1 -1
  15. package/dist/cli/commands/logs.js.map +1 -1
  16. package/dist/cli/commands/mcp.d.ts.map +1 -1
  17. package/dist/cli/commands/mcp.js +3 -5
  18. package/dist/cli/commands/mcp.js.map +1 -1
  19. package/dist/cli/commands/menu.d.ts.map +1 -1
  20. package/dist/cli/commands/menu.js +4 -3
  21. package/dist/cli/commands/menu.js.map +1 -1
  22. package/dist/cli/commands/stats.d.ts.map +1 -1
  23. package/dist/cli/commands/stats.js +2 -3
  24. package/dist/cli/commands/stats.js.map +1 -1
  25. package/dist/cli/commands/status.js +2 -2
  26. package/dist/cli/commands/status.js.map +1 -1
  27. package/dist/cli/commands/trace.d.ts.map +1 -1
  28. package/dist/cli/commands/trace.js +11 -23
  29. package/dist/cli/commands/trace.js.map +1 -1
  30. package/dist/cli/init/hook-manager.d.ts.map +1 -1
  31. package/dist/cli/init/hook-manager.js +2 -2
  32. package/dist/cli/init/hook-manager.js.map +1 -1
  33. package/dist/core/ai/provider.js +2 -2
  34. package/dist/core/ai/provider.js.map +1 -1
  35. package/dist/core/constants.d.ts +12 -1
  36. package/dist/core/constants.d.ts.map +1 -1
  37. package/dist/core/constants.js +15 -1
  38. package/dist/core/constants.js.map +1 -1
  39. package/dist/core/event-fields.d.ts +16 -0
  40. package/dist/core/event-fields.d.ts.map +1 -0
  41. package/dist/core/event-fields.js +19 -0
  42. package/dist/core/event-fields.js.map +1 -0
  43. package/dist/core/queue/index.d.ts.map +1 -1
  44. package/dist/core/queue/index.js +3 -4
  45. package/dist/core/queue/index.js.map +1 -1
  46. package/dist/core/storage/base.d.ts +36 -3
  47. package/dist/core/storage/base.d.ts.map +1 -1
  48. package/dist/core/storage/base.js +101 -58
  49. package/dist/core/storage/base.js.map +1 -1
  50. package/dist/core/storage/events.d.ts +92 -3
  51. package/dist/core/storage/events.d.ts.map +1 -1
  52. package/dist/core/storage/events.js +147 -0
  53. package/dist/core/storage/events.js.map +1 -1
  54. package/dist/core/storage/routing.d.ts +54 -1
  55. package/dist/core/storage/routing.d.ts.map +1 -1
  56. package/dist/core/storage/routing.js +99 -1
  57. package/dist/core/storage/routing.js.map +1 -1
  58. package/dist/core/storage/schema.sql +12 -2
  59. package/dist/core/storage/sessions.d.ts +20 -0
  60. package/dist/core/storage/sessions.d.ts.map +1 -1
  61. package/dist/core/storage/sessions.js +59 -0
  62. package/dist/core/storage/sessions.js.map +1 -1
  63. package/dist/core/storage/skills.d.ts +23 -0
  64. package/dist/core/storage/skills.d.ts.map +1 -1
  65. package/dist/core/storage/skills.js +47 -0
  66. package/dist/core/storage/skills.js.map +1 -1
  67. package/dist/core/storage/sqlite.d.ts +35 -2
  68. package/dist/core/storage/sqlite.d.ts.map +1 -1
  69. package/dist/core/storage/sqlite.js +93 -4
  70. package/dist/core/storage/sqlite.js.map +1 -1
  71. package/dist/core/storage/tasks.d.ts +49 -0
  72. package/dist/core/storage/tasks.d.ts.map +1 -1
  73. package/dist/core/storage/tasks.js +143 -1
  74. package/dist/core/storage/tasks.js.map +1 -1
  75. package/dist/core/storage/token-usage.d.ts +1 -1
  76. package/dist/core/storage/token-usage.d.ts.map +1 -1
  77. package/dist/core/storage/token-usage.js +1 -1
  78. package/dist/core/storage/token-usage.js.map +1 -1
  79. package/dist/core/types.d.ts +24 -3
  80. package/dist/core/types.d.ts.map +1 -1
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/core/utils/error-handler.d.ts.map +1 -1
  83. package/dist/core/utils/error-handler.js +3 -2
  84. package/dist/core/utils/error-handler.js.map +1 -1
  85. package/dist/core/utils/git.d.ts +10 -0
  86. package/dist/core/utils/git.d.ts.map +1 -0
  87. package/dist/core/utils/git.js +24 -0
  88. package/dist/core/utils/git.js.map +1 -0
  89. package/dist/core/utils/logger.d.ts.map +1 -1
  90. package/dist/core/utils/logger.js +15 -1
  91. package/dist/core/utils/logger.js.map +1 -1
  92. package/dist/core/utils/lru-cache.d.ts +1 -0
  93. package/dist/core/utils/lru-cache.d.ts.map +1 -1
  94. package/dist/core/utils/lru-cache.js +3 -0
  95. package/dist/core/utils/lru-cache.js.map +1 -1
  96. package/dist/core/utils/token-tracker.js +1 -1
  97. package/dist/core/utils/token-tracker.js.map +1 -1
  98. package/dist/daemon/event-parser.d.ts.map +1 -1
  99. package/dist/daemon/event-parser.js +2 -1
  100. package/dist/daemon/event-parser.js.map +1 -1
  101. package/dist/daemon/handlers/history-exporter.js.map +1 -1
  102. package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
  103. package/dist/daemon/handlers/post-tool-use.js +7 -3
  104. package/dist/daemon/handlers/post-tool-use.js.map +1 -1
  105. package/dist/daemon/handlers/stop.d.ts +4 -0
  106. package/dist/daemon/handlers/stop.d.ts.map +1 -1
  107. package/dist/daemon/handlers/stop.js +23 -35
  108. package/dist/daemon/handlers/stop.js.map +1 -1
  109. package/dist/daemon/handlers/user-prompt.d.ts +3 -3
  110. package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
  111. package/dist/daemon/handlers/user-prompt.js +12 -22
  112. package/dist/daemon/handlers/user-prompt.js.map +1 -1
  113. package/dist/daemon/hook-sync.d.ts +17 -0
  114. package/dist/daemon/hook-sync.d.ts.map +1 -0
  115. package/dist/daemon/hook-sync.js +74 -0
  116. package/dist/daemon/hook-sync.js.map +1 -0
  117. package/dist/daemon/index.d.ts.map +1 -1
  118. package/dist/daemon/index.js +33 -9
  119. package/dist/daemon/index.js.map +1 -1
  120. package/dist/daemon/lifecycle.js +3 -4
  121. package/dist/daemon/lifecycle.js.map +1 -1
  122. package/dist/daemon/server.d.ts +6 -4
  123. package/dist/daemon/server.d.ts.map +1 -1
  124. package/dist/daemon/server.js +76 -85
  125. package/dist/daemon/server.js.map +1 -1
  126. package/dist/daemon/services/task-segmenter.js +1 -1
  127. package/dist/daemon/services/task-segmenter.js.map +1 -1
  128. package/dist/hooks/hook-lib.sh +37 -0
  129. package/dist/hooks/notification.sh +2 -2
  130. package/dist/hooks/post-tool-use.sh +2 -2
  131. package/dist/hooks/pre-tool-use.sh +2 -2
  132. package/dist/hooks/stop.sh +9 -6
  133. package/dist/hooks/user-prompt-submit.sh +2 -2
  134. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
  135. package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
  136. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
  137. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
  138. package/dist/web/analytics/drift-detector.d.ts.map +1 -0
  139. package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
  140. package/dist/web/analytics/drift-detector.js.map +1 -0
  141. package/dist/web/analytics/weekly-report.d.ts.map +1 -0
  142. package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
  143. package/dist/web/analytics/weekly-report.js.map +1 -0
  144. package/dist/web/auth-middleware.d.ts.map +1 -1
  145. package/dist/web/auth-middleware.js +1 -2
  146. package/dist/web/auth-middleware.js.map +1 -1
  147. package/dist/web/routes/_helpers.d.ts +16 -0
  148. package/dist/web/routes/_helpers.d.ts.map +1 -0
  149. package/dist/web/routes/_helpers.js +32 -0
  150. package/dist/web/routes/_helpers.js.map +1 -0
  151. package/dist/web/routes/drift.js +1 -1
  152. package/dist/web/routes/drift.js.map +1 -1
  153. package/dist/web/routes/insights.js +1 -1
  154. package/dist/web/routes/insights.js.map +1 -1
  155. package/dist/web/routes/reports.js +1 -1
  156. package/dist/web/routes/reports.js.map +1 -1
  157. package/dist/web/routes/rules.d.ts +3 -0
  158. package/dist/web/routes/rules.d.ts.map +1 -1
  159. package/dist/web/routes/rules.js +28 -52
  160. package/dist/web/routes/rules.js.map +1 -1
  161. package/dist/web/routes/sessions.d.ts.map +1 -1
  162. package/dist/web/routes/sessions.js +16 -30
  163. package/dist/web/routes/sessions.js.map +1 -1
  164. package/dist/web/routes/skill-stats.d.ts +2 -0
  165. package/dist/web/routes/skill-stats.d.ts.map +1 -1
  166. package/dist/web/routes/skill-stats.js +28 -64
  167. package/dist/web/routes/skill-stats.js.map +1 -1
  168. package/dist/web/routes/skills.d.ts.map +1 -1
  169. package/dist/web/routes/skills.js +5 -4
  170. package/dist/web/routes/skills.js.map +1 -1
  171. package/dist/web/routes/stats.d.ts +4 -0
  172. package/dist/web/routes/stats.d.ts.map +1 -1
  173. package/dist/web/routes/stats.js +19 -21
  174. package/dist/web/routes/stats.js.map +1 -1
  175. package/dist/web/routes/tasks.d.ts.map +1 -1
  176. package/dist/web/routes/tasks.js +17 -42
  177. package/dist/web/routes/tasks.js.map +1 -1
  178. package/dist/web/routes/trace.d.ts.map +1 -1
  179. package/dist/web/routes/trace.js +7 -17
  180. package/dist/web/routes/trace.js.map +1 -1
  181. package/dist/web/routes/types.d.ts.map +1 -1
  182. package/dist/web/routes/types.js +4 -3
  183. package/dist/web/routes/types.js.map +1 -1
  184. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
  185. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
  186. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
  187. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
  188. package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
  189. package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
  190. package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
  191. package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
  192. package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
  193. package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
  194. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
  195. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
  196. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
  197. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
  198. package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
  199. package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
  200. package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
  201. package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
  202. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
  203. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
  204. package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
  205. package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
  206. package/dist/web/static/assets/index-CBX47X8l.js +3 -0
  207. package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
  208. package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
  209. package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
  210. package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
  211. package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
  212. package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
  213. package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
  214. package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
  215. package/dist/web/static/index.html +4 -4
  216. package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
  217. package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
  218. package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
  219. package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
  220. package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
  221. package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
  222. package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
  223. package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
  224. package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
  225. package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
  226. package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
  227. package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
  228. package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
  229. package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
  230. package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
  231. package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
  232. package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
  233. package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
  234. package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
  235. package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
  236. package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
  237. package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
  238. package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
  239. package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
  240. package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
  241. package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
  242. package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
  243. package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
  244. package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
  245. package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
  246. package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
  247. package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
  248. package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
  249. package/docs/reviews/task-title-summary.md +92 -0
  250. package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
  251. package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
  252. package/package.json +2 -2
  253. package/src/claudemd/claudemd-generator.ts +29 -238
  254. package/src/claudemd/resume-manager.ts +1 -1
  255. package/src/claudemd/templates/swarm-protocol.md +222 -0
  256. package/src/cli/commands/daemon.ts +6 -6
  257. package/src/cli/commands/executions.ts +4 -3
  258. package/src/cli/commands/init.ts +2 -2
  259. package/src/cli/commands/logs.ts +1 -1
  260. package/src/cli/commands/mcp.ts +3 -5
  261. package/src/cli/commands/menu.ts +4 -3
  262. package/src/cli/commands/stats.ts +2 -3
  263. package/src/cli/commands/status.ts +2 -2
  264. package/src/cli/commands/trace.ts +10 -26
  265. package/src/cli/init/hook-manager.ts +2 -2
  266. package/src/core/ai/provider.ts +2 -2
  267. package/src/core/constants.ts +18 -1
  268. package/src/core/event-fields.ts +32 -0
  269. package/src/core/queue/index.ts +3 -4
  270. package/src/core/storage/base.ts +132 -56
  271. package/src/core/storage/events.ts +183 -4
  272. package/src/core/storage/routing.ts +129 -1
  273. package/src/core/storage/schema.sql +12 -2
  274. package/src/core/storage/sessions.ts +64 -0
  275. package/src/core/storage/skills.ts +69 -0
  276. package/src/core/storage/sqlite.ts +103 -4
  277. package/src/core/storage/tasks.ts +149 -1
  278. package/src/core/storage/token-usage.ts +1 -1
  279. package/src/core/types.ts +30 -3
  280. package/src/core/utils/error-handler.ts +3 -2
  281. package/src/core/utils/git.ts +23 -0
  282. package/src/core/utils/logger.ts +16 -1
  283. package/src/core/utils/lru-cache.ts +4 -0
  284. package/src/core/utils/token-tracker.ts +1 -1
  285. package/src/daemon/event-parser.ts +4 -3
  286. package/src/daemon/handlers/history-exporter.ts +1 -1
  287. package/src/daemon/handlers/post-tool-use.ts +7 -3
  288. package/src/daemon/handlers/stop.ts +32 -39
  289. package/src/daemon/handlers/user-prompt.ts +12 -22
  290. package/src/daemon/hook-sync.ts +91 -0
  291. package/src/daemon/index.ts +34 -10
  292. package/src/daemon/lifecycle.ts +3 -3
  293. package/src/daemon/server.ts +76 -89
  294. package/src/daemon/services/task-segmenter.ts +1 -1
  295. package/src/hooks/hook-lib.sh +37 -0
  296. package/src/hooks/notification.sh +2 -2
  297. package/src/hooks/post-tool-use.sh +2 -2
  298. package/src/hooks/pre-tool-use.sh +2 -2
  299. package/src/hooks/stop.sh +9 -6
  300. package/src/hooks/user-prompt-submit.sh +2 -2
  301. package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
  302. package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
  303. package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
  304. package/src/web/auth-middleware.ts +1 -2
  305. package/src/web/routes/_helpers.ts +34 -0
  306. package/src/web/routes/drift.ts +1 -1
  307. package/src/web/routes/insights.ts +1 -1
  308. package/src/web/routes/reports.ts +1 -1
  309. package/src/web/routes/rules.ts +31 -56
  310. package/src/web/routes/sessions.ts +18 -30
  311. package/src/web/routes/skill-stats.ts +29 -69
  312. package/src/web/routes/skills.ts +5 -4
  313. package/src/web/routes/stats.ts +19 -29
  314. package/src/web/routes/tasks.ts +17 -42
  315. package/src/web/routes/trace.ts +7 -19
  316. package/src/web/routes/types.ts +4 -3
  317. package/tests/integration/claudemd-generator.test.ts +90 -0
  318. package/tests/integration/web-analytics.integration.test.ts +133 -0
  319. package/tests/integration/web-stats.integration.test.ts +135 -0
  320. package/tests/integration/web-trace.integration.test.ts +175 -0
  321. package/tests/unit/core/forge-paths.test.ts +99 -0
  322. package/tests/unit/daemon/hook-sync.test.ts +71 -0
  323. package/tests/unit/daemon/post-tool-use.test.ts +121 -0
  324. package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
  325. package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
  326. package/tests/unit/event-fields.test.ts +88 -0
  327. package/tests/unit/event-parser.test.ts +55 -0
  328. package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
  329. package/tests/unit/socket-server.test.ts +183 -0
  330. package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
  331. package/tests/unit/storage/migration-idempotent.test.ts +304 -0
  332. package/tests/unit/storage/routing-aggregates.test.ts +276 -0
  333. package/tests/unit/storage/routing.test.ts +117 -0
  334. package/tests/unit/storage/schema-missing.test.ts +81 -0
  335. package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
  336. package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
  337. package/tests/unit/storage/skills-aggregates.test.ts +104 -0
  338. package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
  339. package/tests/unit/storage/task-operations-counts.test.ts +46 -0
  340. package/tests/unit/storage/tasks-getById.test.ts +343 -0
  341. package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
  342. package/tests/unit/token-usage.test.ts +6 -6
  343. package/tests/unit/web/navigation-back-contract.test.ts +134 -0
  344. package/tests/unit/web/routes-rules.test.ts +182 -0
  345. package/tests/unit/web/routes-tasks.test.ts +34 -0
  346. package/tests/unit/web/task-title-contract.test.ts +210 -0
  347. package/tests/unit/web/tasks-component-contract.test.ts +179 -0
  348. package/vitest.config.ts +1 -1
  349. package/web/src/pages/TaskDetail.tsx +9 -5
  350. package/web/src/pages/Tasks.tsx +315 -50
  351. package/web/src/utils/navigation.ts +25 -0
  352. package/web/src/utils/task-title.ts +49 -0
  353. package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
  354. package/dist/daemon/services/drift-detector.d.ts.map +0 -1
  355. package/dist/daemon/services/drift-detector.js.map +0 -1
  356. package/dist/daemon/services/weekly-report.d.ts.map +0 -1
  357. package/dist/daemon/services/weekly-report.js.map +0 -1
  358. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
  359. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
  360. package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
  361. package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
  362. package/dist/web/static/assets/index-D8AKj26b.css +0 -1
  363. package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
  364. package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
  365. package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
  366. /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
  367. /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { syncHooks } from '../../../src/daemon/hook-sync.js';
6
+
7
+ describe('syncHooks', () => {
8
+ let tmpRoot: string;
9
+ let sourceDir: string;
10
+ let targetDir: string;
11
+
12
+ beforeEach(() => {
13
+ tmpRoot = mkdtempSync(join(tmpdir(), 'forge-hook-sync-'));
14
+ sourceDir = join(tmpRoot, 'src-hooks');
15
+ targetDir = join(tmpRoot, 'target-hooks');
16
+ mkdirSync(sourceDir, { recursive: true });
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(tmpRoot, { recursive: true, force: true });
21
+ });
22
+
23
+ it('target dir not exist → returns zero counts (user did not init)', () => {
24
+ writeFileSync(join(sourceDir, 'pre-tool-use.sh'), '#!/bin/bash\necho "new"\n');
25
+ const result = syncHooks({ sourceDir, targetDir });
26
+ expect(result.copied).toBe(0);
27
+ expect(result.checked).toBe(0);
28
+ });
29
+
30
+ it('source and target identical → no copy', () => {
31
+ mkdirSync(targetDir, { recursive: true });
32
+ const content = '#!/bin/bash\necho "same"\n';
33
+ writeFileSync(join(sourceDir, 'pre-tool-use.sh'), content);
34
+ writeFileSync(join(targetDir, 'pre-tool-use.sh'), content);
35
+
36
+ const result = syncHooks({ sourceDir, targetDir });
37
+ expect(result.copied).toBe(0);
38
+ expect(result.checked).toBe(1);
39
+ });
40
+
41
+ it('source and target differ → copies new content', () => {
42
+ mkdirSync(targetDir, { recursive: true });
43
+ writeFileSync(join(sourceDir, 'pre-tool-use.sh'), '#!/bin/bash\necho "NEW"\n');
44
+ writeFileSync(join(targetDir, 'pre-tool-use.sh'), '#!/bin/bash\necho "old"\n');
45
+
46
+ const result = syncHooks({ sourceDir, targetDir });
47
+ expect(result.copied).toBe(1);
48
+ expect(result.checked).toBe(1);
49
+ expect(readFileSync(join(targetDir, 'pre-tool-use.sh'), 'utf-8')).toContain('NEW');
50
+ });
51
+
52
+ it('target missing file but exists in source → copies it', () => {
53
+ mkdirSync(targetDir, { recursive: true });
54
+ writeFileSync(join(sourceDir, 'hook-lib.sh'), '# lib\n');
55
+ // target has no hook-lib.sh
56
+
57
+ const result = syncHooks({ sourceDir, targetDir });
58
+ expect(result.copied).toBe(1);
59
+ expect(existsSync(join(targetDir, 'hook-lib.sh'))).toBe(true);
60
+ });
61
+
62
+ it('source missing some files → skips them silently', () => {
63
+ mkdirSync(targetDir, { recursive: true });
64
+ // Only one source file out of 6
65
+ writeFileSync(join(sourceDir, 'pre-tool-use.sh'), '#!/bin/bash\necho "x"\n');
66
+
67
+ const result = syncHooks({ sourceDir, targetDir });
68
+ expect(result.checked).toBe(1);
69
+ expect(result.skipped).toBe(5);
70
+ });
71
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * M7: PostToolUseHandler routing_event association behavior.
3
+ *
4
+ * Targets the time-ordering bug where 2+ Agent invocations within a single
5
+ * user prompt could overwrite the same routing_event repeatedly. After M7,
6
+ * the handler must only touch the *pending* (obeyed IS NULL) recent event;
7
+ * later Agents within the same prompt are silently skipped (M7 limitation).
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import { mkdtempSync, rmSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
15
+ import { PostToolUseHandler } from '../../../src/daemon/handlers/post-tool-use.js';
16
+ import type { PostToolUseEvent } from '../../../src/core/types.js';
17
+
18
+ const PROJECT = '/tmp/m7-handler-proj';
19
+
20
+ function makeAgentEvent(session_id: string, agentType: string): PostToolUseEvent {
21
+ return {
22
+ session_id,
23
+ project_path: PROJECT,
24
+ timestamp: new Date().toISOString(),
25
+ hook_type: 'PostToolUse',
26
+ tool_name: 'Agent',
27
+ tool_input: { subagent_type: agentType },
28
+ tool_output: {},
29
+ };
30
+ }
31
+
32
+ describe('PostToolUseHandler — routing_event association (M7)', () => {
33
+ let tmp: string;
34
+ let storage: SQLiteStorage;
35
+ let handler: PostToolUseHandler;
36
+
37
+ beforeEach(() => {
38
+ tmp = mkdtempSync(join(tmpdir(), 'forge-m7-handler-'));
39
+ storage = new SQLiteStorage(join(tmp, 'data.db'));
40
+ handler = new PostToolUseHandler(storage);
41
+ });
42
+
43
+ afterEach(() => {
44
+ try { storage.close(); } catch { /* ignore */ }
45
+ rmSync(tmp, { recursive: true, force: true });
46
+ });
47
+
48
+ it('case 1: same prompt, 2 Agent calls — first updates pending row, second does NOT re-overwrite', async () => {
49
+ const sess = 'sess-same-prompt';
50
+
51
+ // UserPromptHandler writes a single pending routing_event.
52
+ const eventId = storage.writeRoutingEvent({
53
+ session_id: sess, project_path: PROJECT, ts: 1000,
54
+ prompt: 'spawn two agents', intent_json: '{}',
55
+ obeyed: null,
56
+ });
57
+
58
+ // Agent #1 fires → first PostToolUse fills the row.
59
+ await handler.handle(makeAgentEvent(sess, 'researcher'));
60
+
61
+ const rowsAfter1 = storage.queryRoutingEvents({ session_id: sess });
62
+ expect(rowsAfter1).toHaveLength(1);
63
+ expect(rowsAfter1[0].id).toBe(eventId);
64
+ expect(rowsAfter1[0].obeyed).toBe(1);
65
+ expect(rowsAfter1[0].routed_to_name).toBe('researcher');
66
+ const firstToolTs = rowsAfter1[0].first_tool_ts;
67
+ expect(firstToolTs).not.toBeNull();
68
+
69
+ // Agent #2 fires → with the pending filter, no pending row remains;
70
+ // the handler must NOT overwrite the existing obeyed=1 row.
71
+ await handler.handle(makeAgentEvent(sess, 'coder'));
72
+
73
+ const rowsAfter2 = storage.queryRoutingEvents({ session_id: sess });
74
+ expect(rowsAfter2).toHaveLength(1);
75
+ expect(rowsAfter2[0].id).toBe(eventId);
76
+ expect(rowsAfter2[0].routed_to_name).toBe('researcher'); // NOT overwritten to 'coder'
77
+ expect(rowsAfter2[0].first_tool_ts).toBe(firstToolTs); // ts not stomped
78
+ expect(rowsAfter2[0].obeyed).toBe(1);
79
+ });
80
+
81
+ it('case 2: cross-prompt — each prompt’s Agent updates its own pending event', async () => {
82
+ const sess = 'sess-cross-prompt';
83
+
84
+ // Prompt 1 → routing_event #1 pending.
85
+ const eid1 = storage.writeRoutingEvent({
86
+ session_id: sess, project_path: PROJECT, ts: 1000,
87
+ prompt: 'p1', intent_json: '{}',
88
+ obeyed: null,
89
+ });
90
+ // Agent for P1.
91
+ await handler.handle(makeAgentEvent(sess, 'researcher'));
92
+
93
+ // Prompt 2 → routing_event #2 pending.
94
+ const eid2 = storage.writeRoutingEvent({
95
+ session_id: sess, project_path: PROJECT, ts: 2000,
96
+ prompt: 'p2', intent_json: '{}',
97
+ obeyed: null,
98
+ });
99
+ // Agent for P2.
100
+ await handler.handle(makeAgentEvent(sess, 'coder'));
101
+
102
+ const rows = storage.queryRoutingEvents({ session_id: sess });
103
+ expect(rows).toHaveLength(2);
104
+
105
+ const byId = new Map(rows.map(r => [r.id, r]));
106
+ const r1 = byId.get(eid1)!;
107
+ const r2 = byId.get(eid2)!;
108
+ expect(r1.routed_to_name).toBe('researcher');
109
+ expect(r1.obeyed).toBe(1);
110
+ expect(r2.routed_to_name).toBe('coder');
111
+ expect(r2.obeyed).toBe(1);
112
+ });
113
+
114
+ it('case 3: no routing_event in DB — handler does not throw', async () => {
115
+ const sess = 'sess-none';
116
+ await expect(handler.handle(makeAgentEvent(sess, 'researcher'))).resolves.toEqual({ allow: true });
117
+
118
+ const rows = storage.queryRoutingEvents({ session_id: sess });
119
+ expect(rows).toHaveLength(0);
120
+ });
121
+ });
@@ -0,0 +1,202 @@
1
+ /**
2
+ * H2 Phase 1 safety-net: StopHandler behavior summary.
3
+ *
4
+ * Locks the output format and aggregation logic of `generateBehaviorSummary`
5
+ * before its inline SQL (events GROUP BY tool_name / Agent-Task subagent_type /
6
+ * COUNT skill_invocations) is migrated into EventOperations / SkillOperations.
7
+ *
8
+ * Since `generateBehaviorSummary` is `private`, we exercise it indirectly via
9
+ * `handle()` and capture the resume content passed to `resume.save`. That is
10
+ * also the actually-exercised path in production, so this gives a higher-value
11
+ * baseline than reflection-style private access.
12
+ *
13
+ * NOTE on test location: the spec listed `src/tests/...`, but vitest.config.ts
14
+ * only discovers `tests/unit/**` and `tests/integration/**`, so the file is
15
+ * placed under `tests/unit/daemon/` to ensure it actually runs. The semantic
16
+ * intent (a unit test, not an integration test) is preserved.
17
+ */
18
+
19
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
20
+ import { StopHandler } from '../../../src/daemon/handlers/stop.js';
21
+ import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
22
+ import type { HistoryExporter } from '../../../src/daemon/handlers/history-exporter.js';
23
+ import type { StopEvent } from '../../../src/core/types.js';
24
+
25
+ const SESSION = 'sess-behavior-summary';
26
+ const PROJECT = '/tmp/proj-behavior-summary';
27
+
28
+ function nowIso(): string {
29
+ return new Date().toISOString();
30
+ }
31
+
32
+ function makeEvent(): StopEvent {
33
+ return {
34
+ session_id: SESSION,
35
+ project_path: PROJECT,
36
+ timestamp: nowIso(),
37
+ hook_type: 'Stop',
38
+ };
39
+ }
40
+
41
+ describe('StopHandler.generateBehaviorSummary (safety-net)', () => {
42
+ let storage: SQLiteStorage;
43
+ let exporter: HistoryExporter;
44
+ let saved: { content: string | null };
45
+
46
+ // Build a mock ResumeManager that returns a known stub from generate() and
47
+ // captures whatever the handler ends up passing to save().
48
+ function makeResumeMock(): { generate: ReturnType<typeof vi.fn>; save: ReturnType<typeof vi.fn>; load: ReturnType<typeof vi.fn>; clear: ReturnType<typeof vi.fn> } {
49
+ return {
50
+ generate: vi.fn().mockReturnValue('STUB_RESUME\n_生成时间: 2026-01-01T00:00:00.000Z_\n'),
51
+ save: vi.fn((_p: string, c: string) => { saved.content = c; }),
52
+ load: vi.fn(),
53
+ clear: vi.fn(),
54
+ };
55
+ }
56
+
57
+ beforeEach(() => {
58
+ storage = new SQLiteStorage(':memory:');
59
+ saved = { content: null };
60
+ exporter = { export: vi.fn().mockResolvedValue(undefined) } as unknown as HistoryExporter;
61
+ });
62
+
63
+ afterEach(() => {
64
+ storage.close();
65
+ });
66
+
67
+ it('does not append summary when storage has no events for the session', async () => {
68
+ const resume = makeResumeMock();
69
+ const handler = new StopHandler(exporter, resume as any, null, storage, null, null);
70
+
71
+ const res = await handler.handle(makeEvent());
72
+ expect(res.allow).toBe(true);
73
+ // resume.save should still be called (resume content exists)
74
+ expect(resume.save).toHaveBeenCalledTimes(1);
75
+ expect(saved.content).not.toBeNull();
76
+ // Behavior summary line MUST NOT appear when there are no tool calls
77
+ expect(saved.content!).not.toContain('会话行为');
78
+ });
79
+
80
+ it('appends a tool-only behavior summary when only tool events exist', async () => {
81
+ // Seed events: 2 Bash, 1 Edit — no Agent/Task, no skill invocations
82
+ storage.writeEvent({
83
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
84
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
85
+ });
86
+ storage.writeEvent({
87
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
88
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'pwd' },
89
+ });
90
+ storage.writeEvent({
91
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
92
+ hook_type: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path: '/x' },
93
+ });
94
+
95
+ const resume = makeResumeMock();
96
+ const handler = new StopHandler(exporter, resume as any, null, storage, null, null);
97
+
98
+ const res = await handler.handle(makeEvent());
99
+ expect(res.allow).toBe(true);
100
+ expect(saved.content).not.toBeNull();
101
+
102
+ // Must contain a behavior summary line with total tool calls
103
+ expect(saved.content!).toContain('**会话行为**');
104
+ expect(saved.content!).toContain('工具调用 3 次');
105
+ expect(saved.content!).toContain('Agent 委托 0 次');
106
+ expect(saved.content!).toContain('Skill 调用 0 次');
107
+ });
108
+
109
+ it('includes Agent delegation counts and agent type names', async () => {
110
+ // 2 normal tools + 2 Task delegations to different subagents
111
+ storage.writeEvent({
112
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
113
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
114
+ });
115
+ storage.writeEvent({
116
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
117
+ hook_type: 'PreToolUse', tool_name: 'Read', tool_input: { file_path: '/a' },
118
+ });
119
+ storage.writeEvent({
120
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
121
+ hook_type: 'PreToolUse', tool_name: 'Task',
122
+ tool_input: { subagent_type: 'explore', description: 'find x' },
123
+ });
124
+ storage.writeEvent({
125
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
126
+ hook_type: 'PreToolUse', tool_name: 'Task',
127
+ tool_input: { subagent_type: 'coder', description: 'fix y' },
128
+ });
129
+
130
+ const resume = makeResumeMock();
131
+ const handler = new StopHandler(exporter, resume as any, null, storage, null, null);
132
+
133
+ await handler.handle(makeEvent());
134
+ expect(saved.content).not.toBeNull();
135
+
136
+ const summary = saved.content!;
137
+ expect(summary).toContain('工具调用 4 次');
138
+ expect(summary).toContain('Agent 委托 2 次');
139
+ // Agent names appear in parenthesised detail
140
+ expect(summary).toMatch(/explore|coder/);
141
+ });
142
+
143
+ it('includes skill invocation count when skill_invocations rows exist', async () => {
144
+ storage.writeEvent({
145
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
146
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
147
+ });
148
+ storage.writeSkillInvocation({
149
+ id: 'si-bs-1', route_request_id: null, session_id: SESSION,
150
+ agent_id: null, skill_id: 'official-tdd',
151
+ invocation_type: 'dynamic', reason: null,
152
+ workflow: null, phase: null, feature_slug: null, artifact_path: null,
153
+ depth: 0, success: 1, error: null, timestamp: Date.now(),
154
+ });
155
+ storage.writeSkillInvocation({
156
+ id: 'si-bs-2', route_request_id: null, session_id: SESSION,
157
+ agent_id: null, skill_id: 'official-debug',
158
+ invocation_type: 'dynamic', reason: null,
159
+ workflow: null, phase: null, feature_slug: null, artifact_path: null,
160
+ depth: 0, success: 1, error: null, timestamp: Date.now(),
161
+ });
162
+
163
+ const resume = makeResumeMock();
164
+ const handler = new StopHandler(exporter, resume as any, null, storage, null, null);
165
+
166
+ await handler.handle(makeEvent());
167
+ expect(saved.content).not.toBeNull();
168
+ expect(saved.content!).toContain('Skill 调用 2 次');
169
+ });
170
+
171
+ it('does not include events from other sessions in the summary', async () => {
172
+ // Same session — 1 tool call
173
+ storage.writeEvent({
174
+ session_id: SESSION, project_path: PROJECT, timestamp: nowIso(),
175
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'ls' },
176
+ });
177
+ // Different session — 5 tool calls + 3 skill invocations: MUST be excluded
178
+ for (let i = 0; i < 5; i++) {
179
+ storage.writeEvent({
180
+ session_id: 'other-session', project_path: PROJECT, timestamp: nowIso(),
181
+ hook_type: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'noise' },
182
+ });
183
+ }
184
+ for (let i = 0; i < 3; i++) {
185
+ storage.writeSkillInvocation({
186
+ id: `si-noise-${i}`, route_request_id: null, session_id: 'other-session',
187
+ agent_id: null, skill_id: 'official-noise',
188
+ invocation_type: 'dynamic', reason: null,
189
+ workflow: null, phase: null, feature_slug: null, artifact_path: null,
190
+ depth: 0, success: 1, error: null, timestamp: Date.now(),
191
+ });
192
+ }
193
+
194
+ const resume = makeResumeMock();
195
+ const handler = new StopHandler(exporter, resume as any, null, storage, null, null);
196
+
197
+ await handler.handle(makeEvent());
198
+ expect(saved.content).not.toBeNull();
199
+ expect(saved.content!).toContain('工具调用 1 次');
200
+ expect(saved.content!).toContain('Skill 调用 0 次');
201
+ });
202
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 单测:TaskSegmenter.completeCurrentTask recover fallback
3
+ *
4
+ * 覆盖:
5
+ * - Map 为空但 DB 有 active task → completeCurrentTask 仍能将其转为 completed
6
+ * - Map 不为空的正常路径不退化
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import { mkdtempSync, rmSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
14
+ import { TaskSegmenter } from '../../../src/daemon/services/task-segmenter.js';
15
+
16
+ const SESSION = 'sess-segmenter-recover';
17
+
18
+ function openStorage(tmp: string): SQLiteStorage {
19
+ return new SQLiteStorage(join(tmp, 'data.db'));
20
+ }
21
+
22
+ describe('TaskSegmenter.completeCurrentTask recover fallback', () => {
23
+ let tmp: string;
24
+ let storage: SQLiteStorage;
25
+
26
+ beforeEach(() => {
27
+ tmp = mkdtempSync(join(tmpdir(), 'forge-segmenter-recover-'));
28
+ storage = openStorage(tmp);
29
+ });
30
+
31
+ afterEach(() => {
32
+ try { storage.close(); } catch { /* ignore */ }
33
+ rmSync(tmp, { recursive: true, force: true });
34
+ });
35
+
36
+ it('Map 为空但 DB 有 active task → completeCurrentTask 将其转 completed', () => {
37
+ // 直接写 active task 到 DB(不经过 processPrompt,模拟 daemon 重启后内存丢失)
38
+ storage.writeTask({
39
+ id: 'task-in-db',
40
+ session_id: SESSION,
41
+ title: 'Persisted active task',
42
+ start_time: '2026-01-01T10:00:00.000Z',
43
+ });
44
+
45
+ // 确认此刻 DB 中是 active
46
+ expect(storage.getTask('task-in-db')?.status).toBe('active');
47
+
48
+ // 创建新的 TaskSegmenter,Map 为空(模拟重启)
49
+ const segmenter = new TaskSegmenter(storage);
50
+
51
+ // Stop hook 触发 completeCurrentTask,Map 中没有该 session 的 entry
52
+ segmenter.completeCurrentTask(SESSION, '2026-01-01T10:30:00.000Z');
53
+
54
+ // task 应该被 recover 并标记为 completed
55
+ const task = storage.getTask('task-in-db');
56
+ expect(task?.status).toBe('completed');
57
+ expect(task?.end_time).toBe('2026-01-01T10:30:00.000Z');
58
+ });
59
+
60
+ it('DB 中无 active task 时 completeCurrentTask 安全退出(不报错)', () => {
61
+ // 完全空 DB,completeCurrentTask 不应抛出
62
+ const segmenter = new TaskSegmenter(storage);
63
+ expect(() => {
64
+ segmenter.completeCurrentTask(SESSION, '2026-01-01T10:30:00.000Z');
65
+ }).not.toThrow();
66
+ });
67
+
68
+ it('Map 有 entry 时(正常路径)completeCurrentTask 正常关闭 task', () => {
69
+ const segmenter = new TaskSegmenter(storage);
70
+
71
+ // 通过 processPrompt 走正常路径,将 task 写入 Map
72
+ const taskId = segmenter.processPrompt(SESSION, 'implement feature', '2026-01-01T10:00:00.000Z');
73
+
74
+ // 确认 DB 中是 active
75
+ expect(storage.getTask(taskId)?.status).toBe('active');
76
+
77
+ // completeCurrentTask 正常关闭
78
+ segmenter.completeCurrentTask(SESSION, '2026-01-01T10:30:00.000Z');
79
+
80
+ const task = storage.getTask(taskId);
81
+ expect(task?.status).toBe('completed');
82
+ expect(task?.end_time).toBe('2026-01-01T10:30:00.000Z');
83
+ });
84
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ getCommand,
4
+ getFilePath,
5
+ getUserPrompt,
6
+ getSubagentType,
7
+ } from '../../src/core/event-fields.js';
8
+ import type { ForgeEvent } from '../../src/core/types.js';
9
+
10
+ function makeEvent(overrides: Partial<ForgeEvent> = {}): ForgeEvent {
11
+ return {
12
+ session_id: 's',
13
+ project_path: '/p',
14
+ timestamp: '2026-05-18T00:00:00Z',
15
+ hook_type: 'PreToolUse',
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ describe('event-fields getters', () => {
21
+ describe('getCommand', () => {
22
+ it('returns undefined when tool_input is missing', () => {
23
+ expect(getCommand(makeEvent())).toBeUndefined();
24
+ });
25
+
26
+ it('returns undefined when command is not a string', () => {
27
+ expect(getCommand(makeEvent({ tool_input: { command: 42 as unknown as string } }))).toBeUndefined();
28
+ });
29
+
30
+ it('returns the command string when present', () => {
31
+ expect(getCommand(makeEvent({ tool_input: { command: 'ls -la' } }))).toBe('ls -la');
32
+ });
33
+ });
34
+
35
+ describe('getFilePath', () => {
36
+ it('returns undefined when tool_input is missing', () => {
37
+ expect(getFilePath(makeEvent())).toBeUndefined();
38
+ });
39
+
40
+ it('returns undefined when file_path is wrong type', () => {
41
+ expect(getFilePath(makeEvent({ tool_input: { file_path: 123 as unknown as string } }))).toBeUndefined();
42
+ });
43
+
44
+ it('returns file_path when present', () => {
45
+ expect(getFilePath(makeEvent({ tool_input: { file_path: '/foo/bar.ts' } }))).toBe('/foo/bar.ts');
46
+ });
47
+
48
+ it('falls back to notebook_path when file_path is missing', () => {
49
+ expect(getFilePath(makeEvent({ tool_input: { notebook_path: '/x.ipynb' } }))).toBe('/x.ipynb');
50
+ });
51
+
52
+ it('returns undefined for empty string', () => {
53
+ expect(getFilePath(makeEvent({ tool_input: { file_path: '' } }))).toBeUndefined();
54
+ });
55
+ });
56
+
57
+ describe('getUserPrompt', () => {
58
+ it('returns undefined when neither field set', () => {
59
+ expect(getUserPrompt(makeEvent())).toBeUndefined();
60
+ });
61
+
62
+ it('returns top-level user_prompt when set', () => {
63
+ expect(getUserPrompt(makeEvent({ user_prompt: 'hi' }))).toBe('hi');
64
+ });
65
+
66
+ it('falls back to tool_input.user_prompt envelope', () => {
67
+ expect(getUserPrompt(makeEvent({ tool_input: { user_prompt: 'envelope' } }))).toBe('envelope');
68
+ });
69
+
70
+ it('returns undefined when fallback is wrong type', () => {
71
+ expect(getUserPrompt(makeEvent({ tool_input: { user_prompt: 7 as unknown as string } }))).toBeUndefined();
72
+ });
73
+ });
74
+
75
+ describe('getSubagentType', () => {
76
+ it('returns undefined when missing', () => {
77
+ expect(getSubagentType(makeEvent())).toBeUndefined();
78
+ });
79
+
80
+ it('returns undefined when wrong type', () => {
81
+ expect(getSubagentType(makeEvent({ tool_input: { subagent_type: {} as unknown as string } }))).toBeUndefined();
82
+ });
83
+
84
+ it('returns the subagent_type string', () => {
85
+ expect(getSubagentType(makeEvent({ tool_input: { subagent_type: 'planner' } }))).toBe('planner');
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { EventParser } from '../../src/daemon/event-parser.js';
3
+
4
+ const parser = new EventParser();
5
+
6
+ function makeRaw(overrides: Record<string, unknown> = {}) {
7
+ return JSON.stringify({
8
+ hook_type: 'PreToolUse',
9
+ timestamp: '2026-05-18T00:00:00Z',
10
+ session_id: 's1',
11
+ project_path: '/p',
12
+ tool_name: 'Bash',
13
+ ...overrides,
14
+ });
15
+ }
16
+
17
+ describe('EventParser', () => {
18
+ it('generates a UUID event_id when none is provided', () => {
19
+ const evt = parser.parse(makeRaw());
20
+ expect(evt.event_id).toMatch(/^[0-9a-f-]{36}$/i);
21
+ });
22
+
23
+ it('passes through a valid externally-supplied event_id', () => {
24
+ const id = '11111111-2222-4333-8444-555555555555';
25
+ const evt = parser.parse(makeRaw({ event_id: id }));
26
+ expect(evt.event_id).toBe(id);
27
+ });
28
+
29
+ it('rejects a malformed event_id via schema', () => {
30
+ expect(() => parser.parse(makeRaw({ event_id: 'not-a-uuid' }))).toThrow();
31
+ });
32
+
33
+ it('lands tool_input fields into ToolInputFields shape', () => {
34
+ const evt = parser.parse(
35
+ makeRaw({ tool_input: { command: 'ls', file_path: '/a/b.ts', extra: 1 } })
36
+ );
37
+ expect(evt.tool_input?.command).toBe('ls');
38
+ expect(evt.tool_input?.file_path).toBe('/a/b.ts');
39
+ expect(evt.tool_input?.extra).toBe(1);
40
+ });
41
+
42
+ it('handles UserPromptSubmit hook with user_prompt at top level', () => {
43
+ const evt = parser.parse(
44
+ JSON.stringify({
45
+ hook_type: 'UserPromptSubmit',
46
+ timestamp: '2026-05-18T00:00:00Z',
47
+ session_id: 's1',
48
+ project_path: '/p',
49
+ user_prompt: 'hi there',
50
+ })
51
+ );
52
+ expect(evt.user_prompt).toBe('hi there');
53
+ expect(evt.event_id).toMatch(/^[0-9a-f-]{36}$/i);
54
+ });
55
+ });