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,246 @@
1
+ // KnowledgeHydrator — syncs knowledge from YAML files into Qdrant's knowledge collection.
2
+ //
3
+ // The YAML files in .forge/knowledge/ are the source of truth. Qdrant is the
4
+ // search index. The hydrator reads all items from the YAML store, embeds them,
5
+ // and upserts into Qdrant. On re-hydration (after a manifest hash change) it
6
+ // first deletes all Qdrant points for the repo to handle removals cleanly.
7
+ //
8
+ // Design notes:
9
+ // - embedBatch() is used for bulk hydration instead of embedText() per item
10
+ // to amortise model inference overhead across all items.
11
+ // - If Qdrant is unavailable the method logs a warning and returns 0 — the
12
+ // caller (RepoRegistry) treats this as a non-fatal result and records the
13
+ // item count as 0 in the registration response.
14
+ // - The class accepts `embedder` as a constructor parameter (a reference to
15
+ // the embedBatch function) so tests can substitute a deterministic fake
16
+ // without dynamic import mocking.
17
+
18
+ import { logger } from '../util/logger.js';
19
+ import type { KnowledgeYamlStore } from './store.js';
20
+ import type { QdrantVectorStore } from '../storage/qdrant-store.js';
21
+ import type { KnowledgeItem, KnowledgePayload } from '../util/types.js';
22
+ import { embedBatch, embedText } from '../ingestion/embedder.js';
23
+
24
+ export class KnowledgeHydrator {
25
+ constructor(
26
+ private readonly yamlStore: KnowledgeYamlStore,
27
+ private readonly vectorStore: QdrantVectorStore,
28
+ // Accept embedder as a parameter for testability. Defaults to the real embedBatch.
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ private readonly embedder: typeof embedText = embedText,
31
+ ) {}
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Public API
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Hydrate the Qdrant knowledge collection from all YAML files for a given
39
+ * repo. Deletes any existing points for this repo first (handles removals),
40
+ * then upserts all current items.
41
+ *
42
+ * Returns the count of items successfully loaded.
43
+ * Returns 0 and logs a warning if Qdrant is unavailable.
44
+ */
45
+ async hydrate(repoId: string, sharing: string): Promise<number> {
46
+ const items = this.yamlStore.readAll();
47
+
48
+ if (items.length === 0) {
49
+ logger.debug('KnowledgeHydrator: no items to hydrate', { repoId });
50
+ return 0;
51
+ }
52
+
53
+ // Check Qdrant availability before attempting any operations.
54
+ const qdrantHealthy = await this.vectorStore.isHealthy();
55
+ if (!qdrantHealthy) {
56
+ logger.warn('KnowledgeHydrator: Qdrant unavailable, skipping hydration', { repoId });
57
+ return 0;
58
+ }
59
+
60
+ try {
61
+ // Remove all existing Qdrant points for this repo so that deletions
62
+ // in the YAML are reflected — a simple upsert-only approach would leave
63
+ // orphaned points for removed items.
64
+ await this.removeRepo(repoId);
65
+
66
+ // Embed all items in one batch call to amortise model init cost.
67
+ const texts = items.map((item) => `${item.title} ${item.content}`);
68
+ let vectors: number[][];
69
+
70
+ // embedBatch returns parallel arrays; embedText (the embedder param)
71
+ // is a single-text function. When the injected embedder is embedText
72
+ // we upgrade to batch mode internally. This gives tests a simple stub
73
+ // interface while production uses the efficient batch path.
74
+ if (this.embedder === embedText) {
75
+ vectors = await embedBatch(texts);
76
+ } else {
77
+ // Fallback: call the injected embedder once per item. Used in tests.
78
+ vectors = await Promise.all(texts.map((t) => this.embedder(t)));
79
+ }
80
+
81
+ // Upsert each item with its vector and full KnowledgePayload.
82
+ const now = Date.now();
83
+ let loadedCount = 0;
84
+
85
+ for (let i = 0; i < items.length; i++) {
86
+ const item = items[i]!;
87
+ const vector = vectors[i]!;
88
+
89
+ const payload: KnowledgePayload = {
90
+ id: item.id,
91
+ repo_id: repoId,
92
+ category: this._categoryFromId(item.id),
93
+ title: item.title,
94
+ content: item.content,
95
+ stack_tags: item.stack_tags,
96
+ confidence: item.confidence,
97
+ source: item.source,
98
+ source_phase: item.source_phase,
99
+ source_agent: item.source_agent,
100
+ sharing,
101
+ created_at: item.created_at,
102
+ updated_at: item.updated_at,
103
+ accessed_at: now,
104
+ access_count: 0,
105
+ };
106
+
107
+ await this.vectorStore.upsertKnowledge(
108
+ item.id,
109
+ vector,
110
+ payload as unknown as Record<string, unknown>,
111
+ );
112
+ loadedCount++;
113
+ }
114
+
115
+ logger.info('KnowledgeHydrator: hydration complete', {
116
+ repoId,
117
+ itemCount: loadedCount,
118
+ sharing,
119
+ });
120
+
121
+ return loadedCount;
122
+ } catch (err) {
123
+ logger.warn('KnowledgeHydrator: hydration failed', {
124
+ repoId,
125
+ error: String(err),
126
+ });
127
+ return 0;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Hydrate a single knowledge item into Qdrant.
133
+ * Used for incremental updates (e.g. after a single item is added via the
134
+ * knowledge-keeper agent) without re-hydrating the entire corpus.
135
+ */
136
+ async hydrateItem(item: KnowledgeItem, repoId: string, sharing: string): Promise<void> {
137
+ const qdrantHealthy = await this.vectorStore.isHealthy();
138
+ if (!qdrantHealthy) {
139
+ logger.warn('KnowledgeHydrator.hydrateItem: Qdrant unavailable', { id: item.id, repoId });
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const text = `${item.title} ${item.content}`;
145
+ const vector = await this.embedder(text);
146
+
147
+ const now = Date.now();
148
+ const payload: KnowledgePayload = {
149
+ id: item.id,
150
+ repo_id: repoId,
151
+ category: this._categoryFromId(item.id),
152
+ title: item.title,
153
+ content: item.content,
154
+ stack_tags: item.stack_tags,
155
+ confidence: item.confidence,
156
+ source: item.source,
157
+ source_phase: item.source_phase,
158
+ source_agent: item.source_agent,
159
+ sharing,
160
+ created_at: item.created_at,
161
+ updated_at: item.updated_at,
162
+ accessed_at: now,
163
+ access_count: 0,
164
+ };
165
+
166
+ await this.vectorStore.upsertKnowledge(
167
+ item.id,
168
+ vector,
169
+ payload as unknown as Record<string, unknown>,
170
+ );
171
+
172
+ logger.debug('KnowledgeHydrator.hydrateItem: item hydrated', {
173
+ id: item.id,
174
+ repoId,
175
+ });
176
+ } catch (err) {
177
+ logger.warn('KnowledgeHydrator.hydrateItem: failed', {
178
+ id: item.id,
179
+ repoId,
180
+ error: String(err),
181
+ });
182
+ throw err;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Remove all knowledge items for a repo from Qdrant.
188
+ * Called by hydrate() before re-upserting, and by RepoRegistry.removeRepo().
189
+ * Silently succeeds if Qdrant is unavailable.
190
+ */
191
+ async removeRepo(repoId: string): Promise<void> {
192
+ try {
193
+ // QdrantVectorStore.deleteKnowledge deletes by point ID.
194
+ // To delete ALL points for a repo we use the scroll+delete pattern via
195
+ // the Qdrant client's filter-based delete which the QdrantVectorStore
196
+ // exposes as deleteKnowledgeByRepo.
197
+ //
198
+ // The QdrantVectorStore may not have a deleteKnowledgeByRepo method yet.
199
+ // We call it if present; otherwise we fall through to the no-op with a
200
+ // log so the caller knows why old points were not removed.
201
+ const store = this.vectorStore as QdrantVectorStore & {
202
+ deleteKnowledgeByRepo?: (repoId: string) => Promise<void>;
203
+ };
204
+
205
+ if (typeof store.deleteKnowledgeByRepo === 'function') {
206
+ await store.deleteKnowledgeByRepo(repoId);
207
+ logger.debug('KnowledgeHydrator.removeRepo: deleted existing points', { repoId });
208
+ } else {
209
+ // Fallback: the store does not yet expose bulk-delete-by-repo for the
210
+ // knowledge collection. Log so the operator is aware that stale items
211
+ // may accumulate. This is non-fatal — upserts will overwrite matching
212
+ // IDs and only truly removed items will remain as orphans.
213
+ logger.debug(
214
+ 'KnowledgeHydrator.removeRepo: deleteKnowledgeByRepo not available, ' +
215
+ 'orphaned points may remain for removed items. Upserts will overwrite matching IDs.',
216
+ { repoId },
217
+ );
218
+ }
219
+ } catch (err) {
220
+ // Graceful degradation: if Qdrant is down or the delete fails, hydration
221
+ // should still proceed with upserts — better to have stale items than
222
+ // to completely block registration.
223
+ logger.warn('KnowledgeHydrator.removeRepo: delete failed (continuing)', {
224
+ repoId,
225
+ error: String(err),
226
+ });
227
+ }
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Private helpers
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /**
235
+ * Derive category string from item id prefix.
236
+ * Mirrors the logic in KnowledgeYamlStore._categoryFromItem().
237
+ */
238
+ private _categoryFromId(id: string): string {
239
+ const lower = id.toLowerCase();
240
+ if (lower.startsWith('gotcha')) return 'gotcha';
241
+ if (lower.startsWith('pattern')) return 'pattern';
242
+ if (lower.startsWith('decision')) return 'decision';
243
+ if (lower.startsWith('convention')) return 'convention';
244
+ return 'pattern';
245
+ }
246
+ }
@@ -0,0 +1,463 @@
1
+ // RepoRegistry — manages repo registration, manifest reading, and git detection.
2
+ //
3
+ // Registration flow:
4
+ // 1. Resolve the repo path (absolute)
5
+ // 2. Read .forge/manifest.yaml — required; if absent, returns an error result
6
+ // 3. Resolve git remote origin URL (best-effort; local-only repos work too)
7
+ // 4. Compute a stable repo ID: SHA-256(remote URL) first 12 hex chars, or
8
+ // SHA-256(absolute path) if no remote
9
+ // 5. Upsert the repo row in SQLite
10
+ // 6. Check if knowledge YAML content hash changed (compare manifest_hash)
11
+ // 7. If changed (or newly registered), trigger KnowledgeHydrator to re-sync
12
+ // all knowledge items into Qdrant's knowledge collection
13
+ // 8. Return registration result
14
+ //
15
+ // The registry operates standalone (no pipeline project required). Used by
16
+ // both the MCP registration tools and the CLI `forge register` command.
17
+
18
+ import { createHash } from 'node:crypto';
19
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
20
+ import { join, resolve, basename } from 'node:path';
21
+ import { execFileSync } from 'node:child_process';
22
+ import { parse, stringify } from 'yaml';
23
+ import type { PipelineDB } from '../storage/sqlite.js';
24
+ import type { QdrantVectorStore } from '../storage/qdrant-store.js';
25
+ import { KnowledgeYamlStore } from './store.js';
26
+ import { KnowledgeHydrator } from './hydrator.js';
27
+ import type { Repo, RepoRow, ForgeManifest, SharingMode } from '../util/types.js';
28
+ import { logger } from '../util/logger.js';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Constants
32
+ // ---------------------------------------------------------------------------
33
+
34
+ const FORGE_DIR_NAME = '.forge';
35
+ const MANIFEST_FILE = 'manifest.yaml';
36
+ const KNOWLEDGE_CATEGORY_FILES = [
37
+ 'gotchas.yaml',
38
+ 'patterns.yaml',
39
+ 'decisions.yaml',
40
+ 'conventions.yaml',
41
+ ] as const;
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Public interfaces
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export interface RegisterOptions {
48
+ auto?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Matches the spec B5 return shape (snake_case for MCP tool compat).
53
+ */
54
+ export interface RegisterResult {
55
+ repo_id: string;
56
+ name: string;
57
+ path: string;
58
+ remote?: string;
59
+ stack: string[];
60
+ sharing: string;
61
+ newly_registered: boolean;
62
+ knowledge_items_loaded: number;
63
+ }
64
+
65
+ export interface InitOptions {
66
+ name: string;
67
+ stack: string[];
68
+ sharing?: SharingMode;
69
+ org?: string;
70
+ }
71
+
72
+ // Backward-compat camelCase result (used internally)
73
+ export interface RegisterResultCamel {
74
+ repoId: string;
75
+ name: string;
76
+ path: string;
77
+ remote?: string;
78
+ stack: string[];
79
+ sharing: SharingMode;
80
+ newlyRegistered: boolean;
81
+ knowledgeItemsLoaded: number;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // RepoRegistry
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export class RepoRegistry {
89
+ constructor(
90
+ private readonly db: PipelineDB,
91
+ private readonly hydrator?: KnowledgeHydrator,
92
+ ) {}
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // register — primary entry point (spec B5 signature)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Register (or update) a repo at `path`.
100
+ *
101
+ * Requires a .forge/manifest.yaml to exist at <path>/.forge/manifest.yaml.
102
+ * If the manifest is absent, throws an error describing the fix.
103
+ *
104
+ * Steps:
105
+ * 1. Read manifest from .forge/manifest.yaml
106
+ * 2. Resolve git origin URL
107
+ * 3. Generate repo_id from origin URL (or path hash if no remote)
108
+ * 4. Upsert into SQLite repos table
109
+ * 5. Compute knowledge content hash and compare with stored hash
110
+ * 6. If hash changed (or newly registered), hydrate Qdrant knowledge collection
111
+ * 7. Return RegisterResult
112
+ */
113
+ async register(
114
+ path: string,
115
+ options?: RegisterOptions,
116
+ ): Promise<RegisterResult> {
117
+ const repoPath = resolve(path);
118
+ const forgePath = join(repoPath, FORGE_DIR_NAME);
119
+ const manifestPath = join(forgePath, MANIFEST_FILE);
120
+
121
+ // Manifest is required. Return a clear error if absent.
122
+ if (!existsSync(manifestPath)) {
123
+ throw new Error(
124
+ `No .forge/manifest.yaml found at ${forgePath}. ` +
125
+ `Run \`forge init\` in the repo first, or call initForge() to create the directory structure.`,
126
+ );
127
+ }
128
+
129
+ const manifest = this.readManifest(forgePath);
130
+ if (!manifest) {
131
+ throw new Error(
132
+ `Failed to parse .forge/manifest.yaml at ${forgePath}. ` +
133
+ `Check the file for syntax errors.`,
134
+ );
135
+ }
136
+
137
+ // Resolve git remote and generate repo ID.
138
+ const remote = this.resolveGitOrigin(repoPath);
139
+ const repoId = manifest.repo_id_override
140
+ ? manifest.repo_id_override
141
+ : this.generateRepoId(remote ?? repoPath);
142
+
143
+ const name = manifest.name;
144
+ const stack = manifest.stack;
145
+ const sharing = manifest.sharing;
146
+ const org = manifest.org ?? null;
147
+
148
+ const now = Date.now();
149
+
150
+ // Compute knowledge content hash for change detection.
151
+ const yamlStore = new KnowledgeYamlStore(forgePath);
152
+ const knowledgeHash = yamlStore.getContentHash();
153
+
154
+ // Check existing row.
155
+ const existing = this.db.get<RepoRow>(`SELECT * FROM repos WHERE id = ?`, [repoId]);
156
+ const newlyRegistered = !existing;
157
+ const knowledgeChanged = !existing || existing.manifest_hash !== knowledgeHash;
158
+
159
+ if (existing) {
160
+ this.db.run(
161
+ `UPDATE repos
162
+ SET name = ?, path = ?, remote = ?, stack = ?, sharing = ?,
163
+ org = ?, last_seen_at = ?, manifest_hash = ?
164
+ WHERE id = ?`,
165
+ [
166
+ name, repoPath, remote ?? null, JSON.stringify(stack),
167
+ sharing, org, now, knowledgeHash, repoId,
168
+ ],
169
+ );
170
+ logger.info('RepoRegistry.register: repo updated', { repoId, name });
171
+ } else {
172
+ this.db.run(
173
+ `INSERT INTO repos
174
+ (id, name, path, remote, stack, sharing, org, registered_at, last_seen_at, manifest_hash)
175
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
176
+ [
177
+ repoId, name, repoPath, remote ?? null, JSON.stringify(stack),
178
+ sharing, org, now, now, knowledgeHash,
179
+ ],
180
+ );
181
+ logger.info('RepoRegistry.register: repo registered', { repoId, name });
182
+ }
183
+
184
+ // Hydrate Qdrant knowledge collection if content changed.
185
+ let knowledgeItemsLoaded = 0;
186
+ if (knowledgeChanged && this.hydrator) {
187
+ try {
188
+ knowledgeItemsLoaded = await this.hydrator.hydrate(repoId, sharing);
189
+ logger.info('RepoRegistry.register: knowledge hydrated', {
190
+ repoId,
191
+ itemCount: knowledgeItemsLoaded,
192
+ });
193
+ } catch (err) {
194
+ // Hydration failure is non-fatal — registration proceeds.
195
+ logger.warn('RepoRegistry.register: knowledge hydration failed (non-fatal)', {
196
+ repoId,
197
+ error: String(err),
198
+ });
199
+ }
200
+ } else if (!this.hydrator) {
201
+ logger.debug('RepoRegistry.register: no hydrator configured, skipping knowledge sync', {
202
+ repoId,
203
+ });
204
+ }
205
+
206
+ return {
207
+ repo_id: repoId,
208
+ name,
209
+ path: repoPath,
210
+ remote: remote ?? undefined,
211
+ stack,
212
+ sharing,
213
+ newly_registered: newlyRegistered,
214
+ knowledge_items_loaded: knowledgeItemsLoaded,
215
+ };
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // initForge — create .forge/ directory structure (spec B5 method)
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * Initialize the .forge/ directory in a repo at `repoPath`.
224
+ * Creates:
225
+ * .forge/manifest.yaml — from provided manifest
226
+ * .forge/knowledge/gotchas.yaml
227
+ * .forge/knowledge/patterns.yaml
228
+ * .forge/knowledge/decisions.yaml
229
+ * .forge/knowledge/conventions.yaml
230
+ * .forge/.gitignore — excludes .forge.db
231
+ *
232
+ * All files are created only if they do not already exist.
233
+ * Returns { forgePath, filesCreated } listing the paths that were created.
234
+ */
235
+ initForge(
236
+ repoPath: string,
237
+ manifest: ForgeManifest,
238
+ ): { forgePath: string; filesCreated: string[] } {
239
+ const absRepoPath = resolve(repoPath);
240
+ const forgePath = join(absRepoPath, FORGE_DIR_NAME);
241
+ const knowledgeDir = join(forgePath, 'knowledge');
242
+ const filesCreated: string[] = [];
243
+
244
+ // Create .forge/ directory
245
+ if (!existsSync(forgePath)) {
246
+ mkdirSync(forgePath, { recursive: true });
247
+ filesCreated.push(forgePath);
248
+ }
249
+
250
+ // Create .forge/knowledge/ subdirectory
251
+ if (!existsSync(knowledgeDir)) {
252
+ mkdirSync(knowledgeDir, { recursive: true });
253
+ filesCreated.push(knowledgeDir);
254
+ }
255
+
256
+ // Write manifest.yaml
257
+ const manifestPath = join(forgePath, MANIFEST_FILE);
258
+ if (!existsSync(manifestPath)) {
259
+ const yamlContent = stringify(manifest, { indent: 2 });
260
+ writeFileSync(manifestPath, yamlContent, 'utf8');
261
+ filesCreated.push(manifestPath);
262
+ }
263
+
264
+ // Create empty YAML category files
265
+ for (const file of KNOWLEDGE_CATEGORY_FILES) {
266
+ const filePath = join(knowledgeDir, file);
267
+ if (!existsSync(filePath)) {
268
+ writeFileSync(filePath, '[]\n', 'utf8');
269
+ filesCreated.push(filePath);
270
+ }
271
+ }
272
+
273
+ // Create .gitignore to exclude local SQLite database
274
+ const gitignorePath = join(forgePath, '.gitignore');
275
+ if (!existsSync(gitignorePath)) {
276
+ writeFileSync(gitignorePath, '.forge.db\n', 'utf8');
277
+ filesCreated.push(gitignorePath);
278
+ }
279
+
280
+ logger.info('RepoRegistry.initForge: .forge/ directory initialized', {
281
+ forgePath,
282
+ filesCreated: filesCreated.length,
283
+ });
284
+
285
+ return { forgePath, filesCreated };
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Manifest reading (spec B5 signature: takes forgePath, not repoPath)
290
+ // ---------------------------------------------------------------------------
291
+
292
+ /**
293
+ * Read and parse .forge/manifest.yaml from `forgePath` (the .forge/ directory).
294
+ * Returns null if the file is absent or unparseable.
295
+ *
296
+ * NOTE: This method takes the .forge/ directory path, NOT the repo root.
297
+ * Callers that have the repo root should pass join(repoPath, '.forge').
298
+ */
299
+ readManifest(forgePath: string): ForgeManifest | null {
300
+ const manifestPath = join(forgePath, MANIFEST_FILE);
301
+ if (!existsSync(manifestPath)) return null;
302
+ return this._parseManifestFile(manifestPath);
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Git origin resolution (public — spec B5)
307
+ // ---------------------------------------------------------------------------
308
+
309
+ /**
310
+ * Resolve the git remote origin URL for the repo at `repoPath`.
311
+ * Uses `git -C <repoPath> config --get remote.origin.url`.
312
+ * Returns null if the path is not a git repo or has no origin.
313
+ */
314
+ resolveGitOrigin(repoPath: string): string | null {
315
+ try {
316
+ const output = execFileSync(
317
+ 'git',
318
+ ['-C', repoPath, 'config', '--get', 'remote.origin.url'],
319
+ {
320
+ encoding: 'utf-8',
321
+ stdio: ['ignore', 'pipe', 'ignore'],
322
+ timeout: 3000,
323
+ },
324
+ ).trim();
325
+ return output || null;
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+
331
+ // ---------------------------------------------------------------------------
332
+ // Repo ID generation (public — spec B5)
333
+ // ---------------------------------------------------------------------------
334
+
335
+ /**
336
+ * Generate a stable repo ID from a remote URL or local path.
337
+ * Uses SHA-256, returns first 12 hex characters.
338
+ *
339
+ * For remote URLs: normalises to strip .git suffix and trailing slashes
340
+ * before hashing so that git@github.com:org/repo and
341
+ * https://github.com/org/repo.git produce the same ID.
342
+ */
343
+ generateRepoId(remoteOrPath: string): string {
344
+ const normalised = remoteOrPath
345
+ .trim()
346
+ .replace(/\.git$/, '')
347
+ .replace(/\/$/, '')
348
+ .toLowerCase();
349
+ return createHash('sha256').update(normalised).digest('hex').slice(0, 12);
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Query helpers
354
+ // ---------------------------------------------------------------------------
355
+
356
+ /**
357
+ * Get a registered repo by ID.
358
+ * Returns null if not found.
359
+ */
360
+ getRepo(repoId: string): Repo | null {
361
+ const row = this.db.get<RepoRow>(`SELECT * FROM repos WHERE id = ?`, [repoId]);
362
+ return row ? this._rowToRepo(row) : null;
363
+ }
364
+
365
+ /**
366
+ * List all registered repos sorted by last_seen_at descending.
367
+ * Returns repos augmented with knowledge_count (total items in Qdrant).
368
+ * knowledge_count is always 0 when Qdrant is not available — it is a
369
+ * best-effort annotation, not a blocking query.
370
+ */
371
+ listRepos(): Array<Repo & { knowledge_count: number }> {
372
+ const rows = this.db.all<RepoRow>(`SELECT * FROM repos ORDER BY last_seen_at DESC`);
373
+ return rows.map((row) => ({
374
+ ...this._rowToRepo(row),
375
+ // knowledge_count: Qdrant query is async; this is a synchronous method.
376
+ // Return 0 — callers that need counts should call vectorStore.getKnowledgeCount()
377
+ // separately or use async listReposWithCounts() if added later.
378
+ knowledge_count: 0,
379
+ }));
380
+ }
381
+
382
+ /**
383
+ * Update the last_seen_at timestamp for a repo.
384
+ * Called on session-start to record that the repo is actively being used.
385
+ */
386
+ touch(repoId: string): void {
387
+ this.db.run(
388
+ `UPDATE repos SET last_seen_at = ? WHERE id = ?`,
389
+ [Date.now(), repoId],
390
+ );
391
+ }
392
+
393
+ /**
394
+ * Remove a repo from the registry.
395
+ * Does NOT remove Qdrant knowledge items — call hydrator.removeRepo() separately
396
+ * if you want to clean up vector data.
397
+ */
398
+ removeRepo(repoId: string): void {
399
+ this.db.run(`DELETE FROM repos WHERE id = ?`, [repoId]);
400
+ logger.info('RepoRegistry.removeRepo: repo removed from SQLite', { repoId });
401
+ }
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Backward-compat helpers (camelCase API — kept for existing callers)
405
+ // ---------------------------------------------------------------------------
406
+
407
+ /** Whether a .forge/ directory exists in the given repo path. */
408
+ forgeDirectoryExists(repoPath?: string): boolean {
409
+ const targetPath = resolve(repoPath ?? process.cwd());
410
+ return existsSync(join(targetPath, FORGE_DIR_NAME));
411
+ }
412
+
413
+ /** Find a registered repo by its filesystem path. */
414
+ findRepoByPath(repoPath: string): Repo | null {
415
+ const absPath = resolve(repoPath);
416
+ const row = this.db.get<RepoRow>(`SELECT * FROM repos WHERE path = ?`, [absPath]);
417
+ return row ? this._rowToRepo(row) : null;
418
+ }
419
+
420
+ // ---------------------------------------------------------------------------
421
+ // Private helpers
422
+ // ---------------------------------------------------------------------------
423
+
424
+ private _parseManifestFile(manifestPath: string): ForgeManifest | null {
425
+ try {
426
+ const content = readFileSync(manifestPath, 'utf8');
427
+ const parsed = parse(content) as ForgeManifest;
428
+ return parsed ?? null;
429
+ } catch (err) {
430
+ logger.warn('RepoRegistry._parseManifestFile: failed to read manifest', {
431
+ path: manifestPath,
432
+ error: String(err),
433
+ });
434
+ return null;
435
+ }
436
+ }
437
+
438
+ private _repoNameFromPath(repoPath: string): string {
439
+ // basename handles both forward and backslash separators via path.basename
440
+ return basename(repoPath) || 'unknown';
441
+ }
442
+
443
+ private _rowToRepo(row: RepoRow): Repo {
444
+ let stack: string[];
445
+ try {
446
+ stack = JSON.parse(row.stack) as string[];
447
+ } catch {
448
+ stack = [];
449
+ }
450
+ return {
451
+ id: row.id,
452
+ name: row.name,
453
+ path: row.path,
454
+ remote: row.remote,
455
+ stack,
456
+ sharing: row.sharing,
457
+ org: row.org,
458
+ registeredAt: row.registered_at,
459
+ lastSeenAt: row.last_seen_at,
460
+ manifestHash: row.manifest_hash,
461
+ };
462
+ }
463
+ }