@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
@@ -83,6 +83,7 @@ export class SQLiteStorage extends EventEmitter {
83
83
  }
84
84
 
85
85
  // ── Database accessors ─────────────────────────────────────────────────
86
+ /** @internal 见 SQLiteBase.getDatabase 注释。Routes/Handlers/Analytics 禁止调用。 */
86
87
  getDatabase(): Database.Database { return this.base.getDatabase(); }
87
88
  getDbPath(): string { return this.base.getDbPath(); }
88
89
 
@@ -96,11 +97,58 @@ export class SQLiteStorage extends EventEmitter {
96
97
  return this.events.searchEvents(filter);
97
98
  }
98
99
 
100
+ // ── H2: Event aggregates (前 routes/handlers/analytics 越权 SQL) ──
101
+ countAllEvents(): number { return this.events.countAllEvents(); }
102
+ aggregateToolUsage(opts?: Parameters<EventOperations['aggregateToolUsage']>[0]): ReturnType<EventOperations['aggregateToolUsage']> {
103
+ return this.events.aggregateToolUsage(opts);
104
+ }
105
+ aggregateDailyEventCounts(opts: Parameters<EventOperations['aggregateDailyEventCounts']>[0]): ReturnType<EventOperations['aggregateDailyEventCounts']> {
106
+ return this.events.aggregateDailyEventCounts(opts);
107
+ }
108
+ aggregateHookTypeBySession(session_id: string): ReturnType<EventOperations['aggregateHookTypeBySession']> {
109
+ return this.events.aggregateHookTypeBySession(session_id);
110
+ }
111
+ aggregateAgentTypeBySession(session_id: string): ReturnType<EventOperations['aggregateAgentTypeBySession']> {
112
+ return this.events.aggregateAgentTypeBySession(session_id);
113
+ }
114
+ aggregateToolUsageBySession(session_id: string): ReturnType<EventOperations['aggregateToolUsageBySession']> {
115
+ return this.events.aggregateToolUsageBySession(session_id);
116
+ }
117
+ countActiveDays(opts: Parameters<EventOperations['countActiveDays']>[0]): number {
118
+ return this.events.countActiveDays(opts);
119
+ }
120
+ aggregateOverviewByRange(opts: Parameters<EventOperations['aggregateOverviewByRange']>[0]): ReturnType<EventOperations['aggregateOverviewByRange']> {
121
+ return this.events.aggregateOverviewByRange(opts);
122
+ }
123
+ queryDistinctProjects(opts: Parameters<EventOperations['queryDistinctProjects']>[0]): string[] {
124
+ return this.events.queryDistinctProjects(opts);
125
+ }
126
+ aggregateToolFailureRate(opts: Parameters<EventOperations['aggregateToolFailureRate']>[0]): ReturnType<EventOperations['aggregateToolFailureRate']> {
127
+ return this.events.aggregateToolFailureRate(opts);
128
+ }
129
+ queryFileEditInputs(opts: Parameters<EventOperations['queryFileEditInputs']>[0]): ReturnType<EventOperations['queryFileEditInputs']> {
130
+ return this.events.queryFileEditInputs(opts);
131
+ }
132
+ queryEventsByTimeRange(opts: Parameters<EventOperations['queryEventsByTimeRange']>[0]): ForgeEvent[] {
133
+ return this.events.queryEventsByTimeRange(opts);
134
+ }
135
+
99
136
  // ── Session operations ─────────────────────────────────────────────────
100
137
  upsertSession(event: ForgeEvent): void { this.sessions.upsertSession(event); }
101
138
  querySessions(filter: Parameters<SessionOperations['querySessions']>[0] = {}): SessionSummary[] {
102
139
  return this.sessions.querySessions(filter);
103
140
  }
141
+ // ── H2: Session aggregates ──
142
+ countAllSessions(): number { return this.sessions.countAllSessions(); }
143
+ aggregateDailySessionCounts(opts: Parameters<SessionOperations['aggregateDailySessionCounts']>[0]): ReturnType<SessionOperations['aggregateDailySessionCounts']> {
144
+ return this.sessions.aggregateDailySessionCounts(opts);
145
+ }
146
+ querySessionsByTimeRange(opts: Parameters<SessionOperations['querySessionsByTimeRange']>[0]): SessionSummary[] {
147
+ return this.sessions.querySessionsByTimeRange(opts);
148
+ }
149
+ countSessionsByRange(opts: Parameters<SessionOperations['countSessionsByRange']>[0]): number {
150
+ return this.sessions.countSessionsByRange(opts);
151
+ }
104
152
 
