forge-server 0.1.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 (412) hide show
  1. package/.claude/hooks/worktree-create.sh +64 -0
  2. package/.claude/hooks/worktree-remove.sh +57 -0
  3. package/.claude/settings.local.json +29 -0
  4. package/.forge/knowledge/conventions.yaml +1 -0
  5. package/.forge/knowledge/decisions.yaml +1 -0
  6. package/.forge/knowledge/gotchas.yaml +1 -0
  7. package/.forge/knowledge/patterns.yaml +1 -0
  8. package/.forge/manifest.yaml +6 -0
  9. package/CLAUDE.md +144 -0
  10. package/bin/setup-forge.sh +132 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +553 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/context/codebase.d.ts +57 -0
  16. package/dist/context/codebase.d.ts.map +1 -0
  17. package/dist/context/codebase.js +301 -0
  18. package/dist/context/codebase.js.map +1 -0
  19. package/dist/context/injector.d.ts +147 -0
  20. package/dist/context/injector.d.ts.map +1 -0
  21. package/dist/context/injector.js +533 -0
  22. package/dist/context/injector.js.map +1 -0
  23. package/dist/context/memory.d.ts +32 -0
  24. package/dist/context/memory.d.ts.map +1 -0
  25. package/dist/context/memory.js +140 -0
  26. package/dist/context/memory.js.map +1 -0
  27. package/dist/context/session-index.d.ts +54 -0
  28. package/dist/context/session-index.d.ts.map +1 -0
  29. package/dist/context/session-index.js +265 -0
  30. package/dist/context/session-index.js.map +1 -0
  31. package/dist/context/session.d.ts +42 -0
  32. package/dist/context/session.d.ts.map +1 -0
  33. package/dist/context/session.js +121 -0
  34. package/dist/context/session.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +37 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/ingestion/chunker.d.ts +19 -0
  40. package/dist/ingestion/chunker.d.ts.map +1 -0
  41. package/dist/ingestion/chunker.js +189 -0
  42. package/dist/ingestion/chunker.js.map +1 -0
  43. package/dist/ingestion/embedder.d.ts +45 -0
  44. package/dist/ingestion/embedder.d.ts.map +1 -0
  45. package/dist/ingestion/embedder.js +152 -0
  46. package/dist/ingestion/embedder.js.map +1 -0
  47. package/dist/ingestion/git-analyzer.d.ts +77 -0
  48. package/dist/ingestion/git-analyzer.d.ts.map +1 -0
  49. package/dist/ingestion/git-analyzer.js +437 -0
  50. package/dist/ingestion/git-analyzer.js.map +1 -0
  51. package/dist/ingestion/indexer.d.ts +79 -0
  52. package/dist/ingestion/indexer.d.ts.map +1 -0
  53. package/dist/ingestion/indexer.js +766 -0
  54. package/dist/ingestion/indexer.js.map +1 -0
  55. package/dist/ingestion/markdown-chunker.d.ts +19 -0
  56. package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
  57. package/dist/ingestion/markdown-chunker.js +243 -0
  58. package/dist/ingestion/markdown-chunker.js.map +1 -0
  59. package/dist/ingestion/markdown-knowledge.d.ts +21 -0
  60. package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
  61. package/dist/ingestion/markdown-knowledge.js +129 -0
  62. package/dist/ingestion/markdown-knowledge.js.map +1 -0
  63. package/dist/ingestion/parser.d.ts +20 -0
  64. package/dist/ingestion/parser.d.ts.map +1 -0
  65. package/dist/ingestion/parser.js +429 -0
  66. package/dist/ingestion/parser.js.map +1 -0
  67. package/dist/ingestion/watcher.d.ts +28 -0
  68. package/dist/ingestion/watcher.d.ts.map +1 -0
  69. package/dist/ingestion/watcher.js +147 -0
  70. package/dist/ingestion/watcher.js.map +1 -0
  71. package/dist/knowledge/hydrator.d.ts +37 -0
  72. package/dist/knowledge/hydrator.d.ts.map +1 -0
  73. package/dist/knowledge/hydrator.js +220 -0
  74. package/dist/knowledge/hydrator.js.map +1 -0
  75. package/dist/knowledge/registry.d.ts +129 -0
  76. package/dist/knowledge/registry.d.ts.map +1 -0
  77. package/dist/knowledge/registry.js +361 -0
  78. package/dist/knowledge/registry.js.map +1 -0
  79. package/dist/knowledge/search.d.ts +114 -0
  80. package/dist/knowledge/search.d.ts.map +1 -0
  81. package/dist/knowledge/search.js +428 -0
  82. package/dist/knowledge/search.js.map +1 -0
  83. package/dist/knowledge/store.d.ts +76 -0
  84. package/dist/knowledge/store.d.ts.map +1 -0
  85. package/dist/knowledge/store.js +230 -0
  86. package/dist/knowledge/store.js.map +1 -0
  87. package/dist/learning/confidence.d.ts +30 -0
  88. package/dist/learning/confidence.d.ts.map +1 -0
  89. package/dist/learning/confidence.js +165 -0
  90. package/dist/learning/confidence.js.map +1 -0
  91. package/dist/learning/patterns.d.ts +52 -0
  92. package/dist/learning/patterns.d.ts.map +1 -0
  93. package/dist/learning/patterns.js +290 -0
  94. package/dist/learning/patterns.js.map +1 -0
  95. package/dist/learning/trajectory.d.ts +55 -0
  96. package/dist/learning/trajectory.d.ts.map +1 -0
  97. package/dist/learning/trajectory.js +200 -0
  98. package/dist/learning/trajectory.js.map +1 -0
  99. package/dist/memory/memory-compat.d.ts +100 -0
  100. package/dist/memory/memory-compat.d.ts.map +1 -0
  101. package/dist/memory/memory-compat.js +146 -0
  102. package/dist/memory/memory-compat.js.map +1 -0
  103. package/dist/memory/observation-store.d.ts +57 -0
  104. package/dist/memory/observation-store.d.ts.map +1 -0
  105. package/dist/memory/observation-store.js +154 -0
  106. package/dist/memory/observation-store.js.map +1 -0
  107. package/dist/memory/session-tracker.d.ts +81 -0
  108. package/dist/memory/session-tracker.d.ts.map +1 -0
  109. package/dist/memory/session-tracker.js +262 -0
  110. package/dist/memory/session-tracker.js.map +1 -0
  111. package/dist/pipeline/engine.d.ts +179 -0
  112. package/dist/pipeline/engine.d.ts.map +1 -0
  113. package/dist/pipeline/engine.js +691 -0
  114. package/dist/pipeline/engine.js.map +1 -0
  115. package/dist/pipeline/events.d.ts +54 -0
  116. package/dist/pipeline/events.d.ts.map +1 -0
  117. package/dist/pipeline/events.js +157 -0
  118. package/dist/pipeline/events.js.map +1 -0
  119. package/dist/pipeline/parallel.d.ts +83 -0
  120. package/dist/pipeline/parallel.d.ts.map +1 -0
  121. package/dist/pipeline/parallel.js +277 -0
  122. package/dist/pipeline/parallel.js.map +1 -0
  123. package/dist/pipeline/state-machine.d.ts +65 -0
  124. package/dist/pipeline/state-machine.d.ts.map +1 -0
  125. package/dist/pipeline/state-machine.js +176 -0
  126. package/dist/pipeline/state-machine.js.map +1 -0
  127. package/dist/query/graph-queries.d.ts +84 -0
  128. package/dist/query/graph-queries.d.ts.map +1 -0
  129. package/dist/query/graph-queries.js +216 -0
  130. package/dist/query/graph-queries.js.map +1 -0
  131. package/dist/query/hybrid-search.d.ts +34 -0
  132. package/dist/query/hybrid-search.d.ts.map +1 -0
  133. package/dist/query/hybrid-search.js +263 -0
  134. package/dist/query/hybrid-search.js.map +1 -0
  135. package/dist/query/intent-detector.d.ts +35 -0
  136. package/dist/query/intent-detector.d.ts.map +1 -0
  137. package/dist/query/intent-detector.js +115 -0
  138. package/dist/query/intent-detector.js.map +1 -0
  139. package/dist/query/ranking.d.ts +57 -0
  140. package/dist/query/ranking.d.ts.map +1 -0
  141. package/dist/query/ranking.js +109 -0
  142. package/dist/query/ranking.js.map +1 -0
  143. package/dist/server.d.ts +3 -0
  144. package/dist/server.d.ts.map +1 -0
  145. package/dist/server.js +291 -0
  146. package/dist/server.js.map +1 -0
  147. package/dist/storage/falkordb-store.d.ts +73 -0
  148. package/dist/storage/falkordb-store.d.ts.map +1 -0
  149. package/dist/storage/falkordb-store.js +346 -0
  150. package/dist/storage/falkordb-store.js.map +1 -0
  151. package/dist/storage/file-cache.d.ts +32 -0
  152. package/dist/storage/file-cache.d.ts.map +1 -0
  153. package/dist/storage/file-cache.js +115 -0
  154. package/dist/storage/file-cache.js.map +1 -0
  155. package/dist/storage/interfaces.d.ts +151 -0
  156. package/dist/storage/interfaces.d.ts.map +1 -0
  157. package/dist/storage/interfaces.js +7 -0
  158. package/dist/storage/interfaces.js.map +1 -0
  159. package/dist/storage/qdrant-store.d.ts +110 -0
  160. package/dist/storage/qdrant-store.d.ts.map +1 -0
  161. package/dist/storage/qdrant-store.js +467 -0
  162. package/dist/storage/qdrant-store.js.map +1 -0
  163. package/dist/storage/schema.d.ts +4 -0
  164. package/dist/storage/schema.d.ts.map +1 -0
  165. package/dist/storage/schema.js +136 -0
  166. package/dist/storage/schema.js.map +1 -0
  167. package/dist/storage/sqlite.d.ts +35 -0
  168. package/dist/storage/sqlite.d.ts.map +1 -0
  169. package/dist/storage/sqlite.js +132 -0
  170. package/dist/storage/sqlite.js.map +1 -0
  171. package/dist/tools/collaboration-tools.d.ts +111 -0
  172. package/dist/tools/collaboration-tools.d.ts.map +1 -0
  173. package/dist/tools/collaboration-tools.js +174 -0
  174. package/dist/tools/collaboration-tools.js.map +1 -0
  175. package/dist/tools/context-tools.d.ts +293 -0
  176. package/dist/tools/context-tools.d.ts.map +1 -0
  177. package/dist/tools/context-tools.js +437 -0
  178. package/dist/tools/context-tools.js.map +1 -0
  179. package/dist/tools/graph-tools.d.ts +129 -0
  180. package/dist/tools/graph-tools.d.ts.map +1 -0
  181. package/dist/tools/graph-tools.js +237 -0
  182. package/dist/tools/graph-tools.js.map +1 -0
  183. package/dist/tools/ingestion-tools.d.ts +96 -0
  184. package/dist/tools/ingestion-tools.d.ts.map +1 -0
  185. package/dist/tools/ingestion-tools.js +90 -0
  186. package/dist/tools/ingestion-tools.js.map +1 -0
  187. package/dist/tools/learning-tools.d.ts +168 -0
  188. package/dist/tools/learning-tools.d.ts.map +1 -0
  189. package/dist/tools/learning-tools.js +158 -0
  190. package/dist/tools/learning-tools.js.map +1 -0
  191. package/dist/tools/memory-tools.d.ts +183 -0
  192. package/dist/tools/memory-tools.d.ts.map +1 -0
  193. package/dist/tools/memory-tools.js +197 -0
  194. package/dist/tools/memory-tools.js.map +1 -0
  195. package/dist/tools/phase-tools.d.ts +954 -0
  196. package/dist/tools/phase-tools.d.ts.map +1 -0
  197. package/dist/tools/phase-tools.js +1215 -0
  198. package/dist/tools/phase-tools.js.map +1 -0
  199. package/dist/tools/pipeline-tools.d.ts +140 -0
  200. package/dist/tools/pipeline-tools.d.ts.map +1 -0
  201. package/dist/tools/pipeline-tools.js +162 -0
  202. package/dist/tools/pipeline-tools.js.map +1 -0
  203. package/dist/tools/registration-tools.d.ts +220 -0
  204. package/dist/tools/registration-tools.d.ts.map +1 -0
  205. package/dist/tools/registration-tools.js +391 -0
  206. package/dist/tools/registration-tools.js.map +1 -0
  207. package/dist/util/circuit-breaker.d.ts +75 -0
  208. package/dist/util/circuit-breaker.d.ts.map +1 -0
  209. package/dist/util/circuit-breaker.js +159 -0
  210. package/dist/util/circuit-breaker.js.map +1 -0
  211. package/dist/util/config.d.ts +23 -0
  212. package/dist/util/config.d.ts.map +1 -0
  213. package/dist/util/config.js +164 -0
  214. package/dist/util/config.js.map +1 -0
  215. package/dist/util/logger.d.ts +13 -0
  216. package/dist/util/logger.d.ts.map +1 -0
  217. package/dist/util/logger.js +45 -0
  218. package/dist/util/logger.js.map +1 -0
  219. package/dist/util/token-counter.d.ts +24 -0
  220. package/dist/util/token-counter.d.ts.map +1 -0
  221. package/dist/util/token-counter.js +48 -0
  222. package/dist/util/token-counter.js.map +1 -0
  223. package/dist/util/types.d.ts +525 -0
  224. package/dist/util/types.d.ts.map +1 -0
  225. package/dist/util/types.js +5 -0
  226. package/dist/util/types.js.map +1 -0
  227. package/docker-compose.yml +20 -0
  228. package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
  229. package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
  230. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
  231. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
  232. package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
  233. package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
  234. package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
  235. package/package.json +39 -0
  236. package/plugin/.claude-plugin/plugin.json +8 -0
  237. package/plugin/.mcp.json +15 -0
  238. package/plugin/README.md +134 -0
  239. package/plugin/agents/architect.md +367 -0
  240. package/plugin/agents/backend-specialist.md +263 -0
  241. package/plugin/agents/brainstormer.md +122 -0
  242. package/plugin/agents/data-specialist.md +266 -0
  243. package/plugin/agents/designer.md +408 -0
  244. package/plugin/agents/frontend-specialist.md +241 -0
  245. package/plugin/agents/inspector.md +406 -0
  246. package/plugin/agents/knowledge-keeper.md +443 -0
  247. package/plugin/agents/platform-engineer.md +326 -0
  248. package/plugin/agents/product-manager.md +268 -0
  249. package/plugin/agents/product-owner.md +438 -0
  250. package/plugin/agents/pulse-checker.md +73 -0
  251. package/plugin/agents/qa-strategist.md +500 -0
  252. package/plugin/agents/self-improver.md +310 -0
  253. package/plugin/agents/strategist.md +360 -0
  254. package/plugin/agents/supervisor.md +380 -0
  255. package/plugin/commands/brainstorm.md +25 -0
  256. package/plugin/commands/forge.md +88 -0
  257. package/plugin/docs/atlassian-integration.md +110 -0
  258. package/plugin/docs/workflow.md +126 -0
  259. package/plugin/skills/agent-development/.skillfish.json +10 -0
  260. package/plugin/skills/agent-development/SKILL.md +415 -0
  261. package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
  262. package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
  263. package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
  264. package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
  265. package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
  266. package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
  267. package/plugin/skills/agent-handoff/SKILL.md +335 -0
  268. package/plugin/skills/anti-stub/SKILL.md +317 -0
  269. package/plugin/skills/brainstorm/SKILL.md +31 -0
  270. package/plugin/skills/debugging/SKILL.md +276 -0
  271. package/plugin/skills/fix/SKILL.md +62 -0
  272. package/plugin/skills/frontend-design/.skillfish.json +10 -0
  273. package/plugin/skills/frontend-design/SKILL.md +42 -0
  274. package/plugin/skills/gotchas/SKILL.md +61 -0
  275. package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
  276. package/plugin/skills/history/SKILL.md +58 -0
  277. package/plugin/skills/impact/SKILL.md +59 -0
  278. package/plugin/skills/implementation-execution/SKILL.md +291 -0
  279. package/plugin/skills/index-repo/SKILL.md +55 -0
  280. package/plugin/skills/interviewing/SKILL.md +225 -0
  281. package/plugin/skills/knowledge-curation/SKILL.md +393 -0
  282. package/plugin/skills/learn/SKILL.md +69 -0
  283. package/plugin/skills/mcp-integration/.skillfish.json +10 -0
  284. package/plugin/skills/mcp-integration/SKILL.md +554 -0
  285. package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
  286. package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
  287. package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
  288. package/plugin/skills/mcp-integration/references/authentication.md +549 -0
  289. package/plugin/skills/mcp-integration/references/server-types.md +536 -0
  290. package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
  291. package/plugin/skills/nestjs/.skillfish.json +10 -0
  292. package/plugin/skills/nestjs/SKILL.md +669 -0
  293. package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
  294. package/plugin/skills/nestjs/reference.md +1447 -0
  295. package/plugin/skills/nestjs/workflow-optimization.md +229 -0
  296. package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
  297. package/plugin/skills/project-discovery/SKILL.md +304 -0
  298. package/plugin/skills/search/SKILL.md +56 -0
  299. package/plugin/skills/security-audit/SKILL.md +362 -0
  300. package/plugin/skills/skill-development/.skillfish.json +10 -0
  301. package/plugin/skills/skill-development/SKILL.md +637 -0
  302. package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
  303. package/plugin/skills/tdd/SKILL.md +273 -0
  304. package/plugin/skills/terminal-presentation/SKILL.md +395 -0
  305. package/plugin/skills/test-strategy/SKILL.md +365 -0
  306. package/plugin/skills/verification-protocol/SKILL.md +256 -0
  307. package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
  308. package/plugin/skills/visual-explainer/LICENSE +21 -0
  309. package/plugin/skills/visual-explainer/README.md +137 -0
  310. package/plugin/skills/visual-explainer/SKILL.md +352 -0
  311. package/plugin/skills/visual-explainer/banner.png +0 -0
  312. package/plugin/skills/visual-explainer/package.json +11 -0
  313. package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
  314. package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
  315. package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
  316. package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
  317. package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
  318. package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
  319. package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
  320. package/plugin/skills/visual-explainer/references/libraries.md +470 -0
  321. package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
  322. package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
  323. package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
  324. package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
  325. package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
  326. package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
  327. package/src/cli.ts +655 -0
  328. package/src/context/.gitkeep +0 -0
  329. package/src/context/codebase.ts +393 -0
  330. package/src/context/injector.ts +797 -0
  331. package/src/context/memory.ts +187 -0
  332. package/src/context/session-index.ts +327 -0
  333. package/src/context/session.ts +152 -0
  334. package/src/index.ts +47 -0
  335. package/src/ingestion/.gitkeep +0 -0
  336. package/src/ingestion/chunker.ts +277 -0
  337. package/src/ingestion/embedder.ts +167 -0
  338. package/src/ingestion/git-analyzer.ts +545 -0
  339. package/src/ingestion/indexer.ts +984 -0
  340. package/src/ingestion/markdown-chunker.ts +337 -0
  341. package/src/ingestion/markdown-knowledge.ts +175 -0
  342. package/src/ingestion/parser.ts +475 -0
  343. package/src/ingestion/watcher.ts +182 -0
  344. package/src/knowledge/.gitkeep +0 -0
  345. package/src/knowledge/hydrator.ts +246 -0
  346. package/src/knowledge/registry.ts +463 -0
  347. package/src/knowledge/search.ts +565 -0
  348. package/src/knowledge/store.ts +262 -0
  349. package/src/learning/.gitkeep +0 -0
  350. package/src/learning/confidence.ts +193 -0
  351. package/src/learning/patterns.ts +360 -0
  352. package/src/learning/trajectory.ts +268 -0
  353. package/src/memory/.gitkeep +0 -0
  354. package/src/memory/memory-compat.ts +233 -0
  355. package/src/memory/observation-store.ts +224 -0
  356. package/src/memory/session-tracker.ts +332 -0
  357. package/src/pipeline/.gitkeep +0 -0
  358. package/src/pipeline/engine.ts +1139 -0
  359. package/src/pipeline/events.ts +253 -0
  360. package/src/pipeline/parallel.ts +394 -0
  361. package/src/pipeline/state-machine.ts +199 -0
  362. package/src/query/.gitkeep +0 -0
  363. package/src/query/graph-queries.ts +262 -0
  364. package/src/query/hybrid-search.ts +337 -0
  365. package/src/query/intent-detector.ts +131 -0
  366. package/src/query/ranking.ts +161 -0
  367. package/src/server.ts +352 -0
  368. package/src/storage/.gitkeep +0 -0
  369. package/src/storage/falkordb-store.ts +388 -0
  370. package/src/storage/file-cache.ts +141 -0
  371. package/src/storage/interfaces.ts +201 -0
  372. package/src/storage/qdrant-store.ts +557 -0
  373. package/src/storage/schema.ts +139 -0
  374. package/src/storage/sqlite.ts +168 -0
  375. package/src/tools/.gitkeep +0 -0
  376. package/src/tools/collaboration-tools.ts +208 -0
  377. package/src/tools/context-tools.ts +493 -0
  378. package/src/tools/graph-tools.ts +295 -0
  379. package/src/tools/ingestion-tools.ts +122 -0
  380. package/src/tools/learning-tools.ts +181 -0
  381. package/src/tools/memory-tools.ts +234 -0
  382. package/src/tools/phase-tools.ts +1452 -0
  383. package/src/tools/pipeline-tools.ts +188 -0
  384. package/src/tools/registration-tools.ts +450 -0
  385. package/src/util/.gitkeep +0 -0
  386. package/src/util/circuit-breaker.ts +193 -0
  387. package/src/util/config.ts +177 -0
  388. package/src/util/logger.ts +53 -0
  389. package/src/util/token-counter.ts +52 -0
  390. package/src/util/types.ts +710 -0
  391. package/tests/context/.gitkeep +0 -0
  392. package/tests/integration/.gitkeep +0 -0
  393. package/tests/knowledge/.gitkeep +0 -0
  394. package/tests/learning/.gitkeep +0 -0
  395. package/tests/pipeline/.gitkeep +0 -0
  396. package/tests/tools/.gitkeep +0 -0
  397. package/tsconfig.json +21 -0
  398. package/vitest.config.ts +10 -0
  399. package/vscode-extension/.vscodeignore +7 -0
  400. package/vscode-extension/README.md +43 -0
  401. package/vscode-extension/out/edge-collector.js +274 -0
  402. package/vscode-extension/out/edge-collector.js.map +1 -0
  403. package/vscode-extension/out/extension.js +264 -0
  404. package/vscode-extension/out/extension.js.map +1 -0
  405. package/vscode-extension/out/forge-client.js +318 -0
  406. package/vscode-extension/out/forge-client.js.map +1 -0
  407. package/vscode-extension/package-lock.json +59 -0
  408. package/vscode-extension/package.json +71 -0
  409. package/vscode-extension/src/edge-collector.ts +320 -0
  410. package/vscode-extension/src/extension.ts +269 -0
  411. package/vscode-extension/src/forge-client.ts +364 -0
  412. package/vscode-extension/tsconfig.json +19 -0
