@documentation-robotics/cli 0.2.0 → 0.4.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 (335) hide show
  1. package/README.md +46 -16
  2. package/dist/analysis/relationship-classifier.js +2 -9
  3. package/dist/analysis/relationship-classifier.js.map +2 -2
  4. package/dist/analyzers/active-analyzer.js +62 -0
  5. package/dist/analyzers/active-analyzer.js.map +7 -0
  6. package/dist/analyzers/call-graph-utils.js +2 -2
  7. package/dist/analyzers/call-graph-utils.js.map +2 -2
  8. package/dist/analyzers/cards/build.js +191 -0
  9. package/dist/analyzers/cards/build.js.map +7 -0
  10. package/dist/analyzers/cards/classify.js +42 -0
  11. package/dist/analyzers/cards/classify.js.map +7 -0
  12. package/dist/analyzers/cards/edge-index.js +44 -0
  13. package/dist/analyzers/cards/edge-index.js.map +7 -0
  14. package/dist/analyzers/cards/extract.js +138 -0
  15. package/dist/analyzers/cards/extract.js.map +7 -0
  16. package/dist/analyzers/cards/facts.js +46 -0
  17. package/dist/analyzers/cards/facts.js.map +7 -0
  18. package/dist/analyzers/cards/graph.js +42 -0
  19. package/dist/analyzers/cards/graph.js.map +7 -0
  20. package/dist/analyzers/cards/index.js +8 -0
  21. package/dist/analyzers/cards/index.js.map +7 -0
  22. package/dist/analyzers/cards/routes.js +96 -0
  23. package/dist/analyzers/cards/routes.js.map +7 -0
  24. package/dist/analyzers/cards/tier0.js +18 -0
  25. package/dist/analyzers/cards/tier0.js.map +7 -0
  26. package/dist/analyzers/cards/types.js +1 -0
  27. package/dist/analyzers/cards/types.js.map +7 -0
  28. package/dist/analyzers/cbm-analyzer.js +270 -174
  29. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  30. package/dist/analyzers/cbm-responses.js +342 -0
  31. package/dist/analyzers/cbm-responses.js.map +7 -0
  32. package/dist/analyzers/code-changes.js +159 -0
  33. package/dist/analyzers/code-changes.js.map +7 -0
  34. package/dist/analyzers/edge-relationships.js +49 -0
  35. package/dist/analyzers/edge-relationships.js.map +7 -0
  36. package/dist/analyzers/extraction-plan.js +541 -0
  37. package/dist/analyzers/extraction-plan.js.map +7 -0
  38. package/dist/analyzers/index-job.js +229 -0
  39. package/dist/analyzers/index-job.js.map +7 -0
  40. package/dist/analyzers/mapping-loader.js +9 -0
  41. package/dist/analyzers/mapping-loader.js.map +2 -2
  42. package/dist/analyzers/tier0-rules.js +153 -0
  43. package/dist/analyzers/tier0-rules.js.map +7 -0
  44. package/dist/analyzers/types.js.map +1 -1
  45. package/dist/audit/relationships/ai/prompts.js +10 -10
  46. package/dist/audit/relationships/ai/prompts.js.map +2 -2
  47. package/dist/audit/relationships/analysis/layer-templates.js +52 -52
  48. package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
  49. package/dist/cli.js +23 -11
  50. package/dist/cli.js.map +2 -2
  51. package/dist/coding-agents/default-permissions.js +31 -1
  52. package/dist/coding-agents/default-permissions.js.map +2 -2
  53. package/dist/coding-agents/drbot-mcp.js +269 -0
  54. package/dist/coding-agents/drbot-mcp.js.map +7 -0
  55. package/dist/commands/add.js +7 -4
  56. package/dist/commands/add.js.map +2 -2
  57. package/dist/commands/analyzer.js +143 -40
  58. package/dist/commands/analyzer.js.map +2 -2
  59. package/dist/commands/changeset-verify.js +63 -0
  60. package/dist/commands/changeset-verify.js.map +7 -0
  61. package/dist/commands/changeset.js +80 -29
  62. package/dist/commands/changeset.js.map +2 -2
  63. package/dist/commands/delete.js +10 -4
  64. package/dist/commands/delete.js.map +2 -2
  65. package/dist/commands/farm.js +176 -60
  66. package/dist/commands/farm.js.map +2 -2
  67. package/dist/commands/init.js +9 -26
  68. package/dist/commands/init.js.map +2 -2
  69. package/dist/commands/mcp.js +40 -21
  70. package/dist/commands/mcp.js.map +3 -3
  71. package/dist/commands/relationship.js +208 -37
  72. package/dist/commands/relationship.js.map +2 -2
  73. package/dist/commands/reports.js +3 -3
  74. package/dist/commands/reports.js.map +2 -2
  75. package/dist/commands/update.js +52 -32
  76. package/dist/commands/update.js.map +2 -2
  77. package/dist/commands/upgrade.js +99 -103
  78. package/dist/commands/upgrade.js.map +3 -3
  79. package/dist/commands/validate.js +19 -3
  80. package/dist/commands/validate.js.map +2 -2
  81. package/dist/commands/version.js +1 -1
  82. package/dist/commands/visualize.js +34 -15
  83. package/dist/commands/visualize.js.map +2 -2
  84. package/dist/core/annotation-store.js +15 -5
  85. package/dist/core/annotation-store.js.map +2 -2
  86. package/dist/core/catalog-migration.js +34 -0
  87. package/dist/core/catalog-migration.js.map +7 -0
  88. package/dist/core/changeset-analysis.js +421 -0
  89. package/dist/core/changeset-analysis.js.map +7 -0
  90. package/dist/core/changeset-base-index.js +160 -0
  91. package/dist/core/changeset-base-index.js.map +7 -0
  92. package/dist/core/changeset-brief-compact.js +256 -0
  93. package/dist/core/changeset-brief-compact.js.map +7 -0
  94. package/dist/core/changeset-brief-delta.js +102 -0
  95. package/dist/core/changeset-brief-delta.js.map +7 -0
  96. package/dist/core/changeset-graph.js +281 -0
  97. package/dist/core/changeset-graph.js.map +7 -0
  98. package/dist/core/changeset-only-guard.js +32 -0
  99. package/dist/core/changeset-only-guard.js.map +7 -0
  100. package/dist/core/changeset-summary.js +68 -0
  101. package/dist/core/changeset-summary.js.map +7 -0
  102. package/dist/core/changeset-validator.js +66 -20
  103. package/dist/core/changeset-validator.js.map +2 -2
  104. package/dist/core/changeset-verify.js +848 -0
  105. package/dist/core/changeset-verify.js.map +7 -0
  106. package/dist/core/changeset.js +56 -9
  107. package/dist/core/changeset.js.map +2 -2
  108. package/dist/core/element-type-migration.js +145 -0
  109. package/dist/core/element-type-migration.js.map +7 -0
  110. package/dist/core/extraction-observability.js +161 -0
  111. package/dist/core/extraction-observability.js.map +7 -0
  112. package/dist/core/farm-manifest.js +12 -6
  113. package/dist/core/farm-manifest.js.map +2 -2
  114. package/dist/core/farm-sync-engine.js +1 -1
  115. package/dist/core/farm-sync-engine.js.map +2 -2
  116. package/dist/core/farm-sync-state.js +10 -2
  117. package/dist/core/farm-sync-state.js.map +2 -2
  118. package/dist/core/migration-registry.js +88 -47
  119. package/dist/core/migration-registry.js.map +2 -2
  120. package/dist/core/model-upgrade.js +102 -0
  121. package/dist/core/model-upgrade.js.map +7 -0
  122. package/dist/core/model.js +82 -30
  123. package/dist/core/model.js.map +2 -2
  124. package/dist/core/mutation-handler.js +71 -6
  125. package/dist/core/mutation-handler.js.map +2 -2
  126. package/dist/core/project-init.js +53 -0
  127. package/dist/core/project-init.js.map +7 -0
  128. package/dist/core/relationship-migration-table.js +6639 -0
  129. package/dist/core/relationship-migration-table.js.map +7 -0
  130. package/dist/core/relationship-migration.js +272 -0
  131. package/dist/core/relationship-migration.js.map +7 -0
  132. package/dist/core/relationship-prune.js +49 -0
  133. package/dist/core/relationship-prune.js.map +7 -0
  134. package/dist/core/relationship-staging.js +161 -0
  135. package/dist/core/relationship-staging.js.map +7 -0
  136. package/dist/core/relationships.js +26 -4
  137. package/dist/core/relationships.js.map +2 -2
  138. package/dist/core/report-data-model.js +7 -13
  139. package/dist/core/report-data-model.js.map +2 -2
  140. package/dist/core/staged-changeset-storage.js +145 -18
  141. package/dist/core/staged-changeset-storage.js.map +2 -2
  142. package/dist/core/staged-source-reference.js +28 -0
  143. package/dist/core/staged-source-reference.js.map +7 -0
  144. package/dist/core/staging-area.js +166 -24
  145. package/dist/core/staging-area.js.map +2 -2
  146. package/dist/core/upgrade-plan.js +117 -0
  147. package/dist/core/upgrade-plan.js.map +7 -0
  148. package/dist/core/virtual-projection.js +13 -5
  149. package/dist/core/virtual-projection.js.map +2 -2
  150. package/dist/export/archimate-exporter.js +1 -0
  151. package/dist/export/archimate-exporter.js.map +2 -2
  152. package/dist/generated/compiled-validators.js +2 -2
  153. package/dist/generated/compiled-validators.js.map +2 -2
  154. package/dist/generated/layer-registry.js +16 -16
  155. package/dist/generated/layer-registry.js.map +2 -2
  156. package/dist/generated/node-types.js +186 -168
  157. package/dist/generated/node-types.js.map +2 -2
  158. package/dist/generated/relationship-index.js +2468 -7913
  159. package/dist/generated/relationship-index.js.map +2 -2
  160. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  161. package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
  162. package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
  163. package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
  164. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  165. package/dist/integrations/claude_code/commands/dr-map.md +20 -1
  166. package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
  167. package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
  168. package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
  169. package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
  170. package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
  171. package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
  172. package/dist/integrations/claude_code/docs/README.md +68 -2
  173. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  174. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  175. package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
  176. package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
  177. package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
  178. package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
  179. package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
  180. package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
  181. package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
  182. package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
  183. package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
  184. package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
  185. package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
  186. package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
  187. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
  188. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
  189. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
  190. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
  191. package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
  192. package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
  193. package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
  194. package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
  195. package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
  196. package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
  197. package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
  198. package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
  199. package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
  200. package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
  201. package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
  202. package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
  203. package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
  204. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
  205. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
  206. package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
  207. package/dist/integrations/hash-utils.js.map +2 -2
  208. package/dist/mcp/api-key-manager.js +14 -1
  209. package/dist/mcp/api-key-manager.js.map +2 -2
  210. package/dist/mcp/changeset-only.js +45 -0
  211. package/dist/mcp/changeset-only.js.map +7 -0
  212. package/dist/mcp/project-root.js +85 -0
  213. package/dist/mcp/project-root.js.map +7 -0
  214. package/dist/mcp/server-instructions.js +27 -0
  215. package/dist/mcp/server-instructions.js.map +7 -0
  216. package/dist/mcp/strict-schema.js +52 -0
  217. package/dist/mcp/strict-schema.js.map +7 -0
  218. package/dist/mcp/tool-registry.js +55 -5
  219. package/dist/mcp/tool-registry.js.map +2 -2
  220. package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
  221. package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
  222. package/dist/mcp/tools/changeset-commit.js +224 -0
  223. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  224. package/dist/mcp/tools/changeset-editing.js +260 -0
  225. package/dist/mcp/tools/changeset-editing.js.map +7 -0
  226. package/dist/mcp/tools/changeset-lifecycle.js +83 -0
  227. package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
  228. package/dist/mcp/tools/changeset-shared.js +31 -0
  229. package/dist/mcp/tools/changeset-shared.js.map +7 -0
  230. package/dist/mcp/tools/changeset-show.js +17 -1
  231. package/dist/mcp/tools/changeset-show.js.map +2 -2
  232. package/dist/mcp/tools/changeset-verify.js +49 -0
  233. package/dist/mcp/tools/changeset-verify.js.map +7 -0
  234. package/dist/mcp/tools/codebase-extract.js +836 -0
  235. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  236. package/dist/mcp/tools/codebase-index.js +125 -0
  237. package/dist/mcp/tools/codebase-index.js.map +7 -0
  238. package/dist/mcp/tools/codebase-shared.js +265 -0
  239. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  240. package/dist/mcp/tools/codebase-verify.js +48 -0
  241. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  242. package/dist/mcp/tools/index.js +34 -1
  243. package/dist/mcp/tools/index.js.map +2 -2
  244. package/dist/mcp/tools/model-add.js +191 -74
  245. package/dist/mcp/tools/model-add.js.map +2 -2
  246. package/dist/mcp/tools/model-delete.js +2 -2
  247. package/dist/mcp/tools/model-delete.js.map +2 -2
  248. package/dist/mcp/tools/model-search.js +14 -0
  249. package/dist/mcp/tools/model-search.js.map +2 -2
  250. package/dist/mcp/tools/model-show.js +29 -2
  251. package/dist/mcp/tools/model-show.js.map +2 -2
  252. package/dist/mcp/tools/model-trace.js +93 -30
  253. package/dist/mcp/tools/model-trace.js.map +2 -2
  254. package/dist/mcp/tools/model-update.js +46 -26
  255. package/dist/mcp/tools/model-update.js.map +2 -2
  256. package/dist/mcp/tools/model-validate.js +46 -4
  257. package/dist/mcp/tools/model-validate.js.map +2 -2
  258. package/dist/mcp/tools/project-status-collectors.js +381 -0
  259. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  260. package/dist/mcp/tools/project-status.js +237 -0
  261. package/dist/mcp/tools/project-status.js.map +7 -0
  262. package/dist/mcp/tools/project-tools.js +505 -0
  263. package/dist/mcp/tools/project-tools.js.map +7 -0
  264. package/dist/mcp/tools/relationship-tools.js +167 -0
  265. package/dist/mcp/tools/relationship-tools.js.map +7 -0
  266. package/dist/mcp/tools/review-open.js +210 -0
  267. package/dist/mcp/tools/review-open.js.map +7 -0
  268. package/dist/mcp/tools/shared.js +35 -4
  269. package/dist/mcp/tools/shared.js.map +2 -2
  270. package/dist/mcp/tools/spec-type.js +90 -0
  271. package/dist/mcp/tools/spec-type.js.map +7 -0
  272. package/dist/reports/model-report-orchestrator.js +31 -2
  273. package/dist/reports/model-report-orchestrator.js.map +2 -2
  274. package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
  275. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  276. package/dist/schemas/bundled/api.json +1029 -1658
  277. package/dist/schemas/bundled/apm.json +499 -734
  278. package/dist/schemas/bundled/application.json +735 -371
  279. package/dist/schemas/bundled/base.json +3 -220
  280. package/dist/schemas/bundled/business.json +452 -366
  281. package/dist/schemas/bundled/data-model.json +395 -411
  282. package/dist/schemas/bundled/data-store.json +334 -558
  283. package/dist/schemas/bundled/manifest.json +18 -18
  284. package/dist/schemas/bundled/motivation.json +349 -368
  285. package/dist/schemas/bundled/navigation.json +415 -731
  286. package/dist/schemas/bundled/product.json +325 -159
  287. package/dist/schemas/bundled/security.json +864 -1232
  288. package/dist/schemas/bundled/technology.json +574 -703
  289. package/dist/schemas/bundled/testing.json +865 -1646
  290. package/dist/schemas/bundled/ux.json +739 -1352
  291. package/dist/server/schemas.js +8 -0
  292. package/dist/server/schemas.js.map +2 -2
  293. package/dist/server/server.js +686 -231
  294. package/dist/server/server.js.map +3 -3
  295. package/dist/server/viewer-registry.js +264 -0
  296. package/dist/server/viewer-registry.js.map +7 -0
  297. package/dist/server-entry.js +2 -1
  298. package/dist/server-entry.js.map +2 -2
  299. package/dist/telemetry/index.js +1 -1
  300. package/dist/utils/ansi-suppressor.js +6 -8
  301. package/dist/utils/ansi-suppressor.js.map +2 -2
  302. package/dist/utils/attribute-patch.js +41 -0
  303. package/dist/utils/attribute-patch.js.map +7 -0
  304. package/dist/utils/error-hints.js +29 -0
  305. package/dist/utils/error-hints.js.map +7 -0
  306. package/dist/utils/errors.js +9 -3
  307. package/dist/utils/errors.js.map +2 -2
  308. package/dist/utils/id-generator.js +5 -2
  309. package/dist/utils/id-generator.js.map +2 -2
  310. package/dist/utils/project-paths.js +26 -1
  311. package/dist/utils/project-paths.js.map +2 -2
  312. package/dist/utils/source-reference.js +64 -31
  313. package/dist/utils/source-reference.js.map +2 -2
  314. package/dist/utils/spec-version.js +2 -2
  315. package/dist/utils/spec-version.js.map +1 -1
  316. package/dist/validators/composed-reference-validator.js +5 -27
  317. package/dist/validators/composed-reference-validator.js.map +2 -2
  318. package/dist/validators/model-warnings.js +28 -0
  319. package/dist/validators/model-warnings.js.map +7 -0
  320. package/dist/validators/reference-validator.js +0 -7
  321. package/dist/validators/reference-validator.js.map +2 -2
  322. package/dist/validators/relationship-schema-validator.js +3 -3
  323. package/dist/validators/relationship-schema-validator.js.map +2 -2
  324. package/dist/validators/schema-validator.js +27 -9
  325. package/dist/validators/schema-validator.js.map +2 -2
  326. package/dist/validators/types.js.map +1 -1
  327. package/dist/viewer/README.md +6 -6
  328. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  329. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  330. package/dist/viewer/index.html +2 -2
  331. package/dist/viewer/manifest.json +10 -10
  332. package/dist/viewer/version.json +1 -1
  333. package/package.json +2 -1
  334. package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
  335. package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
