@winspan/claude-forge 8.50.6 → 8.51.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 (361) 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/index.d.ts.map +1 -1
  114. package/dist/daemon/index.js +23 -9
  115. package/dist/daemon/index.js.map +1 -1
  116. package/dist/daemon/lifecycle.js +3 -4
  117. package/dist/daemon/lifecycle.js.map +1 -1
  118. package/dist/daemon/server.d.ts +6 -4
  119. package/dist/daemon/server.d.ts.map +1 -1
  120. package/dist/daemon/server.js +76 -85
  121. package/dist/daemon/server.js.map +1 -1
  122. package/dist/daemon/services/task-segmenter.js +1 -1
  123. package/dist/daemon/services/task-segmenter.js.map +1 -1
  124. package/dist/hooks/hook-lib.sh +37 -0
  125. package/dist/hooks/notification.sh +2 -2
  126. package/dist/hooks/post-tool-use.sh +2 -2
  127. package/dist/hooks/pre-tool-use.sh +2 -2
  128. package/dist/hooks/stop.sh +9 -6
  129. package/dist/hooks/user-prompt-submit.sh +2 -2
  130. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
  131. package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
  132. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
  133. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
  134. package/dist/web/analytics/drift-detector.d.ts.map +1 -0
  135. package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
  136. package/dist/web/analytics/drift-detector.js.map +1 -0
  137. package/dist/web/analytics/weekly-report.d.ts.map +1 -0
  138. package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
  139. package/dist/web/analytics/weekly-report.js.map +1 -0
  140. package/dist/web/auth-middleware.d.ts.map +1 -1
  141. package/dist/web/auth-middleware.js +1 -2
  142. package/dist/web/auth-middleware.js.map +1 -1
  143. package/dist/web/routes/_helpers.d.ts +16 -0
  144. package/dist/web/routes/_helpers.d.ts.map +1 -0
  145. package/dist/web/routes/_helpers.js +32 -0
  146. package/dist/web/routes/_helpers.js.map +1 -0
  147. package/dist/web/routes/drift.js +1 -1
  148. package/dist/web/routes/drift.js.map +1 -1
  149. package/dist/web/routes/insights.js +1 -1
  150. package/dist/web/routes/insights.js.map +1 -1
  151. package/dist/web/routes/reports.js +1 -1
  152. package/dist/web/routes/reports.js.map +1 -1
  153. package/dist/web/routes/rules.d.ts +3 -0
  154. package/dist/web/routes/rules.d.ts.map +1 -1
  155. package/dist/web/routes/rules.js +28 -52
  156. package/dist/web/routes/rules.js.map +1 -1
  157. package/dist/web/routes/sessions.d.ts.map +1 -1
  158. package/dist/web/routes/sessions.js +16 -30
  159. package/dist/web/routes/sessions.js.map +1 -1
  160. package/dist/web/routes/skill-stats.d.ts +2 -0
  161. package/dist/web/routes/skill-stats.d.ts.map +1 -1
  162. package/dist/web/routes/skill-stats.js +28 -64
  163. package/dist/web/routes/skill-stats.js.map +1 -1
  164. package/dist/web/routes/skills.d.ts.map +1 -1
  165. package/dist/web/routes/skills.js +5 -4
  166. package/dist/web/routes/skills.js.map +1 -1
  167. package/dist/web/routes/stats.d.ts +4 -0
  168. package/dist/web/routes/stats.d.ts.map +1 -1
  169. package/dist/web/routes/stats.js +19 -21
  170. package/dist/web/routes/stats.js.map +1 -1
  171. package/dist/web/routes/tasks.d.ts.map +1 -1
  172. package/dist/web/routes/tasks.js +17 -42
  173. package/dist/web/routes/tasks.js.map +1 -1
  174. package/dist/web/routes/trace.d.ts.map +1 -1
  175. package/dist/web/routes/trace.js +7 -17
  176. package/dist/web/routes/trace.js.map +1 -1
  177. package/dist/web/routes/types.d.ts.map +1 -1
  178. package/dist/web/routes/types.js +4 -3
  179. package/dist/web/routes/types.js.map +1 -1
  180. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
  181. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
  182. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
  183. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
  184. package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
  185. package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
  186. package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
  187. package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
  188. package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
  189. package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
  190. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
  191. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
  192. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
  193. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
  194. package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
  195. package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
  196. package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
  197. package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
  198. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
  199. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
  200. package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
  201. package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
  202. package/dist/web/static/assets/index-CBX47X8l.js +3 -0
  203. package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
  204. package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
  205. package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
  206. package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
  207. package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
  208. package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
  209. package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
  210. package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
  211. package/dist/web/static/index.html +4 -4
  212. package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
  213. package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
  214. package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
  215. package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
  216. package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
  217. package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
  218. package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
  219. package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
  220. package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
  221. package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
  222. package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
  223. package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
  224. package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
  225. package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
  226. package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
  227. package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
  228. package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
  229. package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
  230. package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
  231. package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
  232. package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
  233. package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
  234. package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
  235. package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
  236. package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
  237. package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
  238. package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
  239. package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
  240. package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
  241. package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
  242. package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
  243. package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
  244. package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
  245. package/docs/reviews/task-title-summary.md +92 -0
  246. package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
  247. package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
  248. package/package.json +2 -2
  249. package/src/claudemd/claudemd-generator.ts +29 -238
  250. package/src/claudemd/resume-manager.ts +1 -1
  251. package/src/claudemd/templates/swarm-protocol.md +222 -0
  252. package/src/cli/commands/daemon.ts +6 -6
  253. package/src/cli/commands/executions.ts +4 -3
  254. package/src/cli/commands/init.ts +2 -2
  255. package/src/cli/commands/logs.ts +1 -1
  256. package/src/cli/commands/mcp.ts +3 -5
  257. package/src/cli/commands/menu.ts +4 -3
  258. package/src/cli/commands/stats.ts +2 -3
  259. package/src/cli/commands/status.ts +2 -2
  260. package/src/cli/commands/trace.ts +10 -26
  261. package/src/cli/init/hook-manager.ts +2 -2
  262. package/src/core/ai/provider.ts +2 -2
  263. package/src/core/constants.ts +18 -1
  264. package/src/core/event-fields.ts +32 -0
  265. package/src/core/queue/index.ts +3 -4
  266. package/src/core/storage/base.ts +132 -56
  267. package/src/core/storage/events.ts +183 -4
  268. package/src/core/storage/routing.ts +129 -1
  269. package/src/core/storage/schema.sql +12 -2
  270. package/src/core/storage/sessions.ts +64 -0
  271. package/src/core/storage/skills.ts +69 -0
  272. package/src/core/storage/sqlite.ts +103 -4
  273. package/src/core/storage/tasks.ts +149 -1
  274. package/src/core/storage/token-usage.ts +1 -1
  275. package/src/core/types.ts +30 -3
  276. package/src/core/utils/error-handler.ts +3 -2
  277. package/src/core/utils/git.ts +23 -0
  278. package/src/core/utils/logger.ts +16 -1
  279. package/src/core/utils/lru-cache.ts +4 -0
  280. package/src/core/utils/token-tracker.ts +1 -1
  281. package/src/daemon/event-parser.ts +4 -3
  282. package/src/daemon/handlers/history-exporter.ts +1 -1
  283. package/src/daemon/handlers/post-tool-use.ts +7 -3
  284. package/src/daemon/handlers/stop.ts +32 -39
  285. package/src/daemon/handlers/user-prompt.ts +12 -22
  286. package/src/daemon/index.ts +24 -10
  287. package/src/daemon/lifecycle.ts +3 -3
  288. package/src/daemon/server.ts +76 -89
  289. package/src/daemon/services/task-segmenter.ts +1 -1
  290. package/src/hooks/hook-lib.sh +37 -0
  291. package/src/hooks/notification.sh +2 -2
  292. package/src/hooks/post-tool-use.sh +2 -2
  293. package/src/hooks/pre-tool-use.sh +2 -2
  294. package/src/hooks/stop.sh +9 -6
  295. package/src/hooks/user-prompt-submit.sh +2 -2
  296. package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
  297. package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
  298. package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
  299. package/src/web/auth-middleware.ts +1 -2
  300. package/src/web/routes/_helpers.ts +34 -0
  301. package/src/web/routes/drift.ts +1 -1
  302. package/src/web/routes/insights.ts +1 -1
  303. package/src/web/routes/reports.ts +1 -1
  304. package/src/web/routes/rules.ts +31 -56
  305. package/src/web/routes/sessions.ts +18 -30
  306. package/src/web/routes/skill-stats.ts +29 -69
  307. package/src/web/routes/skills.ts +5 -4
  308. package/src/web/routes/stats.ts +19 -29
  309. package/src/web/routes/tasks.ts +17 -42
  310. package/src/web/routes/trace.ts +7 -19
  311. package/src/web/routes/types.ts +4 -3
  312. package/tests/integration/claudemd-generator.test.ts +90 -0
  313. package/tests/integration/web-analytics.integration.test.ts +133 -0
  314. package/tests/integration/web-stats.integration.test.ts +135 -0
  315. package/tests/integration/web-trace.integration.test.ts +175 -0
  316. package/tests/unit/core/forge-paths.test.ts +99 -0
  317. package/tests/unit/daemon/post-tool-use.test.ts +121 -0
  318. package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
  319. package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
  320. package/tests/unit/event-fields.test.ts +88 -0
  321. package/tests/unit/event-parser.test.ts +55 -0
  322. package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
  323. package/tests/unit/socket-server.test.ts +183 -0
  324. package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
  325. package/tests/unit/storage/migration-idempotent.test.ts +304 -0
  326. package/tests/unit/storage/routing-aggregates.test.ts +276 -0
  327. package/tests/unit/storage/routing.test.ts +117 -0
  328. package/tests/unit/storage/schema-missing.test.ts +81 -0
  329. package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
  330. package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
  331. package/tests/unit/storage/skills-aggregates.test.ts +104 -0
  332. package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
  333. package/tests/unit/storage/task-operations-counts.test.ts +46 -0
  334. package/tests/unit/storage/tasks-getById.test.ts +343 -0
  335. package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
  336. package/tests/unit/token-usage.test.ts +6 -6
  337. package/tests/unit/web/navigation-back-contract.test.ts +134 -0
  338. package/tests/unit/web/routes-rules.test.ts +182 -0
  339. package/tests/unit/web/routes-tasks.test.ts +34 -0
  340. package/tests/unit/web/task-title-contract.test.ts +210 -0
  341. package/tests/unit/web/tasks-component-contract.test.ts +179 -0
  342. package/vitest.config.ts +1 -1
  343. package/web/src/pages/TaskDetail.tsx +9 -5
  344. package/web/src/pages/Tasks.tsx +315 -50
  345. package/web/src/utils/navigation.ts +25 -0
  346. package/web/src/utils/task-title.ts +49 -0
  347. package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
  348. package/dist/daemon/services/drift-detector.d.ts.map +0 -1
  349. package/dist/daemon/services/drift-detector.js.map +0 -1
  350. package/dist/daemon/services/weekly-report.d.ts.map +0 -1
  351. package/dist/daemon/services/weekly-report.js.map +0 -1
  352. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
  353. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
  354. package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
  355. package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
  356. package/dist/web/static/assets/index-D8AKj26b.css +0 -1
  357. package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
  358. package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
  359. package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
  360. /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
  361. /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