105
153
  // ── Injection operations ───────────────────────────────────────────────
106
154
  writeInjection(injection: Injection): void { this.injections.writeInjection(injection); }
@@ -127,7 +175,29 @@ export class SQLiteStorage extends EventEmitter {
127
175
  queryTaskProjects(): string[] {
128
176
  return this.tasks.queryTaskProjects();
129
177
  }
178
+ // ── H2: Task counts ──
179
+ countTasksByRange(opts: Parameters<TaskOperations['countTasksByRange']>[0]): number {
180
+ return this.tasks.countTasksByRange(opts);
181
+ }
130
182
  getTaskEventIds(taskId: string): string[] { return this.tasks.getTaskEventIds(taskId); }
183
+ completeStaleActiveTasks(idleMinutes: number): number {
184
+ return this.tasks.completeStaleActiveTasks(idleMinutes);
185
+ }
186
+ getTask(taskId: string): TaskRecord | null {
187
+ return this.tasks.getTask(taskId);
188
+ }
189
+ queryEventsByTaskId(taskId: string, opts?: Parameters<TaskOperations['queryEventsByTaskId']>[1]): ForgeEvent[] {
190
+ return this.tasks.queryEventsByTaskId(taskId, opts);
191
+ }
192
+ queryInjectionsByTaskId(taskId: string): Injection[] {
193
+ return this.tasks.queryInjectionsByTaskId(taskId);
194
+ }
195
+ querySkillInvocationsByTaskWindow(
196
+ taskId: string,
197
+ opts?: Parameters<TaskOperations['querySkillInvocationsByTaskWindow']>[1],
198
+ ): SkillInvocationRow[] {
199
+ return this.tasks.querySkillInvocationsByTaskWindow(taskId, opts);
200
+ }
131
201
 
132
202
  // ── Routing events ─────────────────────────────────────────────────────
133
203
  writeRoutingEvent(record: RoutingEventRecord): number {
@@ -139,12 +209,25 @@ export class SQLiteStorage extends EventEmitter {
139
209
  queryRoutingEvents(filter: Parameters<RoutingOperations['queryRoutingEvents']>[0] = {}): RoutingEventRow[] {
140
210
  return this.routing.queryRoutingEvents(filter);
141
211
  }
142
- findPendingRoutingEvents(ageMs: number): RoutingEventRow[] {
143
- return this.routing.findPendingRoutingEvents(ageMs);
212
+ aggregateRoutingStats(
213
+ filter: Parameters<RoutingOperations['aggregateRoutingStats']>[0],
214
+ ): ReturnType<RoutingOperations['aggregateRoutingStats']> {
215
+ return this.routing.aggregateRoutingStats(filter);
216
+ }
217
+ aggregateRoutingTrendByDay(
218
+ filter: Parameters<RoutingOperations['aggregateRoutingTrendByDay']>[0],
219
+ ): ReturnType<RoutingOperations['aggregateRoutingTrendByDay']> {
220
+ return this.routing.aggregateRoutingTrendByDay(filter);
221
+ }
222
+ queryPendingRoutingEvents(ageMs: number): RoutingEventRow[] {
223
+ return this.routing.queryPendingRoutingEvents(ageMs);
144
224
  }
145
225
  getRecentRoutingEvent(sessionId: string): { id: number } | null {
146
226
  return this.routing.getRecentRoutingEvent(sessionId);
147
227
  }
228
+ getRecentPendingRoutingEvent(sessionId: string): { id: number } | null {
229
+ return this.routing.getRecentPendingRoutingEvent(sessionId);
230
+ }
148
231
 
149
232
  // ── Experiment assignments ─────────────────────────────────────────────
150
233
  getExperimentAssignment(sessionId: string, experimentId: string): string | null {
@@ -158,8 +241,8 @@ export class SQLiteStorage extends EventEmitter {
158
241
  }
159
242
 
160
243
  // ── Token usage ────────────────────────────────────────────────────────
161
- recordTokenUsage(params: Parameters<TokenUsageOperations['recordTokenUsage']>[0]): void {
162
- this.tokenUsage.recordTokenUsage(params);
244
+ writeTokenUsage(params: Parameters<TokenUsageOperations['writeTokenUsage']>[0]): void {
245
+ this.tokenUsage.writeTokenUsage(params);
163
246
  }
164
247
  getTokenUsageBySession(session_id: string): ReturnType<TokenUsageOperations['getTokenUsageBySession']> {
165
248
  return this.tokenUsage.getTokenUsageBySession(session_id);
@@ -175,9 +258,25 @@ export class SQLiteStorage extends EventEmitter {
175
258
  querySkillInvocations(filter: Parameters<SkillOperations['querySkillInvocations']>[0] = {}): SkillInvocationRow[] {
176
259
  return this.skills.querySkillInvocations(filter);
177
260
  }
261
+ aggregateSkillInvocationsBySkill(
262
+ filter: Parameters<SkillOperations['aggregateSkillInvocationsBySkill']>[0],
263
+ ): ReturnType<SkillOperations['aggregateSkillInvocationsBySkill']> {
264
+ return this.skills.aggregateSkillInvocationsBySkill(filter);
265
+ }
178
266
  queryWorkflowProgress(filter: Parameters<SkillOperations['queryWorkflowProgress']>[0] = {}): ReturnType<SkillOperations['queryWorkflowProgress']> {
179
267
  return this.skills.queryWorkflowProgress(filter);
180
268
  }
269
+ // ── H2: Skill counts ──
270
+ countAllSkillInvocations(): number { return this.skills.countAllSkillInvocations(); }
271
+ countSkillInvocationsBySession(session_id: string): number {
272
+ return this.skills.countSkillInvocationsBySession(session_id);
273
+ }
274
+ countDistinctSkillsSince(since_ms: number): number {
275
+ return this.skills.countDistinctSkillsSince(since_ms);
276
+ }
277
+ queryDistinctSkillIdsBySession(session_id: string): string[] {
278
+ return this.skills.queryDistinctSkillIdsBySession(session_id);
279
+ }
181
280
 
182
281
  // ── Maintenance ────────────────────────────────────────────────────────
183
282
  cleanOldEvents(daysToKeep: number = 30): number { return this.maintenance.cleanOldEvents(daysToKeep); }
@@ -5,6 +5,9 @@
5
5
  */
6
6
 
7
7
  import type Database from 'better-sqlite3';
8
+ import type { ForgeEvent } from '../types.js';
9
+ import type { SkillInvocationRow } from './rows.js';
10
+ import type { Injection } from './injections.js';
8
11
 
9
12
  export interface TaskRecord {
10
13
  id: string;
@@ -16,6 +19,9 @@ export interface TaskRecord {
16
19
  status: 'active' | 'completed' | 'abandoned';
17
20
  event_count: number;
18
21
  project_path?: string;
22
+ /** First UserPromptSubmit user_prompt text for this task's session window.
23
+ * Populated by queryTasksFiltered via correlated subquery on idx_events_session_hook. */
24
+ first_prompt?: string | null;
19
25
  }
20
26
 
21
27
  export interface TaskFilter {
@@ -92,7 +98,14 @@ export class TaskOperations {
92
98
  // Data query
93
99
  const dataParams = [...params, limit, offset];
94
100
  const rows = this.db.prepare(
95
- `SELECT t.*, s.project_path
101
+ `SELECT t.*, s.project_path,
102
+ (SELECT e.user_prompt
103
+ FROM events e
104
+ WHERE e.session_id = t.session_id
105
+ AND e.hook_type = 'UserPromptSubmit'
106
+ AND e.timestamp >= t.start_time
107
+ AND (t.end_time IS NULL OR e.timestamp <= t.end_time)
108
+ ORDER BY e.timestamp ASC LIMIT 1) AS first_prompt
96
109
  FROM tasks t
97
110
  JOIN sessions s ON t.session_id = s.session_id
98
111
  ${where}
@@ -104,6 +117,19 @@ export class TaskOperations {
104
117
  return { items, total, has_more: offset + items.length < total };
105
118
  }
106
119
 
120
+ /** Count of tasks with start_time in [since, until?). */
121
+ countTasksByRange(opts: { since: string; until?: string }): number {
122
+ const conditions: string[] = ['start_time >= ?'];
123
+ const params: unknown[] = [opts.since];
124
+ if (opts.until !== undefined) {
125
+ conditions.push('start_time < ?');
126
+ params.push(opts.until);
127
+ }
128
+ const sql = `SELECT COUNT(*) as cnt FROM tasks WHERE ${conditions.join(' AND ')}`;
129
+ const row = this.db.prepare(sql).get(...params) as { cnt: number } | undefined;
130
+ return row?.cnt ?? 0;
131
+ }
132
+
107
133
  /** Returns distinct project paths that have at least one task. */
108
134
  queryTaskProjects(): string[] {
109
135
  const rows = this.db.prepare(
@@ -120,6 +146,127 @@ export class TaskOperations {
120
146
  return rows.map(r => r.event_id);
121
147
  }
122
148
 
149
+ /**
150
+ * 将 idle 超过阈值的 active task 批量转为 completed。
151
+ * 条件:status='active' AND end_time IS NOT NULL
152
+ * AND (now - end_time) > idleMinutes 分钟。
153
+ * 返回:受影响行数。
154
+ */
155
+ completeStaleActiveTasks(idleMinutes: number): number {
156
+ const result = this.db.prepare(`
157
+ UPDATE tasks
158
+ SET status = 'completed'
159
+ WHERE status = 'active'
160
+ AND end_time IS NOT NULL
161
+ AND (julianday('now') - julianday(end_time)) * 24 * 60 > ?
162
+ `).run(idleMinutes);
163
+ return result.changes as number;
164
+ }
165
+
166
+ // ── H1: detail route 用的 PK 直查 / JOIN 接口 ──────────────────────
167
+
168
+ /**
169
+ * 按 taskId 直查单个 task(LEFT JOIN sessions 取 project_path)。
170
+ * 替代旧 detail route 的「拉 5000 tasks 再 find(id)」。
171
+ */
172
+ getTask(taskId: string): TaskRecord | null {
173
+ const row = this.db.prepare(`
174
+ SELECT t.*, s.project_path
175
+ FROM tasks t
176
+ LEFT JOIN sessions s ON s.session_id = t.session_id
177
+ WHERE t.id = ?
178
+ `).get(taskId) as Record<string, unknown> | undefined;
179
+ if (!row) return null;
180
+ return this.mapRow(row);
181
+ }
182
+
183
+ /**
184
+ * 按 taskId JOIN task_events 拉关联事件,按 timestamp 升序。
185
+ * 默认 limit 5000(防御过大 task;前端不分页但要有上限)。
186
+ */
187
+ queryEventsByTaskId(taskId: string, opts: { limit?: number } = {}): ForgeEvent[] {
188
+ const limit = opts.limit ?? 5000;
189
+ const rows = this.db.prepare(`
190
+ SELECT e.*
191
+ FROM events e
192
+ JOIN task_events te ON te.event_id = e.event_id
193
+ WHERE te.task_id = ?
194
+ ORDER BY e.timestamp ASC
195
+ LIMIT ?
196
+ `).all(taskId, limit) as Array<Record<string, unknown>>;
197
+ return rows.map(r => this.rowToEvent(r));
198
+ }
199
+
200
+ /**
201
+ * 按 taskId JOIN task_events 拉关联 injections,按 timestamp 升序。
202
+ * 替代「拉 500 injections + JS Set.has 过滤」。
203
+ */
204
+ queryInjectionsByTaskId(taskId: string): Injection[] {
205
+ const rows = this.db.prepare(`
206
+ SELECT i.*
207
+ FROM injections i
208
+ JOIN task_events te ON te.event_id = i.event_id
209
+ WHERE te.task_id = ?
210
+ ORDER BY i.timestamp ASC
211
+ `).all(taskId) as Array<Record<string, unknown>>;
212
+ return rows.map(r => ({
213
+ id: r.id as string,
214
+ event_id: (r.event_id as string) || undefined,
215
+ session_id: r.session_id as string,
216
+ timestamp: r.timestamp as string,
217
+ source_handler: r.source_handler as string,
218
+ injection_type: r.injection_type as Injection['injection_type'],
219
+ content: r.content as string,
220
+ }));
221
+ }
222
+
223
+ /**
224
+ * 按 task 的时间窗口 + session_id 过滤 skill_invocations。
225
+ * 替代「拉 200 invocations + JS 时间比较」。
226
+ * `now_ms` 通过参数注入(end_time IS NULL 时取此值),便于测试时 mock。
227
+ *
228
+ * 时间转换:strftime('%s', ISO_TS) 返回 unix 秒(丢毫秒),× 1000 → ms。
229
+ * 与旧实现 `new Date(taskStart).getTime()` 相比可能损失最多 999ms,
230
+ * 但 task 时间窗口通常远大于秒级,对结果集影响可忽略。
231
+ */
232
+ querySkillInvocationsByTaskWindow(taskId: string, opts: { now_ms?: number } = {}): SkillInvocationRow[] {
233
+ const nowMs = opts.now_ms ?? Date.now();
234
+ const rows = this.db.prepare(`
235
+ SELECT si.*
236
+ FROM skill_invocations si
237
+ JOIN tasks t ON t.session_id = si.session_id
238
+ WHERE t.id = ?
239
+ AND si.timestamp >= CAST(strftime('%s', t.start_time) AS INTEGER) * 1000
240
+ AND si.timestamp <= COALESCE(
241
+ CAST(strftime('%s', t.end_time) AS INTEGER) * 1000 + 999,
242
+ ?
243
+ )
244
+ ORDER BY si.timestamp ASC
245
+ `).all(taskId, nowMs) as SkillInvocationRow[];
246
+ return rows;
247
+ }
248
+
249
+ /** Map a raw events row to ForgeEvent (parses JSON columns). */
250
+ private rowToEvent(row: Record<string, unknown>): ForgeEvent {
251
+ const parseJson = (val: unknown): unknown => {
252
+ if (val == null) return undefined;
253
+ if (typeof val !== 'string') return val;
254
+ try { return JSON.parse(val); } catch { return val; }
255
+ };
256
+ return {
257
+ event_id: row.event_id as string,
258
+ session_id: row.session_id as string,
259
+ project_path: row.project_path as string,
260
+ timestamp: row.timestamp as string,
261
+ hook_type: row.hook_type as ForgeEvent['hook_type'],
262
+ tool_name: (row.tool_name as string) || undefined,
263
+ tool_input: parseJson(row.tool_input) as ForgeEvent['tool_input'],
264
+ tool_output: parseJson(row.tool_output) as ForgeEvent['tool_output'],
265
+ user_prompt: (row.user_prompt as string) || undefined,
266
+ ai_response: (row.ai_response as string) || undefined,
267
+ };
268
+ }
269
+
123
270
  private buildWhereConditions(filter: TaskFilter): { conditions: string[]; params: unknown[] } {
124
271
  const conditions: string[] = [];
125
272
  const params: unknown[] = [];
@@ -165,6 +312,7 @@ export class TaskOperations {
165
312
  status: (r.status as TaskRecord['status']) || 'active',
166
313
  event_count: (r.event_count as number) || 0,
167
314
  project_path: (r.project_path as string) || undefined,
315
+ first_prompt: r.first_prompt !== undefined ? (r.first_prompt as string | null) : undefined,
168
316
  };
169
317
  }
170
318
  }
@@ -10,7 +10,7 @@ import { logger } from '../utils/logger.js';
10
10
  export class TokenUsageOperations {
11
11
  constructor(private db: Database.Database) {}
12
12
 
13
- recordTokenUsage(params: {
13
+ writeTokenUsage(params: {
14
14
  session_id: string;
15
15
  input_tokens: number;
16
16
  output_tokens: number;
package/src/core/types.ts CHANGED
@@ -21,17 +21,45 @@
21
21
  * (no more `event.tool_name!`) without forcing every caller of the wire
22
22
  * shape to do a 5-way switch.
23
23
  */
24
+ /**
25
+ * Lenient bag of common tool_input fields across all Claude Code tools.
26
+ *
27
+ * Intentionally not a discriminated union — the tool set is open-ended and
28
+ * almost all access sites are defensive (`?.command ?? ''`). The index
29
+ * signature keeps forward compatibility with unknown tool kinds; prefer
30
+ * the typed getters in `core/event-fields.ts` over direct field access
31
+ * when the value is fed downstream as `string`.
32
+ */
33
+ export interface ToolInputFields {
34
+ // Bash
35
+ command?: string;
36
+ description?: string;
37
+ // Edit / Write / Read / NotebookEdit
38
+ file_path?: string;
39
+ notebook_path?: string;
40
+ old_string?: string;
41
+ new_string?: string;
42
+ content?: string;
43
+ // Agent / Task
44
+ subagent_type?: string;
45
+ name?: string;
46
+ prompt?: string;
47
+ // UserPromptSubmit fallback envelope (some hook flows pack user_prompt here)
48
+ user_prompt?: string;
49
+ // Forward-compat for unknown tools / new Claude Code releases
50
+ [key: string]: unknown;
51
+ }
52
+
24
53
  export interface ForgeEvent {
25
54
  session_id: string;
26
55
  project_path: string;
27
56
  timestamp: string;
28
57
  hook_type: 'PreToolUse' | 'PostToolUse' | 'UserPromptSubmit' | 'Notification' | 'Stop';
29
58
  tool_name?: string;
30
- tool_input?: Record<string, unknown>;
59
+ tool_input?: ToolInputFields;
31
60
  tool_output?: Record<string, unknown>;
32
61
  user_prompt?: string;
33
62
  ai_response?: string;
34
- pipeline_id?: string;
35
63
  event_id?: string;
36
64
  }
37
65
 
@@ -40,7 +68,6 @@ interface BaseEvent {
40
68
  project_path: string;
41
69
  timestamp: string;
42
70
  event_id?: string;
43
- pipeline_id?: string;
44
71
  }
45
72
 
46
73
  export interface PreToolUseEvent extends BaseEvent {
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { logger } from './logger.js';
8
+ import { DEFAULTS } from '../constants.js';
8
9
  import chalk from 'chalk';
9
10
 
10
11
  export interface ErrorSolution {
@@ -221,7 +222,7 @@ export class ErrorHandler {
221
222
 
222
223
  // 根据错误类型自动识别并生成解决方案
223
224
  if (error.code === 'EADDRINUSE') {
224
- const port = parseInt(error.message?.match(/:(\d+)/)?.[1] || '3721');
225
+ const port = parseInt(error.message?.match(/:(\d+)/)?.[1] || String(DEFAULTS.WEB_PORT));
225
226
  solution = this.handlePortInUse(port, error);
226
227
  } else if (error.code === 'EACCES' || error.code === 'EPERM') {
227
228
  const path = error.path || error.message?.match(/['"]([^'"]+)['"]/)?.[1] || 'unknown';
@@ -235,7 +236,7 @@ export class ErrorHandler {
235
236
  const dbPath = error.message?.match(/['"]([^'"]+\.db)['"]/)?.[1] || '~/.claude-forge/data.db';
236
237
  solution = this.handleDatabaseError(dbPath, error);
237
238
  } else if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') {
238
- const socketPath = error.path || '~/.claude-forge/forge.sock';
239
+ const socketPath = error.path || '~/.claude-forge/daemon.sock';
239
240
  solution = this.handleSocketError(socketPath, error);
240
241
  } else {
241
242
  // 通用错误
@@ -0,0 +1,23 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ /**
4
+ * Walk up the directory tree from `start` using `git rev-parse --show-toplevel`
5
+ * to find the git repository root.
6
+ *
7
+ * Fallback: returns `start` unchanged if:
8
+ * - `start` is not inside a git repository
9
+ * - the git command fails for any reason (git not on PATH, permission error, etc.)
10
+ */
11
+ export function resolveGitRoot(start: string = process.cwd()): string {
12
+ try {
13
+ const result = execSync('git rev-parse --show-toplevel', {
14
+ cwd: start,
15
+ stdio: ['ignore', 'pipe', 'ignore'],
16
+ })
17
+ .toString()
18
+ .trim();
19
+ return result || start;
20
+ } catch {
21
+ return start;
22
+ }
23
+ }
@@ -12,7 +12,22 @@ export enum LogLevel {
12
12
  ERROR = 3,
13
13
  }
14
14
 
15
- let currentLevel = LogLevel.INFO;
15
+ /**
16
+ * Parse log level from LOG_LEVEL env var.
17
+ * Accepts: debug, info, warn, error (case-insensitive). Falls back to INFO.
18
+ */
19
+ function getLogLevelFromEnv(): LogLevel {
20
+ const envLevel = (process.env.LOG_LEVEL ?? '').toLowerCase();
21
+ const levelMap: Record<string, LogLevel> = {
22
+ debug: LogLevel.DEBUG,
23
+ info: LogLevel.INFO,
24
+ warn: LogLevel.WARN,
25
+ error: LogLevel.ERROR,
26
+ };
27
+ return levelMap[envLevel] ?? LogLevel.INFO;
28
+ }
29
+
30
+ let currentLevel: LogLevel = getLogLevelFromEnv();
16
31
  let logFilePath: string | null = null;
17
32
  let lastSizeCheck = 0;
18
33
  let cachedSize = 0;
@@ -40,6 +40,10 @@ export class LRUCache<K, V> {
40
40
  return this.cache.has(key);
41
41
  }
42
42
 
43
+ delete(key: K): boolean {
44
+ return this.cache.delete(key);
45
+ }
46
+
43
47
  clear(): void {
44
48
  this.cache.clear();
45
49
  }
@@ -33,7 +33,7 @@ export class TokenTracker {
33
33
  */
34
34
  record(params: TokenRecordParams): void {
35
35
  try {
36
- this.storage.recordTokenUsage({
36
+ this.storage.writeTokenUsage({
37
37
  session_id: params.session_id,
38
38
  input_tokens: params.usage.input_tokens,
39
39
  output_tokens: params.usage.output_tokens,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import type { ForgeEvent } from '../core/types.js';
2
+ import type { ForgeEvent, ToolInputFields } from '../core/types.js';
3
3
  import { randomUUID } from 'crypto';
4
4
 
5
5
  const RawHookEventSchema = z.object({
@@ -12,6 +12,7 @@ const RawHookEventSchema = z.object({
12
12
  tool_output: z.record(z.string(), z.unknown()).optional(),
13
13
  user_prompt: z.string().optional(),
14
14
  ai_response: z.string().optional(),
15
+ event_id: z.string().uuid().optional(),
15
16
  });
16
17
 
17
18
  export class EventParser {
@@ -20,13 +21,13 @@ export class EventParser {
20
21
  const validated = RawHookEventSchema.parse(json);
21
22
 
22
23
  return {
23
- event_id: randomUUID(),
24
+ event_id: validated.event_id ?? randomUUID(),
24
25
  session_id: validated.session_id,
25
26
  project_path: validated.project_path,
26
27
  timestamp: validated.timestamp,
27
28
  hook_type: validated.hook_type,
28
29
  tool_name: validated.tool_name,
29
- tool_input: validated.tool_input as Record<string, unknown> | undefined,
30
+ tool_input: validated.tool_input as ToolInputFields | undefined,
30
31
  tool_output: validated.tool_output as Record<string, unknown> | undefined,
31
32
  user_prompt: validated.user_prompt,
32
33
  ai_response: validated.ai_response,
@@ -99,7 +99,7 @@ export class HistoryExporter {
99
99
  lines.push('');
100
100
  for (const t of toolCalls.slice(-30)) {
101
101
  const time = new Date(t.timestamp).toLocaleTimeString();
102
- const input = t.tool_input as Record<string, unknown> | undefined;
102
+ const input = t.tool_input;
103
103
  const filePath = input?.file_path ?? input?.command ?? '';
104
104
  const summary =
105
105
  typeof filePath === 'string' ? filePath.slice(0, 80) : '';
@@ -9,6 +9,7 @@ import type { PostToolUseEvent, HookResult } from '../../core/types.js';
9
9
  import type { SQLiteStorage } from '../../core/storage/sqlite.js';
10
10
  import { logger } from '../../core/utils/logger.js';
11
11
  import { TokenTracker } from '../../core/utils/token-tracker.js';
12
+ import { getSubagentType } from '../../core/event-fields.js';
12
13
 
13
14
  export class PostToolUseHandler {
14
15
  private tokenTracker: TokenTracker | null = null;
@@ -26,10 +27,13 @@ export class PostToolUseHandler {
26
27
  if (this.storage && event.tool_name === 'Agent') {
27
28
  try {
28
29
  // PostToolUseEvent.tool_input is already a Record (narrowed by type guard).
29
- const agentType = (event.tool_input.subagent_type as string) || 'general-purpose';
30
+ const agentType = getSubagentType(event) ?? 'general-purpose';
30
31
 
31
- // Find the most recent routing_event for this session
32
- const recentEvent = this.storage.getRecentRoutingEvent(event.session_id);
32
+ // Find the most recent PENDING routing_event for this session
33
+ // (obeyed IS NULL). Filtering on pending prevents re-overwriting an
34
+ // already-completed event when 2+ Agents fire within the same prompt,
35
+ // or when an old obeyed=1 row leaks past a missed UserPromptSubmit.
36
+ const recentEvent = this.storage.getRecentPendingRoutingEvent(event.session_id);
33
37
  if (recentEvent) {
34
38
  this.storage.updateRoutingEvent(recentEvent.id, {
35
39
  routed_to_type: 'agent',