@@ -1,25 +1,30 @@
1
1
  import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
2
2
  import { cors } from "hono/cors";
3
3
  import { swaggerUI } from "@hono/swagger-ui";
4
- import { serve } from "@hono/node-server";
4
+ import { createAdaptorServer } from "@hono/node-server";
5
5
  import { createNodeWebSocket } from "@hono/node-ws";
6
6
  import { spawn, spawnSync } from "child_process";
7
7
  import chokidar from "chokidar";
8
- import { randomBytes } from "crypto";
8
+ import { randomBytes, randomUUID } from "crypto";
9
+ import { existsSync, realpathSync } from "fs";
10
+ import { homedir } from "os";
9
11
  import { createRequire } from "module";
10
12
  import { fileURLToPath } from "url";
11
13
  import path from "path";
12
14
  const require2 = createRequire(import.meta.url);
13
15
  import { Model } from "../core/model.js";
14
16
  import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
17
+ import { StagingAreaManager } from "../core/staging-area.js";
15
18
  import { CANONICAL_LAYER_NAMES } from "../core/layers.js";
19
+ import { AnnotationStore } from "../core/annotation-store.js";
16
20
  import { telemetryMiddleware } from "./telemetry-middleware.js";
17
21
  import { ClaudeCodeClient } from "../coding-agents/claude-code-client.js";
18
22
  import { CopilotClient } from "../coding-agents/copilot-client.js";
19
23
  import { detectAvailableClients, selectChatClient } from "../coding-agents/chat-utils.js";
20
- import { formatForClaudeCode, applyCopilotPermissions } from "../coding-agents/default-permissions.js";
24
+ import { formatForClaudeCode, formatForClaudeCodeWithChangesets, applyCopilotPermissions, CHANGESET_ONLY_ENV } from "../coding-agents/default-permissions.js";
25
+ import { DR_ARCHITECT_AGENT, drBotMcpPrompt, drMcpAllowedTools, prepareDrMcpLaunch, prepareDrBotAgent } from "../coding-agents/drbot-mcp.js";
21
26
  import { getErrorMessage } from "../utils/errors.js";