@@ -0,0 +1,360 @@
1
+ // PatternExtractor — B11
2
+ //
3
+ // Extracts learnable knowledge items from completed project trajectories.
4
+ // Called by the knowledge_collection phase tool handler after a project
5
+ // reaches the inspection->completed transition.
6
+ //
7
+ // Two operations:
8
+ //
9
+ // extractFromProject(projectId)
10
+ // Reads all trajectory steps for the project, looks for high-quality
11
+ // steps (qualityScore >= 0.7) and steps with "gotcha"-like language,
12
+ // and promotes the best observations to KnowledgeItem records.
13
+ // Writes to both the YAML store (durable) and Qdrant (searchable).
14
+ //
15
+ // promoteToKnowledge(observation, category, repoId, ...)
16
+ // One-shot promotion: takes a raw observation string and creates a
17
+ // full KnowledgeItem with embedding, persisting to YAML and Qdrant.
18
+ // Used by the knowledge-keeper agent for manual promotion.
19
+
20
+ import { randomUUID } from 'node:crypto';
21
+ import type { PipelineDB } from '../storage/sqlite.js';
22
+ import type { KnowledgeYamlStore } from '../knowledge/store.js';
23
+ import type { KnowledgeSearch } from '../knowledge/search.js';
24
+ import type {
25
+ KnowledgeItem,
26
+ KnowledgeCategory,
27
+ Phase,
28
+ PipelinePhase,
29
+ TrajectoryStepRow,
30
+ TrajectoryRow,
31
+ } from '../util/types.js';
32
+ import { embedText } from '../ingestion/embedder.js';
33
+ import { logger } from '../util/logger.js';
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Constants
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Minimum quality score for a step to be considered a candidate. */
40
+ const QUALITY_THRESHOLD = 0.7;
41
+
42
+ /** Words that strongly suggest a step contains a gotcha / pain point. */
43
+ const GOTCHA_KEYWORDS = [
44
+ 'gotcha',
45
+ 'bug',
46
+ 'fix',
47
+ 'broken',
48
+ 'failed',
49
+ 'error',
50
+ 'issue',
51
+ 'problem',
52
+ 'mistake',
53
+ 'avoid',
54
+ 'warning',
55
+ 'careful',
56
+ 'unexpected',
57
+ 'pitfall',
58
+ 'caveat',
59
+ ];
60
+
61
+ /** Words that suggest a step contains a positive pattern. */
62
+ const PATTERN_KEYWORDS = [
63
+ 'pattern',
64
+ 'approach',
65
+ 'works',
66
+ 'solution',
67
+ 'discovered',
68
+ 'learned',
69
+ 'efficient',
70
+ 'best',
71
+ 'prefer',
72
+ 'recommend',
73
+ ];
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // PatternExtractor
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export class PatternExtractor {
80
+ constructor(
81
+ private readonly db: PipelineDB,
82
+ private readonly knowledgeStore: KnowledgeYamlStore,
83
+ private readonly knowledgeSearch: KnowledgeSearch,
84
+ private readonly embedder: typeof embedText,
85
+ ) {}
86
+
87
+ /**
88
+ * Extract knowledge from all trajectory steps of a completed project.
89
+ *
90
+ * Algorithm:
91
+ * 1. Load all trajectories + their steps for the project.
92
+ * 2. Collect steps that are either:
93
+ * a. High quality (qualityScore >= QUALITY_THRESHOLD), or
94
+ * b. Contain gotcha-like language in action/result text.
95
+ * 3. For each candidate step, determine whether it looks like a gotcha
96
+ * or a pattern (keyword matching).
97
+ * 4. Deduplicate against existing knowledge (semantic similarity check).
98
+ * 5. Promote unique candidates to KnowledgeItem records.
99
+ * 6. Return the list of newly created items.
100
+ */
101
+ async extractFromProject(projectId: string): Promise<KnowledgeItem[]> {
102
+ // Load all trajectories for the project
103
+ const trajectoryRows = this.db.all<TrajectoryRow>(
104
+ `SELECT * FROM trajectories WHERE project_id = ? ORDER BY started_at ASC`,
105
+ [projectId],
106
+ );
107
+
108
+ if (trajectoryRows.length === 0) {
109
+ logger.debug('PatternExtractor: no trajectories for project', { projectId });
110
+ return [];
111
+ }
112
+
113
+ // Load all steps for these trajectories in one query
114
+ const trajectoryIds = trajectoryRows.map((t) => t.id);
115
+ const placeholders = trajectoryIds.map(() => '?').join(', ');
116
+ const stepRows = this.db.all<TrajectoryStepRow>(
117
+ `SELECT * FROM trajectory_steps WHERE trajectory_id IN (${placeholders}) ORDER BY created_at ASC`,
118
+ trajectoryIds,
119
+ );
120
+
121
+ // Build a map from trajectory_id -> trajectory for phase/agent lookup
122
+ const trajectoryMap = new Map<string, TrajectoryRow>(
123
+ trajectoryRows.map((t) => [t.id, t]),
124
+ );
125
+
126
+ // Identify candidate steps
127
+ const candidates = stepRows.filter((step) => {
128
+ const hasHighQuality =
129
+ step.quality_score !== null && step.quality_score >= QUALITY_THRESHOLD;
130
+ const text = `${step.action} ${step.result ?? ''}`.toLowerCase();
131
+ const hasGotchaLanguage = GOTCHA_KEYWORDS.some((kw) => text.includes(kw));
132
+ const hasPatternLanguage = PATTERN_KEYWORDS.some((kw) => text.includes(kw));
133
+ return hasHighQuality || hasGotchaLanguage || hasPatternLanguage;
134
+ });
135
+
136
+ if (candidates.length === 0) {
137
+ logger.debug('PatternExtractor: no candidate steps found', { projectId });
138
+ return [];
139
+ }
140
+
141
+ // Look up repo_id from the project
142
+ const projectRow = this.db.get<{ repo_id: string }>(
143
+ `SELECT repo_id FROM projects WHERE id = ?`,
144
+ [projectId],
145
+ );
146
+ const repoId = projectRow?.repo_id ?? 'unknown';
147
+
148
+ // Promote each candidate (with deduplication)
149
+ const promoted: KnowledgeItem[] = [];
150
+
151
+ for (const step of candidates) {
152
+ const traj = trajectoryMap.get(step.trajectory_id);
153
+ const observation = buildObservationText(step);
154
+ const category = classifyCategory(step);
155
+
156
+ try {
157
+ // Check for near-duplicate in existing knowledge
158
+ const isDuplicate = await this.isDuplicateKnowledge(observation, category);
159
+ if (isDuplicate) {
160
+ logger.debug('PatternExtractor: skipping duplicate', {
161
+ action: step.action.slice(0, 60),
162
+ });
163
+ continue;
164
+ }
165
+
166
+ const item = await this.promoteToKnowledge(
167
+ observation,
168
+ category,
169
+ repoId,
170
+ traj ? (traj.phase as Phase) : undefined,
171
+ traj?.agent,
172
+ );
173
+ promoted.push(item);
174
+ } catch (err) {
175
+ // Non-fatal: log and continue to next candidate
176
+ logger.warn('PatternExtractor: failed to promote step', {
177
+ trajectoryId: step.trajectory_id,
178
+ error: String(err),
179
+ });
180
+ }
181
+ }
182
+
183
+ logger.info('PatternExtractor: extraction complete', {
184
+ projectId,
185
+ candidateCount: candidates.length,
186
+ promotedCount: promoted.length,
187
+ });
188
+
189
+ return promoted;
190
+ }
191
+
192
+ /**
193
+ * Promote a raw observation string to a full KnowledgeItem.
194
+ *
195
+ * Steps:
196
+ * 1. Synthesise a title from the first sentence of the observation.
197
+ * 2. Build a KnowledgeItem with appropriate defaults.
198
+ * 3. Add to the YAML store (the git-tracked source of truth).
199
+ * 4. Embed and upsert to Qdrant via hydrateItem() (if available).
200
+ * 5. Return the new item.
201
+ */
202
+ async promoteToKnowledge(
203
+ observation: string,
204
+ category: KnowledgeCategory,
205
+ repoId: string,
206
+ phase?: Phase | PipelinePhase,
207
+ agent?: string,
208
+ ): Promise<KnowledgeItem> {
209
+ const now = Date.now();
210
+ const prefix = categoryPrefix(category);
211
+ const id = `${prefix}-${randomUUID().slice(0, 8)}`;
212
+ const title = deriveTitle(observation, category);
213
+
214
+ const item: KnowledgeItem = {
215
+ id,
216
+ title,
217
+ content: observation,
218
+ stack_tags: [], // Extracted without stack context; caller can enrich
219
+ confidence: 0.6, // New items start at moderate confidence
220
+ source: 'agent',
221
+ // Phase values from both Phase and PipelinePhase unions are valid strings
222
+ // in YAML. We cast to satisfy the KnowledgeItem type declaration.
223
+ source_phase: (phase ?? null) as PipelinePhase | null,
224
+ source_agent: agent ?? null,
225
+ created_at: now,
226
+ updated_at: now,
227
+ };
228
+
229
+ // Persist to YAML (source of truth)
230
+ this.knowledgeStore.addItem(item);
231
+
232
+ // Embed and upsert to Qdrant via the KnowledgeSearch's backing store
233
+ // The KnowledgeSearch class does not expose hydrateItem directly — we
234
+ // go through the embedder + vectorStore path manually.
235
+ try {
236
+ const text = `${title} ${observation}`;
237
+ const vector = await this.embedder(text);
238
+
239
+ // Access the backing QdrantVectorStore from KnowledgeSearch
240
+ const store = (this.knowledgeSearch as unknown as {
241
+ vectorStore?: { upsertKnowledge?: (id: string, vector: number[], payload: Record<string, unknown>) => Promise<void> };
242
+ }).vectorStore;
243
+
244
+ if (store?.upsertKnowledge) {
245
+ await store.upsertKnowledge(id, vector, {
246
+ id,
247
+ repo_id: repoId,
248
+ category,
249
+ title,
250
+ content: observation,
251
+ stack_tags: [],
252
+ confidence: 0.6,
253
+ source: 'agent',
254
+ source_phase: phase ?? null,
255
+ source_agent: agent ?? null,
256
+ sharing: 'private',
257
+ created_at: now,
258
+ updated_at: now,
259
+ accessed_at: now,
260
+ access_count: 0,
261
+ });
262
+ logger.debug('PatternExtractor: item upserted to Qdrant', { id });
263
+ } else {
264
+ logger.debug('PatternExtractor: Qdrant upsert unavailable, YAML only', { id });
265
+ }
266
+ } catch (err) {
267
+ // Non-fatal: YAML write succeeded, Qdrant will be synced on next hydration
268
+ logger.warn('PatternExtractor: Qdrant upsert failed (YAML write succeeded)', {
269
+ id,
270
+ error: String(err),
271
+ });
272
+ }
273
+
274
+ logger.info('PatternExtractor: knowledge item promoted', {
275
+ id,
276
+ category,
277
+ title,
278
+ repoId,
279
+ });
280
+
281
+ return item;
282
+ }
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Private helpers
286
+ // ---------------------------------------------------------------------------
287
+
288
+ /**
289
+ * Check whether the observation text is semantically too close to an
290
+ * existing knowledge item (similarity > 0.92). If so, we skip promotion
291
+ * to avoid building up near-duplicate entries over many projects.
292
+ */
293
+ private async isDuplicateKnowledge(
294
+ observation: string,
295
+ category: KnowledgeCategory,
296
+ ): Promise<boolean> {
297
+ try {
298
+ const results = await this.knowledgeSearch.search(observation, {
299
+ category,
300
+ limit: 1,
301
+ min_confidence: 0.0,
302
+ });
303
+
304
+ return results.length > 0 && results[0]!.relevance_score > 0.92;
305
+ } catch {
306
+ // If search fails, assume not duplicate so we don't lose knowledge
307
+ return false;
308
+ }
309
+ }
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Internal utilities
314
+ // ---------------------------------------------------------------------------
315
+
316
+ function buildObservationText(step: TrajectoryStepRow): string {
317
+ const parts: string[] = [step.action];
318
+ if (step.result) parts.push(step.result);
319
+ return parts.join('\n').trim();
320
+ }
321
+
322
+ function classifyCategory(step: TrajectoryStepRow): KnowledgeCategory {
323
+ const text = `${step.action} ${step.result ?? ''}`.toLowerCase();
324
+ const gotchaScore = GOTCHA_KEYWORDS.filter((kw) => text.includes(kw)).length;
325
+ const patternScore = PATTERN_KEYWORDS.filter((kw) => text.includes(kw)).length;
326
+
327
+ if (gotchaScore > patternScore) return 'gotcha';
328
+ if (patternScore > gotchaScore) return 'pattern';
329
+
330
+ // If quality score is high, lean toward "pattern" (positive finding)
331
+ if (step.quality_score !== null && step.quality_score >= QUALITY_THRESHOLD) {
332
+ return 'pattern';
333
+ }
334
+
335
+ return 'gotcha';
336
+ }
337
+
338
+ function categoryPrefix(category: KnowledgeCategory): string {
339
+ switch (category) {
340
+ case 'gotcha': return 'gotcha';
341
+ case 'pattern': return 'pattern';
342
+ case 'decision': return 'decision';
343
+ case 'convention': return 'convention';
344
+ }
345
+ }
346
+
347
+ function deriveTitle(observation: string, category: KnowledgeCategory): string {
348
+ // Use the first sentence (up to 80 chars) as the title
349
+ const firstSentence = observation.split(/[.!?\n]/)[0]?.trim() ?? observation;
350
+ const truncated = firstSentence.length > 80
351
+ ? firstSentence.slice(0, 77) + '...'
352
+ : firstSentence;
353
+
354
+ // Prefix with category for clarity when browsing YAML files
355
+ const prefix = category.charAt(0).toUpperCase() + category.slice(1);
356
+ return `${prefix}: ${truncated}`;
357
+ }
358
+
359
+ // Re-export keyword arrays so callers (e.g., tests) can reason about classification
360
+ export { GOTCHA_KEYWORDS, PATTERN_KEYWORDS, QUALITY_THRESHOLD };
@@ -0,0 +1,268 @@
1
+ // TrajectoryRecorder — B10
2
+ //
3
+ // Server-side automatic trajectory recording. Agents do NOT call this directly
4
+ // — the pipeline tool handlers call startTrajectory() and recordStep() as a
5
+ // side effect of processing forge.start_* and forge.submit_* tool calls.
6
+ //
7
+ // Storage: SQLite `trajectories` and `trajectory_steps` tables.
8
+ // All operations are synchronous (better-sqlite3). No async needed.
9
+ //
10
+ // Design notes:
11
+ // - startTrajectory() is safe to call multiple times for the same
12
+ // (projectId, phase, agent) tuple — it creates a fresh trajectory each
13
+ // time, returning the new ID. Old active trajectories for the same project
14
+ // are left in the DB as historical records.
15
+ // - recordStep() silently returns if `trajectoryId` is not found. This
16
+ // prevents a missing trajectory from crashing a tool handler.
17
+ // - completeTrajectory() and failTrajectory() are idempotent — they only
18
+ // update rows whose status is 'active'.
19
+
20
+ import { randomUUID } from 'node:crypto';
21
+ import type { PipelineDB } from '../storage/sqlite.js';
22
+ import type {
23
+ Trajectory,
24
+ TrajectoryRow,
25
+ TrajectoryStep,
26
+ TrajectoryStepRow,
27
+ TrajectoryStepMetadata,
28
+ PipelinePhase,
29
+ } from '../util/types.js';
30
+ import { logger } from '../util/logger.js';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Row deserializers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function rowToTrajectory(row: TrajectoryRow): Trajectory {
37
+ return {
38
+ id: row.id,
39
+ projectId: row.project_id,
40
+ phase: row.phase as PipelinePhase,
41
+ agent: row.agent,
42
+ status: row.status,
43
+ startedAt: row.started_at,
44
+ completedAt: row.completed_at ?? null,
45
+ success: row.success === null ? null : row.success === 1,
46
+ feedback: row.feedback ?? null,
47
+ };
48
+ }
49
+
50
+ function rowToStep(row: TrajectoryStepRow): TrajectoryStep {
51
+ let metadata: TrajectoryStepMetadata = {};
52
+ try {
53
+ metadata = JSON.parse(row.metadata) as TrajectoryStepMetadata;
54
+ } catch {
55
+ metadata = {};
56
+ }
57
+ return {
58
+ id: row.id,
59
+ trajectoryId: row.trajectory_id,
60
+ action: row.action,
61
+ result: row.result ?? null,
62
+ qualityScore: row.quality_score ?? null,
63
+ metadata,
64
+ createdAt: row.created_at,
65
+ };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // TrajectoryRecorder
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export class TrajectoryRecorder {
73
+ constructor(private readonly db: PipelineDB) {}
74
+
75
+ /**
76
+ * Start a new trajectory for the given project / phase / agent.
77
+ * Returns the UUID of the newly created trajectory.
78
+ *
79
+ * Side-effect: any previously active trajectory for this project is NOT
80
+ * auto-closed — they are left as historical data. The caller is responsible
81
+ * for calling completeTrajectory() or failTrajectory() when appropriate.
82
+ */
83
+ startTrajectory(projectId: string, phase: PipelinePhase, agent: string): string {
84
+ const id = randomUUID();
85
+ const now = Date.now();
86
+
87
+ this.db.run(
88
+ `INSERT INTO trajectories (id, project_id, phase, agent, status, started_at, completed_at, success, feedback)
89
+ VALUES (?, ?, ?, ?, 'active', ?, NULL, NULL, NULL)`,
90
+ [id, projectId, phase, agent, now],
91
+ );
92
+
93
+ logger.debug('TrajectoryRecorder: trajectory started', {
94
+ id,
95
+ projectId,
96
+ phase,
97
+ agent,
98
+ });
99
+
100
+ return id;
101
+ }
102
+
103
+ /**
104
+ * Record a step in an existing trajectory.
105
+ * Silently returns when the trajectoryId does not exist in the DB — this
106
+ * prevents missing trajectories from surfacing as user-facing errors.
107
+ */
108
+ recordStep(
109
+ trajectoryId: string,
110
+ action: string,
111
+ result?: string,
112
+ qualityScore?: number,
113
+ metadata?: Record<string, unknown>,
114
+ ): void {
115
+ const now = Date.now();
116
+
117
+ try {
118
+ this.db.run(
119
+ `INSERT INTO trajectory_steps (trajectory_id, action, result, quality_score, metadata, created_at)
120
+ VALUES (?, ?, ?, ?, ?, ?)`,
121
+ [
122
+ trajectoryId,
123
+ action,
124
+ result ?? null,
125
+ qualityScore ?? null,
126
+ JSON.stringify(metadata ?? {}),
127
+ now,
128
+ ],
129
+ );
130
+
131
+ logger.debug('TrajectoryRecorder: step recorded', {
132
+ trajectoryId,
133
+ action: action.slice(0, 80),
134
+ qualityScore,
135
+ });
136
+ } catch (err) {
137
+ // Non-fatal — step recording must never crash the tool handler
138
+ logger.warn('TrajectoryRecorder: failed to record step (non-fatal)', {
139
+ trajectoryId,
140
+ error: String(err),
141
+ });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Mark a trajectory as successfully completed.
147
+ * Idempotent — only updates rows with status='active'.
148
+ */
149
+ completeTrajectory(trajectoryId: string, feedback?: string): void {
150
+ this.db.run(
151
+ `UPDATE trajectories
152
+ SET status = 'complete', completed_at = ?, success = 1, feedback = ?
153
+ WHERE id = ? AND status = 'active'`,
154
+ [Date.now(), feedback ?? null, trajectoryId],
155
+ );
156
+ logger.debug('TrajectoryRecorder: trajectory completed', { trajectoryId });
157
+ }
158
+
159
+ /**
160
+ * Mark a trajectory as failed.
161
+ * Idempotent — only updates rows with status='active'.
162
+ */
163
+ failTrajectory(trajectoryId: string, feedback?: string): void {
164
+ this.db.run(
165
+ `UPDATE trajectories
166
+ SET status = 'failed', completed_at = ?, success = 0, feedback = ?
167
+ WHERE id = ? AND status = 'active'`,
168
+ [Date.now(), feedback ?? null, trajectoryId],
169
+ );
170
+ logger.debug('TrajectoryRecorder: trajectory failed', { trajectoryId });
171
+ }
172
+
173
+ /**
174
+ * Return the most recent active trajectory for a project, or null.
175
+ * "Active" means status = 'active'.
176
+ */
177
+ getActiveTrajectory(projectId: string): Trajectory | null {
178
+ const row = this.db.get<TrajectoryRow>(
179
+ `SELECT * FROM trajectories
180
+ WHERE project_id = ? AND status = 'active'
181
+ ORDER BY started_at DESC LIMIT 1`,
182
+ [projectId],
183
+ );
184
+ return row ? rowToTrajectory(row) : null;
185
+ }
186
+
187
+ /**
188
+ * Return all trajectories for a project, ordered by started_at ascending.
189
+ */
190
+ getTrajectories(projectId: string): Trajectory[] {
191
+ const rows = this.db.all<TrajectoryRow>(
192
+ `SELECT * FROM trajectories WHERE project_id = ? ORDER BY started_at ASC`,
193
+ [projectId],
194
+ );
195
+ return rows.map(rowToTrajectory);
196
+ }
197
+
198
+ /**
199
+ * Return all steps for a trajectory, ordered by created_at ascending.
200
+ */
201
+ getSteps(trajectoryId: string): TrajectoryStep[] {
202
+ const rows = this.db.all<TrajectoryStepRow>(
203
+ `SELECT * FROM trajectory_steps WHERE trajectory_id = ? ORDER BY created_at ASC`,
204
+ [trajectoryId],
205
+ );
206
+ return rows.map(rowToStep);
207
+ }
208
+
209
+ /**
210
+ * Produce a summary of all trajectory activity for a project.
211
+ * Used by the knowledge_collection phase to feed the PatternExtractor.
212
+ */
213
+ getSummary(projectId: string): {
214
+ totalSteps: number;
215
+ phasesCompleted: number;
216
+ cycles: Record<string, number>;
217
+ durationMinutes: number;
218
+ } {
219
+ const trajectories = this.getTrajectories(projectId);
220
+
221
+ if (trajectories.length === 0) {
222
+ return {
223
+ totalSteps: 0,
224
+ phasesCompleted: 0,
225
+ cycles: {},
226
+ durationMinutes: 0,
227
+ };
228
+ }
229
+
230
+ // Count total steps across all trajectories
231
+ const totalStepsRow = this.db.get<{ cnt: number }>(
232
+ `SELECT COUNT(*) as cnt FROM trajectory_steps ts
233
+ JOIN trajectories t ON ts.trajectory_id = t.id
234
+ WHERE t.project_id = ?`,
235
+ [projectId],
236
+ );
237
+ const totalSteps = totalStepsRow?.cnt ?? 0;
238
+
239
+ // Count completed phases
240
+ const phasesCompleted = trajectories.filter(
241
+ (t) => t.status === 'complete' && t.success === true,
242
+ ).length;
243
+
244
+ // Count cycles: phases that appear more than once in completed trajectories
245
+ const phaseCompletions = trajectories
246
+ .filter((t) => t.status === 'complete')
247
+ .reduce<Record<string, number>>((acc, t) => {
248
+ acc[t.phase] = (acc[t.phase] ?? 0) + 1;
249
+ return acc;
250
+ }, {});
251
+
252
+ const cycles: Record<string, number> = {};
253
+ for (const [phase, count] of Object.entries(phaseCompletions)) {
254
+ if (count > 1) {
255
+ cycles[phase] = count - 1; // cycles = extra runs beyond the first
256
+ }
257
+ }
258
+
259
+ // Duration: from first startedAt to last completedAt (or now if still active)
260
+ const firstStart = Math.min(...trajectories.map((t) => t.startedAt));
261
+ const lastEnd = Math.max(
262
+ ...trajectories.map((t) => t.completedAt ?? Date.now()),
263
+ );
264
+ const durationMinutes = Math.round((lastEnd - firstStart) / 60_000);
265
+
266
+ return { totalSteps, phasesCompleted, cycles, durationMinutes };
267
+ }
268
+ }
File without changes