@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
@@ -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({
@@ -8,6 +8,8 @@ import type { RouteContext } from './types.js';
8
8
  * - /api/skill-stats/distribution — routing type distribution (agent/skill/none)
9
9
  * - /api/skill-stats/frequency — skill usage frequency
10
10
  * - /api/skill-stats/trend — skill usage rate trend over time
11
+ *
12
+ * H1: All endpoints now use storage-layer SQL aggregation (no JS GROUP BY).
11
13
  */
12
14
  export function registerSkillStatsRoutes(app: Application, ctx: RouteContext): void {
13
15
  const { storage } = ctx;
@@ -16,23 +18,14 @@ export function registerSkillStatsRoutes(app: Application, ctx: RouteContext): v
16
18
  app.get('/api/skill-stats/distribution', (req, res) => {
17
19
  const windowHours = parseInt((req.query.window as string) || '168'); // default 7d
18
20
  const since = Date.now() - windowHours * 3600 * 1000;
19
- const events = storage.queryRoutingEvents({ since_ts: since, limit: 10000 });
20
-
21
- const distribution: Record<string, number> = {
22
- agent: 0,
23
- skill: 0,
24
- none: 0,
25
- };
21
+ const agg = storage.aggregateRoutingStats({ since_ts: since });
26
22
 
27
- for (const e of events) {
28
- const type = e.routed_to_type || 'none';
29
- distribution[type] = (distribution[type] || 0) + 1;
23
+ // Ensure stable ordering and presence of all three buckets (agent/skill/none)
24
+ const counts: Record<string, number> = { agent: 0, skill: 0, none: 0 };
25
+ for (const row of agg.by_type) {
26
+ counts[row.type] = row.count;
30
27
  }
31
-
32
- const result = Object.entries(distribution).map(([type, count]) => ({
33
- type,
34
- count,
35
- }));
28
+ const result = Object.entries(counts).map(([type, count]) => ({ type, count }));
36
29
 
37
30
  res.json({ windowHours, distribution: result });
38
31
  });
@@ -41,19 +34,9 @@ export function registerSkillStatsRoutes(app: Application, ctx: RouteContext): v
41
34
  app.get('/api/skill-stats/frequency', (req, res) => {
42
35
  const windowHours = parseInt((req.query.window as string) || '168');
43
36
  const since = Date.now() - windowHours * 3600 * 1000;
44
- const events = storage.queryRoutingEvents({ since_ts: since, limit: 10000 });
45
-
46
- const frequency = new Map<string, number>();
37
+ const agg = storage.aggregateRoutingStats({ since_ts: since });
47
38
 
48
- for (const e of events) {
49
- if (e.routed_to_type === 'skill' && e.routed_to_name) {
50
- frequency.set(e.routed_to_name, (frequency.get(e.routed_to_name) || 0) + 1);
51
- }
52
- }
53
-
54
- const result = Array.from(frequency.entries())
55
- .map(([name, count]) => ({ name, count }))
56
- .sort((a, b) => b.count - a.count);
39
+ const result = agg.by_skill_routed.map(({ skill, count }) => ({ name: skill, count }));
57
40
 
58
41
  res.json({ windowHours, frequency: result });
59
42
  });
@@ -62,25 +45,12 @@ export function registerSkillStatsRoutes(app: Application, ctx: RouteContext): v
62
45
  app.get('/api/skill-stats/trend', (req, res) => {
63
46
  const windowHours = parseInt((req.query.window as string) || '168');
64
47
  const since = Date.now() - windowHours * 3600 * 1000;
65
- const events = storage.queryRoutingEvents({ since_ts: since, limit: 10000 });
66
-
67
- // Group by day
68
- const dailyStats = new Map<string, { total: number; skill: number }>();
69
-
70
- for (const e of events) {
71
- const day = new Date(e.ts).toISOString().split('T')[0];
72
- const stats = dailyStats.get(day) || { total: 0, skill: 0 };
73
- stats.total++;
74
- if (e.routed_to_type === 'skill') {
75
- stats.skill++;
76
- }
77
- dailyStats.set(day, stats);
78
- }
48
+ const rows = storage.aggregateRoutingTrendByDay({ since_ts: since });
79
49
 
80
- const result = Array.from(dailyStats.entries())
81
- .map(([day, stats]) => ({
82
- day,
83
- skillRate: stats.total > 0 ? Math.round((stats.skill / stats.total) * 100) : 0,
50
+ const result = rows
51
+ .map(r => ({
52
+ day: r.day,
53
+ skillRate: r.total > 0 ? Math.round((r.skill / r.total) * 100) : 0,
84
54
  }))
85
55
  .sort((a, b) => a.day.localeCompare(b.day));
86
56
 
@@ -91,41 +61,31 @@ export function registerSkillStatsRoutes(app: Application, ctx: RouteContext): v
91
61
  app.get('/api/skill-stats/invocations', (req, res) => {
92
62
  const days = parseInt((req.query.days as string) || '7');
93
63
  const since = Date.now() - days * 24 * 3600 * 1000;
94
- const invocations = storage.querySkillInvocations({ since, limit: 1000 });
95
-
96
- const stats = new Map<string, { skill_id: string; total: number; success: number; failed: number }>();
97
- for (const inv of invocations) {
98
- const cur = stats.get(inv.skill_id) || { skill_id: inv.skill_id, total: 0, success: 0, failed: 0 };
99
- cur.total++;
100
- if (inv.success === 1) cur.success++;
101
- else cur.failed++;
102
- stats.set(inv.skill_id, cur);
103
- }
64
+ const rows = storage.aggregateSkillInvocationsBySkill({ since });
65
+
66
+ const total = rows.reduce((sum, r) => sum + r.total, 0);
67
+ const result = rows.map(r => ({
68
+ skill_id: r.skill_id,
69
+ total: r.total,
70
+ success: r.success,
71
+ failed: r.failed,
72
+ }));
104
73
 
105
- const result = Array.from(stats.values()).sort((a, b) => b.total - a.total);
106
- res.json({ days, total: invocations.length, invocations: result });
74
+ res.json({ days, total, invocations: result });
107
75
  });
108
76
 
109
77
  // Routing/Agent call rate stats
110
78
  app.get('/api/routing/stats', (req, res) => {
111
79
  const days = parseInt((req.query.days as string) || '7');
112
80
  const since = Date.now() - days * 24 * 3600 * 1000;
113
- const events = storage.queryRoutingEvents({ since_ts: since, limit: 10000 });
81
+ const agg = storage.aggregateRoutingStats({ since_ts: since });
114
82
 
115
- const total = events.length;
116
- const agentCalls = events.filter(e => e.obeyed === 1).length;
83
+ const total = agg.total;
84
+ const agentCalls = agg.obeyed;
117
85
  const noAgent = total - agentCalls;
118
86
  const agentRate = total > 0 ? Math.round((agentCalls / total) * 1000) / 10 : 0;
119
87
 
120
- const byAgentMap = new Map<string, number>();
121
- for (const e of events) {
122
- if (e.routed_to_name && e.obeyed === 1) {
123
- byAgentMap.set(e.routed_to_name, (byAgentMap.get(e.routed_to_name) || 0) + 1);
124
- }
125
- }
126
- const byAgent = Array.from(byAgentMap.entries())
127
- .map(([agent, count]) => ({ agent, count }))
128
- .sort((a, b) => b.count - a.count);
88
+ const byAgent = agg.by_agent.map(({ agent, count }) => ({ agent, count }));
129
89
 
130
90
  res.json({ days, total, agentCalls, noAgent, agentRate, byAgent });
131
91
  });
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { homedir } from 'os';
5
5
  import { logger } from '../../core/utils/logger.js';
6
6
  import type { RouteContext } from './types.js';
7
+ import { FORGE_PATHS } from '../../core/constants.js';
7
8
 
8
9
  /**
9
10
  * /api/skills/* — list, read, update, version, rollback.
@@ -205,7 +206,7 @@ export function registerSkillsRoutes(app: Application, ctx: RouteContext): void
205
206
  return;
206
207
  }
207
208
 
208
- const backupDir = path.join(homedir(), '.claude-forge', 'backups', 'skills');
209
+ const backupDir = FORGE_PATHS.backups('skills');
209
210
  fs.mkdirSync(backupDir, { recursive: true });
210
211
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
211
212
  const backupPath = path.join(backupDir, `${name}-${timestamp}.md`);
@@ -232,7 +233,7 @@ export function registerSkillsRoutes(app: Application, ctx: RouteContext): void
232
233
  return;
233
234
  }
234
235
 
235
- const backupDir = path.join(homedir(), '.claude-forge', 'backups', 'skills');
236
+ const backupDir = FORGE_PATHS.backups('skills');
236
237
 
237
238
  if (!fs.existsSync(backupDir)) {
238
239
  res.json({ versions: [] });
@@ -276,7 +277,7 @@ export function registerSkillsRoutes(app: Application, ctx: RouteContext): void
276
277
  return;
277
278
  }
278
279
 
279
- const backupDir = path.join(homedir(), '.claude-forge', 'backups', 'skills');
280
+ const backupDir = FORGE_PATHS.backups('skills');
280
281
  const filename = `${name}-${timestamp}.md`;
281
282
  const filePath = path.join(backupDir, filename);
282
283
 
@@ -318,7 +319,7 @@ export function registerSkillsRoutes(app: Application, ctx: RouteContext): void
318
319
 
319
320
  const skillsDir = path.join(homedir(), '.claude', 'skills');
320
321
  const currentPath = path.join(skillsDir, `${name}.md`);
321
- const backupDir = path.join(homedir(), '.claude-forge', 'backups', 'skills');
322
+ const backupDir = FORGE_PATHS.backups('skills');
322
323
  const versionPath = path.join(backupDir, `${name}-${timestamp}.md`);
323
324
 
324
325
  if (!fs.existsSync(currentPath)) {
@@ -3,51 +3,41 @@ import type { RouteContext } from './types.js';
3
3
 
4
4
  /**
5
5
  * /api/stats — System-wide statistics for the Dashboard.
6
+ *
7
+ * H2 Phase 3: 替换原 db.prepare 直写 SQL 为 SQLiteStorage facade 聚合方法。
8
+ * 响应 shape 保持不变(前端契约):totalSessions / totalEvents / toolUsage /
9
+ * dailyActivity / skillInvocations。
6
10
  */
7
11
  export function registerStatsRoutes(app: Application, ctx: RouteContext): void {
8
12
  const { storage } = ctx;
9
13
 
10
14
  app.get('/api/stats', (_req, res) => {
11
- const db = storage.getDatabase();
15
+ const totalEvents = storage.countAllEvents();
16
+ const totalSessions = storage.countAllSessions();
12
17
 
13
- const totalEvents = (db.prepare('SELECT COUNT(*) as cnt FROM events').get() as any).cnt;
14
- const totalSessions = (db.prepare('SELECT COUNT(*) as cnt FROM sessions').get() as any).cnt;
15
-
16
- // Tool usage distribution
17
- const toolRows = db.prepare(
18
- `SELECT tool_name, COUNT(*) as cnt FROM events
19
- WHERE tool_name IS NOT NULL AND tool_name != ''
20
- GROUP BY tool_name ORDER BY cnt DESC LIMIT 15`
21
- ).all() as Array<{ tool_name: string; cnt: number }>;
18
+ // Tool usage distribution (top 15)
19
+ const toolRows = storage.aggregateToolUsage({ limit: 15 });
22
20
  const toolUsage: Record<string, number> = {};
23
21
  for (const r of toolRows) {
24
- toolUsage[r.tool_name] = r.cnt;
22
+ toolUsage[r.tool_name] = r.count;
25
23
  }
26
24
 
27
25
  // Daily activity (last 7 days)
28
- const dailyRows = db.prepare(
29
- `SELECT date(timestamp) as d, COUNT(*) as eventCount
30
- FROM events
31
- WHERE timestamp >= date('now', '-7 days')
32
- GROUP BY d ORDER BY d`
33
- ).all() as Array<{ d: string; eventCount: number }>;
34
-
35
- const sessionDailyRows = db.prepare(
36
- `SELECT date(start_time) as d, COUNT(*) as sessionCount
37
- FROM sessions
38
- WHERE start_time >= date('now', '-7 days')
39
- GROUP BY d ORDER BY d`
40
- ).all() as Array<{ d: string; sessionCount: number }>;
26
+ // SQL: timestamp >= date('now', '-7 days')
27
+ // 新口径:调用方计算 ISO 字符串,传给 facade。
28
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
29
+ const dailyRows = storage.aggregateDailyEventCounts({ since: sevenDaysAgo });
30
+ const sessionDailyRows = storage.aggregateDailySessionCounts({ since: sevenDaysAgo });
41
31
 
42
- const sessionMap = new Map(sessionDailyRows.map(r => [r.d, r.sessionCount]));
32
+ const sessionMap = new Map(sessionDailyRows.map(r => [r.date, r.count]));
43
33
  const dailyActivity = dailyRows.map(r => ({
44
- date: r.d,
45
- eventCount: r.eventCount,
46
- sessionCount: sessionMap.get(r.d) || 0,
34
+ date: r.date,
35
+ eventCount: r.count,
36
+ sessionCount: sessionMap.get(r.date) || 0,
47
37
  }));
48
38
 
49
39
  // Skill invocation count
50
- const skillCount = (db.prepare('SELECT COUNT(*) as cnt FROM skill_invocations').get() as any)?.cnt ?? 0;
40
+ const skillCount = storage.countAllSkillInvocations();
51
41
 
52
42
  res.json({
53
43
  totalSessions,
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import type { Application } from 'express';
3
3
  import type { RouteContext } from './types.js';
4
+ import { truncateField } from './_helpers.js';
5
+ import { getFilePath, getUserPrompt } from '../../core/event-fields.js';
4
6
 
5
7
  /**
6
8
  * /api/tasks — Task execution view.
@@ -59,28 +61,20 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
59
61
  app.get('/api/tasks/:taskId', (req, res) => {
60
62
  const taskId = req.params.taskId;
61
63
 
62
- // Find the task
63
- const allTasks = storage.queryTasks({ limit: 5000 });
64
- const task = allTasks.find(t => t.id === taskId);
64
+ // H1: PK direct fetch (was: queryTasks({limit:5000}).find)
65
+ const task = storage.getTask(taskId);
65
66
  if (!task) {
66
67
  res.status(404).json({ error: 'Task not found' });
67
68
  return;
68
69
  }
69
70
 
70
- // Get event IDs associated with this task
71
- const eventIds = new Set(storage.getTaskEventIds(taskId));
72
-
73
- // Fetch all events for the session, then filter to task events
74
- const allEvents = storage.queryEvents({ session_id: task.session_id, limit: 5000 });
75
- const taskEvents = allEvents.filter(e => e.event_id && eventIds.has(e.event_id));
76
-
77
- // Sort by timestamp ascending (chronological order)
78
- taskEvents.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
71
+ // H1: JOIN task_events (was: queryEvents({limit:5000}) + Set.has filter)
72
+ const taskEvents = storage.queryEventsByTaskId(taskId, { limit: 5000 });
79
73
 
80
74
  // ── Injections ──────────────────────────────────────────────────────
81
- const allInjections = storage.queryInjections({ session_id: task.session_id, limit: 500 });
75
+ // H1: JOIN task_events (was: queryInjections({limit:500}) + Set.has filter)
76
+ const allInjections = storage.queryInjectionsByTaskId(taskId);
82
77
  const injections = allInjections
83
- .filter(inj => inj.event_id && eventIds.has(inj.event_id))
84
78
  .map(inj => ({
85
79
  id: inj.id,
86
80
  timestamp: inj.timestamp,
@@ -92,20 +86,6 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
92
86
  }));
93
87
 
94
88
  // ── Timeline ────────────────────────────────────────────────────────
95
- const truncateField = (obj: unknown, maxLen = 300): unknown => {
96
- if (!obj) return obj;
97
- if (typeof obj === 'string') return obj.length > maxLen ? obj.slice(0, maxLen) + '...' : obj;
98
- if (Array.isArray(obj)) return obj.slice(0, 10);
99
- if (typeof obj === 'object') {
100
- const result: Record<string, unknown> = {};
101
- for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
102
- result[k] = truncateField(v, maxLen);
103
- }
104
- return result;
105
- }
106
- return obj;
107
- };
108
-
109
89
  const timeline = taskEvents
110
90
  .filter(e => e.tool_name && (e.hook_type === 'PreToolUse' || e.hook_type === 'PostToolUse'))
111
91
  .reduce<Array<{
@@ -129,7 +109,7 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
129
109
  return acc;
130
110
  }
131
111
 
132
- const toolInput = e.tool_input as Record<string, unknown> | null;
112
+ const toolInput = e.tool_input ?? null;
133
113
  const isAgentCall = e.tool_name === 'Agent' || e.tool_name === 'Task';
134
114
 
135
115
  const entry: (typeof acc)[number] = {
@@ -143,8 +123,8 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
143
123
 
144
124
  if (isAgentCall && toolInput) {
145
125
  entry.agent_detail = {
146
- subagent_type: toolInput.subagent_type as string | undefined,
147
- name: toolInput.name as string | undefined,
126
+ subagent_type: typeof toolInput.subagent_type === 'string' ? toolInput.subagent_type : undefined,
127
+ name: typeof toolInput.name === 'string' ? toolInput.name : undefined,
148
128
  prompt: typeof toolInput.prompt === 'string'
149
129
  ? toolInput.prompt
150
130
  : undefined,
@@ -156,15 +136,10 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
156
136
  }, []);
157
137
 
158
138
  // ── Skill Invocations ───────────────────────────────────────────────
159
- const allSkillInvocations = storage.querySkillInvocations({ session_id: task.session_id, limit: 200 });
160
- // Filter by timestamp range of the task
161
- const taskStart = task.start_time;
162
- const taskEnd = task.end_time || new Date().toISOString();
163
- const taskStartMs = new Date(taskStart).getTime();
164
- const taskEndMs = new Date(taskEnd).getTime();
139
+ // H1: SQL window filter (was: querySkillInvocations({limit:200}) + JS time filter)
140
+ const allSkillInvocations = storage.querySkillInvocationsByTaskWindow(taskId);
165
141
 
166
142
  const skillInvocations = allSkillInvocations
167
- .filter(si => si.timestamp >= taskStartMs && si.timestamp <= taskEndMs)
168
143
  .map(si => ({
169
144
  id: si.id,
170
145
  skill_id: si.skill_id,
@@ -184,17 +159,17 @@ export function registerTasksRoutes(app: Application, ctx: RouteContext): void {
184
159
  taskEvents
185
160
  .filter(e =>
186
161
  (e.tool_name === 'Edit' || e.tool_name === 'Write') &&
187
- (e.tool_input as Record<string, unknown> | null)?.file_path,
162
+ getFilePath(e),
188
163
  )
189
- .map(e => (e.tool_input as Record<string, unknown>).file_path as string),
164
+ .map(e => getFilePath(e)!),
190
165
  )];
191
166
 
192
167
  // ── User Prompts ────────────────────────────────────────────────────
193
168
  const userPrompts = taskEvents
194
- .filter(e => e.hook_type === 'UserPromptSubmit' && (e.user_prompt || (e.tool_input as any)?.user_prompt))
169
+ .filter(e => e.hook_type === 'UserPromptSubmit' && getUserPrompt(e))
195
170
  .map(e => ({
196
171
  timestamp: e.timestamp,
197
- content: e.user_prompt || (e.tool_input as any)?.user_prompt,
172
+ content: getUserPrompt(e),
198
173
  }));
199
174
 
200
175
  res.json({
@@ -92,7 +92,7 @@ export function registerTraceRoutes(app: Application, ctx: RouteContext): void {
92
92
  }
93
93
 
94
94
  // 4. Query session details
95
- const db = storage.getDatabase();
95
+ // H2 Phase 3: 改用 facade 聚合方法,消除 db.prepare 直写。
96
96
  const allSessions = storage.querySessions({ limit: 5000 });
97
97
  const sessionDetails = sessionIds.map(sid => {
98
98
  const session = allSessions.find(s => s.session_id === sid || s.session_id.startsWith(sid));
@@ -103,21 +103,9 @@ export function registerTraceRoutes(app: Application, ctx: RouteContext): void {
103
103
  const end = new Date(session.end_time);
104
104
  const durationMin = Math.round((end.getTime() - start.getTime()) / 60000);
105
105
 
106
- const eventBreakdown = db.prepare(`
107
- SELECT hook_type, COUNT(*) as cnt FROM events
108
- WHERE session_id = ? GROUP BY hook_type
109
- `).all(fullId) as Array<{ hook_type: string; cnt: number }>;
110
-
111
- const agentRows = db.prepare(`
112
- SELECT json_extract(tool_input, '$.subagent_type') as agent_type, COUNT(*) as cnt
113
- FROM events
114
- WHERE session_id = ? AND tool_name IN ('Agent', 'Task') AND tool_input IS NOT NULL
115
- GROUP BY agent_type
116
- `).all(fullId) as Array<{ agent_type: string | null; cnt: number }>;
117
-
118
- const skillRows = db.prepare(`
119
- SELECT DISTINCT skill_id FROM skill_invocations WHERE session_id = ?
120
- `).all(fullId) as Array<{ skill_id: string }>;
106
+ const eventBreakdown = storage.aggregateHookTypeBySession(fullId);
107
+ const agentRows = storage.aggregateAgentTypeBySession(fullId);
108
+ const skillIds = storage.queryDistinctSkillIdsBySession(fullId);
121
109
 
122
110
  return {
123
111
  session_id: fullId,
@@ -127,9 +115,9 @@ export function registerTraceRoutes(app: Application, ctx: RouteContext): void {
127
115
  end_time: session.end_time,
128
116
  duration_min: durationMin,
129
117
  event_count: session.event_count,
130
- event_breakdown: Object.fromEntries(eventBreakdown.map(r => [r.hook_type, r.cnt])),
131
- agent_calls: agentRows.filter(r => r.agent_type).map(r => ({ type: r.agent_type, count: r.cnt })),
132
- skills: skillRows.map(r => r.skill_id),
118
+ event_breakdown: Object.fromEntries(eventBreakdown.map(r => [r.hook_type, r.count])),
119
+ agent_calls: agentRows.filter(r => r.agent_type).map(r => ({ type: r.agent_type, count: r.count })),
120
+ skills: skillIds,
133
121
  };
134
122
  });
135
123
 
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  import { homedir } from 'os';
9
9
  import type { SQLiteStorage } from '../../core/storage/sqlite.js';
10
10
  import type { SkillRegistry } from '../../skills/registry.js';
11
+ import { FORGE_PATHS } from '../../core/constants.js';
11
12
 
12
13
  export interface RouteContext {
13
14
  storage: SQLiteStorage;
@@ -29,13 +30,13 @@ export function resolvePatchTarget(
29
30
  if (targetType === 'skill') {
30
31
  return {
31
32
  filePath: path.join(homedir(), '.claude', 'skills', `${targetName}.md`),
32
- backupDir: path.join(homedir(), '.claude-forge', 'backups', 'skills'),
33
+ backupDir: FORGE_PATHS.backups('skills'),
33
34
  };
34
35
  }
35
36
  if (targetType === 'routing_rule') {
36
37
  return {
37
- filePath: path.join(homedir(), '.claude-forge', 'routing.yaml'),
38
- backupDir: path.join(homedir(), '.claude-forge', 'backups', 'routing'),
38
+ filePath: FORGE_PATHS.routingYaml(),
39
+ backupDir: FORGE_PATHS.backups('routing'),
39
40
  };
40
41
  }
41
42
  throw new Error(`Unsupported targetType: ${targetType}`);