22
- import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
27
+ import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ChangesetActionResultSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
23
28
  const JSONRPC_ERRORS = {
24
29
  // Standard JSON-RPC errors
25
30
  INVALID_PARAMS: -32602,
@@ -32,21 +37,37 @@ const JSONRPC_ERRORS = {
32
37
  NO_CLIENT_AVAILABLE: -32001
33
38
  // No chat client available
34
39
  };
40
+ function annotationFingerprint(annotation) {
41
+ return {
42
+ elementId: annotation.elementId,
43
+ state: JSON.stringify([annotation.content, annotation.tags, annotation.resolved, annotation.updatedAt ?? null]),
44
+ replyIds: annotation.replies.map((reply) => reply.id)
45
+ };
46
+ }
47
+ function toolResultText(content) {
48
+ return typeof content == "string" ? content : Array.isArray(content) ? content.map((part) => part && typeof part == "object" && typeof part.text == "string" ? part.text : "").filter(Boolean).join(`
49
+ `) : "";
50
+ }
35
51
  class VisualizationServer {
36
52
  app;
37
53
  model;
38
- _server;
39
- // Node HTTP server returned by @hono/node-server
54
+ _servers = [];
55
+ // One per loopback address the viewer listens on
40
56
  upgradeWebSocket;
41
57
  injectWebSocket;
42
58
  clients = /* @__PURE__ */ new Set();
43
59
  watcher;
44
- annotations = /* @__PURE__ */ new Map();
45
- // annotationId -> annotation
46
- annotationsByElement = /* @__PURE__ */ new Map();
47
- // elementId -> Set<annotationId>
48
- replies = /* @__PURE__ */ new Map();
49
- // annotationId -> replies[]
60
+ /**
61
+ * Annotations live on disk (documentation-robotics/annotations/), shared with the MCP
62
+ * annotation_* tools, which run in other processes.
63
+ */
64
+ annotationStore;
65
+ /** What this server last saw of each annotation, to tell which changes to broadcast. */
66
+ annotationSnapshot = /* @__PURE__ */ new Map();
67
+ /** Serializes annotation writes and snapshot syncs, so a sync never sees half of a write. */
68
+ annotationQueue = Promise.resolve();
69
+ /** Pending debounced annotation sync from the file watcher. */
70
+ annotationsBroadcastTimer;
50
71
  changesets = /* @__PURE__ */ new Map();
51
72
  // changesetId -> changeset
52
73
  changesetStorage;
@@ -54,17 +75,43 @@ class VisualizationServer {
54
75
  authEnabled = !0;
55
76
  withDanger = !1;
56
77
  // Danger mode disabled by default
78
+ withChangesets = !1;
79
+ // Changeset-only write mode disabled by default
57
80
  viewerPath;
58
81
  // Optional custom viewer path
59
82
  activeChatProcesses = /* @__PURE__ */ new Map();
60
83
  // conversationId -> spawned chat process
61
84
  chatConversationCounter = 0;
85
+ /**
86
+ * Viewer conversation id -> Claude Code session id. The first send in a conversation starts the
87
+ * session with --session-id; `started` is set once Claude Code has created it, and later sends
88
+ * pass --resume. A new conversation id starts a new session.
89
+ */
90
+ chatSessions = /* @__PURE__ */ new Map();
91
+ /**
92
+ * Conversation id -> the running turn's token. A conversation runs one turn at a time: it is
93
+ * reserved when chat.send arrives and released only after that turn's process has exited.
94
+ */
95
+ busyChatConversations = /* @__PURE__ */ new Map();
96
+ /** Chat processes stopped by chat.cancel: nothing more from them reaches the client. */
97
+ cancelledChatProcesses = /* @__PURE__ */ new WeakSet();
98
+ /** Test hook: Claude Code's config directory (default ~/.claude or CLAUDE_CONFIG_DIR). */
99
+ _testClaudeConfigDir;
100
+ /** Pending debounced `changesets.updated` broadcast from the file watcher. */
101
+ changesetsBroadcastTimer;
102
+ /** Debounce for changeset-directory events: one write is several file events. */
103
+ changesetsDebounceMs = 150;
62
104
  selectedChatClient;
63
105
  // Selected chat client for server
64
106
  chatInitializationError;
65
107
  // Store initialization error for status endpoint
66
108
  _testClaudeCmdOverride;
67
109
  _testCopilotCmdOverride;
110
+ /** Test hook: prepare DrBot's MCP launch with these (scratch dirs) even under _testClaudeCmdOverride. */
111
+ _testDrMcpOptions;
112
+ /** Test hook: the claude arguments the last chat launch built, before any _testClaudeCmdOverride. */
113
+ _testLastClaudeChatArgs;
114
+ drBotWarnings = /* @__PURE__ */ new Set();
68
115
  /**
69
116
  * Valid spec node IDs per layer
70
117
  * Dynamically loaded from compiled spec (spec/dist/{layer}.json at module load time)
@@ -116,7 +163,7 @@ class VisualizationServer {
116
163
  }), ids;
117
164
  })();
118
165
  constructor(model, options) {
119
- this.app = new OpenAPIHono(), this.model = model, this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
166
+ this.app = new OpenAPIHono(), this.model = model, this.annotationStore = new AnnotationStore(model.rootPath), this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.withChangesets = options?.withChangesets || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
120
167
  if (!this.authEnabled)
121
168
  return next();
122
169
  const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
@@ -161,21 +208,79 @@ class VisualizationServer {
161
208
  * Performs partial updates with atomic operations
162
209
  */
163
210
  async updateAnnotation(annotationId, body) {
164
- const annotation = this.annotations.get(annotationId);
165
- if (!annotation)
166
- return null;
167
- const updatedAnnotation = {
168
- ...annotation,
169
- content: body.content ?? annotation.content,
170
- tags: body.tags ?? annotation.tags,
171
- resolved: body.resolved ?? annotation.resolved,
172
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
173
- };
174
- return this.annotations.set(annotationId, updatedAnnotation), await this.broadcastMessage({
211
+ const { updated, messages } = await this.withAnnotations(async () => {
212
+ const updated2 = await this.annotationStore.update(annotationId, body);
213
+ return { updated: updated2, messages: updated2 ? this.diffAnnotation(updated2) : [] };
214
+ });
215
+ return updated ? (await this.broadcastAll(messages), updated) : null;
216
+ }
217
+ /** Broadcasts each message, in order. */
218
+ async broadcastAll(messages) {
219
+ for (const message of messages)
220
+ await this.broadcastMessage(message);
221
+ }
222
+ /** Runs an annotation write or sync after the ones queued before it. */
223
+ withAnnotations(fn) {
224
+ const run = this.annotationQueue.then(fn, fn);
225
+ return this.annotationQueue = run.catch(() => {
226
+ }), run;
227
+ }
228
+ /**
229
+ * Records an annotation as seen and returns a broadcast for each difference from what this
230
+ * server last saw of it: the change a route just made, and any change another process made
231
+ * that the watcher has not synced yet (so recording it as seen doesn't swallow that change).
232
+ * Call it inside withAnnotations.
233
+ */
234
+ diffAnnotation(annotation) {
235
+ const before = this.annotationSnapshot.get(annotation.id), after = annotationFingerprint(annotation);
236
+ if (this.annotationSnapshot.set(annotation.id, after), !before)
237
+ return [
238
+ {
239
+ type: "annotation.added",
240
+ annotationId: annotation.id,
241
+ elementId: annotation.elementId,
242
+ timestamp: annotation.createdAt
243
+ }
244
+ ];
245
+ const out = [];
246
+ before.state !== after.state && out.push({
175
247
  type: "annotation.updated",
176
- annotationId: updatedAnnotation.id,
177
- timestamp: updatedAnnotation.updatedAt
178
- }), updatedAnnotation;
248
+ annotationId: annotation.id,
249
+ timestamp: annotation.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
250
+ });
251
+ for (const reply of annotation.replies)
252
+ before.replyIds.includes(reply.id) || out.push({
253
+ type: "annotation.reply.added",
254
+ annotationId: annotation.id,
255
+ replyId: reply.id,
256
+ timestamp: reply.createdAt
257
+ });
258
+ return out;
259
+ }
260
+ /**
261
+ * Compares the annotation files with what this server last saw, and broadcasts each change
262
+ * another process made (the MCP annotation tools, a hand edit) with the same messages the
263
+ * REST routes send: annotation.added, annotation.updated, annotation.reply.added and
264
+ * annotation.deleted.
265
+ */
266
+ async syncAnnotations(options) {
267
+ const messages = await this.withAnnotations(async () => {
268
+ const current = await this.annotationStore.list(), now = (/* @__PURE__ */ new Date()).toISOString(), out = [], seen = /* @__PURE__ */ new Set();
269
+ for (const annotation of current)
270
+ seen.add(annotation.id), out.push(...this.diffAnnotation(annotation));
271
+ for (const id of [...this.annotationSnapshot.keys()])
272
+ seen.has(id) || (this.annotationSnapshot.delete(id), out.push({ type: "annotation.deleted", annotationId: id, timestamp: now }));
273
+ return out;
274
+ });
275
+ options.broadcast && await this.broadcastAll(messages);
276
+ }
277
+ /** Syncs the annotations once their files have been quiet for a moment. */
278
+ scheduleAnnotationsSync() {
279
+ this.annotationsBroadcastTimer && clearTimeout(this.annotationsBroadcastTimer), this.annotationsBroadcastTimer = setTimeout(() => {
280
+ this.annotationsBroadcastTimer = void 0, this.syncAnnotations({ broadcast: !0 }).catch((error) => {
281
+ console.error(`[Watcher] Failed to broadcast annotations: ${getErrorMessage(error)}`);
282
+ });
283
+ }, this.changesetsDebounceMs);
179
284
  }
180
285
  /**
181
286
  * Setup HTTP routes and WebSocket handler
@@ -368,14 +473,12 @@ class VisualizationServer {
368
473
  }
369
474
  }
370
475
  });
371
- this.app.openapi(getAnnotationsRoute, (c) => {
372
- const elementId = c.req.valid("query").elementId;
373
- if (elementId) {
374
- const annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
375
- return c.json({ annotations });
376
- } else {
377
- const annotations = Array.from(this.annotations.values());
476
+ this.app.openapi(getAnnotationsRoute, async (c) => {
477
+ try {
478
+ const query = c.req.valid("query"), annotations = await this.annotationStore.list(query.elementId);
378
479
  return c.json({ annotations });
480
+ } catch (error) {
481
+ return c.json({ error: getErrorMessage(error) }, 500);
379
482
  }
380
483
  });
381
484
  const createAnnotationRoute = createRoute({
@@ -432,24 +535,18 @@ class VisualizationServer {
432
535
  this.app.openapi(createAnnotationRoute, async (c) => {
433
536
  try {
434
537
  const body = c.req.valid("json");
435
- if (!await this.findElement(body.elementId))
538
+ if (!await this.elementExists(body.elementId))
436
539
  return c.json({ error: "Element not found" }, 404);
437
- const annotation = {
438
- id: this.generateAnnotationId(),
439
- elementId: body.elementId,
440
- author: body.author,
441
- content: body.content,
442
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
443
- tags: body.tags,
444
- resolved: !1
445
- // Default to unresolved
446
- };
447
- return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(annotation.elementId) || this.annotationsByElement.set(annotation.elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(annotation.elementId).add(annotation.id), await this.broadcastMessage({
448
- type: "annotation.added",
449
- annotationId: annotation.id,
450
- elementId: annotation.elementId,
451
- timestamp: annotation.createdAt
452
- }), c.json(annotation, 201);
540
+ const { annotation, messages } = await this.withAnnotations(async () => {
541
+ const created = await this.annotationStore.create({
542
+ elementId: body.elementId,
543
+ author: body.author,
544
+ content: body.content,
545
+ tags: body.tags
546
+ });
547
+ return { annotation: created, messages: this.diffAnnotation(created) };
548
+ });
549
+ return await this.broadcastAll(messages), c.json(annotation, 201);
453
550
  } catch (error) {
454
551
  const message = getErrorMessage(error);
455
552
  return c.json({ error: message }, 500);
@@ -494,7 +591,7 @@ class VisualizationServer {
494
591
  });
495
592
  this.app.openapi(getAnnotationRoute, async (c) => {
496
593
  try {
497
- const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
594
+ const { annotationId } = c.req.valid("param"), annotation = await this.annotationStore.get(annotationId);
498
595
  return annotation ? c.json(annotation) : c.json({ error: "Annotation not found" }, 404);
499
596
  } catch (error) {
500
597
  const message = getErrorMessage(error);
@@ -641,16 +738,15 @@ class VisualizationServer {
641
738
  });
642
739
  this.app.openapi(deleteAnnotationRoute, async (c) => {
643
740
  try {
644
- const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
645
- if (!annotation)
646
- return c.json({ error: "Annotation not found" }, 404);
647
- this.annotations.delete(annotationId), this.replies.delete(annotationId);
648
- const elementAnnotations = this.annotationsByElement.get(annotation.elementId);
649
- return elementAnnotations && elementAnnotations.delete(annotationId), await this.broadcastMessage({
741
+ const { annotationId } = c.req.valid("param");
742
+ return await this.withAnnotations(async () => {
743
+ const existed = await this.annotationStore.delete(annotationId);
744
+ return this.annotationSnapshot.delete(annotationId), existed;
745
+ }) ? (await this.broadcastMessage({
650
746
  type: "annotation.deleted",
651
747
  annotationId,
652
748
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
653
- }), c.body(null, 204);
749
+ }), c.body(null, 204)) : c.json({ error: "Annotation not found" }, 404);
654
750
  } catch (error) {
655
751
  const message = getErrorMessage(error);
656
752
  return c.json({ error: message }, 500);
@@ -685,12 +781,13 @@ class VisualizationServer {
685
781
  }
686
782
  }
687
783
  });
688
- this.app.openapi(getAnnotationRepliesRoute, (c) => {
689
- const { annotationId } = c.req.valid("param");
690
- if (!this.annotations.get(annotationId))
691
- return c.json({ error: "Annotation not found" }, 404);
692
- const replies = this.replies.get(annotationId) || [];
693
- return c.json({ replies });
784
+ this.app.openapi(getAnnotationRepliesRoute, async (c) => {
785
+ try {
786
+ const { annotationId } = c.req.valid("param"), annotation = await this.annotationStore.get(annotationId);
787
+ return annotation ? c.json({ replies: annotation.replies }) : c.json({ error: "Annotation not found" }, 404);
788
+ } catch (error) {
789
+ return c.json({ error: getErrorMessage(error) }, 500);
790
+ }
694
791
  });
695
792
  const createAnnotationReplyRoute = createRoute({
696
793
  method: "post",
@@ -738,21 +835,22 @@ class VisualizationServer {
738
835
  });
739
836
  this.app.openapi(createAnnotationReplyRoute, async (c) => {
740
837
  try {
741
- const { annotationId } = c.req.valid("param");
742
- if (!this.annotations.get(annotationId))
743
- return c.json({ error: "Annotation not found" }, 404);
744
- const body = c.req.valid("json"), reply = {
745
- id: this.generateReplyId(),
746
- author: body.author,
747
- content: body.content,
748
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
749
- };
750
- return this.replies.has(annotationId) || this.replies.set(annotationId, []), this.replies.get(annotationId).push(reply), await this.broadcastMessage({
751
- type: "annotation.reply.added",
752
- annotationId,
753
- replyId: reply.id,
754
- timestamp: reply.createdAt
755
- }), c.json(reply, 201);
838
+ const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), { reply, messages } = await this.withAnnotations(async () => {
839
+ const added = await this.annotationStore.addReply(annotationId, {
840
+ author: body.author,
841
+ content: body.content
842
+ });
843
+ if (!added)
844
+ return { reply: null, messages: [] };
845
+ const annotation = await this.annotationStore.get(annotationId), own = {
846
+ type: "annotation.reply.added",
847
+ annotationId,
848
+ replyId: added.id,
849
+ timestamp: added.createdAt
850
+ };
851
+ return { reply: added, messages: annotation ? this.diffAnnotation(annotation) : [own] };
852
+ });
853
+ return reply ? (await this.broadcastAll(messages), c.json(reply, 201)) : c.json({ error: "Annotation not found" }, 404);
756
854
  } catch (error) {
757
855
  const message = getErrorMessage(error);
758
856
  return c.json({ error: message }, 500);
@@ -776,7 +874,8 @@ class VisualizationServer {
776
874
  }
777
875
  }
778
876
  });
779
- this.app.openapi(getChangesetsRoute, (c) => {
877
+ this.app.openapi(getChangesetsRoute, async (c) => {
878
+ await this.loadChangesetsFromDisk();
780
879
  const changesets = {};
781
880
  for (const [id, changeset] of this.changesets)
782
881
  changesets[id] = {
@@ -819,8 +918,10 @@ class VisualizationServer {
819
918
  }
820
919
  }
821
920
  });
822
- this.app.openapi(getChangesetRoute, (c) => {
823
- const { changesetId } = c.req.valid("param"), changeset = this.changesets.get(changesetId);
921
+ this.app.openapi(getChangesetRoute, async (c) => {
922
+ const { changesetId } = c.req.valid("param");
923
+ await this.loadChangesetsFromDisk();
924
+ const changeset = this.changesets.get(changesetId);
824
925
  return changeset ? c.json({
825
926
  id: changeset.id,
826
927
  name: changeset.name,
@@ -832,6 +933,51 @@ class VisualizationServer {
832
933
  stats: changeset.stats,
833
934
  changes: changeset.changes
834
935
  }) : c.json({ error: "Changeset not found" }, 404);
936
+ });
937
+ const changesetActionResponses = {
938
+ 200: {
939
+ description: "Action completed",
940
+ content: { "application/json": { schema: ChangesetActionResultSchema } }
941
+ },
942
+ 404: {
943
+ description: "Changeset not found",
944
+ content: { "application/json": { schema: ErrorResponseSchema } }
945
+ },
946
+ 409: {
947
+ description: "Changeset is not staged, or the model drifted since it was created",
948
+ content: { "application/json": { schema: ErrorResponseSchema } }
949
+ },
950
+ 422: {
951
+ description: "The changeset failed validation; nothing was applied",
952
+ content: { "application/json": { schema: ErrorResponseSchema } }
953
+ }
954
+ }, commitChangesetRoute = createRoute({
955
+ method: "post",
956
+ path: "/api/changesets/:changesetId/commit",
957
+ tags: ["Changesets"],
958
+ summary: "Commit changeset",
959
+ description: "Apply a staged changeset to the model (same as `dr changeset commit`): validates it, applies every change atomically, marks it committed, and deactivates it if it was active. Refused when the model changed since the changeset was created (use the CLI with --force).",
960
+ security: [{ bearerAuth: [] }],
961
+ request: { params: z.object({ changesetId: IdSchema }) },
962
+ responses: changesetActionResponses
963
+ });
964
+ this.app.openapi(commitChangesetRoute, async (c) => {
965
+ const { changesetId } = c.req.valid("param");
966
+ return this.runChangesetAction(c, changesetId, "commit");
967
+ });
968
+ const discardChangesetRoute = createRoute({
969
+ method: "post",
970
+ path: "/api/changesets/:changesetId/discard",
971
+ tags: ["Changesets"],
972
+ summary: "Discard changeset",
973
+ description: "Throw away a staged changeset's changes (same as `dr changeset discard`): clears them, marks it discarded, and deactivates it if it was active. The model is not touched.",
974
+ security: [{ bearerAuth: [] }],
975
+ request: { params: z.object({ changesetId: IdSchema }) },
976
+ responses: changesetActionResponses
977
+ });
978
+ this.app.openapi(discardChangesetRoute, async (c) => {
979
+ const { changesetId } = c.req.valid("param");
980
+ return this.runChangesetAction(c, changesetId, "discard");
835
981
  }), this.app.doc("/api-spec.yaml", {
836
982
  openapi: "3.1.0",
837
983
  info: {
@@ -1067,20 +1213,6 @@ class VisualizationServer {
1067
1213
  }
1068
1214
  return layer in VisualizationServer.LAYER_FALLBACK ? VisualizationServer.LAYER_FALLBACK[layer] : "data-store.database";
1069
1215
  }
1070
- /**
1071
- * Generate unique annotation ID
1072
- * Uses cryptographically secure random bytes for collision-resistant IDs
1073
- */
1074
- generateAnnotationId() {
1075
- return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
1076
- }
1077
- /**
1078
- * Generate unique reply ID
1079
- * Uses cryptographically secure random bytes for collision-resistant IDs
1080
- */
1081
- generateReplyId() {
1082
- return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
1083
- }
1084
1216
  /**
1085
1217
  * Broadcast message to all connected WebSocket clients
1086
1218
  */
@@ -1136,6 +1268,20 @@ class VisualizationServer {
1136
1268
  }
1137
1269
  return null;
1138
1270
  }
1271
+ /**
1272
+ * Whether an element can be annotated: it is in the model, or a changeset that is still open
1273
+ * (staged, not committed or discarded) adds it. A reviewer comments mostly on what the plan
1274
+ * adds, so those count too.
1275
+ */
1276
+ async elementExists(id) {
1277
+ if (await this.findElement(id))
1278
+ return !0;
1279
+ await this.loadChangesetsFromDisk();
1280
+ for (const changeset of this.changesets.values())
1281
+ if (changeset.status === "staged" && changeset.changes.some((change) => change.type === "add" && change.elementId === id))
1282
+ return !0;
1283
+ return !1;
1284
+ }
1139
1285
  /**
1140
1286
  * Handle WebSocket messages
1141
1287
  */
@@ -1217,14 +1363,32 @@ class VisualizationServer {
1217
1363
  sendError(JSONRPC_ERRORS.NO_CLIENT_AVAILABLE, "No chat client available. Install Claude Code or GitHub Copilot to enable chat.");
1218
1364
  return;
1219
1365
  }
1220
- const conversationId = `conv-${++this.chatConversationCounter}-${Date.now()}`;
1221
- await this.launchChat(ws, conversationId, params.message, id);
1366
+ const clientConversationId = params.conversation_id;
1367
+ if (clientConversationId !== void 0 && (typeof clientConversationId != "string" || clientConversationId.trim() === "" || clientConversationId.length > 200)) {
1368
+ sendError(JSONRPC_ERRORS.INVALID_PARAMS, "conversation_id must be a non-empty string of at most 200 characters");
1369
+ return;
1370
+ }
1371
+ const conversationId = clientConversationId ?? `conv-${++this.chatConversationCounter}-${Date.now()}`;
1372
+ if (this.busyChatConversations.has(conversationId)) {
1373
+ sendError(JSONRPC_ERRORS.INVALID_PARAMS, "A reply is still running in this conversation; wait for it or cancel it first");
1374
+ return;
1375
+ }
1376
+ const turn = this.reserveChatConversation(conversationId);
1377
+ try {
1378
+ await this.launchChat(ws, conversationId, params.message, id, {
1379
+ resumable: clientConversationId !== void 0,
1380
+ turn
1381
+ });
1382
+ } catch (error) {
1383
+ throw this.releaseChatConversation(conversationId, turn), error;
1384
+ }
1222
1385
  break;
1223
1386
  case "chat.cancel":
1224
1387
  let cancelled = !1, cancelledConvId = null;
1225
- for (const [convId, process2] of this.activeChatProcesses)
1388
+ const cancelConvId = params?.conversation_id, cancelCandidates = [...this.activeChatProcesses].filter(([convId, proc]) => !this.cancelledChatProcesses.has(proc) && (typeof cancelConvId != "string" || convId === cancelConvId));
1389
+ for (const [convId, process2] of cancelCandidates)
1226
1390
  try {
1227
- process2.kill(), this.activeChatProcesses.delete(convId), cancelled = !0, cancelledConvId = convId;
1391
+ this.cancelledChatProcesses.add(process2), process2.kill(), cancelled = !0, cancelledConvId = convId;
1228
1392
  break;
1229
1393
  } catch (error) {
1230
1394
  console.warn(`[Chat] Failed to cancel process for conversation ${convId}: ${getErrorMessage(error)}`);
@@ -1245,9 +1409,9 @@ class VisualizationServer {
1245
1409
  /**
1246
1410
  * Launch chat with selected client and stream responses via WebSocket
1247
1411
  */
1248
- async launchChat(ws, conversationId, message, requestId) {
1412
+ async launchChat(ws, conversationId, message, requestId, options = {}) {
1249
1413
  if (!this.selectedChatClient) {
1250
- ws.send(JSON.stringify({
1414
+ options.turn && this.releaseChatConversation(conversationId, options.turn), ws.send(JSON.stringify({
1251
1415
  jsonrpc: "2.0",
1252
1416
  error: {
1253
1417
  code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
@@ -1257,21 +1421,121 @@ class VisualizationServer {
1257
1421
  }));
1258
1422
  return;
1259
1423
  }
1260
- this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId) : ws.send(JSON.stringify({
1424
+ this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId, options) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId, options) : (options.turn && this.releaseChatConversation(conversationId, options.turn), ws.send(JSON.stringify({
1261
1425
  jsonrpc: "2.0",
1262
1426
  error: {
1263
1427
  code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
1264
1428
  message: "Unknown chat client type"
1265
1429
  },
1266
1430
  id: requestId
1267
- }));
1431
+ })));
1268
1432
  }
1269
1433
  /**
1270
1434
  * Build command arguments for Claude Code launch
1271
1435
  */
1272
- buildClaudeChatArgs() {
1273
- const cmd = ["claude", "--agent", "dr-architect", "--print"];
1274
- return this.withDanger ? cmd.push("--dangerously-skip-permissions") : cmd.push("--allowedTools", formatForClaudeCode()), cmd.push("--verbose", "--output-format", "stream-json"), cmd;
1436
+ buildClaudeChatArgs(mcpConfigPath, session, agentArgs = []) {
1437
+ const cmd = ["claude", "--agent", DR_ARCHITECT_AGENT, ...agentArgs, "--print"], mode = this.drBotMode(), mcpRules = mcpConfigPath ? drMcpAllowedTools(mode) : [];
1438
+ if (mode === "danger")
1439
+ cmd.push("--dangerously-skip-permissions");
1440
+ else {
1441
+ const bashRules = mode === "changesets" ? formatForClaudeCodeWithChangesets() : formatForClaudeCode();
1442
+ cmd.push("--allowedTools", [bashRules, ...mcpRules].join(","));
1443
+ }
1444
+ return mcpConfigPath && cmd.push("--mcp-config", mcpConfigPath, "--append-system-prompt", drBotMcpPrompt(mode)), session && cmd.push(session.started ? "--resume" : "--session-id", session.sessionId), cmd.push("--verbose", "--output-format", "stream-json"), cmd;
1445
+ }
1446
+ /**
1447
+ * The Claude Code session for a viewer conversation, created on its first send. A send
1448
+ * without a conversation id gets none, so it runs in a fresh session as before.
1449
+ */
1450
+ chatSessionFor(conversationId, resumable) {
1451
+ if (!resumable)
1452
+ return;
1453
+ let session = this.chatSessions.get(conversationId);
1454
+ return session || (session = { sessionId: randomUUID(), started: !1 }, this.chatSessions.set(conversationId, session)), session;
1455
+ }
1456
+ /** Marks a conversation busy for one turn; the token releases it. */
1457
+ reserveChatConversation(conversationId) {
1458
+ const turn = Symbol(conversationId);
1459
+ return this.busyChatConversations.set(conversationId, turn), turn;
1460
+ }
1461
+ /** Ends a turn's hold on its conversation. A stale token (an older turn) releases nothing. */
1462
+ releaseChatConversation(conversationId, turn) {
1463
+ this.busyChatConversations.get(conversationId) === turn && this.busyChatConversations.delete(conversationId);
1464
+ }
1465
+ /** Drops a turn's process from activeChatProcesses, unless a newer turn's has replaced it. */
1466
+ forgetChatProcess(conversationId, proc) {
1467
+ this.activeChatProcesses.get(conversationId) === proc && this.activeChatProcesses.delete(conversationId);
1468
+ }
1469
+ /**
1470
+ * Whether Claude Code already has this session on disk
1471
+ * (`<config dir>/projects/<cwd with non-alphanumerics as "-">/<id>.jsonl`). A first send that
1472
+ * died before the server saw it start can still have created it, and --session-id would then
1473
+ * fail with "already in use".
1474
+ */
1475
+ claudeSessionExists(sessionId) {
1476
+ const configDir = this._testClaudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? path.join(homedir(), ".claude"), roots = /* @__PURE__ */ new Set([this.model.rootPath]);
1477
+ try {
1478
+ roots.add(realpathSync(this.model.rootPath));
1479
+ } catch {
1480
+ }
1481
+ for (const root of roots) {
1482
+ const projectDir = root.replace(/[^a-zA-Z0-9]/g, "-");
1483
+ if (existsSync(path.join(configDir, "projects", projectDir, `${sessionId}.jsonl`)))
1484
+ return !0;
1485
+ }
1486
+ return !1;
1487
+ }
1488
+ /** DrBot's permission mode; danger wins over changeset-only. */
1489
+ drBotMode() {
1490
+ return this.withDanger ? "danger" : this.withChangesets ? "changesets" : "read-safe";
1491
+ }
1492
+ /**
1493
+ * Resolves the dr-architect DrBot runs, once: the extra launch arguments (--agents for the
1494
+ * bundled copy) and the check of its tools both come from this lookup.
1495
+ */
1496
+ async resolveDrBotAgentLaunch() {
1497
+ const overrides = this._testDrMcpOptions ?? {};
1498
+ try {
1499
+ return await prepareDrBotAgent(this.model.rootPath, this.drBotMode(), {
1500
+ homeDir: overrides.homeDir,
1501
+ env: overrides.env,
1502
+ bundledPath: overrides.bundledPath
1503
+ });
1504
+ } catch (error) {
1505
+ return this.warnDrBotOnce(`[Claude Code] Could not check the installed ${DR_ARCHITECT_AGENT} agent's tools (${getErrorMessage(error)}); connecting the dr MCP server anyway.`), { agent: null, args: [], check: null };
1506
+ }
1507
+ }
1508
+ /**
1509
+ * Writes the MCP config that connects DrBot to `dr mcp` for this model. Returns null when it
1510
+ * can't (logged), or when the agent DrBot runs can't see the dr tools: DrBot then launches
1511
+ * without MCP and uses its Bash rules.
1512
+ */
1513
+ async prepareDrBotMcp(check) {
1514
+ const mode = this.drBotMode(), overrides = this._testDrMcpOptions ?? {};
1515
+ if (check && (this.warnDrBotOnce(check.error ? `[Claude Code] Could not pass the bundled ${DR_ARCHITECT_AGENT} agent (${check.path}: ${check.error}), so the chat may run one that can't see the dr MCP tools; it falls back to Bash dr commands.` : check.noneVisible ? `[Claude Code] The installed ${DR_ARCHITECT_AGENT} agent (${check.path}) predates the dr MCP tools, so the chat can't use them and falls back to Bash dr commands. Run \`dr claude upgrade\` to update it.` : `[Claude Code] The installed ${DR_ARCHITECT_AGENT} agent (${check.path}) doesn't list ${check.missing.join(", ")}, so the chat can't use those dr MCP tools. Run \`dr claude upgrade\` to update it.`), check.noneVisible))
1516
+ return null;
1517
+ try {
1518
+ return await prepareDrMcpLaunch({
1519
+ rootPath: this.model.rootPath,
1520
+ changesetOnly: mode === "changesets",
1521
+ drCommand: overrides.drCommand,
1522
+ tempRoot: overrides.tempRoot
1523
+ });
1524
+ } catch (error) {
1525
+ return console.warn(`[Claude Code] dr MCP server unavailable for chat, falling back to Bash dr commands: ${getErrorMessage(error)}`), null;
1526
+ }
1527
+ }
1528
+ /** Logs a DrBot setup warning once per server rather than on every chat message. */
1529
+ warnDrBotOnce(message) {
1530
+ this.drBotWarnings.has(message) || (this.drBotWarnings.add(message), console.warn(message));
1531
+ }
1532
+ /**
1533
+ * Environment for chat agent processes. In changeset-only mode it sets
1534
+ * CHANGESET_ONLY_ENV, so any `dr` command the agent runs refuses to save the base
1535
+ * model and can only stage changes in an active changeset (core/changeset-only-guard.ts).
1536
+ */
1537
+ buildChatEnv() {
1538
+ return this.withChangesets && !this.withDanger ? { ...process.env, [CHANGESET_ONLY_ENV]: "true" } : process.env;
1275
1539
  }
1276
1540
  /**
1277
1541
  * Add permission flags to Copilot command based on withDanger setting
@@ -1287,17 +1551,28 @@ class VisualizationServer {
1287
1551
  /**
1288
1552
  * Launch Claude Code CLI with dr-architect agent and stream responses
1289
1553
  */
1290
- async launchClaudeCodeChat(ws, conversationId, message, requestId) {
1554
+ async launchClaudeCodeChat(ws, conversationId, message, requestId, options = {}) {
1555
+ let drMcp = null;
1556
+ const session = this.chatSessionFor(conversationId, options.resumable === !0), turnToken = options.turn ?? this.reserveChatConversation(conversationId), release = () => this.releaseChatConversation(conversationId, turnToken);
1291
1557
  try {
1292
- const cmd = this.buildClaudeChatArgs();
1293
- this._testClaudeCmdOverride && cmd.splice(0, cmd.length, ...this._testClaudeCmdOverride);
1558
+ const agent = this._testClaudeCmdOverride !== void 0 && this._testDrMcpOptions === void 0 ? null : await this.resolveDrBotAgentLaunch();
1559
+ drMcp = agent ? await this.prepareDrBotMcp(agent.check) : null, session && !session.started && session.attempted && this.claudeSessionExists(session.sessionId) && (session.started = !0);
1560
+ const usedSessionIdFlag = session !== void 0 && !session.started;
1561
+ session && usedSessionIdFlag && (session.attempted = !0);
1562
+ const cmd = this.buildClaudeChatArgs(drMcp?.configPath, session, agent?.args ?? []);
1563
+ this._testLastClaudeChatArgs = [...cmd], this._testClaudeCmdOverride && cmd.splice(0, cmd.length, ...this._testClaudeCmdOverride);
1294
1564
  const proc = spawn(cmd[0], cmd.slice(1), {
1295
1565
  cwd: this.model.rootPath,
1566
+ env: this.buildChatEnv(),
1296
1567
  stdio: ["pipe", "pipe", "pipe"]
1297
1568
  });
1569
+ if (drMcp) {
1570
+ const cleanup = drMcp.cleanup;
1571
+ proc.once("exit", () => void cleanup()), proc.once("error", () => void cleanup());
1572
+ }
1298
1573
  proc.on("error", (err) => {
1299
1574
  const errorMsg = getErrorMessage(err);
1300
- console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
1575
+ console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.forgetChatProcess(conversationId, proc), release();
1301
1576
  try {
1302
1577
  ws.send(JSON.stringify({
1303
1578
  jsonrpc: "2.0",
@@ -1311,10 +1586,35 @@ class VisualizationServer {
1311
1586
  console.error(`[Claude Code] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
1312
1587
  }
1313
1588
  }), proc.stdin?.on("error", () => {
1314
- }), this.activeChatProcesses.set(conversationId, proc), proc.stdin?.write(message), proc.stdin?.end();
1589
+ });
1590
+ let stderrTail = "";
1591
+ proc.stderr?.on("data", (data) => {
1592
+ stderrTail = (stderrTail + data.toString()).slice(-4096);
1593
+ }), this.activeChatProcesses.set(conversationId, proc);
1594
+ const isCancelled = () => this.cancelledChatProcesses.has(proc);
1595
+ proc.stdin?.write(message), proc.stdin?.end();
1315
1596
  const decoder = new TextDecoder();
1316
1597
  (async () => {
1317
- let accumulatedText = "", buffer = "";
1598
+ const turn = { accumulatedText: "", pendingTools: /* @__PURE__ */ new Map(), session, sawEvent: !1 };
1599
+ let buffer = "";
1600
+ const handleLine = (line, isLast) => {
1601
+ if (isCancelled())
1602
+ return;
1603
+ let event;
1604
+ try {
1605
+ event = JSON.parse(line);
1606
+ } catch (parseError) {
1607
+ process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), this.sendChatNotification(ws, "chat.response.chunk", {
1608
+ conversation_id: conversationId,
1609
+ content: isLast ? line : line + `
1610
+ `,
1611
+ is_final: isLast,
1612
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1613
+ });
1614
+ return;
1615
+ }
1616
+ turn.sawEvent = !0, this.handleClaudeStreamEvent(ws, conversationId, event, turn);
1617
+ };
1318
1618
  try {
1319
1619
  for await (const chunkBuf of proc.stdout) {
1320
1620
  const chunk = decoder.decode(chunkBuf, { stream: !0 });
@@ -1323,79 +1623,34 @@ class VisualizationServer {
1323
1623
  `);
1324
1624
  buffer = lines.pop() || "";
1325
1625
  for (const line of lines)
1326
- if (line.trim())
1327
- try {
1328
- const event = JSON.parse(line);
1329
- if (event.type === "assistant") {
1330
- const content = event.message?.content || [];
1331
- for (const block of content)
1332
- block.type === "text" ? (accumulatedText += block.text, ws.send(JSON.stringify({
1333
- jsonrpc: "2.0",
1334
- method: "chat.response.chunk",
1335
- params: {
1336
- conversation_id: conversationId,
1337
- content: block.text,
1338
- is_final: !1,
1339
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1340
- }
1341
- }))) : block.type === "tool_use" && ws.send(JSON.stringify({
1342
- jsonrpc: "2.0",
1343
- method: "chat.tool.invoke",
1344
- params: {
1345
- conversation_id: conversationId,
1346
- tool_name: block.name,
1347
- tool_input: block.input,
1348
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1349
- }
1350
- }));
1351
- } else event.type === "result" && ws.send(JSON.stringify({
1352
- jsonrpc: "2.0",
1353
- method: "chat.tool.result",
1354
- params: {
1355
- conversation_id: conversationId,
1356
- result: event.result,
1357
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1358
- }
1359
- }));
1360
- } catch (parseError) {
1361
- process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), ws.send(JSON.stringify({
1362
- jsonrpc: "2.0",
1363
- method: "chat.response.chunk",
1364
- params: {
1365
- conversation_id: conversationId,
1366
- content: line + `
1367
- `,
1368
- is_final: !1,
1369
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1370
- }
1371
- }));
1372
- }
1626
+ line.trim() && handleLine(line, !1);
1373
1627
  }
1374
- buffer.trim() && ws.send(JSON.stringify({
1375
- jsonrpc: "2.0",
1376
- method: "chat.response.chunk",
1377
- params: {
1378
- conversation_id: conversationId,
1379
- content: buffer,
1380
- is_final: !0,
1381
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1382
- }
1383
- }));
1628
+ buffer.trim() && handleLine(buffer, !0);
1384
1629
  const exitCode = await this.waitForExit(proc);
1385
- this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
1630
+ if (this.forgetChatProcess(conversationId, proc), session && exitCode === 0 && (session.started = !0), session && usedSessionIdFlag && !options.retried && !isCancelled() && exitCode !== 0 && !turn.sawEvent && /already in use/i.test(stderrTail)) {
1631
+ session.started = !0, await this.launchClaudeCodeChat(ws, conversationId, message, requestId, {
1632
+ ...options,
1633
+ turn: turnToken,
1634
+ retried: !0
1635
+ });
1636
+ return;
1637
+ }
1638
+ isCancelled() || this.failPendingTools(ws, conversationId, turn, "The tool reported no result before the reply ended"), release(), ws.send(JSON.stringify({
1386
1639
  jsonrpc: "2.0",
1387
1640
  result: {
1388
1641
  conversation_id: conversationId,
1389
1642
  status: exitCode === 0 ? "complete" : "error",
1390
1643
  exit_code: exitCode,
1391
- full_response: accumulatedText,
1644
+ full_response: turn.accumulatedText,
1392
1645
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1393
1646
  },
1394
1647
  id: requestId
1395
1648
  }));
1396
1649
  } catch (error) {
1397
- if (!proc.pid)
1650
+ if (!proc.pid) {
1651
+ release();
1398
1652
  return;
1653
+ }
1399
1654
  const errorMsg = getErrorMessage(error);
1400
1655
  ws.send(JSON.stringify({
1401
1656
  jsonrpc: "2.0",
@@ -1404,16 +1659,18 @@ class VisualizationServer {
1404
1659
  message: `Chat failed: ${errorMsg}`
1405
1660
  },
1406
1661
  id: requestId
1407
- })), this.activeChatProcesses.delete(conversationId);
1662
+ })), this.forgetChatProcess(conversationId, proc);
1408
1663
  try {
1409
1664
  await this.killProcessWithRetry(proc, conversationId, "Claude Code");
1410
1665
  } catch (error2) {
1411
1666
  console.error(`[Claude Code] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
1667
+ } finally {
1668
+ release();
1412
1669
  }
1413
1670
  }
1414
1671
  })().catch((err) => {
1415
1672
  const errorMsg = getErrorMessage(err);
1416
- console.error(`[Claude Code] Stream error: ${errorMsg}`);
1673
+ console.error(`[Claude Code] Stream error: ${errorMsg}`), release();
1417
1674
  try {
1418
1675
  ws.send(JSON.stringify({
1419
1676
  jsonrpc: "2.0",
@@ -1428,6 +1685,7 @@ class VisualizationServer {
1428
1685
  }
1429
1686
  });
1430
1687
  } catch (error) {
1688
+ drMcp?.cleanup(), release();
1431
1689
  const errorMsg = getErrorMessage(error);
1432
1690
  ws.send(JSON.stringify({
1433
1691
  jsonrpc: "2.0",
@@ -1439,10 +1697,77 @@ class VisualizationServer {
1439
1697
  }));
1440
1698
  }
1441
1699
  }
1700
+ /** Sends a JSON-RPC notification to one chat client. */
1701
+ sendChatNotification(ws, method, params) {
1702
+ ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
1703
+ }
1704
+ /**
1705
+ * Turns one Claude Code stream-json event into the chat notifications the viewer reads:
1706
+ * - assistant text -> `chat.response.chunk`
1707
+ * - assistant tool_use -> `chat.tool.invoke` with `status: "executing"`
1708
+ * - user tool_result -> `chat.tool.invoke` again, same `tool_use_id`, `completed` or `failed`
1709
+ * - result (end of the turn) -> `chat.response.complete`
1710
+ */
1711
+ handleClaudeStreamEvent(ws, conversationId, event, turn) {
1712
+ if (turn.session && typeof event?.session_id == "string" && (turn.session.started = !0), event?.type === "assistant") {
1713
+ const content = Array.isArray(event.message?.content) ? event.message.content : [];
1714
+ for (const block of content)
1715
+ block?.type === "text" ? (turn.accumulatedText += block.text, this.sendChatNotification(ws, "chat.response.chunk", {
1716
+ conversation_id: conversationId,
1717
+ content: block.text,
1718
+ is_final: !1,
1719
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1720
+ })) : block?.type === "tool_use" && (typeof block.id == "string" && turn.pendingTools.set(block.id, { name: block.name, input: block.input }), this.sendChatNotification(ws, "chat.tool.invoke", {
1721
+ conversation_id: conversationId,
1722
+ tool_use_id: block.id,
1723
+ tool_name: block.name,
1724
+ tool_input: block.input,
1725
+ status: "executing",
1726
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1727
+ }));
1728
+ } else if (event?.type === "user") {
1729
+ const content = Array.isArray(event.message?.content) ? event.message.content : [];
1730
+ for (const block of content)
1731
+ block?.type !== "tool_result" || typeof block.tool_use_id != "string" || this.sendToolOutcome(ws, conversationId, turn, block.tool_use_id, block.is_error === !0, block.content);
1732
+ } else if (event?.type === "result") {
1733
+ const denials = Array.isArray(event.permission_denials) ? event.permission_denials : [];
1734
+ for (const denial of denials) {
1735
+ const id = denial?.tool_use_id;
1736
+ typeof id == "string" && turn.pendingTools.has(id) && this.sendToolOutcome(ws, conversationId, turn, id, !0, `Permission denied: ${denial.tool_name ?? "tool"}`);
1737
+ }
1738
+ this.sendChatNotification(ws, "chat.response.complete", {
1739
+ conversation_id: conversationId,
1740
+ result: event.result,
1741
+ is_error: event.is_error === !0,
1742
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1743
+ });
1744
+ }
1745
+ }
1746
+ /** Sends a tool's final `chat.tool.invoke` (completed or failed) and stops tracking it. */
1747
+ sendToolOutcome(ws, conversationId, turn, toolUseId, isError, result) {
1748
+ const tool = turn.pendingTools.get(toolUseId);
1749
+ turn.pendingTools.delete(toolUseId), this.sendChatNotification(ws, "chat.tool.invoke", {
1750
+ conversation_id: conversationId,
1751
+ tool_use_id: toolUseId,
1752
+ tool_name: tool?.name,
1753
+ tool_input: tool?.input,
1754
+ status: isError ? "failed" : "completed",
1755
+ is_error: isError,
1756
+ result,
1757
+ ...isError ? { error: toolResultText(result) || "Tool call failed" } : {},
1758
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1759
+ });
1760
+ }
1761
+ /** Reports every tool still waiting for a result as failed. */
1762
+ failPendingTools(ws, conversationId, turn, reason) {
1763
+ for (const toolUseId of [...turn.pendingTools.keys()])
1764
+ this.sendToolOutcome(ws, conversationId, turn, toolUseId, !0, reason);
1765
+ }
1442
1766
  /**
1443
1767
  * Launch GitHub Copilot CLI and stream responses via WebSocket
1444
1768
  */
1445
- async launchCopilotChat(ws, conversationId, message, requestId) {
1769
+ async launchCopilotChat(ws, conversationId, message, requestId, options = {}) {
1770
+ const turnToken = options.turn ?? this.reserveChatConversation(conversationId), release = () => this.releaseChatConversation(conversationId, turnToken);
1446
1771
  try {
1447
1772
  let cmd;
1448
1773
  if (this._testCopilotCmdOverride)
@@ -1459,7 +1784,7 @@ class VisualizationServer {
1459
1784
  });
1460
1785
  proc.on("error", (err) => {
1461
1786
  const errorMsg = getErrorMessage(err);
1462
- console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
1787
+ console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.forgetChatProcess(conversationId, proc), release();
1463
1788
  try {
1464
1789
  ws.send(JSON.stringify({
1465
1790
  jsonrpc: "2.0",
@@ -1480,7 +1805,7 @@ class VisualizationServer {
1480
1805
  try {
1481
1806
  for await (const chunkBuf of proc.stdout) {
1482
1807
  const chunk = decoder.decode(chunkBuf, { stream: !0 });
1483
- accumulatedText += chunk, ws.send(JSON.stringify({
1808
+ accumulatedText += chunk, !this.cancelledChatProcesses.has(proc) && ws.send(JSON.stringify({
1484
1809
  jsonrpc: "2.0",
1485
1810
  method: "chat.response.chunk",
1486
1811
  params: {
@@ -1492,7 +1817,7 @@ class VisualizationServer {
1492
1817
  }));
1493
1818
  }
1494
1819
  const exitCode = await this.waitForExit(proc);
1495
- this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
1820
+ this.forgetChatProcess(conversationId, proc), release(), ws.send(JSON.stringify({
1496
1821
  jsonrpc: "2.0",
1497
1822
  result: {
1498
1823
  conversation_id: conversationId,
@@ -1504,8 +1829,10 @@ class VisualizationServer {
1504
1829
  id: requestId
1505
1830
  }));
1506
1831
  } catch (error) {
1507
- if (!proc.pid)
1832
+ if (!proc.pid) {
1833
+ release();
1508
1834
  return;
1835
+ }
1509
1836
  const errorMsg = getErrorMessage(error);
1510
1837
  ws.send(JSON.stringify({
1511
1838
  jsonrpc: "2.0",
@@ -1514,16 +1841,18 @@ class VisualizationServer {
1514
1841
  message: `Chat failed: ${errorMsg}`
1515
1842
  },
1516
1843
  id: requestId
1517
- })), this.activeChatProcesses.delete(conversationId);
1844
+ })), this.forgetChatProcess(conversationId, proc);
1518
1845
  try {
1519
1846
  await this.killProcessWithRetry(proc, conversationId, "Copilot");
1520
1847
  } catch (error2) {
1521
1848
  console.error(`[Copilot] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
1849
+ } finally {
1850
+ release();
1522
1851
  }
1523
1852
  }
1524
1853
  })().catch((err) => {
1525
1854
  const errorMsg = getErrorMessage(err);
1526
- console.error(`[Copilot] Stream error: ${errorMsg}`);
1855
+ console.error(`[Copilot] Stream error: ${errorMsg}`), release();
1527
1856
  try {
1528
1857
  ws.send(JSON.stringify({
1529
1858
  jsonrpc: "2.0",
@@ -1538,6 +1867,7 @@ class VisualizationServer {
1538
1867
  }
1539
1868
  });
1540
1869
  } catch (error) {
1870
+ release();
1541
1871
  const errorMsg = getErrorMessage(error);
1542
1872
  ws.send(JSON.stringify({
1543
1873
  jsonrpc: "2.0",
@@ -1550,41 +1880,68 @@ class VisualizationServer {
1550
1880
  }
1551
1881
  }
1552
1882
  /**
1553
- * Setup file watcher for model changes
1883
+ * Watch the model and the changesets for changes made outside the server (the CLI, DrBot).
1884
+ * A model change reloads and broadcasts `model.updated`. A changeset change, including the
1885
+ * `.active` marker, broadcasts a debounced `changesets.updated`. An annotation change (the
1886
+ * MCP annotation_* tools write the same files) broadcasts what changed, debounced.
1554
1887
  */
1555
1888
  setupFileWatcher() {
1556
- const modelPath = `${this.model.rootPath}/documentation-robotics/model`;
1889
+ const drRoot = path.join(this.model.rootPath, "documentation-robotics"), modelPath = path.join(drRoot, "model"), changesetsPath = path.join(drRoot, "changesets"), annotationsPath = path.join(drRoot, "annotations"), isWithin = (filePath, dir) => filePath === dir || filePath.startsWith(dir + path.sep);
1557
1890
  try {
1558
- this.watcher = chokidar.watch(modelPath, {
1891
+ this.watcher = chokidar.watch(drRoot, {
1559
1892
  persistent: !0,
1560
- ignoreInitial: !0
1561
- }), this.watcher.on("all", async (_event, path2) => {
1562
- process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
1563
- try {
1564
- this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
1565
- const modelData = await this.serializeModel(), message = JSON.stringify({
1566
- type: "model.updated",
1567
- data: modelData,
1568
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1569
- });
1570
- for (const client of this.clients)
1571
- try {
1572
- client.send(message);
1573
- } catch (error) {
1574
- const msg = getErrorMessage(error);
1575
- console.warn(`[Watcher] Failed to send update: ${msg}`);
1576
- }
1577
- } catch (error) {
1578
- const message = getErrorMessage(error);
1579
- console.error(`[Watcher] Failed to reload model: ${message}`);
1893
+ ignoreInitial: !0,
1894
+ ignored: (filePath) => !(filePath === drRoot || isWithin(filePath, modelPath) || isWithin(filePath, changesetsPath) || isWithin(filePath, annotationsPath))
1895
+ }), this.watcher.on("all", async (_event, filePath) => {
1896
+ if (process.env.VERBOSE && console.log(`[Watcher] File changed: ${filePath}`), isWithin(filePath, annotationsPath)) {
1897
+ this.scheduleAnnotationsSync();
1898
+ return;
1580
1899
  }
1900
+ if (isWithin(filePath, changesetsPath)) {
1901
+ this.scheduleChangesetsBroadcast();
1902
+ return;
1903
+ }
1904
+ if (isWithin(filePath, modelPath))
1905
+ try {
1906
+ await this.reloadAndBroadcast();
1907
+ } catch (error) {
1908
+ const message = getErrorMessage(error);
1909
+ console.error(`[Watcher] Failed to reload model: ${message}`);
1910
+ }
1581
1911
  }), this.watcher.on("error", (err) => {
1582
- console.warn(`[Watcher] Could not watch ${modelPath}: ${getErrorMessage(err)}`);
1912
+ console.warn(`[Watcher] Could not watch ${drRoot}: ${getErrorMessage(err)}`);
1583
1913
  });
1584
1914
  } catch (err) {
1585
- console.warn(`[Watcher] Could not watch ${modelPath}: ${err}`);
1915
+ console.warn(`[Watcher] Could not watch ${drRoot}: ${err}`);
1586
1916
  }
1587
1917
  }
1918
+ /** Broadcasts `changesets.updated` once the changeset files have been quiet for a moment. */
1919
+ scheduleChangesetsBroadcast() {
1920
+ this.changesetsBroadcastTimer && clearTimeout(this.changesetsBroadcastTimer), this.changesetsBroadcastTimer = setTimeout(() => {
1921
+ this.changesetsBroadcastTimer = void 0, this.broadcastChangesetsUpdated().catch((error) => {
1922
+ console.error(`[Watcher] Failed to broadcast changesets: ${getErrorMessage(error)}`);
1923
+ });
1924
+ }, this.changesetsDebounceMs);
1925
+ }
1926
+ /**
1927
+ * Reload the changesets from disk and send every client `changesets.updated` with their ids
1928
+ * and the active one. Clients fetch `GET /api/changesets` for the details.
1929
+ */
1930
+ async broadcastChangesetsUpdated() {
1931
+ await this.loadChangesetsFromDisk();
1932
+ let active = null;
1933
+ try {
1934
+ active = await new StagingAreaManager(this.model.rootPath).getActiveId();
1935
+ } catch (error) {
1936
+ console.warn(`[Changesets] Could not read the active changeset: ${getErrorMessage(error)}`);
1937
+ }
1938
+ await this.broadcastMessage({
1939
+ type: "changesets.updated",
1940
+ ids: [...this.changesets.keys()],
1941
+ active,
1942
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1943
+ });
1944
+ }
1588
1945
  /**
1589
1946
  * Load all JSON schemas from bundled directory (recursive)
1590
1947
  * Loads schemas from all subdirectories: base/, nodes/, relationships/, etc.
@@ -2263,32 +2620,132 @@ class VisualizationServer {
2263
2620
  }
2264
2621
  }
2265
2622
  /**
2266
- * Load all changesets from disk into the in-memory map
2623
+ * Reload the model and changesets from disk and send the new model to every client.
2267
2624
  */
2268
- async loadChangesetsFromDisk() {
2269
- if (this.changesetStorage)
2625
+ async reloadAndBroadcast() {
2626
+ this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
2627
+ const modelData = await this.serializeModel(), message = JSON.stringify({
2628
+ type: "model.updated",
2629
+ data: modelData,
2630
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2631
+ });
2632
+ for (const client of this.clients)
2270
2633
  try {
2271
- const changesets = await this.changesetStorage.list();
2272
- this.changesets.clear();
2273
- for (const cs of changesets)
2274
- this.changesets.set(cs.id, cs);
2275
- process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
2276
- } catch (err) {
2277
- console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
2634
+ client.send(message);
2635
+ } catch (error) {
2636
+ const msg = getErrorMessage(error);
2637
+ console.warn(`[Watcher] Failed to send update: ${msg}`);
2278
2638
  }
2279
2639
  }
2640
+ /**
2641
+ * Commit or discard a staged changeset for POST /api/changesets/:id/{commit,discard}.
2642
+ *
2643
+ * Works on a freshly loaded model rather than the server's in-memory copy, so a CLI
2644
+ * change since the last reload can't be overwritten. Afterwards the server reloads and
2645
+ * broadcasts `model.updated` and `changesets.updated` at once, rather than wait for the watcher.
2646
+ */
2647
+ async runChangesetAction(c, changesetId, action) {
2648
+ const model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), manager = new StagingAreaManager(model.rootPath, model), changeset = await manager.load(changesetId);
2649
+ if (!changeset)
2650
+ return c.json({ error: "Changeset not found" }, 404);
2651
+ if (changeset.status !== "staged")
2652
+ return c.json({ error: `Changeset is already ${changeset.status}` }, 409);
2653
+ let result = {};
2654
+ try {
2655
+ if (action === "commit") {
2656
+ const commit = await manager.commit(model, changesetId, { validate: !0 });
2657
+ result = {
2658
+ committed: commit.committed,
2659
+ skipped: commit.skipped ?? 0,
2660
+ failed: commit.failed,
2661
+ preExistingErrors: commit.validation.preExistingErrors ?? 0
2662
+ };
2663
+ } else
2664
+ await manager.discard(changesetId);
2665
+ } catch (error) {
2666
+ const message = getErrorMessage(error);
2667
+ if (message.startsWith("Validation failed"))
2668
+ return c.json({ error: message }, 422);
2669
+ if (message.includes("drifted"))
2670
+ return c.json({ error: message }, 409);
2671
+ throw error;
2672
+ }
2673
+ await manager.getActiveId() === changesetId && await manager.clearActive();
2674
+ try {
2675
+ await this.reloadAndBroadcast();
2676
+ } catch (error) {
2677
+ console.error(`[Changesets] Reload after ${action} failed: ${getErrorMessage(error)}`);
2678
+ }
2679
+ try {
2680
+ await this.broadcastChangesetsUpdated();
2681
+ } catch (error) {
2682
+ console.error(`[Changesets] Broadcast after ${action} failed: ${getErrorMessage(error)}`);
2683
+ }
2684
+ const updated = this.changesets.get(changesetId) ?? await manager.load(changesetId);
2685
+ return c.json({
2686
+ id: changesetId,
2687
+ name: changeset.name,
2688
+ status: updated?.status ?? (action === "commit" ? "committed" : "discarded"),
2689
+ ...result
2690
+ });
2691
+ }
2692
+ /**
2693
+ * Load all changesets from disk into the in-memory map
2694
+ */
2695
+ async loadChangesetsFromDisk() {
2696
+ this.changesetStorage ??= new StagedChangesetStorage(this.model.rootPath);
2697
+ try {
2698
+ const changesets = await this.changesetStorage.list();
2699
+ this.changesets.clear();
2700
+ for (const cs of changesets)
2701
+ this.changesets.set(cs.id, cs);
2702
+ process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
2703
+ } catch (err) {
2704
+ console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
2705
+ }
2706
+ }
2280
2707
  /**
2281
2708
  * Start the server
2282
2709
  */
2283
2710
  async start(port = 8080) {
2284
- this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher(), this._server = serve({
2285
- port,
2286
- fetch: this.app.fetch
2287
- }), await new Promise((resolve, reject) => {
2288
- this._server.once("listening", resolve), this._server.once("error", (err) => {
2289
- err.code === "EADDRINUSE" ? reject(new Error(`Port ${port} is already in use. Try a different port with --port <port>.`)) : reject(err);
2711
+ this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), await this.syncAnnotations({ broadcast: !1 }).catch((error) => {
2712
+ console.warn(`[Annotations] Could not read the annotations: ${getErrorMessage(error)}`);
2713
+ });
2714
+ try {
2715
+ this._servers.push(await this.listenOn(port, "127.0.0.1"));
2716
+ try {
2717
+ this._servers.push(await this.listenOn(port, "::1"));
2718
+ } catch (error) {
2719
+ const code = error.code;
2720
+ if (code !== "EADDRNOTAVAIL" && code !== "EAFNOSUPPORT")
2721
+ throw error;
2722
+ }
2723
+ } catch (error) {
2724
+ throw this.closeServers(), error.code === "EADDRINUSE" ? new Error(`Port ${port} is already in use. Try a different port with --port <port>.`) : error;
2725
+ }
2726
+ for (const server of this._servers)
2727
+ this.injectWebSocket?.(server);
2728
+ this.setupFileWatcher(), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
2729
+ }
2730
+ /**
2731
+ * One HTTP server for the app, listening on host:port. Node reports bind failures (e.g.
2732
+ * EADDRINUSE) through an 'error' event, not a throw; this turns that into a rejection
2733
+ * instead of an unhandled 'error' that would crash the process via server-entry.ts's
2734
+ * uncaughtException handler.
2735
+ */
2736
+ async listenOn(port, host) {
2737
+ const server = createAdaptorServer({ fetch: this.app.fetch });
2738
+ return await new Promise((resolve, reject) => {
2739
+ server.once("listening", resolve), server.once("error", reject), server.listen(port, host);
2740
+ }), server;
2741
+ }
2742
+ /** Closes every HTTP server this instance listens on. */
2743
+ closeServers() {
2744
+ for (const server of this._servers)
2745
+ server.close((error) => {
2746
+ error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
2290
2747
  });
2291
- }), this.injectWebSocket?.(this._server), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
2748
+ this._servers = [];
2292
2749
  }
2293
2750
  /**
2294
2751
  * Get the current auth token
@@ -2310,11 +2767,11 @@ class VisualizationServer {
2310
2767
  */
2311
2768
  waitForExit(proc) {
2312
2769
  return new Promise((resolve) => {
2313
- if (proc.exitCode !== null) {
2314
- resolve(proc.exitCode);
2770
+ if (this.hasExited(proc)) {
2771
+ resolve(proc.exitCode ?? 1);
2315
2772
  return;
2316
2773
  }
2317
- proc.once("exit", (code) => resolve(code ?? 0));
2774
+ proc.once("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
2318
2775
  });
2319
2776
  }
2320
2777
  /**
@@ -2365,9 +2822,7 @@ class VisualizationServer {
2365
2822
  const errorMessage = error instanceof Error ? error.message : String(error);
2366
2823
  errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
2367
2824
  }
2368
- this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.close((error) => {
2369
- error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
2370
- });
2825
+ this.clients.clear(), this.watcher && this.watcher.close?.(), this.changesetsBroadcastTimer && (clearTimeout(this.changesetsBroadcastTimer), this.changesetsBroadcastTimer = void 0), this.annotationsBroadcastTimer && (clearTimeout(this.annotationsBroadcastTimer), this.annotationsBroadcastTimer = void 0), this.closeServers();
2371
2826
  }
2372
2827
  }
2373
2828
  export {