@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
@@ -16,10 +16,13 @@ import type { SQLiteStorage } from '../../core/storage/sqlite.js';
16
16
  import type { InvocationGuard } from '../../skills/invocation-guard.js';
17
17
  import type { UserPromptHandler } from './user-prompt.js';
18
18
  import { logger } from '../../core/utils/logger.js';
19
- import { execFileSync } from 'node:child_process';
19
+ import { execFile } from 'node:child_process';
20
+ import { promisify } from 'node:util';
20
21
  import { formatError, truncateString } from '../../core/utils/format.js';
21
22
  import { truncateSessionId } from '../../core/utils/session.js';
22
23
 
24
+ const execFileAsync = promisify(execFile);
25
+
23
26
  export class StopHandler {
24
27
  constructor(
25
28
  private exporter: HistoryExporter,
@@ -80,8 +83,9 @@ export class StopHandler {
80
83
  }
81
84
  }
82
85
 
83
- // 6. Write session_id to git note on HEAD commit
84
- this.writeGitNote(event);
86
+ // 6. Write session_id to git note on HEAD commit (fire-and-forget, async)
87
+ // 不 await:git 操作不影响 handler 响应,失败已在内部 catch
88
+ void this.writeGitNote(event);
85
89
 
86
90
  return { allow: true };
87
91
  } catch (err) {
@@ -96,36 +100,40 @@ export class StopHandler {
96
100
  * Write session_id into a git note on the current HEAD commit.
97
101
  * Uses `git notes append` so multiple sessions can be associated with one commit.
98
102
  * Silently ignores failures (no git repo, no commits, etc.).
103
+ *
104
+ * 异步实现:3 个 git 调用有序依赖(rev-parse HEAD 依赖 work-tree 检查通过;
105
+ * notes append 依赖 HEAD hash),因此 rev-parse 串行;但整体 fire-and-forget,
106
+ * 不阻塞 handler 主响应。
99
107
  */
100
- private writeGitNote(event: StopEvent): void {
108
+ private async writeGitNote(event: StopEvent): Promise<void> {
101
109
  const projectPath = event.project_path;
102
110
  if (!projectPath) return;
103
111
 
112
+ const opts = {
113
+ cwd: projectPath,
114
+ timeout: 5000,
115
+ };
116
+
104
117
  try {
105
118
  // Check if inside a git work tree
106
- execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
107
- cwd: projectPath,
108
- stdio: 'pipe',
109
- timeout: 5000,
110
- });
119
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], opts);
111
120
 
112
121
  // Get HEAD commit hash
113
- const head = execFileSync('git', ['rev-parse', 'HEAD'], {
114
- cwd: projectPath,
115
- stdio: 'pipe',
116
- timeout: 5000,
117
- }).toString().trim();
122
+ const { stdout: headStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], opts);
123
+ const head = headStdout.toString().trim();
118
124
 
119
125
  if (!head) return;
120
126
 
121
127
  // Append session_id to git note on HEAD
122
- execFileSync('git', ['notes', 'append', '-m', `forge-session: ${event.session_id}`, head], {
123
- cwd: projectPath,
124
- stdio: 'pipe',
125
- timeout: 5000,
126
- });
128
+ await execFileAsync(
129
+ 'git',
130
+ ['notes', 'append', '-m', `forge-session: ${event.session_id}`, head],
131
+ opts,
132
+ );
127
133
 
128
- logger.debug(`[Stop] Git note appended: forge-session: ${truncateSessionId(event.session_id)} on ${truncateString(head, 8)}`);
134
+ logger.debug(
135
+ `[Stop] Git note appended: forge-session: ${truncateSessionId(event.session_id)} on ${truncateString(head, 8)}`,
136
+ );
129
137
  } catch (err) {
130
138
  logger.debug(`[Stop] Git note write skipped: ${formatError(err)}`);
131
139
  }
