borgmcp 1.1.15 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (459) hide show
  1. package/CONTRIBUTING.md +15 -0
  2. package/LICENSE +201 -33
  3. package/NOTICE +2 -0
  4. package/README.md +85 -36
  5. package/SECURITY.md +7 -0
  6. package/dist/agent-runtime.d.ts.map +1 -0
  7. package/dist/agent-runtime.js +63 -1
  8. package/dist/agent-runtime.js.map +1 -0
  9. package/dist/assimilate-cmd.d.ts +96 -17
  10. package/dist/assimilate-cmd.d.ts.map +1 -0
  11. package/dist/assimilate-cmd.js +1396 -48
  12. package/dist/assimilate-cmd.js.map +1 -0
  13. package/dist/assimilate-deps.d.ts.map +1 -0
  14. package/dist/assimilate-deps.js +384 -3
  15. package/dist/assimilate-deps.js.map +1 -0
  16. package/dist/assimilate-guard.d.ts +5 -7
  17. package/dist/assimilate-guard.d.ts.map +1 -0
  18. package/dist/assimilate-guard.js +60 -2
  19. package/dist/assimilate-guard.js.map +1 -0
  20. package/dist/assimilate-welcome.d.ts +1 -1
  21. package/dist/assimilate-welcome.d.ts.map +1 -0
  22. package/dist/assimilate-welcome.js +32 -2
  23. package/dist/assimilate-welcome.js.map +1 -0
  24. package/dist/auth-env.d.ts +7 -50
  25. package/dist/auth-env.d.ts.map +1 -0
  26. package/dist/auth-env.js +22 -1
  27. package/dist/auth-env.js.map +1 -0
  28. package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
  29. package/dist/backends/launch-all-pastelist.js +11 -4
  30. package/dist/backends/launch-all-pastelist.js.map +1 -0
  31. package/dist/backends/launch-all-tmux.d.ts.map +1 -0
  32. package/dist/backends/launch-all-tmux.js +49 -1
  33. package/dist/backends/launch-all-tmux.js.map +1 -0
  34. package/dist/backends/launch-all-windows.d.ts.map +1 -0
  35. package/dist/backends/launch-all-windows.js +86 -12
  36. package/dist/backends/launch-all-windows.js.map +1 -0
  37. package/dist/bare-launch-menu.d.ts.map +1 -0
  38. package/dist/bare-launch-menu.js +83 -6
  39. package/dist/bare-launch-menu.js.map +1 -0
  40. package/dist/claude-launch-args.d.ts.map +1 -0
  41. package/dist/claude-launch-args.js +34 -1
  42. package/dist/claude-launch-args.js.map +1 -0
  43. package/dist/claude.d.ts.map +1 -0
  44. package/dist/claude.js +486 -14
  45. package/dist/claude.js.map +1 -0
  46. package/dist/cleanup-cmd.d.ts +5 -5
  47. package/dist/cleanup-cmd.d.ts.map +1 -0
  48. package/dist/cleanup-cmd.js +453 -14
  49. package/dist/cleanup-cmd.js.map +1 -0
  50. package/dist/clear-rewake-core.d.ts.map +1 -0
  51. package/dist/clear-rewake-core.js +23 -2
  52. package/dist/clear-rewake-core.js.map +1 -0
  53. package/dist/clear-rewake.d.ts.map +1 -0
  54. package/dist/clear-rewake.js +19 -1
  55. package/dist/clear-rewake.js.map +1 -0
  56. package/dist/cli-help.d.ts +5 -0
  57. package/dist/cli-help.d.ts.map +1 -0
  58. package/dist/cli-help.js +104 -56
  59. package/dist/cli-help.js.map +1 -0
  60. package/dist/cli-platform.d.ts.map +1 -0
  61. package/dist/cli-platform.js +106 -4
  62. package/dist/cli-platform.js.map +1 -0
  63. package/dist/cli-tool-approval.d.ts +84 -0
  64. package/dist/cli-tool-approval.d.ts.map +1 -0
  65. package/dist/cli-tool-approval.js +583 -0
  66. package/dist/cli-tool-approval.js.map +1 -0
  67. package/dist/codex-app-server.d.ts.map +1 -0
  68. package/dist/codex-app-server.js +228 -4
  69. package/dist/codex-app-server.js.map +1 -0
  70. package/dist/codex-app-wake.d.ts.map +1 -0
  71. package/dist/codex-app-wake.js +432 -2
  72. package/dist/codex-app-wake.js.map +1 -0
  73. package/dist/codex-launch.d.ts.map +1 -0
  74. package/dist/codex-launch.js +102 -2
  75. package/dist/codex-launch.js.map +1 -0
  76. package/dist/codex-remote.d.ts +6 -1
  77. package/dist/codex-remote.d.ts.map +1 -0
  78. package/dist/codex-remote.js +394 -1
  79. package/dist/codex-remote.js.map +1 -0
  80. package/dist/codex-wake-resolve.d.ts.map +1 -0
  81. package/dist/codex-wake-resolve.js +127 -1
  82. package/dist/codex-wake-resolve.js.map +1 -0
  83. package/dist/config-utils.d.ts +2 -2
  84. package/dist/config-utils.d.ts.map +1 -0
  85. package/dist/config-utils.js +766 -3
  86. package/dist/config-utils.js.map +1 -0
  87. package/dist/config.d.ts +98 -61
  88. package/dist/config.d.ts.map +1 -0
  89. package/dist/config.js +426 -1
  90. package/dist/config.js.map +1 -0
  91. package/dist/console-prefix.d.ts.map +1 -0
  92. package/dist/console-prefix.js +96 -1
  93. package/dist/console-prefix.js.map +1 -0
  94. package/dist/cube-name.d.ts.map +1 -0
  95. package/dist/cube-name.js +67 -1
  96. package/dist/cube-name.js.map +1 -0
  97. package/dist/cubes.d.ts +126 -18
  98. package/dist/cubes.d.ts.map +1 -0
  99. package/dist/cubes.js +412 -4
  100. package/dist/cubes.js.map +1 -0
  101. package/dist/debug.d.ts.map +1 -0
  102. package/dist/debug.js +71 -1
  103. package/dist/debug.js.map +1 -0
  104. package/dist/direct-log.d.ts.map +1 -0
  105. package/dist/direct-log.js +11 -1
  106. package/dist/direct-log.js.map +1 -0
  107. package/dist/docs-sections.d.ts +11 -10
  108. package/dist/docs-sections.d.ts.map +1 -0
  109. package/dist/docs-sections.js +113 -3
  110. package/dist/docs-sections.js.map +1 -0
  111. package/dist/drone-lifecycle.d.ts +5 -21
  112. package/dist/drone-lifecycle.d.ts.map +1 -0
  113. package/dist/drone-lifecycle.js +57 -8
  114. package/dist/drone-lifecycle.js.map +1 -0
  115. package/dist/drone-management.d.ts +29 -0
  116. package/dist/drone-management.d.ts.map +1 -0
  117. package/dist/drone-management.js +109 -0
  118. package/dist/drone-management.js.map +1 -0
  119. package/dist/ensure-mcp-config.d.ts.map +1 -0
  120. package/dist/ensure-mcp-config.js +36 -1
  121. package/dist/ensure-mcp-config.js.map +1 -0
  122. package/dist/evict-drone.d.ts +10 -2
  123. package/dist/evict-drone.d.ts.map +1 -0
  124. package/dist/evict-drone.js +60 -1
  125. package/dist/evict-drone.js.map +1 -0
  126. package/dist/gc-orphan-inboxes.d.ts.map +1 -0
  127. package/dist/gc-orphan-inboxes.js +177 -1
  128. package/dist/gc-orphan-inboxes.js.map +1 -0
  129. package/dist/inbox-monitor.d.ts +1 -1
  130. package/dist/inbox-monitor.d.ts.map +1 -0
  131. package/dist/inbox-monitor.js +856 -3
  132. package/dist/inbox-monitor.js.map +1 -0
  133. package/dist/index.d.ts +6 -4
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +1034 -37
  136. package/dist/index.js.map +1 -0
  137. package/dist/launch-all-cmd.d.ts.map +1 -0
  138. package/dist/launch-all-cmd.js +377 -37
  139. package/dist/launch-all-cmd.js.map +1 -0
  140. package/dist/launch-all-command.d.ts.map +1 -0
  141. package/dist/launch-all-command.js +24 -1
  142. package/dist/launch-all-command.js.map +1 -0
  143. package/dist/launch-all-deps.d.ts +3 -8
  144. package/dist/launch-all-deps.d.ts.map +1 -0
  145. package/dist/launch-all-deps.js +99 -1
  146. package/dist/launch-all-deps.js.map +1 -0
  147. package/dist/launch-all-discovery.d.ts +1 -0
  148. package/dist/launch-all-discovery.d.ts.map +1 -0
  149. package/dist/launch-all-discovery.js +90 -4
  150. package/dist/launch-all-discovery.js.map +1 -0
  151. package/dist/launch-all-locks.d.ts.map +1 -0
  152. package/dist/launch-all-locks.js +54 -1
  153. package/dist/launch-all-locks.js.map +1 -0
  154. package/dist/launch-gate.d.ts +2 -2
  155. package/dist/launch-gate.d.ts.map +1 -0
  156. package/dist/launch-gate.js +73 -1
  157. package/dist/launch-gate.js.map +1 -0
  158. package/dist/lifecycle-log-guard.d.ts.map +1 -0
  159. package/dist/lifecycle-log-guard.js +93 -2
  160. package/dist/lifecycle-log-guard.js.map +1 -0
  161. package/dist/list-roles-render.d.ts.map +1 -0
  162. package/dist/list-roles-render.js +40 -6
  163. package/dist/list-roles-render.js.map +1 -0
  164. package/dist/local-log-routing.d.ts +19 -0
  165. package/dist/local-log-routing.d.ts.map +1 -0
  166. package/dist/local-log-routing.js +67 -0
  167. package/dist/local-log-routing.js.map +1 -0
  168. package/dist/local-manage-tool-result.d.ts +8 -0
  169. package/dist/local-manage-tool-result.d.ts.map +1 -0
  170. package/dist/local-manage-tool-result.js +10 -0
  171. package/dist/local-manage-tool-result.js.map +1 -0
  172. package/dist/local-server-cursor.d.ts +34 -0
  173. package/dist/local-server-cursor.d.ts.map +1 -0
  174. package/dist/local-server-cursor.js +151 -0
  175. package/dist/local-server-cursor.js.map +1 -0
  176. package/dist/log-audit.d.ts.map +1 -0
  177. package/dist/log-audit.js +194 -3
  178. package/dist/log-audit.js.map +1 -0
  179. package/dist/log-stream.d.ts +31 -18
  180. package/dist/log-stream.d.ts.map +1 -0
  181. package/dist/log-stream.js +1214 -12
  182. package/dist/log-stream.js.map +1 -0
  183. package/dist/model-presets.d.ts.map +1 -0
  184. package/dist/model-presets.js +19 -1
  185. package/dist/model-presets.js.map +1 -0
  186. package/dist/name-validator.d.ts.map +1 -0
  187. package/dist/name-validator.js +22 -1
  188. package/dist/name-validator.js.map +1 -0
  189. package/dist/opencode-drone.d.ts.map +1 -0
  190. package/dist/opencode-drone.js +438 -4
  191. package/dist/opencode-drone.js.map +1 -0
  192. package/dist/opencode-plugin.d.ts +1 -1
  193. package/dist/opencode-plugin.d.ts.map +1 -0
  194. package/dist/opencode-plugin.js +29 -4
  195. package/dist/opencode-plugin.js.map +1 -0
  196. package/dist/parse-assimilate-args.d.ts +1 -1
  197. package/dist/parse-assimilate-args.d.ts.map +1 -0
  198. package/dist/parse-assimilate-args.js +125 -1
  199. package/dist/parse-assimilate-args.js.map +1 -0
  200. package/dist/parse-launch-all-args.d.ts.map +1 -0
  201. package/dist/parse-launch-all-args.js +70 -1
  202. package/dist/parse-launch-all-args.js.map +1 -0
  203. package/dist/postinstall-banner.d.ts.map +1 -0
  204. package/dist/postinstall-banner.js +27 -2
  205. package/dist/postinstall-banner.js.map +1 -0
  206. package/dist/postinstall.d.ts.map +1 -0
  207. package/dist/postinstall.js +32 -5
  208. package/dist/postinstall.js.map +1 -0
  209. package/dist/readiness-probe.d.ts.map +1 -0
  210. package/dist/readiness-probe.js +9 -1
  211. package/dist/readiness-probe.js.map +1 -0
  212. package/dist/regen-format.d.ts.map +1 -0
  213. package/dist/regen-format.js +545 -13
  214. package/dist/regen-format.js.map +1 -0
  215. package/dist/regen.d.ts.map +1 -0
  216. package/dist/regen.js +141 -5
  217. package/dist/regen.js.map +1 -0
  218. package/dist/remote-client.d.ts +65 -175
  219. package/dist/remote-client.d.ts.map +1 -0
  220. package/dist/remote-client.js +942 -1
  221. package/dist/remote-client.js.map +1 -0
  222. package/dist/reset-local-seat-cmd.d.ts +50 -0
  223. package/dist/reset-local-seat-cmd.d.ts.map +1 -0
  224. package/dist/reset-local-seat-cmd.js +169 -0
  225. package/dist/reset-local-seat-cmd.js.map +1 -0
  226. package/dist/role-match.d.ts.map +1 -0
  227. package/dist/role-match.js +23 -1
  228. package/dist/role-match.js.map +1 -0
  229. package/dist/role-resolver.d.ts.map +1 -0
  230. package/dist/role-resolver.js +60 -1
  231. package/dist/role-resolver.js.map +1 -0
  232. package/dist/roster-render.d.ts.map +1 -0
  233. package/dist/roster-render.js +121 -3
  234. package/dist/roster-render.js.map +1 -0
  235. package/dist/seat-probe.d.ts +32 -13
  236. package/dist/seat-probe.d.ts.map +1 -0
  237. package/dist/seat-probe.js +78 -1
  238. package/dist/seat-probe.js.map +1 -0
  239. package/dist/seat-store.d.ts +76 -0
  240. package/dist/seat-store.d.ts.map +1 -0
  241. package/dist/seat-store.js +309 -0
  242. package/dist/seat-store.js.map +1 -0
  243. package/dist/seats.d.ts +303 -0
  244. package/dist/seats.d.ts.map +1 -0
  245. package/dist/seats.js +551 -0
  246. package/dist/seats.js.map +1 -0
  247. package/dist/self-path.d.ts +27 -0
  248. package/dist/self-path.d.ts.map +1 -0
  249. package/dist/self-path.js +43 -0
  250. package/dist/self-path.js.map +1 -0
  251. package/dist/server-endpoint.d.ts +9 -0
  252. package/dist/server-endpoint.d.ts.map +1 -0
  253. package/dist/server-endpoint.js +38 -0
  254. package/dist/server-endpoint.js.map +1 -0
  255. package/dist/server-errors.d.ts +50 -0
  256. package/dist/server-errors.d.ts.map +1 -0
  257. package/dist/server-errors.js +79 -0
  258. package/dist/server-errors.js.map +1 -0
  259. package/dist/server-handshake.d.ts +197 -0
  260. package/dist/server-handshake.d.ts.map +1 -0
  261. package/dist/server-handshake.js +505 -0
  262. package/dist/server-handshake.js.map +1 -0
  263. package/dist/server-response.d.ts +3 -0
  264. package/dist/server-response.d.ts.map +1 -0
  265. package/dist/server-response.js +67 -0
  266. package/dist/server-response.js.map +1 -0
  267. package/dist/server-trust.d.ts +14 -0
  268. package/dist/server-trust.d.ts.map +1 -0
  269. package/dist/server-trust.js +225 -0
  270. package/dist/server-trust.js.map +1 -0
  271. package/dist/setup-confirm.d.ts +3 -3
  272. package/dist/setup-confirm.d.ts.map +1 -0
  273. package/dist/setup-confirm.js +126 -2
  274. package/dist/setup-confirm.js.map +1 -0
  275. package/dist/setup.d.ts +5 -3
  276. package/dist/setup.d.ts.map +1 -0
  277. package/dist/setup.js +169 -46
  278. package/dist/setup.js.map +1 -0
  279. package/dist/shell-escape.d.ts.map +1 -0
  280. package/dist/shell-escape.js +22 -1
  281. package/dist/shell-escape.js.map +1 -0
  282. package/dist/spawn.d.ts +2 -2
  283. package/dist/spawn.d.ts.map +1 -0
  284. package/dist/spawn.js +29 -10
  285. package/dist/spawn.js.map +1 -0
  286. package/dist/startup-services.d.ts +0 -1
  287. package/dist/startup-services.d.ts.map +1 -0
  288. package/dist/startup-services.js +26 -1
  289. package/dist/startup-services.js.map +1 -0
  290. package/dist/stream-owner.d.ts +6 -2
  291. package/dist/stream-owner.d.ts.map +1 -0
  292. package/dist/stream-owner.js +547 -2
  293. package/dist/stream-owner.js.map +1 -0
  294. package/dist/stream-status.d.ts +1 -1
  295. package/dist/stream-status.d.ts.map +1 -0
  296. package/dist/stream-status.js +268 -3
  297. package/dist/stream-status.js.map +1 -0
  298. package/dist/sync-roles-render.d.ts.map +1 -0
  299. package/dist/sync-roles-render.js +118 -3
  300. package/dist/sync-roles-render.js.map +1 -0
  301. package/dist/sync.d.ts +1 -1
  302. package/dist/sync.d.ts.map +1 -0
  303. package/dist/sync.js +286 -22
  304. package/dist/sync.js.map +1 -0
  305. package/dist/terminal-title.d.ts.map +1 -0
  306. package/dist/terminal-title.js +68 -1
  307. package/dist/terminal-title.js.map +1 -0
  308. package/dist/token-store.d.ts +17 -111
  309. package/dist/token-store.d.ts.map +1 -0
  310. package/dist/token-store.js +81 -1
  311. package/dist/token-store.js.map +1 -0
  312. package/dist/tool-manifest.d.ts +4 -8
  313. package/dist/tool-manifest.d.ts.map +1 -0
  314. package/dist/tool-manifest.js +554 -1
  315. package/dist/tool-manifest.js.map +1 -0
  316. package/dist/tool-scope.d.ts +8 -12
  317. package/dist/tool-scope.d.ts.map +1 -0
  318. package/dist/tool-scope.js +97 -1
  319. package/dist/tool-scope.js.map +1 -0
  320. package/dist/types.d.ts +1 -5
  321. package/dist/types.d.ts.map +1 -0
  322. package/dist/types.js +5 -0
  323. package/dist/types.js.map +1 -0
  324. package/dist/unknown-subcommand.d.ts +1 -1
  325. package/dist/unknown-subcommand.d.ts.map +1 -0
  326. package/dist/unknown-subcommand.js +37 -1
  327. package/dist/unknown-subcommand.js.map +1 -0
  328. package/dist/version.d.ts +1 -1
  329. package/dist/version.d.ts.map +1 -0
  330. package/dist/version.js +78 -2
  331. package/dist/version.js.map +1 -0
  332. package/dist/working-repo.d.ts.map +1 -0
  333. package/dist/working-repo.js +81 -1
  334. package/dist/working-repo.js.map +1 -0
  335. package/dist/worktree-lifecycle.d.ts.map +1 -0
  336. package/dist/worktree-lifecycle.js +205 -2
  337. package/dist/worktree-lifecycle.js.map +1 -0
  338. package/docs/EXTRACTION_PROVENANCE.md +40 -0
  339. package/docs/LOCAL_SERVER.md +103 -0
  340. package/docs/RELEASING.md +120 -0
  341. package/package.json +52 -34
  342. package/src/agent-runtime.ts +69 -0
  343. package/src/assimilate-cmd.ts +1864 -0
  344. package/src/assimilate-deps.ts +444 -0
  345. package/src/assimilate-guard.ts +84 -0
  346. package/src/assimilate-welcome.ts +39 -0
  347. package/src/auth-env.ts +21 -0
  348. package/src/backends/launch-all-pastelist.ts +18 -0
  349. package/src/backends/launch-all-tmux.ts +75 -0
  350. package/src/backends/launch-all-windows.ts +110 -0
  351. package/src/bare-launch-menu.ts +148 -0
  352. package/src/claude-launch-args.ts +35 -0
  353. package/src/claude.ts +561 -0
  354. package/src/cleanup-cmd.ts +581 -0
  355. package/src/clear-rewake-core.ts +34 -0
  356. package/src/clear-rewake.ts +18 -0
  357. package/src/cli-help.ts +116 -0
  358. package/src/cli-platform.ts +127 -0
  359. package/src/cli-tool-approval.ts +716 -0
  360. package/src/codex-app-server.ts +238 -0
  361. package/src/codex-app-wake.ts +512 -0
  362. package/src/codex-launch.ts +140 -0
  363. package/src/codex-remote.ts +500 -0
  364. package/src/codex-wake-resolve.ts +155 -0
  365. package/src/config-utils.ts +811 -0
  366. package/src/config.ts +595 -0
  367. package/src/console-prefix.ts +99 -0
  368. package/src/cube-name.ts +69 -0
  369. package/src/cubes.ts +583 -0
  370. package/src/debug.ts +73 -0
  371. package/src/direct-log.ts +9 -0
  372. package/src/docs-sections.ts +129 -0
  373. package/src/drone-lifecycle.ts +62 -0
  374. package/src/drone-management.ts +162 -0
  375. package/src/ensure-mcp-config.ts +54 -0
  376. package/src/evict-drone.ts +82 -0
  377. package/src/gc-orphan-inboxes.ts +229 -0
  378. package/src/inbox-monitor.ts +1005 -0
  379. package/src/index.ts +1201 -0
  380. package/src/launch-all-cmd.ts +450 -0
  381. package/src/launch-all-command.ts +30 -0
  382. package/src/launch-all-deps.ts +188 -0
  383. package/src/launch-all-discovery.ts +118 -0
  384. package/src/launch-all-locks.ts +74 -0
  385. package/src/launch-gate.ts +80 -0
  386. package/src/lifecycle-log-guard.ts +144 -0
  387. package/src/list-roles-render.ts +52 -0
  388. package/src/local-log-routing.ts +106 -0
  389. package/src/local-manage-tool-result.ts +10 -0
  390. package/src/local-server-cursor.ts +188 -0
  391. package/src/log-audit.ts +207 -0
  392. package/src/log-stream.ts +1505 -0
  393. package/src/model-presets.ts +22 -0
  394. package/src/name-validator.ts +23 -0
  395. package/src/opencode-drone.ts +528 -0
  396. package/src/opencode-plugin.ts +38 -0
  397. package/src/parse-assimilate-args.ts +125 -0
  398. package/src/parse-launch-all-args.ts +100 -0
  399. package/src/postinstall-banner.ts +37 -0
  400. package/src/postinstall.ts +36 -0
  401. package/src/readiness-probe.ts +12 -0
  402. package/src/regen-format.ts +651 -0
  403. package/src/regen.ts +150 -0
  404. package/src/remote-client.ts +1379 -0
  405. package/src/reset-local-seat-cmd.ts +227 -0
  406. package/src/role-match.ts +29 -0
  407. package/src/role-resolver.ts +93 -0
  408. package/src/roster-render.ts +196 -0
  409. package/src/seat-probe.ts +115 -0
  410. package/src/seat-store.ts +333 -0
  411. package/src/seats.ts +741 -0
  412. package/src/self-path.ts +51 -0
  413. package/src/server-endpoint.ts +38 -0
  414. package/src/server-errors.ts +93 -0
  415. package/src/server-handshake.ts +783 -0
  416. package/src/server-response.ts +76 -0
  417. package/src/server-trust.ts +245 -0
  418. package/src/setup-confirm.ts +171 -0
  419. package/src/setup.ts +186 -0
  420. package/src/shell-escape.ts +22 -0
  421. package/src/spawn.ts +31 -0
  422. package/src/startup-services.ts +36 -0
  423. package/src/stream-owner.ts +686 -0
  424. package/src/stream-status.ts +337 -0
  425. package/src/sync-roles-render.ts +163 -0
  426. package/src/sync.ts +363 -0
  427. package/src/terminal-title.ts +76 -0
  428. package/src/token-store.ts +102 -0
  429. package/src/tool-manifest.ts +610 -0
  430. package/src/tool-scope.ts +110 -0
  431. package/src/types.ts +5 -0
  432. package/src/unknown-subcommand.ts +35 -0
  433. package/src/version.ts +82 -0
  434. package/src/working-repo.ts +99 -0
  435. package/src/worktree-lifecycle.ts +254 -0
  436. package/dist/auth-recovery.d.ts +0 -33
  437. package/dist/auth-recovery.js +0 -1
  438. package/dist/auth.d.ts +0 -103
  439. package/dist/auth.js +0 -39
  440. package/dist/device-auth.d.ts +0 -75
  441. package/dist/device-auth.js +0 -1
  442. package/dist/drone-address.d.ts +0 -26
  443. package/dist/drone-address.js +0 -1
  444. package/dist/get-started.d.ts +0 -20
  445. package/dist/get-started.js +0 -2
  446. package/dist/health-beat.d.ts +0 -134
  447. package/dist/health-beat.js +0 -1
  448. package/dist/role-section.d.ts +0 -110
  449. package/dist/role-section.js +0 -8
  450. package/dist/setup-action.d.ts +0 -20
  451. package/dist/setup-action.js +0 -1
  452. package/dist/stale-version-check.d.ts +0 -40
  453. package/dist/stale-version-check.js +0 -1
  454. package/dist/subscription-retry.d.ts +0 -40
  455. package/dist/subscription-retry.js +0 -1
  456. package/dist/templates.d.ts +0 -107
  457. package/dist/templates.js +0 -359
  458. package/dist/token-crypto.d.ts +0 -50
  459. package/dist/token-crypto.js +0 -1
