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