@@ -140,36 +148,21 @@ export class StopHandler {
140
148
  if (!this.storage) return null;
141
149
 
142
150
  try {
143
- const db = this.storage.getDatabase();
151
+ // H2 Phase 3: 改用 facade 聚合方法,消除 db.prepare 直写。
144
152
 
145
153
  // 1. Tool usage stats
146
- const toolStats = db.prepare(`
147
- SELECT tool_name, COUNT(*) as count FROM events
148
- WHERE session_id = ? AND tool_name IS NOT NULL
149
- GROUP BY tool_name ORDER BY count DESC
150
- `).all(sessionId) as Array<{ tool_name: string; count: number }>;
151
-
154
+ const toolStats = this.storage.aggregateToolUsageBySession(sessionId);
152
155
  const totalToolCalls = toolStats.reduce((sum, row) => sum + row.count, 0);
153
156
 
154
157
  // 2. Agent/Task delegation stats
155
- const agentStats = db.prepare(`
156
- SELECT json_extract(tool_input, '$.subagent_type') as agent_type, COUNT(*) as count
157
- FROM events
158
- WHERE session_id = ? AND tool_name IN ('Agent', 'Task') AND tool_input IS NOT NULL
159
- GROUP BY agent_type
160
- `).all(sessionId) as Array<{ agent_type: string | null; count: number }>;
161
-
158
+ const agentStats = this.storage.aggregateAgentTypeBySession(sessionId);
162
159
  const totalAgentCalls = agentStats.reduce((sum, row) => sum + row.count, 0);
163
160
  const agentNames = agentStats
164
161
  .filter(row => row.agent_type)
165
162
  .map(row => row.agent_type as string);
166
163
 
167
164
  // 3. Skill invocation count
168
- const skillRow = db.prepare(`
169
- SELECT COUNT(*) as count FROM skill_invocations WHERE session_id = ?
170
- `).get(sessionId) as { count: number } | undefined;
171
-
172
- const skillCount = skillRow?.count ?? 0;
165
+ const skillCount = this.storage.countSkillInvocationsBySession(sessionId);
173
166
 
174
167
  // Build summary
175
168
  if (totalToolCalls === 0) return null;
@@ -14,11 +14,15 @@ import type { SkillRegistry } from '../../skills/registry.js';
14
14
  import { logger } from '../../core/utils/logger.js';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import { truncateString } from '../../core/utils/format.js';
17
+ import { LRUCache } from '../../core/utils/lru-cache.js';
17
18
 
18
19
  export class UserPromptHandler {
19
- private resumeInjected = new Map<string, number>();
20
- private conventionInjected = new Map<string, number>();
21
20
  private static readonly MAX_INJECTION_KEYS = 1000;
21
+ // Two independent caches: resume vs convention have separate inject-once
22
+ // semantics, so we keep them as separate LRU instances rather than merging
23
+ // their key spaces. Both store sessionKey → injection timestamp.
24
+ private resumeInjected = new LRUCache<string, number>(UserPromptHandler.MAX_INJECTION_KEYS);
25
+ private conventionInjected = new LRUCache<string, number>(UserPromptHandler.MAX_INJECTION_KEYS);
22
26
 
23
27
  constructor(
24
28
  private resume: ResumeManager | null = null,
@@ -137,31 +141,17 @@ export class UserPromptHandler {
137
141
  }
138
142
  // ── Private: Injection tracking helpers ────────────────────────────────
139
143
 
140
- /** Check if a sessionKey has been injected in the given map. */
141
- private hasInjected(map: Map<string, number>, key: string): boolean {
142
- return map.has(key);
144
+ /** Check if a sessionKey has been injected in the given cache. */
145
+ private hasInjected(cache: LRUCache<string, number>, key: string): boolean {
146
+ return cache.has(key);
143
147
  }
144
148
 
145
149
  /**
146
150
  * Mark a sessionKey as injected, recording the current timestamp.
147
- * If map size reaches MAX_INJECTION_KEYS, evict the oldest entry (LRU cap).
151
+ * LRUCache transparently evicts the oldest entry when capacity is reached.
148
152
  */
149
- private markInjected(map: Map<string, number>, key: string): void {
150
- if (map.size >= UserPromptHandler.MAX_INJECTION_KEYS) {
151
- // Evict the entry with the smallest timestamp (oldest)
152
- let oldestKey: string | undefined;
153
- let oldestTs = Infinity;
154
- for (const [k, ts] of map) {
155
- if (ts < oldestTs) {
156
- oldestTs = ts;
157
- oldestKey = k;
158
- }
159
- }
160
- if (oldestKey !== undefined) {
161
- map.delete(oldestKey);
162
- }
163
- }
164
- map.set(key, Date.now());
153
+ private markInjected(cache: LRUCache<string, number>, key: string): void {
154
+ cache.set(key, Date.now());
165
155
  }
166
156
 
167
157
  // ── Public: Session cleanup ──────────────────────────────────────────────
@@ -0,0 +1,91 @@
1
+ import { existsSync, readFileSync, copyFileSync, chmodSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { FORGE_PATHS } from '../core/constants.js';
6
+ import { logger } from '../core/utils/logger.js';
7
+
8
+ // All hook files to sync (5 hooks + shared lib)
9
+ const HOOK_FILES = [
10
+ 'pre-tool-use.sh',
11
+ 'post-tool-use.sh',
12
+ 'user-prompt-submit.sh',
13
+ 'notification.sh',
14
+ 'stop.sh',
15
+ 'hook-lib.sh',
16
+ ];
17
+
18
+ function sha256(content: Buffer): string {
19
+ return createHash('sha256').update(content).digest('hex');
20
+ }
21
+
22
+ function getSourceHooksDir(): string {
23
+ // Compiled: dist/daemon/hook-sync.js → dist/hooks
24
+ // Dev (vitest): src/daemon/hook-sync.ts → src/hooks
25
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
26
+ }
27
+
28
+ export interface SyncResult {
29
+ copied: number;
30
+ checked: number;
31
+ skipped: number;
32
+ }
33
+
34
+ /**
35
+ * Sync hooks from the package source (dist/hooks) to ~/.claude-forge/hooks/.
36
+ * Uses SHA-256 comparison to skip identical files.
37
+ *
38
+ * Accepts optional overrides for source/target dirs — used in unit tests.
39
+ * Any failure logs a warning and continues; never throws.
40
+ */
41
+ export function syncHooks(opts?: { sourceDir?: string; targetDir?: string }): SyncResult {
42
+ const result: SyncResult = { copied: 0, checked: 0, skipped: 0 };
43
+ const sourceDir = opts?.sourceDir ?? getSourceHooksDir();
44
+ const targetDir = opts?.targetDir ?? FORGE_PATHS.hooks();
45
+
46
+ if (!existsSync(sourceDir)) {
47
+ logger.warn(`[HookSync] source hooks dir not found at ${sourceDir}, skipping`);
48
+ return result;
49
+ }
50
+
51
+ if (!existsSync(targetDir)) {
52
+ // User has not run `claude-forge init` yet — skip silently
53
+ logger.debug(`[HookSync] target dir not found: ${targetDir} (user may not have run \`claude-forge init\`)`);
54
+ return result;
55
+ }
56
+
57
+ for (const file of HOOK_FILES) {
58
+ const src = join(sourceDir, file);
59
+ const dest = join(targetDir, file);
60
+
61
+ if (!existsSync(src)) {
62
+ result.skipped++;
63
+ continue;
64
+ }
65
+
66
+ result.checked++;
67
+
68
+ try {
69
+ const srcContent = readFileSync(src);
70
+ if (existsSync(dest)) {
71
+ const destContent = readFileSync(dest);
72
+ if (sha256(srcContent) === sha256(destContent)) {
73
+ continue; // identical — no copy needed
74
+ }
75
+ }
76
+
77
+ copyFileSync(src, dest);
78
+ chmodSync(dest, 0o755);
79
+ result.copied++;
80
+ logger.info(`[HookSync] updated ${file}`);
81
+ } catch (err) {
82
+ logger.warn(`[HookSync] failed to sync ${file}: ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+ }
85
+
86
+ if (result.copied > 0) {
87
+ logger.info(`[HookSync] synced ${result.copied}/${result.checked} hook files`);
88
+ }
89
+
90
+ return result;
91
+ }
@@ -20,7 +20,7 @@ import {
20
20
  removeAuthToken,
21
21
  } from './lifecycle.js';
22
22
  import { routeEvent, type Handlers } from './router.js';
23
- import { logger, setLogLevel, LogLevel } from '../core/utils/logger.js';
23
+ import { logger } from '../core/utils/logger.js';
24
24
  import { ConfigManager } from '../core/config.js';
25
25
  import { SQLiteStorage } from '../core/storage/sqlite.js';
26
26
  import { expandPath } from '../core/utils/path.js';
@@ -34,8 +34,11 @@ import { ConventionExtractor } from '../claudemd/convention-extractor.js';
34
34
  import { UserPromptHandler } from './handlers/user-prompt.js';
35
35
  import { PostToolUseHandler } from './handlers/post-tool-use.js';
36
36
  import { StopHandler } from './handlers/stop.js';
37
+ import { syncHooks } from './hook-sync.js';
37
38
  import { replayQueue } from '../core/queue/index.js';
39
+ import { DEFAULTS } from '../core/constants.js';
38
40
  import type { ForgeEvent } from '../core/types.js';
41
+ import { getUserPrompt } from '../core/event-fields.js';
39
42
  import type { HookResponse } from './server.js';
40
43
 
41
44
  export interface DaemonOptions {
@@ -56,13 +59,8 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
56
59
  });
57
60
 
58
61
  // ── 1. Config & Logging ────────────────────────────────────────────────────
62
+ // Log level is auto-parsed from LOG_LEVEL env var at logger module load time.
59
63
  const config = new ConfigManager().get();
60
-
61
- const envLevel = (process.env.LOG_LEVEL ?? '').toLowerCase();
62
- const levelMap: Record<string, LogLevel> = {
63
- debug: LogLevel.DEBUG, info: LogLevel.INFO, warn: LogLevel.WARN, error: LogLevel.ERROR,
64
- };
65
- setLogLevel(levelMap[envLevel] ?? LogLevel.INFO);
66
64
  logger.info('Claude Forge daemon starting...');
67
65
 
68
66
  // ── 2. Lifecycle (PID, socket, auth token) ─────────────────────────────────
@@ -70,13 +68,22 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
70
68
  writePidFile();
71
69
  const authToken = writeAuthToken();
72
70
  logger.info('[Security] Auth token generated');
73
- syncMcpToken(authToken, config.web?.port ?? 3721);
71
+ syncMcpToken(authToken, config.web?.port ?? DEFAULTS.WEB_PORT);
74
72
 
75
73
  // ── 3. Storage ─────────────────────────────────────────────────────────────
76
74
  const dbPath = expandPath(config.storage.path);
77
75
  const storage = new SQLiteStorage(dbPath);
78
76
  logger.info(`Storage initialized: ${dbPath}`);
79
77
 
78
+ // ── 3.5. Auto-sync hooks ────────────────────────────────────────────────
79
+ // npm upgrade 不会自动更新 ~/.claude-forge/hooks/,每次 daemon 启动
80
+ // 用 SHA-256 比对源 dist/hooks 与本地副本,不一致则覆盖。
81
+ try {
82
+ syncHooks();
83
+ } catch (err) {
84
+ logger.warn(`[HookSync] unexpected error: ${err}`);
85
+ }
86
+
80
87
  // ── 4. AI Provider ─────────────────────────────────────────────────────────
81
88
  const apiKey = config.ai.api_key || process.env.ANTHROPIC_API_KEY || '';
82
89
  if (!apiKey) {
@@ -108,7 +115,7 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
108
115
  // ── 5.5. Schedule daily maintenance ────────────────────────────────────────
109
116
  // Clean old events every 24 hours
110
117
  const MAINTENANCE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
111
- setInterval(() => {
118
+ const maintenanceInterval = setInterval(() => {
112
119
  logger.info('[Daemon] Running daily maintenance: cleaning old events');
113
120
  try {
114
121
  storage.cleanOldEvents(30);
@@ -118,6 +125,21 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
118
125
  }, MAINTENANCE_INTERVAL);
119
126
  logger.info('[Daemon] Scheduled daily maintenance (clean events older than 30 days)');
120
127
 
128
+ // ── 5.6. Stale task GC (every 5 minutes) ──────────────────────────────────
129
+ const STALE_TASK_GC_INTERVAL = 5 * 60 * 1000; // 5 分钟
130
+ const STALE_TASK_IDLE_MINUTES = 10; // idle 超 10 分钟视为滞留
131
+
132
+ const staleTaskGcInterval = setInterval(() => {
133
+ try {
134
+ const closed = storage.completeStaleActiveTasks(STALE_TASK_IDLE_MINUTES);
135
+ if (closed > 0) {
136
+ logger.info(`[Maintenance] Auto-completed ${closed} stale active task(s)`);
137
+ }
138
+ } catch (err) {
139
+ logger.error(`[Maintenance] Stale task GC failed: ${err}`);
140
+ }
141
+ }, STALE_TASK_GC_INTERVAL);
142
+
121
143
  // ── 6. Create handlers ─────────────────────────────────────────────────────
122
144
  const userPromptHandler = new UserPromptHandler(
123
145
  resume,
@@ -157,7 +179,7 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
157
179
  logger.debug(`[Event] ${event.hook_type} | tool=${event.tool_name ?? 'N/A'}`);
158
180
 
159
181
  // Task segmentation: UserPromptSubmit starts/continues tasks
160
- const prompt = event.user_prompt || (event.tool_input as any)?.user_prompt;
182
+ const prompt = getUserPrompt(event);
161
183
  if (event.hook_type === 'UserPromptSubmit' && prompt) {
162
184
  taskSegmenter.processPrompt(event.session_id, prompt, event.timestamp, event.event_id);
163
185
  } else if (event.event_id) {
@@ -250,6 +272,8 @@ export async function startDaemon(foreground: boolean = false, options: DaemonOp
250
272
 
251
273
  if (ai) logger.info(`[AI Stats] ${ai.formatStats()}`);
252
274
 
275
+ clearInterval(maintenanceInterval);
276
+ clearInterval(staleTaskGcInterval);
253
277
  if (webServer) await webServer.stop();
254
278
  await server.close();
255
279
  storage.close();
@@ -6,8 +6,8 @@ import { logger } from '../core/utils/logger.js';
6
6
  import { FORGE_PATHS } from '../core/constants.js';
7
7
 
8
8
  const FORGE_HOME = FORGE_PATHS.home();
9
- const TOKEN_FILE = path.join(FORGE_HOME, 'daemon.token');
10
- const PID_FILE = path.join(FORGE_HOME, 'daemon.pid');
9
+ const TOKEN_FILE = FORGE_PATHS.daemonToken();
10
+ const PID_FILE = FORGE_PATHS.daemonPid();
11
11
 
12
12
  export function writePidFile(): void {
13
13
  if (!fs.existsSync(FORGE_HOME)) {
@@ -49,7 +49,7 @@ export function isRunning(): boolean {
49
49
  }
50
50
 
51
51
  export function getSocketPath(): string {
52
- return path.join(FORGE_HOME, 'daemon.sock');
52
+ return FORGE_PATHS.daemonSocket();
53
53
  }
54
54
 
55
55
  export function cleanSocket(): void {
@@ -72,61 +72,39 @@ export class SocketServer {
72
72
  return;
73
73
  }
74
74
 
75
- try {
76
- // 认证检查:在 EventParser 之前提取 _auth 字段(zod 会剥离未知字段)
77
- if (this.authToken) {
78
- try {
79
- const raw = JSON.parse(buffer);
80
- if (raw._auth !== this.authToken) {
81
- logger.warn('[Socket] 认证失败,关闭连接');
82
- buffer = '';
83
- if (connectionTimeout) clearTimeout(connectionTimeout);
84
- socket.destroy();
85
- return;
86
- }
87
- } catch {
88
- // JSON 解析失败,交给下面的 EventParser 处理
89
- }
90
- }
91
-
92
- const event = this.parser.parse(buffer);
93
- buffer = '';
94
- if (connectionTimeout) clearTimeout(connectionTimeout);
95
-
96
- const result = await this.handler(event);
75
+ // 性能修复(L3):hook 端用 `echo` 发送 JSON,结尾自带换行符。
76
+ // 只在 buffer 中遇到换行符时才尝试解析,避免大事件(接近 512KB)
77
+ // 在分片到达时每个 chunk 都跑一次完整 JSON.parse 造成的 N² 行为。
78
+ // 兼容:如果数据无换行(旧客户端、其他来源),等到 socket 关闭时
79
+ // 'end' 事件做最后一次解析尝试。
80
+ const newlineIdx = buffer.indexOf('\n');
81
+ if (newlineIdx === -1) {
82
+ // 尚未收到完整消息,继续等待
83
+ return;
84
+ }
97
85
 
98
- // 双向通信:如果 handler 返回了结果,写回给 hook 脚本
99
- if (result) {
100
- // 清理无意义内容,避免 Claude Code 注入干扰上下文
101
- const cleaned = { ...result } as Record<string, unknown>;
102
- if (cleaned['additionalContext'] && !this.isValidContent(cleaned['additionalContext'] as string)) {
103
- delete cleaned['additionalContext'];
104
- }
105
- if (cleaned['systemMessage'] && !this.isValidContent(cleaned['systemMessage'] as string)) {
106
- delete cleaned['systemMessage'];
107
- }
86
+ // 取换行前的完整消息;忽略行后可能存在的多余数据(hook 单连接一事件)
87
+ const message = buffer.slice(0, newlineIdx);
88
+ buffer = '';
108
89
 
109
- const payload = JSON.stringify(cleaned);
110
- socket.write(payload, () => socket.end());
111
- } else {
112
- socket.end();
113
- }
114
- } catch (err) {
115
- // 区分"JSON 不完整"和"格式错误"
116
- if (err instanceof SyntaxError) {
117
- const trimmed = buffer.trim();
118
- if (trimmed.startsWith('{') && !this.isCompleteJSON(trimmed)) {
119
- // JSON 不完整,继续等待
120
- logger.debug(`缓冲区不完整(${buffer.length} 字节),等待更多数据`);
121
- return;
122
- }
123
- }
90
+ try {
91
+ await this.processMessage(socket, message);
92
+ } finally {
93
+ if (connectionTimeout) clearTimeout(connectionTimeout);
94
+ }
95
+ });
124
96
 
125
- logger.error(`事件解析失败:${formatError(err)}`);
126
- logger.debug(`无效缓冲区内容:${truncateString(buffer, 500)}`);
127
- buffer = '';
97
+ // 兜底:连接关闭时若 buffer 仍有内容(无换行结尾的旧客户端),尝试解析一次
98
+ socket.on('end', async () => {
99
+ const remaining = buffer.trim();
100
+ buffer = '';
101
+ if (remaining.length === 0) return;
102
+ try {
103
+ await this.processMessage(socket, remaining);
104
+ } catch {
105
+ // processMessage 内部已记录错误
106
+ } finally {
128
107
  if (connectionTimeout) clearTimeout(connectionTimeout);
129
- socket.destroy();
130
108
  }
131
109
  });
132
110
 
@@ -140,6 +118,53 @@ export class SocketServer {
140
118
  });
141
119
  }
142
120
 
121
+ /**
122
+ * 解析单条完整消息(一次性,无 N² 行为)并执行 handler。
123
+ *
124
+ * 注意:调用方需确保 message 是一条完整的 JSON 文本(已根据换行符切分)。
125
+ */
126
+ private async processMessage(socket: net.Socket, message: string): Promise<void> {
127
+ try {
128
+ // 认证检查:在 EventParser 之前提取 _auth 字段(zod 会剥离未知字段)
129
+ if (this.authToken) {
130
+ try {
131
+ const raw = JSON.parse(message);
132
+ if (raw._auth !== this.authToken) {
133
+ logger.warn('[Socket] 认证失败,关闭连接');
134
+ socket.destroy();
135
+ return;
136
+ }
137
+ } catch {
138
+ // JSON 解析失败,交给下面的 EventParser 处理
139
+ }
140
+ }
141
+
142
+ const event = this.parser.parse(message);
143
+ const result = await this.handler(event);
144
+
145
+ // 双向通信:如果 handler 返回了结果,写回给 hook 脚本
146
+ if (result) {
147
+ // 清理无意义内容,避免 Claude Code 注入干扰上下文
148
+ const cleaned = { ...result } as Record<string, unknown>;
149
+ if (cleaned['additionalContext'] && !this.isValidContent(cleaned['additionalContext'] as string)) {
150
+ delete cleaned['additionalContext'];
151
+ }
152
+ if (cleaned['systemMessage'] && !this.isValidContent(cleaned['systemMessage'] as string)) {
153
+ delete cleaned['systemMessage'];
154
+ }
155
+
156
+ const payload = JSON.stringify(cleaned);
157
+ socket.write(payload, () => socket.end());
158
+ } else {
159
+ socket.end();
160
+ }
161
+ } catch (err) {
162
+ logger.error(`事件解析失败:${formatError(err)}`);
163
+ logger.debug(`无效消息内容:${truncateString(message, 500)}`);
164
+ socket.destroy();
165
+ }
166
+ }
167
+
143
168
  /**
144
169
  * 判断内容是否有意义,过滤空字符串、纯标点/符号等
145
170
  */
@@ -160,44 +185,6 @@ export class SocketServer {
160
185
  return true;
161
186
  }
162
187
 
163
- /**
164
- * 简单检查 JSON 是否完整(启发式方法)
165
- */
166
- private isCompleteJSON(str: string): boolean {
167
- let braceCount = 0;
168
- let bracketCount = 0;
169
- let inString = false;
170
- let escaped = false;
171
-
172
- for (let i = 0; i < str.length; i++) {
173
- const char = str[i];
174
-
175
- if (escaped) {
176
- escaped = false;
177
- continue;
178
- }
179
-
180
- if (char === '\\') {
181
- escaped = true;
182
- continue;
183
- }
184
-
185
- if (char === '"') {
186
- inString = !inString;
187
- continue;
188
- }
189
-
190
- if (inString) continue;
191
-
192
- if (char === '{') braceCount++;
193
- else if (char === '}') braceCount--;
194
- else if (char === '[') bracketCount++;
195
- else if (char === ']') bracketCount--;
196
- }
197
-
198
- return braceCount === 0 && bracketCount === 0 && !inString;
199
- }
200
-
201
188
  close(): Promise<void> {
202
189
  return new Promise((resolve) => {
203
190
  this.server.close(() => {
@@ -75,7 +75,7 @@ export class TaskSegmenter {
75
75
  }
76
76
 
77
77
  completeCurrentTask(sessionId: string, timestamp: string): void {
78
- const current = this.currentTasks.get(sessionId);
78
+ const current = this.currentTasks.get(sessionId) ?? this.recoverActiveTask(sessionId);
79
79
  if (!current) return;
80
80
  this.storage.updateTask(current.id, {
81
81
  status: 'completed',
@@ -10,6 +10,43 @@
10
10
  SOCKET_PATH="${CLAUDE_FORGE_SOCKET:-$HOME/.claude-forge/daemon.sock}"
11
11
  QUEUE_DIR="$HOME/.claude-forge/queue"
12
12
 
13
+ # resolve_project_path <input_cwd>
14
+ #
15
+ # Walk up the directory tree from <input_cwd> to find the nearest ancestor that
16
+ # contains a `.git` entry (directory for normal clones, file for worktrees /
17
+ # submodules). Print the resolved git-root path on stdout.
18
+ #
19
+ # Fallback rules:
20
+ # - Empty input → start from $PWD
21
+ # - Relative input → prefixed with $PWD to absolutise
22
+ # - No .git found within 64 levels → print original input unchanged
23
+ #
24
+ # POSIX-only: uses `dirname` and `case`; does NOT depend on `realpath` or any
25
+ # GNU coreutils extensions, so it works on macOS BSD and Linux alike.
26
+ resolve_project_path() {
27
+ local input="${1:-}"
28
+ local dir="${input:-$PWD}"
29
+
30
+ # Absolutise: prefix relative paths with $PWD (no realpath dependency)
31
+ case "$dir" in
32
+ /*) ;;
33
+ *) dir="$PWD/$dir" ;;
34
+ esac
35
+
36
+ local guard=0
37
+ while [ "$dir" != "/" ] && [ "$dir" != "." ] && [ $guard -lt 64 ]; do
38
+ if [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
39
+ printf '%s' "$dir"
40
+ return 0
41
+ fi
42
+ dir=$(dirname "$dir")
43
+ guard=$((guard + 1))
44
+ done
45
+
46
+ # No git ancestor found → fall back to the caller's original cwd
47
+ printf '%s' "${input:-$PWD}"
48
+ }
49
+
13
50
  # send_event_or_enqueue <event_json> [timeout_seconds]
14
51
  #
15
52
  # 1. If socket file does not exist → enqueue in background → return empty string
@@ -10,8 +10,8 @@ AUTH_TOKEN=$(cat "$HOME/.claude-forge/daemon.token" 2>/dev/null || echo '')
10
10
  INPUT=$(cat)
11
11
 
12
12
  # 提取 cwd(jq 替代 python3)
13
- PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
14
- PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
13
+ RAW_CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
14
+ PROJECT_PATH=$(resolve_project_path "${RAW_CWD:-$PWD}")
15
15
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // .sessionId // ""')
16
16
  SESSION_ID="${SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-cli}}"
17
17
 
@@ -14,8 +14,8 @@ INPUT=$(cat)
14
14
  TOOL_NAME="${CLAUDE_TOOL_NAME:-$(echo "$INPUT" | jq -r '.tool_name // ""')}"
15
15
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
16
16
  TOOL_OUTPUT=$(echo "$INPUT" | jq -c '.tool_response // {}')
17
- PROJECT_PATH=$(echo "$INPUT" | jq -r '.cwd // ""')
18
- PROJECT_PATH="${PROJECT_PATH:-${PWD}}"
17
+ RAW_CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
18
+ PROJECT_PATH=$(resolve_project_path "${RAW_CWD:-$PWD}")
19
19
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // .sessionId // ""')
20
20
  SESSION_ID="${SESSION_ID:-${CLAUDE_CODE_SESSION_ID:-cli}}"
21
21