package/dist/index.js CHANGED
@@ -1,38 +1,1035 @@
1
1
  #!/usr/bin/env node
2
- import{Server as ee}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as te}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as oe,ListToolsRequestSchema as re,ListPromptsRequestSchema as ne,GetPromptRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{assertRoleMatches as ie}from"./role-match.js";import{getCubeInfo as ae,getRoleInfo as L,getRoleInfoByName as ce,getRoster as de,readLog as le,appendLog as ue,submitReport as pe,fetchReports as me,ackLogEntry as be,recordDecision as fe,removeDecision as ge,listDecisions as he,regen as O,listCubes as _e,createCube as ye,updateCube as B,deleteCube as we,createRole as xe,updateRole as $e,patchRoleSection as C,patchTaxonomyClass as F,deleteRole as ve,reassignDrone as ke,evictDrone as Re,getCube as $,checkSubscriptionStatus as Ee,createBillingPortalSession as Ce,createSubscription as Se,syncRoles as Te,applyTemplate as Ie,whoami as qe,roleRationale as De,getValidToken as Pe}from"./remote-client.js";import{startHealthBeatTick as Ue}from"./health-beat.js";import{getTemplate as S,listTemplateNames as T,resolveCubeDirectiveForCreate as Me,resolveCubeDirectiveForApply as Ne,resolveMessageTaxonomyForCreate as je}from"./templates.js";import{activeCubeWithFreshRegenIdentity as H,getActiveCube as _,setActiveCube as W,findProjectRoot as I,inboxPathForDrone as q}from"./cubes.js";import{monitorStateRootForWorktree as D}from"./inbox-monitor.js";import{addSessionStartHook as Ae,addUserPromptSubmitHook as Le}from"./config-utils.js";import{humanAgo as V,formatLogEntryMarkdown as Oe,formatRegenMarkdown as z,getDronePlaybook as Be,getDronePlaybookChapter as Fe,nullTaxonomyTip as He,regenWakePathDroneLabel as We}from"./regen-format.js";import{startLogStream as Ve,getStreamStatus as P}from"./log-stream.js";import{isMcpReadinessProbe as ze}from"./readiness-probe.js";import{runMcpStartupServices as Ke}from"./startup-services.js";import{TOOL_MANIFEST as Je}from"./tool-manifest.js";import{DOCS_SECTIONS as Qe,matchDocsSections as Ye,formatDocsIndex as Ge}from"./docs-sections.js";import{renderRoleList as Xe}from"./list-roles-render.js";import{filterToolsForRole as Ze}from"./tool-scope.js";import{getPackageVersion as v,getOnDiskVersion as et,handleVersionFlag as tt}from"./version.js";import{renderStreamStatus as ot,checkInboxMonitorHealthy as U,formatWakePathPrefix as rt,shouldShowWakePathWarning as nt}from"./stream-status.js";import{formatRoleAgentLabel as st,formatWorkingRepoLabel as it,renderRoster as at}from"./roster-render.js";import{resolveWorkingRepo as K}from"./working-repo.js";import{resolveDroneIdByLabel as ct,isUuidShape as dt}from"./evict-drone.js";import{authRecoveryMessage as lt}from"./auth-recovery.js";import{DroneEvictedError as ut,DroneFrozenError as pt,formatEvictedToolResult as mt,formatFrozenToolResult as bt}from"./drone-lifecycle.js";import{classifyInSessionAssimilate as ft,reattachOnlyRefusal as gt,reattachFailureMessage as ht}from"./assimilate-guard.js";import{gateAllowsActivation as _t,borgSessionToolNotice as yt}from"./launch-gate.js";import{renderSyncRolesResult as wt}from"./sync-roles-render.js";import{initConsolePrefix as xt,consolePrefix as k}from"./console-prefix.js";import{isCodexRemoteWakeEnabled as $t,resolveSessionAgentKind as M,probeCodexBridgeArmed as vt}from"./codex-app-wake.js";import{connectOpenCodeDrone as kt,injectOpenCodeEntry as Rt,probeOpenCodeDroneArmed as Et,computeOpenCodePort as Ct}from"./opencode-drone.js";import{installBorgPlugin as St}from"./opencode-plugin.js";import{setModuleInjectOpenCode as Tt}from"./log-stream.js";import{lifecycleSignalForMessage as It,recordLifecycleLog as J,shouldSuppressLifecycleLog as qt}from"./lifecycle-log-guard.js";import{normalizeDirectLogRecipients as Dt}from"./direct-log.js";import Q from"open";import Pt from"os";function Ut(){try{const b=Pt.hostname();return b&&b.trim()?b.trim().slice(0,255):null}catch{return null}}async function Y(b,y){return await Ie(b,y.name)}async function h(){const b=await _();if(!b)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");return b}async function Mt(){tt();const b=ze();await Ke(b,{sessionStartHook:()=>{Ae()},auditHook:()=>{Le()},sseStream:()=>{Ve()},openCode:async()=>{St();const d=await _();if(d&&process.env.BORG_OPENCODE==="1"){const o=`http://127.0.0.1:${Ct(d.droneId)}`;await kt({serverUrl:o,directory:process.cwd(),droneLabel:d.droneLabel,cubeName:d.name}),Tt(Rt)}},healthBeat:()=>{Ue({getActiveCube:_,getStreamConnected:()=>P().connected,getInboxPath:d=>q(d.cubeId,d.droneId),checkMonitor:d=>U(d,D(I())),isCodexRemoteWake:$t,probeBridgeArmed:d=>vt({cubeId:d.cubeId,droneId:d.droneId}),probeOpenCodeDrone:()=>Et(),resolveAgentKind:M,resolveHostname:Ut,resolveVersion:v,getToken:Pe,fetchImpl:globalThis.fetch.bind(globalThis)})}});const y=new ee({name:"borg-mcp-client",version:v()},{capabilities:{tools:{},prompts:{}}}),N=Je;y.setRequestHandler(re,async()=>{let d=null;try{const u=await _();u&&(d={roleName:u.roleName,roleClass:u.roleClass,isHumanSeat:u.isHumanSeat})}catch{d=null}return{tools:Ze(N,d)}}),y.setRequestHandler(oe,async d=>{let{name:u,arguments:o}=d.params;if(u==="borg_describe-tool"){const e=typeof o?.name=="string"?o.name:"",t=N.find(r=>r.name===e);return t?{content:[{type:"text",text:JSON.stringify({name:t.name,description:t.description,inputSchema:t.inputSchema},null,2)}]}:{content:[{type:"text",text:`Unknown borg tool: ${e||"(none)"}. Pass { name: "<borg_tool>" }.`}],isError:!0}}if(u==="borg_tool"){const e=typeof o?.name=="string"?o.name:"";if(!e||e==="borg_tool"||e==="borg_describe-tool")return{content:[{type:"text",text:'borg_tool: pass { name: "<borg_tool>", arguments: {...} } naming a real borg tool (not the dispatcher itself).'}],isError:!0};o=o?.arguments&&typeof o.arguments=="object"?o.arguments:{},u=e}if(!_t(`tool ${u}`))return{content:[{type:"text",text:yt(u)}],isError:!0};try{switch(u){case"borg_regen":{const e=await _();if(!e)return{content:[{type:"text",text:'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.'}]};const t=typeof o?.since=="string"?o.since:void 0,r=o?.mode==="lite"?"lite":"full",n=typeof o?.model=="string"?o.model:void 0,i=await O(e.sessionToken,e.apiUrl,{since:t,reportedModel:n,workingRepo:K()}),s=H(e,i);s!==e&&await W(s);const a=P(),c=q(s.cubeId,s.droneId),m=D(I()),f=M()==="claude"?U(c,m):!0,l=nt(a,f)?rt({inboxPath:c,monitorStateRoot:m,droneLabel:We(i,s.droneLabel),cubeName:s.name}):"";let g="";try{const w=v(),x=et();if(w!=="unknown"&&x!=="unknown"&&x!==w){const[R,j,X]=w.split(".").map(Number),[E,A,Z]=x.split(".").map(Number);(E>R||E===R&&A>j||E===R&&A===j&&Z>X)&&(g=`## \u{1F504} borgmcp ${x} installed \u2014 run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${w}.
3
-
4
- `)}}catch{}return{content:[{type:"text",text:g+l+z(i,{mode:r})}]}}case"borg_subscribe":return{content:[{type:"text",text:`Complete your subscription at: ${await Se()}`}]};case"borg_upgrade-subscription":{const e=await Ce();try{await Q(e)}catch{}return{content:[{type:"text",text:`Manage your Borg MCP subscription at: ${e}`}]}}case"borg_subscription_status":{const e=await Ee();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}case"borg_open_dashboard":{const e="https://borgmcp.ai/dashboard";return await Q(e),{content:[{type:"text",text:`\u25FC Opened dashboard in browser: ${e}`}]}}case"borg_assimilate":{const e=o?.cube_name;if(!e)throw new Error("cube_name is required");const t=await _(),r=ft(t,e);if(r.kind!=="reattach")return{content:[{type:"text",text:gt(r,e)}],isError:!0};try{const n=await O(t.sessionToken,t.apiUrl,{workingRepo:K()}),i=H(t,n);return i!==t&&await W(i),{content:[{type:"text",text:[`# Re-attached to cube: ${i.name}`,"",`**Drone label:** ${i.droneLabel}`,"**Seat:** existing identity reused \u2014 no new drone minted (gh#780)","",""].join(`
5
- `)+z(n,{mode:"full"})}]}}catch(n){const i=ht(n??{});if(!i)throw n;return{content:[{type:"text",text:i}],isError:!0}}}case"borg_version":return{content:[{type:"text",text:`borgmcp ${v()}`}]};case"borg_playbook":return{content:[{type:"text",text:Fe()}]};case"borg_docs":{const e=typeof o?.topic=="string"?o.topic.trim():"",t=e?Ye(e):[],r=t.length>0?t:Qe;return{content:[{type:"text",text:`${e&&t.length>0?`Best-matching docs section(s) for "${e}" \u2014 WebFetch the URL for the full page:`:e?`No exact match for "${e}". Full Borg MCP docs index \u2014 WebFetch the URL you need:`:"Borg MCP docs index \u2014 WebFetch the URL of the section you need:"}
6
-
7
- ${Ge(r)}`}]}}case"borg_whoami":{const e=await h(),t=await qe(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:JSON.stringify(t,null,2)}]}}case"borg_cube":{const e=await h(),[{cube:t,roles:r}]=await Promise.all([ae(e.sessionToken,e.apiUrl),L(e.sessionToken,e.apiUrl)]),n=[];n.push(`# Cube: ${t.name}`),n.push(""),n.push("## Cube directive"),n.push(t.cube_directive||"_(none)_"),n.push("");const i=He(t.message_taxonomy);if(i&&(n.push(i),n.push("")),n.push("## Roles in this cube"),!r.length)n.push("_(no roles defined)_");else{for(const s of r){const a=[s.role_class==="queen"?"Queen":null,s.is_human_seat?"human-seat":null,s.is_default?"default":null,s.is_mandatory?"mandatory":null].filter(Boolean).join(", "),c=a?` (${a})`:"",m=s.short_description||"_(no description)_";n.push(`- **${s.name}**${c} \u2014 ${m}`)}n.push(""),n.push("_(Coordinator-class drones can fetch role IDs via `borg_list-roles` for use with `borg_reassign-drone`.)_")}return n.push(""),n.push(Be()),{content:[{type:"text",text:n.join(`
8
- `)}]}}case"borg_role":{const e=await h(),t=typeof o?.role=="string"?o.role.trim():"";if(t){const{role:i}=await ce(e.sessionToken,e.apiUrl,t);return ie(t,i),{content:[{type:"text",text:[`# Role: ${i.name}`,"",i.detailed_description||"_(no detailed description set)_"].join(`
9
- `)}]}}const{role:r}=await L(e.sessionToken,e.apiUrl);return{content:[{type:"text",text:[`# Your role: ${r.name}`,"",r.detailed_description||"_(no detailed description set)_"].join(`
10
- `)}]}}case"borg_role-rationale":{const e=await h(),t=typeof o?.role=="string"?o.role:"",r=typeof o?.section=="string"?o.section:"",n=await De(e.sessionToken,e.apiUrl,t,r);return{content:[{type:"text",text:[`# Role rationale: ${n.role} \u2014 ${n.section}`,"",n.body||"_(empty)_"].join(`
11
- `)}]}}case"borg_roster":{const e=await h(),t=typeof o?.since=="string"?o.since:void 0,{drones:r,roles:n,since:i}=await de(e.sessionToken,e.apiUrl,t);return{content:[{type:"text",text:at({cubeName:e.name,drones:r,roles:n,resolvedSince:i??null,humanAgo:V})}]}}case"borg_stream-status":{const e=P(),t=await _(),r=t?q(t.cubeId,t.droneId):null,n=t?D(I()):null,i=t&&M()!=="claude",s=t?i?!0:U(r,n):null;let a="";e.runLoopHealth==="silent-inert"&&(a=`## \u26A0 SSE stream loop silent-inert \u2014 run /mcp and reconnect to restart
12
-
13
- The log-stream consumer started but never connected. This drone will not receive real-time cube events.
14
-
15
- `);const c=ot({status:e,inboxMonitorHealthy:s,inboxPath:r,monitorStateRoot:n,droneLabel:t?.droneLabel??null,cubeName:t?.name??null,humanAgo:V});return{content:[{type:"text",text:a+c}]}}case"borg_read-log":{const e=await h(),t=typeof o?.since=="string"?o.since:void 0,r=typeof o?.limit=="number"?o.limit:void 0,n=o?.unread_only===!0||o?.unread_only==="true",{entries:i,drones:s,roles:a,behind_by:c,has_more:m}=await le(e.sessionToken,e.apiUrl,{since:t,limit:r,unreadOnly:n}),p=new Map;for(const g of s)p.set(g.id,g);const f=new Map;for(const g of a)f.set(g.id,g);const l=[];if(l.push(`# Activity log: ${e.name}`),l.push(""),!i.length)l.push("_(no entries)_");else for(const g of i)l.push(Oe(g,p,f));return m===!0?(l.push(""),l.push("\u26A0 has_more: true \u2014 call `borg_read-log unread_only=true` again until has_more=false so you finish draining unread entries.")):typeof c=="number"&&c>0&&(l.push(""),l.push(`\u26A0 behind_by: ${c} more unread ${c===1?"entry":"entries"} addressed to you \u2014 call \`borg_read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`)),{content:[{type:"text",text:l.join(`
16
- `)}]}}case"borg_log":{const e=o?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await _();if(!t)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");if(It(e)){const l=await qt(t,e);if(l.suppress)return await J(t,e),{content:[{type:"text",text:`Suppressed duplicate ${l.signal?.toUpperCase()} lifecycle log for ${t.droneLabel}; recent cube log already contains this signal.`}]}}const r=Object.prototype.hasOwnProperty.call(o??{},"to"),n=r?Dt(o?.to):void 0,i=typeof o?.class=="string"?o.class:void 0,s=o?.visibility==="broadcast"||o?.visibility==="direct"?o.visibility:void 0,a={...i?{class:i}:{},...r?{to:n??[]}:{},...s?{visibility:s}:{}},c=await ue(t.sessionToken,t.apiUrl,e,a);await J(t,e);const m=c.routing?.message?`
17
- ${c.routing.message}`:"",p=c.unreachableRecipients?.length?`
18
- \u26A0 ${c.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${c.unreachableRecipients.map(l=>l.label).join(", ")}. Message delivered \u2014 they'll read it when they return.`:"";return{content:[{type:"text",text:`Logged to cube "${t.name}" as ${t.droneLabel}. (entry id: ${c.entry.id})${m}${p}`}]}}case"borg_report-friction":{const e=o?.message;if(!e||typeof e!="string")throw new Error("message is required");const t=await _();if(!t)throw new Error("Not assimilated to a cube. Use borg_assimilate <cube-name> first.");const r=o?.kind==="bug"?"bug":"friction",n=o?.metadata&&typeof o.metadata=="object"&&!Array.isArray(o.metadata)?o.metadata:void 0;return{content:[{type:"text",text:(await pe(t.sessionToken,t.apiUrl,{kind:r,message:e,metadata:n})).ok?"Report submitted \u2014 thank you. The borgmcp team will see it. (Write-only: you cannot read reports back.)":"Report did not submit. Try again, or raise it in the cube log."}]}}case"borg_reports":{const e=await me();if(e.forbidden)return{content:[{type:"text",text:"Reports triage is builder/dogfooder-tier only. Your account is not on the dogfooder (builder) tier, so the friction-reports store is not readable. (Server-enforced gate.)"}]};if(!e.reports.length)return{content:[{type:"text",text:"No reports yet. Submissions via borg_report-friction will appear here, newest first."}]};const t=e.reports.map(r=>{const n=r.metadata&&Object.keys(r.metadata).length?" \xB7 "+Object.entries(r.metadata).map(([s,a])=>`${s}=${a}`).join(", "):"",i=r.redacted?" \xB7 [secrets-scrubbed]":"";return`**[${r.kind}]** ${r.created_at} \xB7 ${r.reporter_email}${n}${i}
19
- ${r.message}`});return{content:[{type:"text",text:`Reports (${e.reports.length}, newest first):
20
-
21
- ${t.join(`
22
-
23
- ---
24
-
25
- `)}`}]}}case"borg_ack":{const e=o?.entry_id;if(!e||typeof e!="string")throw new Error("entry_id is required");const t=o?.kind==="claim"?"claim":"ack",r=await h();return await be(r.sessionToken,r.apiUrl,e,t),{content:[{type:"text",text:t==="claim"?`Claimed entry ${e} in cube "${r.name}" (advisory \u2014 merge stays keyed on REVIEW-APPROVED).`:`Acked entry ${e} in cube "${r.name}".`}]}}case"borg_decide":{const e=o?.topic,t=o?.decision;if(!e||typeof e!="string")throw new Error("topic is required");if(!t||typeof t!="string")throw new Error("decision is required");const r=typeof o?.rationale=="string"?o.rationale:void 0,n=await h(),{decision:i}=await fe(n.sessionToken,n.apiUrl,{topic:e,decision:t,...r!==void 0?{rationale:r}:{}}),s=i?.supersedes?" (superseded the prior decision on this topic)":"";return{content:[{type:"text",text:`Recorded ratified decision on "${e}" in cube "${n.name}"${s}. Cite it via borg_decisions; it surfaces in borg_regen.`}]}}case"borg_decisions":{const e=typeof o?.topic=="string"?o.topic:void 0,t=await h(),{decisions:r}=await he(t.sessionToken,t.apiUrl,e);return{content:[{type:"text",text:r.length===0?e?`No active ratified decision on "${e}" in cube "${t.name}".`:`No active ratified decisions in cube "${t.name}".`:r.map(i=>`**${i.topic}:** ${i.decision}${i.rationale?` \u2014 ${i.rationale}`:""}`).join(`
26
- `)}]}}case"borg_remove-decision":{const e=typeof o?.topic=="string"?o.topic:void 0,t=typeof o?.decision_id=="string"?o.decision_id:void 0;if(+(e!==void 0)+ +(t!==void 0)!=1)throw new Error("provide exactly one of topic or decision_id");const r=await h(),n=e!==void 0?{topic:e}:{decision_id:t},{decision:i}=await ge(r.sessionToken,r.apiUrl,n);return{content:[{type:"text",text:`Removed the active ratified decision on "${i.topic}" from cube "${r.name}".`}]}}case"borg_list-cubes":{const{cubes:e}=await _e();if(!e.length)return{content:[{type:"text",text:"No cubes yet. Use borg_create-cube to make your first one."}]};const t=e.map(r=>`- **${r.name}** (id: ${r.id})
27
- ${(r.cube_directive||"_(no directive set)_").split(`
28
- `)[0].slice(0,120)}`);return{content:[{type:"text",text:`Your cubes (${e.length}):
29
-
30
- ${t.join(`
31
-
32
- `)}`}]}}case"borg_create-cube":{const e=o?.name,t=o?.cube_directive,r=o?.template;if(!e)throw new Error("name is required");if(t===void 0)throw new Error("cube_directive is required (pass empty string if none)");let n=null;if(r&&(n=S(r),!n))throw new Error(`Unknown template "${r}". Available: ${T().join(", ")}`);const i=Me(t,n),s=je(void 0,n),a=await ye(e,i,{message_taxonomy:s});if(n){const m=await Y(a.id,n),p=i!==t?" Template cube directive applied (operator passed empty).":"";return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}) with template **${r}** applied \u2014 ${m.created} role(s) created, ${m.updated} updated.${p} Use borg_assimilate ${a.name} to join as a drone.`}]}}return{content:[{type:"text",text:`Created cube **${a.name}** (id: ${a.id}). A default "Drone" role was seeded \u2014 rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${a.name} to join as a drone.`}]}}case"borg_update-cube":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const t={};if(typeof o?.name=="string"&&(t.name=o.name),typeof o?.cube_directive=="string"&&(t.cube_directive=o.cube_directive),Array.isArray(o?.message_taxonomy)&&(t.message_taxonomy=o.message_taxonomy),Object.keys(t).length===0)throw new Error("Pass at least one of: name, cube_directive, message_taxonomy.");const{cube:r}=await B(e,t);return{content:[{type:"text",text:`Updated cube **${r.name}** (id: ${r.id}).`}]}}case"borg_patch-taxonomy-class":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const t=o?.action;if(t!=="add"&&t!=="replace"&&t!=="remove")throw new Error("action must be one of: add, replace, remove.");let r,n;if(t==="remove"){const s=o?.class;if(!s)throw new Error("class is required for remove.");({cube:r}=await F(e,{action:t,class:s})),n=s}else{const s=o?.class_def;if(s==null||typeof s!="object"||Array.isArray(s))throw new Error("class_def (object) is required for add/replace.");({cube:r}=await F(e,{action:t,class_def:s})),n=String(s.class??"")}return{content:[{type:"text",text:`${t==="add"?"Added":t==="replace"?"Replaced":"Removed"} taxonomy class **${n}** in cube **${r.name}** (id: ${r.id}).`}]}}case"borg_delete-cube":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");return await we(e),{content:[{type:"text",text:`Deleted cube ${e} (and all its roles, drones, log entries).`}]}}case"borg_create-role":{const e=o?.cube_id,t=o?.name,r=o?.short_description,n=o?.detailed_description;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("name is required");if(r===void 0)throw new Error("short_description is required (pass empty string if none)");if(n===void 0)throw new Error("detailed_description is required (pass empty string if none)");const i=o?.is_default===!0,s=o?.is_mandatory===!0,a=o?.is_human_seat===!0,c=o?.can_broadcast===!0,m=o?.receives_all_direct===!0,{role:p}=await xe(e,{name:t,short_description:r,detailed_description:n,is_default:i,is_mandatory:s,is_human_seat:a,can_broadcast:c,receives_all_direct:m,...typeof o?.default_model=="string"?{default_model:o.default_model}:{}}),f=[p.role_class==="queen"?"Queen":null,p.is_human_seat?"human-seat":null,p.is_default?"default":null,p.is_mandatory?"mandatory":null].filter(Boolean).join(", "),l=f?` (${f})`:"";return{content:[{type:"text",text:`Created role **${p.name}**${l} (id: ${p.id}) in cube ${e}.`}]}}case"borg_update-role":{const e=o?.role_id;if(!e)throw new Error("role_id is required");const t={};if(typeof o?.name=="string"&&(t.name=o.name),typeof o?.short_description=="string"&&(t.short_description=o.short_description),typeof o?.detailed_description=="string"&&(t.detailed_description=o.detailed_description),typeof o?.is_default=="boolean"&&(t.is_default=o.is_default),typeof o?.is_mandatory=="boolean"&&(t.is_mandatory=o.is_mandatory),typeof o?.is_human_seat=="boolean"&&(t.is_human_seat=o.is_human_seat),typeof o?.can_broadcast=="boolean"&&(t.can_broadcast=o.can_broadcast),typeof o?.receives_all_direct=="boolean"&&(t.receives_all_direct=o.receives_all_direct),typeof o?.default_model=="string"&&(t.default_model=o.default_model),Object.keys(t).length===0)throw new Error("Pass at least one of: name, short_description, detailed_description, is_default, is_mandatory, is_human_seat, can_broadcast, receives_all_direct.");const{role:r}=await $e(e,t),n=[r.role_class==="queen"?"Queen":null,r.is_human_seat?"human-seat":null,r.is_default?"default":null,r.is_mandatory?"mandatory":null].filter(Boolean).join(", "),i=n?` (${n})`:"";return{content:[{type:"text",text:`Updated role **${r.name}**${i} (id: ${r.id}).`}]}}case"borg_patch-role-section":{const e=o?.role_id;if(!e)throw new Error("role_id is required");const t=o?.action;if(t!=="replace"&&t!=="insert"&&t!=="delete")throw new Error("action must be one of: replace, insert, delete.");const r=o?.heading;if(!r)throw new Error("heading is required");let n;if(t==="delete")({role:n}=await C(e,{action:t,heading:r}));else{const s=o?.body;if(typeof s!="string")throw new Error("body is required for replace/insert (pass empty string for an empty section).");if(t==="insert"){const a=typeof o?.after=="string"?o.after:null;({role:n}=await C(e,{action:t,heading:r,body:s,after:a}))}else({role:n}=await C(e,{action:t,heading:r,body:s}))}return{content:[{type:"text",text:`${t==="replace"?"Replaced":t==="insert"?"Inserted":"Deleted"} section **${r}** in role **${n.name}** (id: ${n.id}).`}]}}case"borg_delete-role":{const e=o?.role_id;if(!e)throw new Error("role_id is required");return await ve(e),{content:[{type:"text",text:`Deleted role ${e}.`}]}}case"borg_reassign-drone":{const e=o?.drone_id,t=o?.role_id;if(!e)throw new Error("drone_id is required");if(!t)throw new Error("role_id is required");const{drone:r}=await ke(e,t);return{content:[{type:"text",text:`Reassigned drone ${r.label} (${r.id}) to role ${r.role_id}.`}]}}case"borg_evict-drone":{const e=o?.drone_id?.trim(),t=o?.label?.trim(),r=o?.cube_id?.trim();let n,i;if(e){if(!dt(e))throw new Error(`drone_id "${e}" is not a UUID \u2014 if that's a drone label, pass it as label + cube_id instead.`);n=e,i=e}else if(t){if(!r)throw new Error("cube_id is required when evicting by label");const{drones:s}=await $(r),a=ct(s,t);if(!a)throw new Error(`No active drone labelled "${t}" in cube ${r} (it may already be evicted; check borg_list-drones).`);n=a.id,i=a.label}else throw new Error("Provide drone_id, or label + cube_id, to identify the drone to evict");return await Re(n),{content:[{type:"text",text:`Evicted drone ${i} (${n}). Soft-deleted: removed from the roster and freed its seat; log history preserved with anonymized attribution.`}]}}case"borg_list-drones":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const{drones:t,roles:r}=await $(e);if(!t.length)return{content:[{type:"text",text:"No drones in this cube yet."}]};const n=new Map(r.map(s=>[s.id,s])),i=t.map(s=>{const a=n.get(s.role_id),c=st(a?.name??"?",s.agent_kind),m=s.wake_path_alert_class&&s.wake_path_alert_class!=="independent"?` \u2014 wake-path-class: ${s.wake_path_alert_class}`:"",p=s.reported_model?` \u2014 Reported model: ${s.reported_model}`:" \u2014 Reported model: not reported",f=it(s),l=f?` \u2014 ${f}`:"";return`- **${s.label}** (id: ${s.id}) \u2014 role: ${c} (${s.role_id}) \u2014 last seen ${s.last_seen}${m}${p}${l}`});return{content:[{type:"text",text:`Drones in cube ${e} (${t.length}):
33
-
34
- ${i.join(`
35
- `)}`}]}}case"borg_list-roles":{const e=o?.cube_id;if(!e)throw new Error("cube_id is required");const{roles:t}=await $(e);return{content:[{type:"text",text:Xe(t,e)}]}}case"borg_list-templates":return{content:[{type:"text",text:`Available templates:
36
-
37
- ${T().map(r=>{const n=S(r);return`- **${r}**: ${n.description}`}).join(`
38
- `)}`}]};case"borg_sync-roles":{const e=o?.cube_id,t=o?.template_name||"software-dev",r=o?.apply===!0,n=o?.decisions&&typeof o.decisions=="object"?o.decisions:void 0;if(!e)throw new Error("cube_id is required");const i=await Te(e,t,r,n);return{content:[{type:"text",text:wt(i,t)}]}}case"borg_apply-template":{const e=o?.cube_id,t=o?.template_name;if(!e)throw new Error("cube_id is required");if(!t)throw new Error("template_name is required");const r=S(t);if(!r)throw new Error(`Unknown template "${t}". Available: ${T().join(", ")}`);const n=await Y(e,r);let i="";const s=await $(e),a=Ne(s.cube_directive,r);return a!==null&&(await B(e,{cube_directive:a}),i=" Template cube directive applied (cube directive was empty)."),{content:[{type:"text",text:`Applied template **${t}** to cube ${e} \u2014 ${n.created} role(s) created, ${n.updated} updated.${i}`}]}}default:throw new Error(`Unknown tool: ${u}`)}}catch(e){if(e instanceof ut)return{content:[{type:"text",text:mt(e.message)}],isError:!0};if(e instanceof pt)return{content:[{type:"text",text:bt(e.message)}],isError:!0};const t=lt(e??{});return t?{content:[{type:"text",text:t}],isError:!0}:{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}),y.setRequestHandler(ne,async()=>({prompts:[{name:"borg_subscribe",description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial."},{name:"dashboard",description:"Open Borg MCP dashboard to manage cubes"}]})),y.setRequestHandler(se,async d=>{const{name:u}=d.params;switch(u){case"borg_subscribe":return{description:"Set up Borg MCP Cube tier subscription ($1/month per cube; each cube adds 8 pooled agent sessions + 1000 req/hr). Free tier is permanent (1 cube + 3 agent sessions + 100 req/hr); no trial.",messages:[{role:"user",content:{type:"text",text:"Please help me set up a Borg MCP subscription using the subscribe tool."}}]};case"dashboard":return{description:"Open Borg MCP dashboard to manage cubes",messages:[{role:"user",content:{type:"text",text:"Please open the Borg MCP dashboard using the borg_open_dashboard tool."}}]};default:throw new Error(`Unknown prompt: ${u}`)}});const G=new te;await y.connect(G),await xt(),console.error(`${k()}\u25FC Borg MCP Client started`),console.error(`${k()}\u25FC Use borg_assimilate <cube-name> to join a cube as a drone`),console.error(`${k()}\u25FC Manage your cubes at https://borgmcp.ai/dashboard`)}Mt().catch(b=>{console.error(`${k()}Fatal error:`,b),process.exit(1)});
2
+ /**
3
+ * Borg MCP Client - Main Entry Point
4
+ *
5
+ * stdio MCP server that:
6
+ * 1. Connects to Claude Code via stdio transport
7
+ * 2. Proxies MCP tools to a verified local (self-hosted) Borg server
8
+ * 3. Provides the borg: cube tool surface (assimilate / cube / role /
9
+ * roster / read-log) so Claude can act as a Drone in a hive of
10
+ * collaborating sessions.
11
+ */
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
15
+ import { assertRoleMatches } from './role-match.js';
16
+ import { getCubeInfo, getRoleInfo, getRoleInfoByName, getRoster, readLog, appendLog, ackLogEntry, recordDecision, removeDecision, listDecisions, regen, listCubes, createCube, updateCube, deleteCube, createRole, updateRole, patchRoleSection, patchTaxonomyClass, deleteRole, getCube, listRoles, syncRoles, applyTemplate, whoami, roleRationale, } from './remote-client.js';
17
+ import { getTemplate, listTemplateNames, resolveCubeDirectiveForCreate, resolveCubeDirectiveForApply, resolveMessageTaxonomyForCreate, } from 'borgmcp-shared/templates';
18
+ import { activeCubeWithFreshRegenIdentity, getActiveCube, refreshActiveCubeMetadata, findProjectRoot, inboxPathForDrone, } from './cubes.js';
19
+ import { isEntryInvocation, monitorStateRootForWorktree } from './inbox-monitor.js';
20
+ import { addSessionStartHook, addUserPromptSubmitHook } from './config-utils.js';
21
+ import { humanAgo, formatLogEntryMarkdown, formatRegenMarkdown, getDronePlaybook, getDronePlaybookChapter, nullTaxonomyTip, regenWakePathDroneLabel, } from './regen-format.js';
22
+ import { startLogStream, getStreamStatus } from './log-stream.js';
23
+ import { isMcpReadinessProbe } from './readiness-probe.js';
24
+ import { runMcpStartupServices } from './startup-services.js';
25
+ import { TOOL_MANIFEST } from './tool-manifest.js';
26
+ import { DOCS_SECTIONS, matchDocsSections, formatDocsIndex } from './docs-sections.js';
27
+ import { renderRoleList } from './list-roles-render.js';
28
+ import { filterToolsForRole } from './tool-scope.js';
29
+ import { getPackageVersion, getOnDiskVersion, handleVersionFlag } from './version.js';
30
+ import { renderStreamStatus, checkInboxMonitorHealthy, formatWakePathPrefix, shouldShowWakePathWarning, } from './stream-status.js';
31
+ import { formatRoleAgentLabel, formatWorkingRepoLabel, renderRoster } from './roster-render.js';
32
+ import { resolveWorkingRepo } from './working-repo.js';
33
+ import { DroneEvictedError, formatEvictedToolResult, } from './drone-lifecycle.js';
34
+ import { classifyInSessionAssimilate, reattachOnlyRefusal, reattachFailureMessage, } from './assimilate-guard.js';
35
+ import { gateAllowsActivation, borgSessionToolNotice } from './launch-gate.js';
36
+ import { renderSyncRolesResult } from './sync-roles-render.js';
37
+ import { initConsolePrefix, consolePrefix } from './console-prefix.js';
38
+ import { resolveSessionAgentKind, } from './codex-app-wake.js';
39
+ import { connectOpenCodeDrone, injectOpenCodeEntry, computeOpenCodePort, } from './opencode-drone.js';
40
+ import { installBorgPlugin } from './opencode-plugin.js';
41
+ import { setModuleInjectOpenCode } from './log-stream.js';
42
+ import { lifecycleSignalForMessage, recordLifecycleLog, shouldSuppressLifecycleLog, } from './lifecycle-log-guard.js';
43
+ import { normalizeDirectLogRecipients, } from './direct-log.js';
44
+ import { formatLocalManageToolResult } from './local-manage-tool-result.js';
45
+ import { runEvictDroneTool, runReassignDroneTool, } from './drone-management.js';
46
+ /**
47
+ * Apply a template's roles + message_taxonomy to a cube.
48
+ *
49
+ * gh#473 PR2 — delegates to the NON-CLOBBERING server route. New roles
50
+ * are inserted; existing template-named roles get ADD fragments (template
51
+ * sections/classes the cube lacks) auto-applied, but EVOLVED (conflicting)
52
+ * fragments are surfaced server-side and KEPT — never silently
53
+ * overwritten. The old per-role blanket `updateRole`/whole-taxonomy
54
+ * `updateCube` overwrite path is removed. Operators who want to take the
55
+ * template version of a conflicting fragment use `borg_sync-roles` with a
56
+ * `decisions` map. Returns `{ created, updated }` for the caller's toast.
57
+ */
58
+ async function applyTemplateToCube(cubeId, template) {
59
+ return await applyTemplate(cubeId, template.name);
60
+ }
61
+ /**
62
+ * Throw a friendly error if the client has not been assimilated to a cube.
63
+ */
64
+ async function requireActiveCube() {
65
+ const active = await getActiveCube();
66
+ if (!active) {
67
+ throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
68
+ }
69
+ return active;
70
+ }
71
+ /**
72
+ * Main entry point - MCP stdio server
73
+ */
74
+ export async function main() {
75
+ // Honor `--version` / `-v` BEFORE any side-effecting work (hooks,
76
+ // readiness checks, stream consumer spawn, MCP handshake). Lets
77
+ // operators run `borg-mcp --version` cleanly to confirm the
78
+ // installed client version.
79
+ handleVersionFlag();
80
+ const readinessProbe = isMcpReadinessProbe();
81
+ await runMcpStartupServices(readinessProbe, {
82
+ // Auto-register the SessionStart hook so existing users get borg-regen
83
+ // auto-orientation on session start without re-running borg setup. Idempotent.
84
+ sessionStartHook: () => {
85
+ addSessionStartHook();
86
+ },
87
+ // Auto-register the UserPromptSubmit audit hook so the drone gets a
88
+ // nudge if the previous assistant span used state-changing tools
89
+ // without calling borg_log. Domain-agnostic — knows nothing about git
90
+ // or any specific convention. Idempotent.
91
+ auditHook: () => {
92
+ addUserPromptSubmitHook();
93
+ },
94
+ // Spawn the SSE log-stream consumer. This gives drones real-time
95
+ // wakeup: when another drone posts to the cube, the worker pushes
96
+ // an `event: log` over SSE, the consumer appends one line to the
97
+ // per-drone inbox file (see inboxPathForDrone in cubes.ts), and the
98
+ // launcher's Monitor wakes the active /loop iteration immediately.
99
+ // Same inbox-file shape as the prior long-poll path — the file is
100
+ // still the harness-side wake primitive — only the wire layer
101
+ // changed. See:
102
+ // docs/superpowers/specs/2026-05-11-server-push-log-subscription.md
103
+ // Failure here is non-fatal — the launcher's fallback heartbeat
104
+ // still keeps things moving.
105
+ sseStream: () => {
106
+ startLogStream();
107
+ },
108
+ // gh#opencode: initialize opencode drone module for autonomous entry
109
+ // injection. Installs the plugin (idempotent) and connects the SDK client
110
+ // when the runtime is opencode (BORG_OPENCODE=1). The module-level
111
+ // injectOpenCode hook routes SSE entries into the TUI's session.
112
+ // Best-effort; never breaks the MCP server.
113
+ openCode: async () => {
114
+ installBorgPlugin();
115
+ const active = await getActiveCube();
116
+ if (active && process.env.BORG_OPENCODE === '1') {
117
+ const port = computeOpenCodePort(active.droneId);
118
+ const serverUrl = `http://127.0.0.1:${port}`;
119
+ await connectOpenCodeDrone({
120
+ serverUrl,
121
+ directory: process.cwd(),
122
+ droneLabel: active.droneLabel,
123
+ cubeName: active.name,
124
+ });
125
+ setModuleInjectOpenCode(injectOpenCodeEntry);
126
+ }
127
+ },
128
+ });
129
+ // Create MCP server. `version` is the installed borgmcp version
130
+ // (T1.4 of 0.6.0): read at runtime from package.json so Claude
131
+ // Code's `/mcp` view shows the real version instead of the
132
+ // long-standing hardcoded "0.1.0".
133
+ const server = new Server({
134
+ name: 'borg-mcp-client',
135
+ version: getPackageVersion(),
136
+ }, {
137
+ capabilities: {
138
+ tools: {},
139
+ prompts: {},
140
+ },
141
+ });
142
+ // gh#899: tool definitions built once at setup, then role-scoped per caller
143
+ // in the ListTools handler below (the dispatcher reaches deferred tools).
144
+ const allToolDefs = TOOL_MANIFEST;
145
+ // Register tool listing — role-scope the native surface (gh#899). Missing role
146
+ // (old cubes.json / pre-assimilate) → full set; deferred tools stay reachable
147
+ // via borg_tool. Never an auth boundary — live per-client cube grants govern.
148
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
149
+ let scope = null;
150
+ try {
151
+ const active = await getActiveCube();
152
+ if (active) {
153
+ scope = { roleName: active.roleName, roleClass: active.roleClass, isHumanSeat: active.isHumanSeat };
154
+ }
155
+ }
156
+ catch {
157
+ scope = null; // unreadable cube state → full set (fail-safe)
158
+ }
159
+ return { tools: filterToolsForRole(allToolDefs, scope) };
160
+ });
161
+ // Register tool execution handler
162
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
163
+ let { name, arguments: args } = request.params;
164
+ // gh#899: borg_describe-tool — schema-only, NEVER executes. Returns the
165
+ // named tool's def from allToolDefs so a role-scoped session can learn a
166
+ // deferred tool's arguments before invoking it via borg_tool.
167
+ if (name === 'borg_describe-tool') {
168
+ const target = typeof args?.name === 'string' ? args.name : '';
169
+ const def = allToolDefs.find((t) => t.name === target);
170
+ if (!def) {
171
+ return {
172
+ content: [{ type: 'text', text: `Unknown borg tool: ${target || '(none)'}. Pass { name: "<borg_tool>" }.` }],
173
+ isError: true,
174
+ };
175
+ }
176
+ return {
177
+ content: [{ type: 'text', text: JSON.stringify({ name: def.name, description: def.description, inputSchema: def.inputSchema }, null, 2) }],
178
+ };
179
+ }
180
+ // gh#899: borg_tool dispatcher — unwrap to the inner tool and fall through
181
+ // to the SAME switch below (identical activation gate + per-tool auth/Zod
182
+ // validation; no weaker entry — the server's live cube-grant boundary is
183
+ // unchanged). Guards against dispatcher self-reference / recursion.
184
+ if (name === 'borg_tool') {
185
+ const inner = typeof args?.name === 'string' ? args.name : '';
186
+ if (!inner || inner === 'borg_tool' || inner === 'borg_describe-tool') {
187
+ return {
188
+ content: [{ type: 'text', text: 'borg_tool: pass { name: "<borg_tool>", arguments: {...} } naming a real borg tool (not the dispatcher itself).' }],
189
+ isError: true,
190
+ };
191
+ }
192
+ args = (args?.arguments && typeof args.arguments === 'object')
193
+ ? args.arguments
194
+ : {};
195
+ name = inner;
196
+ }
197
+ // gh#673 P1 (WI-5): the borg tool surface activates only in
198
+ // borg-launched sessions — BOTH harnesses ride the same gate. A
199
+ // vanilla `claude`/`codex` gets a non-silent re-launch notice per
200
+ // tool, never a half-activated, un-wakeable drone; ListTools stays
201
+ // intact so the agent learns WHY. Claude children inherit
202
+ // BORG_SESSION from the wrapper's launchEnv; codex children receive
203
+ // it via the per-launch `-c mcp_servers.borg.env.BORG_SESSION="1"`
204
+ // override (codexBorgSessionConfigArgs — inherited env never reaches
205
+ // codex MCP children, V2/V2b probes). ACTIVATION-only per the SR
206
+ // binding — never a security decision (server auth unchanged).
207
+ if (!gateAllowsActivation(`tool ${name}`)) {
208
+ return {
209
+ content: [{ type: 'text', text: borgSessionToolNotice(name) }],
210
+ isError: true,
211
+ };
212
+ }
213
+ try {
214
+ switch (name) {
215
+ case 'borg_regen': {
216
+ const active = await getActiveCube();
217
+ if (!active) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: 'Not connected to a cube. Use `borg_assimilate cube_name="<name>"` to join one.',
223
+ },
224
+ ],
225
+ };
226
+ }
227
+ const since = typeof args?.since === 'string' ? args.since : undefined;
228
+ const mode = args?.mode === 'lite' ? 'lite' : 'full';
229
+ const reportedModel = typeof args?.model === 'string' ? args.model : undefined;
230
+ const result = await regen(active.sessionToken, active.apiUrl, {
231
+ since,
232
+ reportedModel,
233
+ workingRepo: resolveWorkingRepo(),
234
+ serverTrustIdentity: active.serverTrustIdentity,
235
+ });
236
+ const freshActive = activeCubeWithFreshRegenIdentity(active, result);
237
+ if (freshActive !== active) {
238
+ await refreshActiveCubeMetadata(freshActive);
239
+ }
240
+ // Wake-path self-heal (gh#43): SSE delivery to the inbox file
241
+ // is independent from Claude Code waking on file writes. The
242
+ // latter requires a `tail -F` Monitor against the inbox path;
243
+ // if that Monitor dies (or was never armed across a session
244
+ // boundary), the drone misses every incoming entry until the
245
+ // /loop fallback heartbeat. Because regen runs on every /loop
246
+ // iteration, surfacing the breakage here gives self-healing at
247
+ // worst-case latency = the heartbeat interval. Mirrors the
248
+ // State-5 self-arm instruction in stream-status.ts.
249
+ const streamStatus = getStreamStatus();
250
+ const inboxPath = inboxPathForDrone(freshActive.cubeId, freshActive.droneId);
251
+ const monitorStateRoot = monitorStateRootForWorktree(findProjectRoot());
252
+ // Non-Claude CLIs do not use the Claude inbox Monitor. Keep the
253
+ // agent CLI distinction independent from whether Codex's optional
254
+ // remote-wake transport is currently armed.
255
+ const agentKind = resolveSessionAgentKind();
256
+ const inboxMonitorHealthy = agentKind === 'claude'
257
+ ? checkInboxMonitorHealthy(inboxPath, monitorStateRoot)
258
+ : true;
259
+ const prefix = shouldShowWakePathWarning(streamStatus, inboxMonitorHealthy)
260
+ ? formatWakePathPrefix({
261
+ inboxPath,
262
+ monitorStateRoot,
263
+ droneLabel: regenWakePathDroneLabel(result, freshActive.droneLabel),
264
+ cubeName: freshActive.name,
265
+ })
266
+ : '';
267
+ // gh#285: version-mismatch nudge when on-disk package is newer.
268
+ let versionHeader = '';
269
+ try {
270
+ const running = getPackageVersion();
271
+ const onDisk = getOnDiskVersion();
272
+ if (running !== 'unknown' && onDisk !== 'unknown' && onDisk !== running) {
273
+ const [rMaj, rMin, rPat] = running.split('.').map(Number);
274
+ const [dMaj, dMin, dPat] = onDisk.split('.').map(Number);
275
+ const onDiskNewer = dMaj > rMaj || (dMaj === rMaj && dMin > rMin) || (dMaj === rMaj && dMin === rMin && dPat > rPat);
276
+ if (onDiskNewer) {
277
+ versionHeader = `## 🔄 borgmcp ${onDisk} installed — run /mcp and reconnect (or restart Claude Code) to apply. Currently running ${running}.\n\n`;
278
+ }
279
+ }
280
+ }
281
+ catch { /* never break regen */ }
282
+ return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(result, { mode }) }] };
283
+ }
284
+ case 'borg_assimilate': {
285
+ const cubeName = args?.cube_name;
286
+ if (!cubeName)
287
+ throw new Error('cube_name is required');
288
+ // gh#780 (Queen ruling 33a62d94): RE-ATTACH-ONLY. The old handler
289
+ // used the retired attach path, which always minted a new drone row —
290
+ // so agents "recovering" from auth blips spawned orphan seats.
291
+ // This tool now re-attaches to the worktree's saved identity or
292
+ // refuses with CLI guidance; it is structurally incapable of
293
+ // minting. Seat creation lives in the CLI (`borg assimilate`).
294
+ const active = await getActiveCube();
295
+ const decision = classifyInSessionAssimilate(active, cubeName);
296
+ if (decision.kind !== 'reattach') {
297
+ return {
298
+ content: [{ type: 'text', text: reattachOnlyRefusal(decision, cubeName) }],
299
+ isError: true,
300
+ };
301
+ }
302
+ // Re-attach = serve the saved identity through the
303
+ // server-validated regen path (SR cond-4: no fabricated success
304
+ // — an evicted/revoked seat FAILS server-side and is surfaced).
305
+ try {
306
+ const result = await regen(active.sessionToken, active.apiUrl, {
307
+ workingRepo: resolveWorkingRepo(),
308
+ serverTrustIdentity: active.serverTrustIdentity,
309
+ });
310
+ const freshActive = activeCubeWithFreshRegenIdentity(active, result);
311
+ if (freshActive !== active) {
312
+ await refreshActiveCubeMetadata(freshActive);
313
+ }
314
+ const header = [
315
+ `# Re-attached to cube: ${freshActive.name}`,
316
+ ``,
317
+ `**Drone label:** ${freshActive.droneLabel}`,
318
+ `**Seat:** existing identity reused — no new drone minted (gh#780)`,
319
+ ``,
320
+ ``,
321
+ ].join('\n');
322
+ return {
323
+ content: [{ type: 'text', text: header + formatRegenMarkdown(result, { mode: 'full' }) }],
324
+ };
325
+ }
326
+ catch (err) {
327
+ const failure = reattachFailureMessage(err ?? {});
328
+ return { content: [{ type: 'text', text: failure }], isError: true };
329
+ }
330
+ }
331
+ case 'borg_version': {
332
+ return { content: [{ type: 'text', text: `borgmcp ${getPackageVersion()}` }] };
333
+ }
334
+ case 'borg_playbook': {
335
+ // gh#912: serve the on-demand operating-playbook chapter (static
336
+ // client-side text — no cube/auth needed; the rule-spine is already
337
+ // inline in regen). Lets the bootstrap regen stay light.
338
+ return { content: [{ type: 'text', text: getDronePlaybookChapter() }] };
339
+ }
340
+ case 'borg_docs': {
341
+ // gh#docs-site B: return the docs index (URLs + summaries) so the agent
342
+ // can WebFetch the right page. No server-side fetch, no search service —
343
+ // the topic match is a lazy keyword filter over DOCS_SECTIONS.
344
+ const topic = typeof args?.topic === 'string' ? args.topic.trim() : '';
345
+ const matched = topic ? matchDocsSections(topic) : [];
346
+ const sections = matched.length > 0 ? matched : DOCS_SECTIONS;
347
+ const header = topic && matched.length > 0
348
+ ? `Best-matching docs section(s) for "${topic}" — WebFetch the URL for the full page:`
349
+ : topic
350
+ ? `No exact match for "${topic}". Full Borg MCP docs index — WebFetch the URL you need:`
351
+ : `Borg MCP docs index — WebFetch the URL of the section you need:`;
352
+ return { content: [{ type: 'text', text: `${header}\n\n${formatDocsIndex(sections)}` }] };
353
+ }
354
+ case 'borg_whoami': {
355
+ const active = await requireActiveCube();
356
+ const result = await whoami(active.sessionToken, active.apiUrl, active.serverTrustIdentity);
357
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
358
+ }
359
+ case 'borg_cube': {
360
+ // No-cache invariant (T1.2 — 0.7.0): both `getCubeInfo` and
361
+ // `getRoleInfo` MUST fetch fresh per invocation. Never
362
+ // memoize / cache the cube or role payload in process memory
363
+ // or in `cubes.json` — `borg_reassign-drone` changes the
364
+ // calling drone's role in the DB, and subsequent
365
+ // `borg_cube` reads MUST reflect the new role within one
366
+ // round-trip. Locked in by the regression probe at
367
+ // `client/__tests__/integration/01-role-cache-freshness.integration.ts`;
368
+ // a future refactor that introduces caching here will fail
369
+ // that probe. See drone-8's 19:38:56 observation + drone-6's
370
+ // 06:00:31 finding for the original incident trace.
371
+ const active = await requireActiveCube();
372
+ const [{ cube, roles }] = await Promise.all([
373
+ getCubeInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity),
374
+ getRoleInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity),
375
+ ]);
376
+ const lines = [];
377
+ lines.push(`# Cube: ${cube.name}`);
378
+ lines.push('');
379
+ lines.push('## Cube directive');
380
+ lines.push(cube.cube_directive || '_(none)_');
381
+ lines.push('');
382
+ const taxonomyTip = nullTaxonomyTip(cube.message_taxonomy);
383
+ if (taxonomyTip) {
384
+ lines.push(taxonomyTip);
385
+ lines.push('');
386
+ }
387
+ lines.push('## Roles in this cube');
388
+ if (!roles.length) {
389
+ lines.push('_(no roles defined)_');
390
+ }
391
+ else {
392
+ for (const r of roles) {
393
+ const tags = [
394
+ r.role_class === 'queen' ? 'Queen' : null,
395
+ r.is_human_seat ? 'human-seat' : null,
396
+ r.is_default ? 'default' : null,
397
+ r.is_mandatory ? 'mandatory' : null,
398
+ ].filter(Boolean).join(', ');
399
+ const marker = tags ? ` (${tags})` : '';
400
+ const desc = r.short_description || '_(no description)_';
401
+ lines.push(`- **${r.name}**${marker} — ${desc}`);
402
+ }
403
+ // Sprint 6 / gh#153 discoverability nudge per drone-7 UX-FEEDBACK:
404
+ // point Coordinator-class readers at the tool that surfaces role IDs.
405
+ lines.push('');
406
+ lines.push('_(Coordinator-class drones can fetch role IDs via `borg_list-roles` for use with `borg_reassign-drone`.)_');
407
+ }
408
+ lines.push('');
409
+ lines.push(getDronePlaybook());
410
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
411
+ }
412
+ case 'borg_role': {
413
+ // No-cache invariant (T1.2 — 0.7.0): role reads MUST fetch
414
+ // fresh per invocation. See 01-role-cache-freshness.integration.ts.
415
+ const active = await requireActiveCube();
416
+ const requested = typeof args?.role === 'string' ? args.role.trim() : '';
417
+ if (requested) {
418
+ const { role } = await getRoleInfoByName(active.sessionToken, active.apiUrl, requested, active.serverTrustIdentity);
419
+ assertRoleMatches(requested, role);
420
+ const text = [
421
+ `# Role: ${role.name}`,
422
+ ``,
423
+ role.detailed_description || '_(no detailed description set)_',
424
+ ].join('\n');
425
+ return { content: [{ type: 'text', text }] };
426
+ }
427
+ const { role } = await getRoleInfo(active.sessionToken, active.apiUrl, active.serverTrustIdentity);
428
+ const text = [
429
+ `# Your role: ${role.name}`,
430
+ ``,
431
+ role.detailed_description || '_(no detailed description set)_',
432
+ ].join('\n');
433
+ return { content: [{ type: 'text', text }] };
434
+ }
435
+ case 'borg_role-rationale': {
436
+ const active = await requireActiveCube();
437
+ const role = typeof args?.role === 'string' ? args.role : '';
438
+ const section = typeof args?.section === 'string' ? args.section : '';
439
+ const result = await roleRationale(active.sessionToken, active.apiUrl, role, section, active.serverTrustIdentity);
440
+ const text = [
441
+ `# Role rationale: ${result.role} — ${result.section}`,
442
+ '',
443
+ result.body || '_(empty)_',
444
+ ].join('\n');
445
+ return { content: [{ type: 'text', text }] };
446
+ }
447
+ case 'borg_roster': {
448
+ const active = await requireActiveCube();
449
+ const since = typeof args?.since === 'string' ? args.since : undefined;
450
+ const { drones, roles, since: resolvedSince } = await getRoster(active.sessionToken, active.apiUrl, since, active.serverTrustIdentity);
451
+ const text = renderRoster({
452
+ cubeName: active.name,
453
+ drones,
454
+ roles,
455
+ resolvedSince: resolvedSince ?? null,
456
+ humanAgo,
457
+ });
458
+ return { content: [{ type: 'text', text }] };
459
+ }
460
+ case 'borg_stream-status': {
461
+ // Probe the in-process SSE consumer state. Does NOT require
462
+ // an active cube — if the consumer hasn't started or is
463
+ // between cubes, the snapshot still reports current values.
464
+ // Also probes wake-path completeness (T1.2): is anyone tailing
465
+ // the inbox file? Without that, SSE delivery still works but
466
+ // the harness `/loop` never wakes on the file write.
467
+ const status = getStreamStatus();
468
+ const active = await getActiveCube();
469
+ const inboxPath = active
470
+ ? inboxPathForDrone(active.cubeId, active.droneId)
471
+ : null;
472
+ const monitorStateRoot = active
473
+ ? monitorStateRootForWorktree(findProjectRoot())
474
+ : null;
475
+ // Non-Claude CLIs have their own wake mechanism, so this Claude-only
476
+ // Monitor diagnostic must not be inferred from Codex transport state.
477
+ const nonClaudeSession = active && resolveSessionAgentKind() !== 'claude';
478
+ const inboxMonitorHealthy = active
479
+ ? nonClaudeSession
480
+ ? true
481
+ : checkInboxMonitorHealthy(inboxPath, monitorStateRoot)
482
+ : null;
483
+ let silentInertWarning = '';
484
+ if (status.runLoopHealth === 'silent-inert') {
485
+ silentInertWarning = '## ⚠ SSE stream loop silent-inert — run /mcp and reconnect to restart\n\n' +
486
+ 'The log-stream consumer started but never connected. This drone will not receive real-time cube events.\n\n';
487
+ }
488
+ const text = renderStreamStatus({
489
+ status,
490
+ inboxMonitorHealthy,
491
+ inboxPath,
492
+ monitorStateRoot,
493
+ droneLabel: active?.droneLabel ?? null,
494
+ cubeName: active?.name ?? null,
495
+ humanAgo,
496
+ });
497
+ return { content: [{ type: 'text', text: silentInertWarning + text }] };
498
+ }
499
+ case 'borg_read-log': {
500
+ const active = await requireActiveCube();
501
+ const since = typeof args?.since === 'string' ? args.since : undefined;
502
+ const limit = typeof args?.limit === 'number' ? args.limit : undefined;
503
+ const unreadOnly = args?.unread_only === true || args?.unread_only === 'true';
504
+ const { entries, drones, roles, behind_by, has_more } = await readLog(active.sessionToken, active.apiUrl, {
505
+ since,
506
+ limit,
507
+ unreadOnly,
508
+ serverTrustIdentity: active.serverTrustIdentity,
509
+ });
510
+ const droneById = new Map();
511
+ for (const d of drones)
512
+ droneById.set(d.id, d);
513
+ const roleById = new Map();
514
+ for (const r of roles)
515
+ roleById.set(r.id, r);
516
+ const lines = [];
517
+ lines.push(`# Activity log: ${active.name}`);
518
+ lines.push('');
519
+ if (!entries.length) {
520
+ lines.push('_(no entries)_');
521
+ }
522
+ else {
523
+ for (const e of entries) {
524
+ lines.push(formatLogEntryMarkdown(e, droneById, roleById));
525
+ }
526
+ }
527
+ // gh#709 part B: nudge a behind drone to drain. `behind_by` is the
528
+ // count of entries VISIBLE to this drone still unread AFTER this read
529
+ // advanced the watermark — if > 0 you under-read (limit-capped or a
530
+ // non-cursor read) and will skip messages unless you keep reading.
531
+ if (has_more === true) {
532
+ lines.push('');
533
+ lines.push(`⚠ has_more: true — call \`borg_read-log unread_only=true\` again until has_more=false so you finish draining unread entries.`);
534
+ }
535
+ else if (typeof behind_by === 'number' && behind_by > 0) {
536
+ lines.push('');
537
+ lines.push(`⚠ behind_by: ${behind_by} more unread ${behind_by === 1 ? 'entry' : 'entries'} addressed to you — call \`borg_read-log unread_only=true\` again until behind_by=0 so you don't skip messages.`);
538
+ }
539
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
540
+ }
541
+ case 'borg_log': {
542
+ const message = args?.message;
543
+ if (!message || typeof message !== 'string')
544
+ throw new Error('message is required');
545
+ const active = await getActiveCube();
546
+ if (!active)
547
+ throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
548
+ if (lifecycleSignalForMessage(message)) {
549
+ const decision = await shouldSuppressLifecycleLog(active, message);
550
+ if (decision.suppress) {
551
+ await recordLifecycleLog(active, message);
552
+ return {
553
+ content: [
554
+ {
555
+ type: 'text',
556
+ text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${active.droneLabel}; recent cube log already contains this signal.`,
557
+ },
558
+ ],
559
+ };
560
+ }
561
+ }
562
+ const hasTo = Object.prototype.hasOwnProperty.call(args ?? {}, 'to');
563
+ const recipients = hasTo ? normalizeDirectLogRecipients(args?.to) : undefined;
564
+ const explicitClass = typeof args?.class === 'string' ? args.class : undefined;
565
+ const visibility = args?.visibility === 'broadcast' || args?.visibility === 'direct'
566
+ ? args.visibility
567
+ : undefined;
568
+ if (!active.serverTrustIdentity) {
569
+ throw new Error('Selected Borg server authority state is missing or unreadable');
570
+ }
571
+ const appendOpts = {
572
+ ...(explicitClass ? { class: explicitClass } : {}),
573
+ ...(hasTo ? { to: recipients ?? [] } : {}),
574
+ ...(visibility ? { visibility } : {}),
575
+ serverTrustIdentity: active.serverTrustIdentity,
576
+ };
577
+ const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
578
+ await recordLifecycleLog(active, message);
579
+ const echo = result.routing?.message ? `\n${result.routing.message}` : '';
580
+ // gh#534: surface to the SENDER which directed recipients are
581
+ // currently unreachable via the wake path. The message is delivered
582
+ // regardless (persisted server-side); they read it when they return.
583
+ const unreachable = result.unreachableRecipients?.length
584
+ ? `\n⚠ ${result.unreachableRecipients.length} directed recipient(s) currently unreachable (wake-path:deaf): ${result.unreachableRecipients
585
+ .map((r) => r.label)
586
+ .join(', ')}. Message delivered — they'll read it when they return.`
587
+ : '';
588
+ const text = `Logged to cube "${active.name}" as ${active.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
589
+ return { content: [{ type: 'text', text }] };
590
+ }
591
+ case 'borg_ack': {
592
+ const entryId = args?.entry_id;
593
+ if (!entryId || typeof entryId !== 'string') {
594
+ throw new Error('entry_id is required');
595
+ }
596
+ // gh#418: default 'ack'. Only 'claim' is the other allowed kind; the
597
+ // worker re-validates at the Zod boundary so an unknown value is
598
+ // rejected server-side, but normalize here to keep the wire clean.
599
+ const kind = args?.kind === 'claim' ? 'claim' : 'ack';
600
+ const active = await requireActiveCube();
601
+ await ackLogEntry(active.sessionToken, active.apiUrl, entryId, kind, active.serverTrustIdentity);
602
+ return {
603
+ content: [
604
+ {
605
+ type: 'text',
606
+ text: kind === 'claim'
607
+ ? `Claimed entry ${entryId} in cube "${active.name}" (advisory — merge stays keyed on REVIEW-APPROVED).`
608
+ : `Acked entry ${entryId} in cube "${active.name}".`,
609
+ },
610
+ ],
611
+ };
612
+ }
613
+ case 'borg_decide': {
614
+ const topic = args?.topic;
615
+ const decision = args?.decision;
616
+ if (!topic || typeof topic !== 'string')
617
+ throw new Error('topic is required');
618
+ if (!decision || typeof decision !== 'string')
619
+ throw new Error('decision is required');
620
+ const rationale = typeof args?.rationale === 'string' ? args.rationale : undefined;
621
+ const active = await requireActiveCube();
622
+ const { decision: row } = await recordDecision(active.sessionToken, active.apiUrl, {
623
+ topic,
624
+ decision,
625
+ ...(rationale !== undefined ? { rationale } : {}),
626
+ }, active.serverTrustIdentity);
627
+ const superseded = row?.supersedes ? ' (superseded the prior decision on this topic)' : '';
628
+ return {
629
+ content: [
630
+ {
631
+ type: 'text',
632
+ text: `Recorded ratified decision on "${topic}" in cube "${active.name}"${superseded}. Cite it via borg_decisions; it surfaces in borg_regen.`,
633
+ },
634
+ ],
635
+ };
636
+ }
637
+ case 'borg_decisions': {
638
+ const topic = typeof args?.topic === 'string' ? args.topic : undefined;
639
+ const active = await requireActiveCube();
640
+ const { decisions } = await listDecisions(active.sessionToken, active.apiUrl, topic, active.serverTrustIdentity);
641
+ const text = decisions.length === 0
642
+ ? topic
643
+ ? `No active ratified decision on "${topic}" in cube "${active.name}".`
644
+ : `No active ratified decisions in cube "${active.name}".`
645
+ : decisions
646
+ .map((d) => `**${d.topic}:** ${d.decision}${d.rationale ? ` — ${d.rationale}` : ''}`)
647
+ .join('\n');
648
+ return { content: [{ type: 'text', text }] };
649
+ }
650
+ case 'borg_remove-decision': {
651
+ const topic = typeof args?.topic === 'string' ? args.topic : undefined;
652
+ const decisionId = typeof args?.decision_id === 'string' ? args.decision_id : undefined;
653
+ if (Number(topic !== undefined) + Number(decisionId !== undefined) !== 1) {
654
+ throw new Error('provide exactly one of topic or decision_id');
655
+ }
656
+ const active = await requireActiveCube();
657
+ const selector = topic !== undefined ? { topic } : { decision_id: decisionId };
658
+ const { decision } = await removeDecision(active.sessionToken, active.apiUrl, selector, active.serverTrustIdentity);
659
+ return {
660
+ content: [{
661
+ type: 'text',
662
+ text: `Removed the active ratified decision on "${decision.topic}" from cube "${active.name}".`,
663
+ }],
664
+ };
665
+ }
666
+ case 'borg_list-cubes': {
667
+ const { cubes } = await listCubes();
668
+ if (!cubes.length) {
669
+ return { content: [{ type: 'text', text: 'No cubes yet. Use borg_create-cube to make your first one.' }] };
670
+ }
671
+ const lines = cubes.map((c) => `- **${c.name}** (id: ${c.id})\n ${(c.cube_directive || '_(no directive set)_').split('\n')[0].slice(0, 120)}`);
672
+ return { content: [{ type: 'text', text: `Your cubes (${cubes.length}):\n\n${lines.join('\n\n')}` }] };
673
+ }
674
+ case 'borg_create-cube': {
675
+ const name = args?.name;
676
+ const cubeDirective = args?.cube_directive;
677
+ const templateName = args?.template;
678
+ if (!name)
679
+ throw new Error('name is required');
680
+ if (cubeDirective === undefined)
681
+ throw new Error('cube_directive is required (pass empty string if none)');
682
+ // Resolve template (validates name early so the cube isn't
683
+ // created in a partial state if the template name is wrong).
684
+ let template = null;
685
+ if (templateName) {
686
+ template = getTemplate(templateName);
687
+ if (!template) {
688
+ throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
689
+ }
690
+ }
691
+ // Sprint 14: template cube_directive fills empty operator input.
692
+ // Operator-supplied text takes precedence — templates fill
693
+ // the blank, never stomp.
694
+ const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
695
+ // v0.9.2: createCube now returns the flat shape directly
696
+ // (see remote-client unwrap). `cube.id` / `cube.name` work
697
+ // verbatim on the returned object.
698
+ const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
699
+ const cube = await createCube(name, resolvedCubeDirective, {
700
+ message_taxonomy: resolvedMessageTaxonomy,
701
+ });
702
+ // Apply template roles if requested. Merges by name: any role the
703
+ // server auto-seeded (e.g. "Drone") that the template doesn't
704
+ // also include stays put; templated roles upsert.
705
+ if (template) {
706
+ const summary = await applyTemplateToCube(cube.id, template);
707
+ const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
708
+ ? ' Template cube directive applied (operator passed empty).'
709
+ : '';
710
+ const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
711
+ return { content: [{ type: 'text', text }] };
712
+ }
713
+ const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${cube.name} to join as a drone.`;
714
+ return { content: [{ type: 'text', text }] };
715
+ }
716
+ case 'borg_update-cube': {
717
+ const cubeId = args?.cube_id;
718
+ if (!cubeId)
719
+ throw new Error('cube_id is required');
720
+ const updates = {};
721
+ if (typeof args?.name === 'string')
722
+ updates.name = args.name;
723
+ if (typeof args?.cube_directive === 'string')
724
+ updates.cube_directive = args.cube_directive;
725
+ if (Array.isArray(args?.message_taxonomy))
726
+ updates.message_taxonomy = args.message_taxonomy;
727
+ if (Object.keys(updates).length === 0)
728
+ throw new Error('Pass at least one of: name, cube_directive, message_taxonomy.');
729
+ const { cube } = await updateCube(cubeId, updates);
730
+ return { content: [{ type: 'text', text: `Updated cube **${cube.name}** (id: ${cube.id}).` }] };
731
+ }
732
+ case 'borg_patch-taxonomy-class': {
733
+ const cubeId = args?.cube_id;
734
+ if (!cubeId)
735
+ throw new Error('cube_id is required');
736
+ const action = args?.action;
737
+ if (action !== 'add' && action !== 'replace' && action !== 'remove') {
738
+ throw new Error('action must be one of: add, replace, remove.');
739
+ }
740
+ let cube;
741
+ let label;
742
+ if (action === 'remove') {
743
+ const className = args?.class;
744
+ if (!className)
745
+ throw new Error('class is required for remove.');
746
+ ({ cube } = await patchTaxonomyClass(cubeId, { action, class: className }));
747
+ label = className;
748
+ }
749
+ else {
750
+ const classDef = args?.class_def;
751
+ if (classDef == null || typeof classDef !== 'object' || Array.isArray(classDef)) {
752
+ throw new Error('class_def (object) is required for add/replace.');
753
+ }
754
+ ({ cube } = await patchTaxonomyClass(cubeId, { action, class_def: classDef }));
755
+ label = String(classDef.class ?? '');
756
+ }
757
+ const verb = action === 'add' ? 'Added' : action === 'replace' ? 'Replaced' : 'Removed';
758
+ return { content: [{ type: 'text', text: `${verb} taxonomy class **${label}** in cube **${cube.name}** (id: ${cube.id}).` }] };
759
+ }
760
+ case 'borg_delete-cube': {
761
+ const cubeId = args?.cube_id;
762
+ if (!cubeId)
763
+ throw new Error('cube_id is required');
764
+ await deleteCube(cubeId);
765
+ return { content: [{ type: 'text', text: `Deleted cube ${cubeId} (and all its roles, drones, log entries).` }] };
766
+ }
767
+ case 'borg_create-role': {
768
+ const cubeId = args?.cube_id;
769
+ const name = args?.name;
770
+ const shortDesc = args?.short_description;
771
+ const detailedDesc = args?.detailed_description;
772
+ if (!cubeId)
773
+ throw new Error('cube_id is required');
774
+ if (!name)
775
+ throw new Error('name is required');
776
+ if (shortDesc === undefined)
777
+ throw new Error('short_description is required (pass empty string if none)');
778
+ if (detailedDesc === undefined)
779
+ throw new Error('detailed_description is required (pass empty string if none)');
780
+ const isDefault = args?.is_default === true;
781
+ const isMandatory = args?.is_mandatory === true;
782
+ const isHumanSeat = args?.is_human_seat === true;
783
+ const canBroadcast = args?.can_broadcast === true;
784
+ const receivesAllDirect = args?.receives_all_direct === true;
785
+ const { role } = await createRole(cubeId, {
786
+ name,
787
+ short_description: shortDesc,
788
+ detailed_description: detailedDesc,
789
+ is_default: isDefault,
790
+ is_mandatory: isMandatory,
791
+ is_human_seat: isHumanSeat,
792
+ can_broadcast: canBroadcast,
793
+ receives_all_direct: receivesAllDirect,
794
+ ...(typeof args?.default_model === 'string' ? { default_model: args.default_model } : {}),
795
+ });
796
+ const tags = [
797
+ role.role_class === 'queen' ? 'Queen' : null,
798
+ role.is_human_seat ? 'human-seat' : null,
799
+ role.is_default ? 'default' : null,
800
+ role.is_mandatory ? 'mandatory' : null,
801
+ ].filter(Boolean).join(', ');
802
+ const tag = tags ? ` (${tags})` : '';
803
+ return { content: [{ type: 'text', text: `Created role **${role.name}**${tag} (id: ${role.id}) in cube ${cubeId}.` }] };
804
+ }
805
+ case 'borg_update-role': {
806
+ const roleId = args?.role_id;
807
+ if (!roleId)
808
+ throw new Error('role_id is required');
809
+ const updates = {};
810
+ if (typeof args?.name === 'string')
811
+ updates.name = args.name;
812
+ if (typeof args?.short_description === 'string')
813
+ updates.short_description = args.short_description;
814
+ if (typeof args?.detailed_description === 'string')
815
+ updates.detailed_description = args.detailed_description;
816
+ if (typeof args?.is_default === 'boolean')
817
+ updates.is_default = args.is_default;
818
+ if (typeof args?.is_mandatory === 'boolean')
819
+ updates.is_mandatory = args.is_mandatory;
820
+ if (typeof args?.is_human_seat === 'boolean')
821
+ updates.is_human_seat = args.is_human_seat;
822
+ if (typeof args?.can_broadcast === 'boolean')
823
+ updates.can_broadcast = args.can_broadcast;
824
+ if (typeof args?.receives_all_direct === 'boolean')
825
+ updates.receives_all_direct = args.receives_all_direct;
826
+ if (typeof args?.default_model === 'string')
827
+ updates.default_model = args.default_model;
828
+ if (Object.keys(updates).length === 0)
829
+ throw new Error('Pass at least one of: name, short_description, detailed_description, is_default, is_mandatory, is_human_seat, can_broadcast, receives_all_direct.');
830
+ const { role } = await updateRole(roleId, updates);
831
+ const tags = [
832
+ role.role_class === 'queen' ? 'Queen' : null,
833
+ role.is_human_seat ? 'human-seat' : null,
834
+ role.is_default ? 'default' : null,
835
+ role.is_mandatory ? 'mandatory' : null,
836
+ ].filter(Boolean).join(', ');
837
+ const tag = tags ? ` (${tags})` : '';
838
+ return { content: [{ type: 'text', text: `Updated role **${role.name}**${tag} (id: ${role.id}).` }] };
839
+ }
840
+ case 'borg_patch-role-section': {
841
+ const roleId = args?.role_id;
842
+ if (!roleId)
843
+ throw new Error('role_id is required');
844
+ const action = args?.action;
845
+ if (action !== 'replace' && action !== 'insert' && action !== 'delete') {
846
+ throw new Error('action must be one of: replace, insert, delete.');
847
+ }
848
+ const heading = args?.heading;
849
+ if (!heading)
850
+ throw new Error('heading is required');
851
+ let role;
852
+ if (action === 'delete') {
853
+ ({ role } = await patchRoleSection(roleId, { action, heading }));
854
+ }
855
+ else {
856
+ const body = args?.body;
857
+ if (typeof body !== 'string') {
858
+ throw new Error('body is required for replace/insert (pass empty string for an empty section).');
859
+ }
860
+ if (action === 'insert') {
861
+ const after = (typeof args?.after === 'string' ? args.after : null);
862
+ ({ role } = await patchRoleSection(roleId, { action, heading, body, after }));
863
+ }
864
+ else {
865
+ ({ role } = await patchRoleSection(roleId, { action, heading, body }));
866
+ }
867
+ }
868
+ const verb = action === 'replace' ? 'Replaced' : action === 'insert' ? 'Inserted' : 'Deleted';
869
+ return { content: [{ type: 'text', text: `${verb} section **${heading}** in role **${role.name}** (id: ${role.id}).` }] };
870
+ }
871
+ case 'borg_delete-role': {
872
+ const roleId = args?.role_id;
873
+ if (!roleId)
874
+ throw new Error('role_id is required');
875
+ await deleteRole(roleId);
876
+ return { content: [{ type: 'text', text: `Deleted role ${roleId}.` }] };
877
+ }
878
+ case 'borg_reassign-drone': {
879
+ return {
880
+ content: [{ type: 'text', text: await runReassignDroneTool({
881
+ droneId: args?.drone_id,
882
+ roleId: args?.role_id,
883
+ }) }],
884
+ };
885
+ }
886
+ case 'borg_evict-drone': {
887
+ return {
888
+ content: [{ type: 'text', text: await runEvictDroneTool(args) }],
889
+ };
890
+ }
891
+ case 'borg_list-drones': {
892
+ const cubeId = args?.cube_id;
893
+ if (!cubeId)
894
+ throw new Error('cube_id is required');
895
+ const { drones, roles } = await getCube(cubeId);
896
+ if (!drones.length) {
897
+ return { content: [{ type: 'text', text: 'No drones in this cube yet.' }] };
898
+ }
899
+ const rolesById = new Map(roles.map((r) => [r.id, r]));
900
+ const lines = drones.map((d) => {
901
+ const r = rolesById.get(d.role_id);
902
+ const roleLabel = formatRoleAgentLabel(r?.name ?? '?', d.agent_kind);
903
+ const wakeClass = d.wake_path_alert_class && d.wake_path_alert_class !== 'independent'
904
+ ? ` — wake-path-class: ${d.wake_path_alert_class}`
905
+ : '';
906
+ const reportedModel = d.reported_model
907
+ ? ` — Reported model: ${d.reported_model}`
908
+ : ' — Reported model: not reported';
909
+ const workingRepo = formatWorkingRepoLabel(d);
910
+ const workingRepoText = workingRepo ? ` — ${workingRepo}` : '';
911
+ return `- **${d.label}** (id: ${d.id}) — role: ${roleLabel} (${d.role_id}) — last seen ${d.last_seen}${wakeClass}${reportedModel}${workingRepoText}`;
912
+ });
913
+ return { content: [{ type: 'text', text: `Drones in cube ${cubeId} (${drones.length}):\n\n${lines.join('\n')}` }] };
914
+ }
915
+ case 'borg_list-roles': {
916
+ // Sprint 6 / gh#153: surface role IDs to Coordinator-class drones
917
+ // for use with borg_reassign-drone (e.g. promoting a drone to Queen).
918
+ // Uses the same cube-grant-scoped GET /api/cubes/:id endpoint as
919
+ // borg_list-drones — data is already accessible to the cube owner
920
+ // over the local server; this tool just makes role IDs
921
+ // discoverable inside the MCP tool namespace per drone-7's UX-FEEDBACK.
922
+ // Render logic extracted to list-roles-render.ts for testability
923
+ // per drone-3 QA-FAIL 2026-05-18T13:27:53Z.
924
+ const cubeId = args?.cube_id;
925
+ if (!cubeId)
926
+ throw new Error('cube_id is required');
927
+ const roles = await listRoles(cubeId);
928
+ return { content: [{ type: 'text', text: renderRoleList(roles, cubeId) }] };
929
+ }
930
+ case 'borg_list-templates': {
931
+ const names = listTemplateNames();
932
+ const lines = names.map((n) => {
933
+ const t = getTemplate(n);
934
+ return `- **${n}**: ${t.description}`;
935
+ });
936
+ return { content: [{ type: 'text', text: `Available templates:\n\n${lines.join('\n')}` }] };
937
+ }
938
+ case 'borg_sync-roles': {
939
+ const cubeId = args?.cube_id;
940
+ const templateName = args?.template_name || 'software-dev';
941
+ const apply = args?.apply === true;
942
+ // gh#473 PR2 — per-conflict-fragment accept/reject decisions.
943
+ // Keyed on the stable fragment key surfaced by the dry-run (e.g.
944
+ // `role:Builder:section:Workflow`). Unspecified conflicts default
945
+ // to REJECT (keep the cube's evolved text).
946
+ const decisions = args?.decisions && typeof args.decisions === 'object'
947
+ ? args.decisions
948
+ : undefined;
949
+ if (!cubeId)
950
+ throw new Error('cube_id is required');
951
+ const result = await syncRoles(cubeId, templateName, apply, decisions);
952
+ return { content: [{ type: 'text', text: renderSyncRolesResult(result, templateName) }] };
953
+ }
954
+ case 'borg_apply-template': {
955
+ const cubeId = args?.cube_id;
956
+ const templateName = args?.template_name;
957
+ if (!cubeId)
958
+ throw new Error('cube_id is required');
959
+ if (!templateName)
960
+ throw new Error('template_name is required');
961
+ const template = getTemplate(templateName);
962
+ if (!template) {
963
+ throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
964
+ }
965
+ const summary = await applyTemplateToCube(cubeId, template);
966
+ // Sprint 14: optionally write template's cube_directive to the
967
+ // cube. No-clobber discipline — only fills empty directives,
968
+ // never overwrites operator-customized text.
969
+ let cubeDirectiveNote = '';
970
+ const cubeForRules = await getCube(cubeId);
971
+ const newCubeDirective = resolveCubeDirectiveForApply(cubeForRules.cube_directive, template);
972
+ if (newCubeDirective !== null) {
973
+ await updateCube(cubeId, { cube_directive: newCubeDirective });
974
+ cubeDirectiveNote = ' Template cube directive applied (cube directive was empty).';
975
+ }
976
+ return { content: [{ type: 'text', text: `Applied template **${templateName}** to cube ${cubeId} — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote}` }] };
977
+ }
978
+ default:
979
+ throw new Error(`Unknown tool: ${name}`);
980
+ }
981
+ }
982
+ catch (error) {
983
+ // gh#877: the drone-lifecycle verdict returns a RECOGNIZABLE tool RESULT
984
+ // (not a generic "Error: ..." the agent retries) so the /loop + role
985
+ // playbook can branch deterministically. Checked FIRST: an evicted seat is
986
+ // a lifecycle terminal — it gets its own recognizable result rather than
987
+ // the generic error rendering below.
988
+ if (error instanceof DroneEvictedError) {
989
+ const active = await getActiveCube();
990
+ return {
991
+ content: [{ type: 'text', text: formatEvictedToolResult(active?.name) }],
992
+ isError: true,
993
+ };
994
+ }
995
+ const localManageResult = formatLocalManageToolResult(error);
996
+ if (localManageResult)
997
+ return localManageResult;
998
+ return {
999
+ content: [
1000
+ {
1001
+ type: 'text',
1002
+ text: `Error: ${error.message}`,
1003
+ },
1004
+ ],
1005
+ isError: true,
1006
+ };
1007
+ }
1008
+ });
1009
+ // Register prompts listing — the client exposes no prompts.
1010
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
1011
+ return { prompts: [] };
1012
+ });
1013
+ // Register prompt getter
1014
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1015
+ const { name } = request.params;
1016
+ throw new Error(`Unknown prompt: ${name}`);
1017
+ });
1018
+ // Create stdio transport
1019
+ const transport = new StdioServerTransport();
1020
+ // Connect server to transport
1021
+ await server.connect(transport);
1022
+ // Resolve drone self-identification prefix before any console output
1023
+ // (gh#25). Falls back to `[unassimilated · <repo>]` if no cube cached.
1024
+ await initConsolePrefix();
1025
+ console.error(`${consolePrefix()}◼ Borg MCP Client started`);
1026
+ console.error(`${consolePrefix()}◼ Use borg_assimilate <cube-name> to join a cube as a drone`);
1027
+ console.error(`${consolePrefix()}◼ Manage your cubes with borg --help`);
1028
+ }
1029
+ if (isEntryInvocation(process.argv[1], import.meta.url)) {
1030
+ main().catch((error) => {
1031
+ console.error(`${consolePrefix()}Fatal error:`, error);
1032
+ process.exit(1);
1033
+ });
1034
+ }
1035
+ //# sourceMappingURL=index.js.map