@@ -55,8 +55,8 @@ export class AntiPatternDetector {
55
55
  const detectedAt = new Date(now).toISOString();
56
56
 
57
57
  // 一次性拉取窗口内事件 + 会话,4 条规则共用
58
- const events = this.fetchEventsSince(sinceISO);
59
- const sessions = this.fetchSessionsSince(sinceISO);
58
+ const events = this.queryEventsSince(sinceISO);
59
+ const sessions = this.querySessionsSince(sinceISO);
60
60
 
61
61
  const patterns: AntiPattern[] = [
62
62
  ...this.detectEditLoop(events, detectedAt),
@@ -76,50 +76,14 @@ export class AntiPatternDetector {
76
76
  /**
77
77
  * 拉取窗口内全部事件。
78
78
  *
79
- * queryEvents 没有时间过滤参数,因此使用 getDatabase 直接窗口查询。
80
- * 这是 daemon/services 层的既定做法(见 DriftDetector)。
79
+ * H2 Phase 3: 改用 facade 聚合方法。
81
80
  */
82
- private fetchEventsSince(sinceISO: string): ForgeEvent[] {
83
- const db = this.storage.getDatabase();
84
- const rows = db.prepare(
85
- `SELECT * FROM events WHERE timestamp >= ? ORDER BY timestamp ASC`
86
- ).all(sinceISO) as Array<Record<string, unknown>>;
87
-
88
- return rows.map((row) => ({
89
- event_id: row.event_id as string,
90
- session_id: row.session_id as string,
91
- project_path: row.project_path as string,
92
- timestamp: row.timestamp as string,
93
- hook_type: row.hook_type as ForgeEvent['hook_type'],
94
- tool_name: (row.tool_name as string) || undefined,
95
- tool_input: row.tool_input ? safeParse(row.tool_input as string) : undefined,
96
- tool_output: row.tool_output ? safeParse(row.tool_output as string) : undefined,
97
- user_prompt: (row.user_prompt as string) || undefined,
98
- ai_response: (row.ai_response as string) || undefined,
99
- }));
81
+ private queryEventsSince(sinceISO: string): ForgeEvent[] {
82
+ return this.storage.queryEventsByTimeRange({ since: sinceISO });
100
83
  }
101
84
 
102
- private fetchSessionsSince(sinceISO: string): SessionSummary[] {
103
- const db = this.storage.getDatabase();
104
- const rows = db.prepare(
105
- `SELECT
106
- session_id,
107
- first_prompt,
108
- event_count,
109
- start_time,
110
- COALESCE(end_time, last_event_time, start_time) AS end_time
111
- FROM sessions
112
- WHERE start_time >= ?
113
- ORDER BY start_time DESC`
114
- ).all(sinceISO) as Array<Record<string, unknown>>;
115
-
116
- return rows.map((r) => ({
117
- session_id: r.session_id as string,
118
- first_prompt: (r.first_prompt as string) || '',
119
- event_count: (r.event_count as number) ?? 0,
120
- start_time: r.start_time as string,
121
- end_time: r.end_time as string,
122
- }));
85
+ private querySessionsSince(sinceISO: string): SessionSummary[] {
86
+ return this.storage.querySessionsByTimeRange({ since: sinceISO });
123
87
  }
124
88
 
125
89
  // ── Rule 1: edit_loop ─────────────────────────────────────────────────
@@ -342,23 +306,14 @@ export class AntiPatternDetector {
342
306
 
343
307
  // ── helpers ─────────────────────────────────────────────────────────────
344
308
 
345
- function safeParse(json: string): Record<string, unknown> | undefined {
346
- try {
347
- const v = JSON.parse(json);
348
- return v && typeof v === 'object' ? (v as Record<string, unknown>) : undefined;
349
- } catch {
350
- return undefined;
351
- }
352
- }
353
-
354
- function extractFilePath(input: Record<string, unknown> | undefined): string | undefined {
309
+ function extractFilePath(input: ForgeEvent['tool_input']): string | undefined {
355
310
  if (!input) return undefined;
356
311
  const fp = input.file_path;
357
312
  if (typeof fp === 'string' && fp.length > 0) return fp;
358
313
  return undefined;
359
314
  }
360
315
 
361
- function extractBashCommand(input: Record<string, unknown> | undefined): string | undefined {
316
+ function extractBashCommand(input: ForgeEvent['tool_input']): string | undefined {
362
317
  if (!input) return undefined;
363
318
  const cmd = input.command;
364
319
  return typeof cmd === 'string' ? cmd : undefined;
@@ -121,12 +121,8 @@ export class DriftDetector {
121
121
  * 预期: 至少 3 个不同 Skill 被调用
122
122
  */
123
123
  private checkActiveSkillCoverage(since: number): DriftCheck {
124
- const db = this.storage.getDatabase();
125
- const row = db.prepare(
126
- `SELECT COUNT(DISTINCT skill_id) as cnt FROM skill_invocations
127
- WHERE timestamp >= ?`
128
- ).get(since) as { cnt: number } | undefined;
129
- const distinctCount = row?.cnt ?? 0;
124
+ // H2 Phase 3: 改用 facade。countDistinctSkillsSince 接收 unix ms。
125
+ const distinctCount = this.storage.countDistinctSkillsSince(since);
130
126
 
131
127
  let status: DriftCheck['status'];
132
128
  if (distinctCount >= 3) {
@@ -150,12 +146,11 @@ export class DriftDetector {
150
146
  * 预期: 最近 7 天中至少 5 天有事件(工作日)
151
147
  */
152
148
  private checkEventContinuity(): DriftCheck {
153
- const db = this.storage.getDatabase();
154
- const row = db.prepare(
155
- `SELECT COUNT(DISTINCT date(timestamp)) as cnt FROM events
156
- WHERE timestamp >= date('now', '-7 days')`
157
- ).get() as { cnt: number } | undefined;
158
- const activeDays = row?.cnt ?? 0;
149
+ // H2 Phase 3: 改用 facade。
150
+ // SQL: timestamp >= date('now', '-7 days')
151
+ // 新口径:调用方计算 ISO 字符串。
152
+ const sevenDaysAgoIso = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
153
+ const activeDays = this.storage.countActiveDays({ since: sevenDaysAgoIso });
159
154
 
160
155
  let status: DriftCheck['status'];
161
156
  if (activeDays >= 5) {
@@ -179,18 +174,10 @@ export class DriftDetector {
179
174
  * 预期: 每个 session 至少有 1 个 task (tasks/sessions >= 0.8)
180
175
  */
181
176
  private checkTaskSegmentation(since: number): DriftCheck {
182
- const db = this.storage.getDatabase();
177
+ // H2 Phase 3: 改用 facade。
183
178
  const sinceISO = new Date(since).toISOString();
184
-
185
- const sessionRow = db.prepare(
186
- `SELECT COUNT(*) as cnt FROM sessions WHERE start_time >= ?`
187
- ).get(sinceISO) as { cnt: number } | undefined;
188
- const sessionCount = sessionRow?.cnt ?? 0;
189
-
190
- const taskRow = db.prepare(
191
- `SELECT COUNT(*) as cnt FROM tasks WHERE start_time >= ?`
192
- ).get(sinceISO) as { cnt: number } | undefined;
193
- const taskCount = taskRow?.cnt ?? 0;
179
+ const sessionCount = this.storage.countSessionsByRange({ since: sinceISO });
180
+ const taskCount = this.storage.countTasksByRange({ since: sinceISO });
194
181
 
195
182
  const ratio = sessionCount > 0
196
183
  ? Math.round((taskCount / sessionCount) * 100) / 100
@@ -95,38 +95,26 @@ export class WeeklyReportGenerator {
95
95
  // ── Aggregation methods ──────────────────────────────────────────────
96
96
 
97
97
  private aggregateOverview(week: WeekRange): WeeklyReport['overview'] {
98
- const db = this.storage.getDatabase();
99
-
100
- const eventsRow = db.prepare(
101
- `SELECT COUNT(*) as cnt, COUNT(DISTINCT session_id) as sessionCount,
102
- COUNT(DISTINCT date(timestamp)) as dayCount
103
- FROM events
104
- WHERE timestamp >= ? AND timestamp < ?`,
105
- ).get(week.startISO, week.endISO) as {
106
- cnt: number;
107
- sessionCount: number;
108
- dayCount: number;
109
- };
110
-
111
- const projectRows = db.prepare(
112
- `SELECT DISTINCT project_path
113
- FROM events
114
- WHERE timestamp >= ? AND timestamp < ?
115
- AND project_path IS NOT NULL AND project_path != ''`,
116
- ).all(week.startISO, week.endISO) as Array<{ project_path: string }>;
117
-
118
- const tasksRow = db.prepare(
119
- `SELECT COUNT(*) as cnt
120
- FROM tasks
121
- WHERE start_time >= ? AND start_time < ?`,
122
- ).get(week.startISO, week.endISO) as { cnt: number };
98
+ // H2 Phase 3: 改用 facade 聚合方法。
99
+ const overview = this.storage.aggregateOverviewByRange({
100
+ since: week.startISO,
101
+ until: week.endISO,
102
+ });
103
+ const activeProjects = this.storage.queryDistinctProjects({
104
+ since: week.startISO,
105
+ until: week.endISO,
106
+ });
107
+ const totalTasks = this.storage.countTasksByRange({
108
+ since: week.startISO,
109
+ until: week.endISO,
110
+ });
123
111
 
124
112
  return {
125
- totalSessions: eventsRow.sessionCount || 0,
126
- totalEvents: eventsRow.cnt || 0,
127
- totalTasks: tasksRow.cnt || 0,
128
- activeProjects: projectRows.map(r => r.project_path),
129
- activeDays: eventsRow.dayCount || 0,
113
+ totalSessions: overview.session_count || 0,
114
+ totalEvents: overview.event_count || 0,
115
+ totalTasks: totalTasks || 0,
116
+ activeProjects,
117
+ activeDays: overview.day_count || 0,
130
118
  };
131
119
  }
132
120
 
@@ -209,42 +197,36 @@ export class WeeklyReportGenerator {
209
197
  }
210
198
 
211
199
  private aggregateTools(week: WeekRange): WeeklyReport['tools'] {
212
- const db = this.storage.getDatabase();
213
-
214
- const toolRows = db.prepare(
215
- `SELECT tool_name, COUNT(*) as cnt
216
- FROM events
217
- WHERE timestamp >= ? AND timestamp < ?
218
- AND tool_name IS NOT NULL AND tool_name != ''
219
- AND hook_type = 'PreToolUse'
220
- GROUP BY tool_name
221
- ORDER BY cnt DESC`,
222
- ).all(week.startISO, week.endISO) as Array<{ tool_name: string; cnt: number }>;
200
+ // H2 Phase 3: 改用 facade。
201
+ // Phase 2 偏差:spec 列出的 aggregateToolUsage({ since, until, ... }) 实际只实现了
202
+ // since/limit/hook_type,缺 until。这里用 queryEventsByTimeRange + 内存聚合保留
203
+ // 旧 SQL 的 [since, until) 半开区间语义(changelog 已记)。
204
+ const preEvents = this.storage.queryEventsByTimeRange({
205
+ since: week.startISO,
206
+ until: week.endISO,
207
+ }).filter(e => e.hook_type === 'PreToolUse' && e.tool_name);
208
+
209
+ const toolCount = new Map<string, number>();
210
+ for (const ev of preEvents) {
211
+ const name = ev.tool_name as string;
212
+ if (!name) continue;
213
+ toolCount.set(name, (toolCount.get(name) || 0) + 1);
214
+ }
215
+ const toolRows = Array.from(toolCount.entries())
216
+ .map(([tool_name, cnt]) => ({ tool_name, cnt }))
217
+ .sort((a, b) => b.cnt - a.cnt);
223
218
 
224
219
  const totalCalls = toolRows.reduce((sum, r) => sum + r.cnt, 0);
225
220
  const topTools = toolRows.slice(0, 5).map(r => ({ name: r.tool_name, count: r.cnt }));
226
221
 
227
- // Failure detection: PostToolUse with error keyword in tool_output
228
- const failureRow = db.prepare(
229
- `SELECT COUNT(*) as failed
230
- FROM events
231
- WHERE timestamp >= ? AND timestamp < ?
232
- AND hook_type = 'PostToolUse'
233
- AND tool_output IS NOT NULL
234
- AND (tool_output LIKE '%"error"%'
235
- OR tool_output LIKE '%"is_error":true%'
236
- OR tool_output LIKE '%"isError":true%')`,
237
- ).get(week.startISO, week.endISO) as { failed: number };
238
-
239
- const postCallRow = db.prepare(
240
- `SELECT COUNT(*) as total
241
- FROM events
242
- WHERE timestamp >= ? AND timestamp < ?
243
- AND hook_type = 'PostToolUse'`,
244
- ).get(week.startISO, week.endISO) as { total: number };
245
-
246
- const failureRate = postCallRow.total > 0
247
- ? `${((failureRow.failed / postCallRow.total) * 100).toFixed(1)}%`
222
+ // Failure rate: facade 提供 [since, until) 半开区间精确计算。
223
+ const failure = this.storage.aggregateToolFailureRate({
224
+ since: week.startISO,
225
+ until: week.endISO,
226
+ });
227
+
228
+ const failureRate = failure.post_total > 0
229
+ ? `${((failure.failed / failure.post_total) * 100).toFixed(1)}%`
248
230
  : '0.0%';
249
231
 
250
232
  return {
@@ -255,17 +237,12 @@ export class WeeklyReportGenerator {
255
237
  }
256
238
 
257
239
  private aggregateFiles(week: WeekRange): WeeklyReport['files'] {
258
- const db = this.storage.getDatabase();
259
-
260
- const placeholders = Array.from(FILE_EDIT_TOOLS).map(() => '?').join(',');
261
- const rows = db.prepare(
262
- `SELECT tool_input
263
- FROM events
264
- WHERE timestamp >= ? AND timestamp < ?
265
- AND hook_type = 'PreToolUse'
266
- AND tool_name IN (${placeholders})
267
- AND tool_input IS NOT NULL`,
268
- ).all(week.startISO, week.endISO, ...Array.from(FILE_EDIT_TOOLS)) as Array<{ tool_input: string }>;
240
+ // H2 Phase 3: 改用 facade 聚合方法。
241
+ const rows = this.storage.queryFileEditInputs({
242
+ since: week.startISO,
243
+ until: week.endISO,
244
+ tool_names: Array.from(FILE_EDIT_TOOLS),
245
+ });
269
246
 
270
247
  const fileMap = new Map<string, number>();
271
248
  let totalEdits = 0;
@@ -291,8 +268,8 @@ export class WeeklyReportGenerator {
291
268
  }
292
269
 
293
270
  private aggregateAnomalies(_week: WeekRange): WeeklyReport['anomalies'] {
294
- // anti-pattern-detector.ts is being implemented by another agent.
295
- // Return empty array until it's available.
271
+ // Anomaly aggregation is delegated to AntiPatternDetector
272
+ // (exposed separately via /api/insights). Keep empty here.
296
273
  return [];
297
274
  }
298
275
 
@@ -11,11 +11,10 @@
11
11
  */
12
12
 
13
13
  import fs from 'fs';
14
- import path from 'path';
15
14
  import type { Request, Response, NextFunction } from 'express';
16
15
  import { FORGE_PATHS } from '../core/constants.js';
17
16
 
18
- const TOKEN_FILE = path.join(FORGE_PATHS.home(), 'daemon.token');
17
+ const TOKEN_FILE = FORGE_PATHS.daemonToken();
19
18
 
20
19
  /** 读取当前 daemon auth token,不存在或读取失败返回 null */
21
20
  export function readAuthToken(): string | null {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared helpers for web routes.
3
+ */
4
+
5
+ /**
6
+ * Recursively truncate string/array fields inside an arbitrary value so the
7
+ * serialised JSON payload stays bounded. Used to keep tool input/output
8
+ * previews small in /api/tasks and /api/sessions.
9
+ *
10
+ * - strings longer than `maxLen` are sliced and suffixed with "..."
11
+ * - arrays are sliced to the first `arrayLimit` elements (NOT recursed into,
12
+ * matching the original call sites' behaviour)
13
+ * - objects are walked recursively
14
+ * - everything else is returned as-is (including null/undefined)
15
+ */
16
+ export function truncateField(
17
+ obj: unknown,
18
+ maxLen = 300,
19
+ arrayLimit = 10,
20
+ ): unknown {
21
+ if (!obj) return obj;
22
+ if (typeof obj === 'string') {
23
+ return obj.length > maxLen ? obj.slice(0, maxLen) + '...' : obj;
24
+ }
25
+ if (Array.isArray(obj)) return obj.slice(0, arrayLimit);
26
+ if (typeof obj === 'object') {
27
+ const result: Record<string, unknown> = {};
28
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
29
+ result[k] = truncateField(v, maxLen, arrayLimit);
30
+ }
31
+ return result;
32
+ }
33
+ return obj;
34
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Application } from 'express';
2
2
  import type { RouteContext } from './types.js';
3
- import { DriftDetector } from '../../daemon/services/drift-detector.js';
3
+ import { DriftDetector } from '../analytics/drift-detector.js';
4
4
 
5
5
  /**
6
6
  * /api/drift/* — CLAUDE.md 漂移检测 API
@@ -4,7 +4,7 @@ import {
4
4
  AntiPatternDetector,
5
5
  type AntiPattern,
6
6
  type AntiPatternSeverity,
7
- } from '../../daemon/services/anti-pattern-detector.js';
7
+ } from '../analytics/anti-pattern-detector.js';
8
8
 
9
9
  /**
10
10
  * /api/insights — 工作流反模式检测 API
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { Application } from 'express';
9
9
  import type { RouteContext } from './types.js';
10
- import { WeeklyReportGenerator } from '../../daemon/services/weekly-report.js';
10
+ import { WeeklyReportGenerator } from '../analytics/weekly-report.js';
11
11
 
12
12
  export function registerReportsRoutes(app: Application, ctx: RouteContext): void {
13
13
  const { storage, skillRegistry } = ctx;
@@ -7,11 +7,19 @@
7
7
  * - Per-skill breakdown
8
8
  * - Per-agent breakdown
9
9
  * - Never-triggered skills
10
+ *
11
+ * H1 refactor: pushed GROUP BY into SQL via storage.aggregateRoutingStats /
12
+ * aggregateSkillInvocationsBySkill — no more 100k-row JS scans.
10
13
  */
11
14
 
12
15
  import type { Application } from 'express';
13
16
  import type { RouteContext } from './types.js';
14
17
 
18
+ function pct(num: number, denom: number): string {
19
+ if (denom <= 0) return '0%';
20
+ return `${(num / denom * 100).toFixed(1)}%`;
21
+ }
22
+
15
23
  export function registerRulesRoutes(app: Application, ctx: RouteContext): void {
16
24
  const { storage, skillRegistry } = ctx;
17
25
 
@@ -20,68 +28,35 @@ export function registerRulesRoutes(app: Application, ctx: RouteContext): void {
20
28
  const since = Date.now() - days * 24 * 3600 * 1000;
21
29
  const sinceISO = new Date(since).toISOString();
22
30
 
23
- // Query routing events
24
- const routingEvents = storage.queryRoutingEvents({ since_ts: since, limit: 100000 });
25
- const totalPrompts = routingEvents.length;
26
-
27
- // Agent delegations: obeyed=1 means the routing was followed
28
- const agentDelegations = routingEvents.filter(e => e.obeyed === 1).length;
29
- const agentRate = totalPrompts > 0
30
- ? `${(agentDelegations / totalPrompts * 100).toFixed(1)}%`
31
- : '0%';
32
-
33
- // Skill invocations
34
- const skillInvocations = storage.querySkillInvocations({ since, limit: 100000 });
35
- const skillTotal = skillInvocations.length;
36
- const skillRate = totalPrompts > 0
37
- ? `${(skillTotal / totalPrompts * 100).toFixed(1)}%`
38
- : '0%';
39
-
40
- // Per-skill aggregation
41
- const skillMap = new Map<string, { total: number; success: number; failed: number }>();
42
- for (const inv of skillInvocations) {
43
- const cur = skillMap.get(inv.skill_id) || { total: 0, success: 0, failed: 0 };
44
- cur.total++;
45
- if (inv.success === 1) cur.success++;
46
- else cur.failed++;
47
- skillMap.set(inv.skill_id, cur);
48
- }
31
+ // SQL-side aggregates (replaces 2 × 100k-row scans + JS reduce)
32
+ const routing = storage.aggregateRoutingStats({ since_ts: since });
33
+ const skillsAgg = storage.aggregateSkillInvocationsBySkill({ since });
49
34
 
50
- const skills = Array.from(skillMap.entries())
51
- .map(([skill_id, s]) => ({
52
- skill_id,
53
- total: s.total,
54
- success: s.success,
55
- failed: s.failed,
56
- rate: totalPrompts > 0
57
- ? `${(s.total / totalPrompts * 100).toFixed(1)}%`
58
- : '0%',
59
- }))
60
- .sort((a, b) => b.total - a.total);
35
+ const totalPrompts = routing.total;
36
+ const agentDelegations = routing.obeyed;
61
37
 
62
- // Per-agent aggregation (only obeyed delegations)
63
- const agentMap = new Map<string, number>();
64
- for (const e of routingEvents) {
65
- if (e.obeyed === 1 && e.routed_to_name) {
66
- agentMap.set(e.routed_to_name, (agentMap.get(e.routed_to_name) || 0) + 1);
67
- }
68
- }
38
+ // Skills frequency (sorted by total desc — SQL ORDER BY total DESC)
39
+ const skillTotal = skillsAgg.reduce((acc, s) => acc + s.total, 0);
40
+ const skills = skillsAgg.map(s => ({
41
+ skill_id: s.skill_id,
42
+ total: s.total,
43
+ success: s.success,
44
+ failed: s.failed,
45
+ rate: pct(s.total, totalPrompts),
46
+ }));
69
47
 
70
- const agents = Array.from(agentMap.entries())
71
- .map(([agent, count]) => ({
72
- agent,
73
- count,
74
- rate: totalPrompts > 0
75
- ? `${(count / totalPrompts * 100).toFixed(1)}%`
76
- : '0%',
77
- }))
78
- .sort((a, b) => b.count - a.count);
48
+ // Agents breakdown (already obeyed=1, sorted by count desc in SQL)
49
+ const agents = routing.by_agent.map(a => ({
50
+ agent: a.agent,
51
+ count: a.count,
52
+ rate: pct(a.count, totalPrompts),
53
+ }));
79
54
 
80
55
  // Never-triggered skills
56
+ const triggeredSkillIds = new Set(skillsAgg.map(s => s.skill_id));
81
57
  const allSkillIds = skillRegistry
82
58
  ? skillRegistry.getAll().map(s => s.id)
83
59
  : [];
84
- const triggeredSkillIds = new Set(skillMap.keys());
85
60
  const neverTriggered = allSkillIds.filter(id => !triggeredSkillIds.has(id));
86
61
 
87
62
  res.json({
@@ -89,9 +64,9 @@ export function registerRulesRoutes(app: Application, ctx: RouteContext): void {
89
64
  summary: {
90
65
  totalPrompts,
91
66
  agentDelegations,
92
- agentRate,
67
+ agentRate: pct(agentDelegations, totalPrompts),
93
68
  skillInvocations: skillTotal,
94
- skillRate,
69
+ skillRate: pct(skillTotal, totalPrompts),
95
70
  },
96
71
  skills,
97
72
  agents,
@@ -1,5 +1,7 @@
1
1
  import type { Application } from 'express';
2
2
  import type { RouteContext } from './types.js';
3
+ import { truncateField } from './_helpers.js';
4
+ import { getCommand, getFilePath, getUserPrompt } from '../../core/event-fields.js';
3
5
 
4
6
  /**
5
7
  * /api/sessions* — session lists, per-session detail, timeline.
@@ -78,22 +80,22 @@ export function registerSessionsRoutes(app: Application, ctx: RouteContext): voi
78
80
 
79
81
  const buildTaskDetail = (events: typeof allEvents, injections: typeof allInjections) => {
80
82
  const prompts = events
81
- .filter(e => e.hook_type === 'UserPromptSubmit' && (e.user_prompt || (e.tool_input as any)?.user_prompt))
82
- .map(e => ({ timestamp: e.timestamp, content: e.user_prompt || (e.tool_input as any)?.user_prompt }));
83
+ .filter(e => e.hook_type === 'UserPromptSubmit' && getUserPrompt(e))
84
+ .map(e => ({ timestamp: e.timestamp, content: getUserPrompt(e) }));
83
85
 
84
86
  const toolUsage: Record<string, number> = {};
85
87
  events.forEach(e => { if (e.tool_name) toolUsage[e.tool_name] = (toolUsage[e.tool_name] || 0) + 1; });
86
88
 
87
89
  const filesChanged = [...new Set(
88
90
  events
89
- .filter(e => (e.tool_name === 'Edit' || e.tool_name === 'Write') && (e.tool_input as any)?.file_path)
90
- .map(e => (e.tool_input as any).file_path),
91
+ .filter(e => (e.tool_name === 'Edit' || e.tool_name === 'Write') && getFilePath(e))
92
+ .map(e => getFilePath(e)!),
91
93
  )];
92
94
 
93
95
  const commits = events
94
- .filter(e => e.tool_name === 'Bash' && (e.tool_input as any)?.command?.includes('git commit'))
96
+ .filter(e => e.tool_name === 'Bash' && getCommand(e)?.includes('git commit'))
95
97
  .map(e => {
96
- const cmd = (e.tool_input as any)?.command || '';
98
+ const cmd = getCommand(e) || '';
97
99
  const match = cmd.match(/git commit.*-m\s+["']([^"']+)["']/);
98
100
  return { timestamp: e.timestamp, message: match ? match[1] : 'commit' };
99
101
  });
@@ -155,12 +157,12 @@ export function registerSessionsRoutes(app: Application, ctx: RouteContext): voi
155
157
 
156
158
  // User inputs
157
159
  events
158
- .filter(e => e.hook_type === 'UserPromptSubmit' && (e.user_prompt || (e.tool_input as any)?.user_prompt))
160
+ .filter(e => e.hook_type === 'UserPromptSubmit' && getUserPrompt(e))
159
161
  .forEach(e => {
160
162
  timeline.push({
161
163
  timestamp: e.timestamp,
162
164
  type: 'user_input',
163
- data: { content: e.user_prompt || (e.tool_input as any)?.user_prompt },
165
+ data: { content: getUserPrompt(e) },
164
166
  });
165
167
  });
166
168
 
@@ -183,30 +185,16 @@ export function registerSessionsRoutes(app: Application, ctx: RouteContext): voi
183
185
  events
184
186
  .filter(e => e.tool_name && e.hook_type === 'PreToolUse')
185
187
  .forEach(e => {
186
- const toolInput = e.tool_input as any;
187
- const toolOutput = e.tool_output as any;
188
-
189
- const truncateField = (obj: any, maxLen = 200): any => {
190
- if (!obj) return obj;
191
- if (typeof obj === 'string') return obj.length > maxLen ? obj.slice(0, maxLen) + '...' : obj;
192
- if (Array.isArray(obj)) return obj.slice(0, 5);
193
- if (typeof obj === 'object') {
194
- const result: any = {};
195
- for (const [k, v] of Object.entries(obj)) {
196
- result[k] = truncateField(v, maxLen);
197
- }
198
- return result;
199
- }
200
- return obj;
201
- };
188
+ const toolInput = e.tool_input;
189
+ const toolOutput = e.tool_output;
202
190
 
203
191
  timeline.push({
204
192
  timestamp: e.timestamp,
205
193
  type: 'tool_call',
206
194
  data: {
207
195
  tool: e.tool_name,
208
- input: truncateField(toolInput),
209
- output: truncateField(toolOutput),
196
+ input: truncateField(toolInput, 200, 5),
197
+ output: truncateField(toolOutput, 200, 5),
210
198
  success: !toolOutput?.error,
211
199
  },
212
200
  });
@@ -233,17 +221,17 @@ export function registerSessionsRoutes(app: Application, ctx: RouteContext): voi
233
221
 
234
222
  // Summary
235
223
  const commits = events
236
- .filter(e => e.tool_name === 'Bash' && (e.tool_input as any)?.command?.includes('git commit'))
224
+ .filter(e => e.tool_name === 'Bash' && getCommand(e)?.includes('git commit'))
237
225
  .map(e => {
238
- const cmd = (e.tool_input as any)?.command || '';
226
+ const cmd = getCommand(e) || '';
239
227
  const match = cmd.match(/git commit.*-m\s+["']([^"']+)["']/);
240
228
  return { message: match ? match[1] : 'commit' };
241
229
  });
242
230
 
243
231
  const filesChanged = [...new Set(
244
232
  events
245
- .filter(e => (e.tool_name === 'Edit' || e.tool_name === 'Write') && (e.tool_input as any)?.file_path)
246
- .map(e => (e.tool_input as any).file_path),
233
+ .filter(e => (e.tool_name === 'Edit' || e.tool_name === 'Write') && getFilePath(e))
234
+ .map(e => getFilePath(e)!),
247
235
  )];
248
236
 
249
237
  res.json({