borgmcp 3.0.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,12 @@
1
1
  # Extraction Provenance
2
2
 
3
+ **This file is a frozen historical record.** It documents the one-time
4
+ extraction of this repository from the monorepo and the state of the client
5
+ line as of that extraction and its immediate releases. Nothing appends to it,
6
+ no release process touches it, and no statement in it describes the current
7
+ version — the authoritative version is `package.json` and the npm registry.
8
+ Facts below were true when recorded and are not maintained.
9
+
3
10
  The initial standalone source was extracted from `Byte-Ventures/borg-mcp` commit `17ff8ce14e12122a8cc9089f6b94174c02fa2a04` on branch `main`.
4
11
 
5
12
  The extraction copied the monorepo's `client/src/` production boundary and top-level `client/__tests__/*.test.ts` unit tests. It did not import monorepo Git history, worker or website source, deployment configuration, local state, credentials files, build output, or the live integration suite.
@@ -17,42 +24,20 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
17
24
  - Kept self-hosted `--host --enroll` preview-only while implementing the
18
25
  client-generated PENDING credential/retry tuple and capability-gated,
19
26
  repository-idempotent cube creation required for local dogfood.
20
- - Initially set the standalone package identity to `2.0.0`. **The current
21
- release identity is `3.0.2`.** Extraction and versioning do not authorize
22
- publication.
23
-
24
- Spent versions — each burned an immutable tag and published nothing:
25
-
26
- | Version | Failed at | Recovered by |
27
- | --- | --- | --- |
28
- | `2.0.0` | lightweight release tag, before packaging | `2.0.1` |
29
- | `2.0.7` | before package creation or npm publication | — |
30
- | `2.10.1` | before artifact creation or publication | — |
31
- | `2.12.0` | before artifact creation or publication | `2.12.1` |
32
- | `3.0.1` | publish job never scheduled; run cancelled after verify and approval | `3.0.2` |
33
-
34
- Every other version in the `2.x` and `3.x` lines published and was
35
- registry-verified.
36
- Record spent versions here when they occur; do not append a clause to this
37
- entry. To confirm the table is still complete, list the repository's version
38
- tags and `npm view borgmcp versions` — a spent version is a tag whose version
39
- never reached the registry, and the two sets should differ by exactly these
40
- rows.
41
-
42
- ## Review Holds
27
+ ## Review Holds Resolved at Extraction (historical)
43
28
 
44
29
  Google OAuth / Cloud sign-in has been fully removed from this local-only client.
45
30
  `src/auth.ts`, `src/device-auth.ts`, and all installed-application OAuth client
46
31
  material are deleted, along with the hosted API default and the Cloud
47
- subscription/billing/dashboard tools. `scripts/verify-public-source.mjs` now
32
+ subscription/billing/dashboard tools. `scripts/verify-public-source.mjs`
48
33
  forbids ANY Google OAuth client ID or `GOCSPX` value anywhere in the source or
49
34
  packed artifact (zero tolerance), and a no-cloud egress guard asserts the packed
50
35
  artifact reaches no hosted authority.
51
36
 
52
- Local enrollment now uses the reviewed client-generated credential/retry
37
+ At extraction, local enrollment used the reviewed client-generated credential/retry
53
38
  contract, with a pre-request `PENDING` record in the local 0600-permission seat
54
- store, exact-tuple ambiguous retry, and verified activation. The current client
55
- candidate resolves to the audited registry `borgmcp-shared@0.9.0` with integrity
39
+ store, exact-tuple ambiguous retry, and verified activation. The client candidate
40
+ at that time resolved to audited registry `borgmcp-shared` version `0.9.0` with integrity
56
41
  `sha512-bfZPP9JGgBQrCFoZetabqKHc8HLaUqHVR3GJLb/1F1oon7z/B4el4aeBHVvXlxN9+2G7kU/ymPZ/K25nVQapmQ==`.
57
42
  The coupled `borgmcp-server@0.9.0` release was published on 2026-08-02 for the
58
43
  client `borgmcp@2.10.0` line. Its annotated tag object
@@ -62,6 +47,7 @@ client `borgmcp@2.10.0` line. Its annotated tag object
62
47
  in the `borgmcp-server-0.9.0-release` registry decision. That server and its
63
48
  client line pin historical `borgmcp-shared` version `0.8.1` and remain
64
49
  immutable. The immutable `v2.0.7` attempt failed before publication and remains
65
- preserved. The current release identity and publication gate remain governed by
66
- the reviewed `v3.0.2` source, a fresh annotated tag, and the exact-artifact and
67
- protected-publication gates.
50
+ preserved. Each release's identity and publication gate are governed by its reviewed
51
+ source at the annotated release tag and the exact-artifact and
52
+ protected-publication gates; the authoritative version is `package.json` and
53
+ the registry, not this document.
@@ -204,7 +204,11 @@ stopped server remains stopped; matching controller and prepared runtime are
204
204
  reported as `prepared; still stopped` without inventing a live protocol check.
205
205
  An absent server is skipped rather than installed. Partial completion prints the
206
206
  safe retry command `borg update --yes`. Borg never starts a stopped server,
207
- daemonizes, or restarts agent processes.
207
+ daemonizes, or restarts agent processes. After the package pair verifies, the
208
+ command replaces stale absolute `borgmcp` package launch paths in Claude Code,
209
+ Codex, and OpenCode MCP registrations with the version-stable `borg-mcp`
210
+ command. It preserves each registration's other settings. It does not create
211
+ absent registrations or modify a `borg` entry that points to another command.
208
212
 
209
213
  `borg server update` remains the server-runtime-only command. It verifies and
210
214
  activates the server artifact but deliberately does not rewrite the global
@@ -258,6 +262,6 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
258
262
 
259
263
  ## Release status
260
264
 
261
- This client candidate pins published `borgmcp-shared@0.9.0`. Its release remains
265
+ This client candidate pins published `borgmcp-shared@0.10.1`. Its release remains
262
266
  coupled to a server artifact rebuilt against the same shared version, with both
263
267
  artifacts required to pass the complete process-level local dogfood gate.
package/docs/RELEASING.md CHANGED
@@ -19,11 +19,11 @@ Before creating the release tag, independently verify all of these conditions:
19
19
  - the extraction review confirms no private backend secrets, deployment
20
20
  configuration, customer data, local state, or duplicated shared contracts
21
21
  entered the public package;
22
- - the exact audited registry dependency `borgmcp-shared@0.9.0` remains locked to
22
+ - the exact audited registry dependency `borgmcp-shared@0.10.1` remains locked to
23
23
  its canonical tarball and integrity
24
- `sha512-bfZPP9JGgBQrCFoZetabqKHc8HLaUqHVR3GJLb/1F1oon7z/B4el4aeBHVvXlxN9+2G7kU/ymPZ/K25nVQapmQ==`;
24
+ `sha512-IAESc6+VpUGECNabFkO60agvga0B8Fx0hk2RMUkSDCnlIjMvDodMFKlqw+TOzdXtjf0Uk9LJi9ZCU4HfRYRRhw==`;
25
25
  - the coupled client/server release is published only after the server artifact
26
- is rebuilt against `borgmcp-shared@0.9.0` and both pass the complete local
26
+ is rebuilt against `borgmcp-shared@0.10.1` and both pass the complete local
27
27
  dogfood gate;
28
28
  - the selected stable client version is unused and the exact release commit is
29
29
  on protected `main`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.4",
74
- "borgmcp-shared": "0.9.0",
74
+ "borgmcp-shared": "0.10.1",
75
75
  "chalk": "^5.3.0",
76
76
  "prompts": "^2.4.2",
77
77
  "which": "^4.0.0"
package/src/cli-help.ts CHANGED
@@ -129,7 +129,11 @@ export function updateHelpText(version: string): string {
129
129
  `provenance fail closed with manual-update guidance.\n\n` +
130
130
  `If no local server is installed, the server phase is skipped. A failure after the client\n` +
131
131
  `succeeds is reported as partial completion with the safe retry command. Borg never starts a\n` +
132
- `stopped server and never restarts agent processes; restart active agent sessions yourself.\n`
132
+ `stopped server. After package verification, Borg replaces stale borgmcp package launch paths\n` +
133
+ `in Claude Code, Codex, and OpenCode registrations while preserving their other settings. It\n` +
134
+ `leaves absent registrations and borg entries that point to another command unchanged. Borg\n` +
135
+ `never restarts agent processes;\n` +
136
+ `restart active agent sessions yourself.\n`
133
137
  );
134
138
  }
135
139
 
@@ -15,7 +15,6 @@ import {
15
15
  withAgentRuntimeEnv,
16
16
  } from './agent-runtime.js';
17
17
  import {
18
- resolveMcpBinaryPath,
19
18
  resolveRegenPath,
20
19
  resolveClearRewakePath,
21
20
  resolveLogAuditPath,
@@ -36,7 +35,7 @@ const HOOK_COMMAND = shellEscape(resolveRegenPath());
36
35
  const CLEAR_REWAKE_HOOK_COMMAND = shellEscape(resolveClearRewakePath());
37
36
  const AUDIT_HOOK_COMMAND = shellEscape(resolveLogAuditPath());
38
37
  const FOREIGN_PATH_REMINDER_HOOK_COMMAND = shellEscape(resolveForeignPathReminderPath());
39
- const MCP_BINARY = resolveMcpBinaryPath();
38
+ const MCP_COMMAND = 'borg-mcp';
40
39
 
41
40
  /**
42
41
  * Claude Code CLI config path. The CLI reads `mcpServers.<name>` from
@@ -678,6 +677,150 @@ export function isCodexMcpServerConfigured(
678
677
  }
679
678
  }
680
679
 
680
+ function configuredMcpCommand(value: unknown): string | null {
681
+ if (typeof value === 'string') return value;
682
+ if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string') {
683
+ return value[0];
684
+ }
685
+ return null;
686
+ }
687
+
688
+ function isStaleBorgMcpCommand(value: unknown): boolean {
689
+ const command = configuredMcpCommand(value);
690
+ if (!command || command === MCP_COMMAND || !path.isAbsolute(command)) return false;
691
+ const normalized = command.replace(/\\/g, '/');
692
+ return /\/(?:node_modules\/borgmcp|borg-mcp-client)\/dist\/index\.js$/.test(normalized);
693
+ }
694
+
695
+ function readClaudeMcpCommand(configPath: string): unknown {
696
+ try {
697
+ return readJsonFile(configPath)?.mcpServers?.[MCP_SERVER_NAME]?.command;
698
+ } catch {
699
+ return undefined;
700
+ }
701
+ }
702
+
703
+ function readCodexMcpCommand(configPath: string): unknown {
704
+ try {
705
+ const text = fs.readFileSync(configPath, 'utf-8');
706
+ if (!isCodexMcpServerConfigured(configPath)) return undefined;
707
+ const header = /^\s*\[mcp_servers\.borg\]\s*$/m.exec(text);
708
+ if (!header) return undefined;
709
+ const tail = text.slice(header.index + header[0].length);
710
+ const nextHeader = /^\s*\[/m.exec(tail);
711
+ const section = nextHeader ? tail.slice(0, nextHeader.index) : tail;
712
+ return section?.match(/^\s*command\s*=\s*"([^"]+)"\s*$/m)?.[1];
713
+ } catch {
714
+ return undefined;
715
+ }
716
+ }
717
+
718
+ function readOpenCodeMcpCommand(configPath: string): unknown {
719
+ try {
720
+ const borgServer = readJsonFile(configPath)?.mcp?.[MCP_SERVER_NAME];
721
+ if (!borgServer || borgServer.type !== 'local') return undefined;
722
+ const environment = borgServer.environment ?? borgServer.env;
723
+ if (environment?.BORG_AGENT_KIND !== 'opencode' && environment?.BORG_OPENCODE !== '1') {
724
+ return undefined;
725
+ }
726
+ return borgServer.command;
727
+ } catch {
728
+ return undefined;
729
+ }
730
+ }
731
+
732
+ export interface RefreshManagedAgentMcpConfigOptions {
733
+ claudeConfigPath?: string;
734
+ codexConfigPath?: string;
735
+ openCodeConfigPath?: string;
736
+ refreshClaude?: () => void;
737
+ refreshCodex?: () => void;
738
+ refreshOpenCode?: () => void;
739
+ }
740
+
741
+ function refreshClaudeMcpCommand(configPath: string): void {
742
+ const config = readJsonFile(configPath);
743
+ config.mcpServers[MCP_SERVER_NAME].command = MCP_COMMAND;
744
+ writeJsonFile(configPath, config);
745
+ }
746
+
747
+ function refreshCodexMcpCommand(configPath: string): void {
748
+ const text = fs.readFileSync(configPath, 'utf-8');
749
+ const header = /^\s*\[mcp_servers\.borg\]\s*$/m.exec(text);
750
+ if (!header) return;
751
+ const sectionStart = header.index + header[0].length;
752
+ const tail = text.slice(sectionStart);
753
+ const nextHeader = /^\s*\[/m.exec(tail);
754
+ const sectionEnd = nextHeader ? sectionStart + nextHeader.index : text.length;
755
+ const section = text.slice(sectionStart, sectionEnd);
756
+ const updatedSection = section.replace(
757
+ /^(\s*command\s*=\s*)"[^"]+"(\s*)$/m,
758
+ `$1"${MCP_COMMAND}"$2`,
759
+ );
760
+ fs.writeFileSync(
761
+ configPath,
762
+ text.slice(0, sectionStart) + updatedSection + text.slice(sectionEnd),
763
+ 'utf-8',
764
+ );
765
+ }
766
+
767
+ function refreshOpenCodeMcpCommand(configPath: string): void {
768
+ const config = readJsonFile(configPath);
769
+ const borgServer = config.mcp[MCP_SERVER_NAME];
770
+ borgServer.command = Array.isArray(borgServer.command) ? [MCP_COMMAND] : MCP_COMMAND;
771
+ writeJsonFile(configPath, config);
772
+ }
773
+
774
+ /**
775
+ * Replace only the command in stale Borg registrations. Other entry fields are
776
+ * preserved, and a `borg` entry that points at another command is not changed.
777
+ */
778
+ export function refreshManagedAgentMcpConfigs(
779
+ options: RefreshManagedAgentMcpConfigOptions = {},
780
+ ): Array<'claude' | 'codex' | 'opencode'> {
781
+ const refreshed: Array<'claude' | 'codex' | 'opencode'> = [];
782
+ const failures: string[] = [];
783
+ const claudeConfigPath = options.claudeConfigPath ?? CLAUDE_CONFIG_PATH;
784
+ const codexConfigPath = options.codexConfigPath ?? CODEX_CONFIG_PATH;
785
+ const openCodeConfigPath = options.openCodeConfigPath ?? OPENCODE_CONFIG_PATH;
786
+ const agents = [
787
+ {
788
+ kind: 'claude' as const,
789
+ label: 'Claude Code',
790
+ command: readClaudeMcpCommand(claudeConfigPath),
791
+ refresh: options.refreshClaude ?? (() => refreshClaudeMcpCommand(claudeConfigPath)),
792
+ },
793
+ {
794
+ kind: 'codex' as const,
795
+ label: 'Codex',
796
+ command: readCodexMcpCommand(codexConfigPath),
797
+ refresh: options.refreshCodex ?? (() => refreshCodexMcpCommand(codexConfigPath)),
798
+ },
799
+ {
800
+ kind: 'opencode' as const,
801
+ label: 'OpenCode',
802
+ command: readOpenCodeMcpCommand(openCodeConfigPath),
803
+ refresh: options.refreshOpenCode ?? (() => refreshOpenCodeMcpCommand(openCodeConfigPath)),
804
+ },
805
+ ];
806
+
807
+ for (const agent of agents) {
808
+ if (!isStaleBorgMcpCommand(agent.command)) continue;
809
+ try {
810
+ agent.refresh();
811
+ refreshed.push(agent.kind);
812
+ } catch (error) {
813
+ const message = error instanceof Error ? error.message : String(error);
814
+ failures.push(`${agent.label}: ${message}`);
815
+ }
816
+ }
817
+
818
+ if (failures.length > 0) {
819
+ throw new Error(`Could not refresh agent MCP configs: ${failures.join('; ')}`);
820
+ }
821
+ return refreshed;
822
+ }
823
+
681
824
  /**
682
825
  * Get absolute path to borg index.js
683
826
  * Returns the actual index.js file, not the npm symlink
@@ -703,9 +846,7 @@ export function addMcpServer(): void {
703
846
  // Ignore - server might not exist yet
704
847
  }
705
848
 
706
- // gh#client#18: use absolute path to THIS installation's binary so the
707
- // registered server always matches the running client version.
708
- const command = `claude mcp add --scope user borg ${shellQuote(MCP_BINARY)}`;
849
+ const command = `claude mcp add --scope user borg ${shellQuote(MCP_COMMAND)}`;
709
850
 
710
851
  execSync(command, {
711
852
  stdio: 'inherit', // Show output to user
@@ -736,7 +877,6 @@ export function addCodexMcpServer(): void {
736
877
  // Identity is durable configuration; remote wake is a per-launch
737
878
  // transport capability. Do not persist a transport marker here: a future
738
879
  // Codex child may launch without a live --remote socket.
739
- // gh#client#18: use absolute path to THIS installation's binary.
740
880
  const apiUrlEnvArg = apiUrl ? ` --env BORG_API_URL=${shellQuote(apiUrl)}` : '';
741
881
  const stateRoot = process.env[BORG_STATE_ROOT_ENV];
742
882
  const stateRootEnvArg = stateRoot
@@ -746,7 +886,7 @@ export function addCodexMcpServer(): void {
746
886
  apiUrlEnvArg +
747
887
  stateRootEnvArg +
748
888
  ` --env ${BORG_AGENT_KIND_ENV}=codex` +
749
- ` -- ${shellQuote(MCP_BINARY)}`, {
889
+ ` -- ${shellQuote(MCP_COMMAND)}`, {
750
890
  stdio: 'inherit',
751
891
  env: codexConfigEnv,
752
892
  });
@@ -965,9 +1105,8 @@ export function addOpenCodeMcpServer(): void {
965
1105
  const stateRootEnvArg = stateRoot
966
1106
  ? ` --env ${BORG_STATE_ROOT_ENV}=${shellQuote(stateRoot)}`
967
1107
  : '';
968
- // gh#client#18: use absolute path to THIS installation's binary.
969
1108
  execSync(
970
- `opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg} -- ${shellQuote(MCP_BINARY)}`,
1109
+ `opencode mcp add borg --env BORG_SESSION=1 --env BORG_AGENT_KIND=opencode --env BORG_OPENCODE=1${apiUrlEnvArg}${stateRootEnvArg} -- ${shellQuote(MCP_COMMAND)}`,
971
1110
  { stdio: 'inherit', env: borgAgentConfigEnv(process.env) }
972
1111
  );
973
1112
  } catch (error: any) {
@@ -66,7 +66,7 @@ async function resolveTargetCube(
66
66
  return {
67
67
  error:
68
68
  `'${args.cubeName}' is ambiguous — ${matches.length} saved connections on this machine share that name:\n${list}\n` +
69
- 'cd into the intended project and re-run without --cube-name (resolves the active cube), ' +
69
+ 'cd into the intended project and re-run as `borg launch-all` without the positional cube name (resolves the active cube), ' +
70
70
  'or clear the stale connection(s) by running `borg reset-local-connection` from the worktree that holds each.',
71
71
  };
72
72
  }
@@ -17,17 +17,23 @@ import { randomUUID } from 'node:crypto';
17
17
  import {
18
18
  createProtocolEnvelope,
19
19
  decodeDeleteCubeResponse,
20
+ decodeDeleteRoleRequest,
21
+ decodeDeleteRoleResult,
20
22
  decodeDroneRuntimeMetadataState,
21
23
  decodeEvictDroneResult,
22
24
  decodeProtocolEnvelope,
23
25
  decodeProtocolErrorEnvelope,
24
26
  decodeReassignDroneResult,
27
+ decodeRoleRationaleRequest,
28
+ decodeRoleRationaleResult,
25
29
  decodeUpdateDroneRuntimeMetadataResponse,
26
30
  ErrorCode,
27
31
  ProtocolContractError,
28
32
  type AgentKind,
33
+ type DeleteRoleResult,
29
34
  type EvictDroneResult,
30
35
  type ReassignDroneResult,
36
+ type RoleRationaleResult,
31
37
  } from 'borgmcp-shared/protocol';
32
38
  import { consolePrefix } from './console-prefix.js';
33
39
  import { debugLog } from './debug.js';
@@ -301,7 +307,10 @@ async function localServerRequest<T>(
301
307
  path: string,
302
308
  method: 'GET' | 'POST' | 'PUT' | 'PATCH',
303
309
  payload?: Record<string, unknown>,
304
- options: { retryMode?: AuthedFetchRetryMode } = {},
310
+ options: {
311
+ retryMode?: AuthedFetchRetryMode;
312
+ decodePayload?: (value: unknown) => T;
313
+ } = {},
305
314
  ): Promise<T | null> {
306
315
  return decodeLocalProtocolResponse<T>((signal) => authedFetch(path, {
307
316
  method,
@@ -318,7 +327,7 @@ async function localServerRequest<T>(
318
327
  body: JSON.stringify(createProtocolEnvelope(randomUUID(), payload)),
319
328
  }),
320
329
  retryMode: options.retryMode,
321
- }), true);
330
+ }), true, options.decodePayload);
322
331
  }
323
332
 
324
333
  export interface LocalManageOperation {
@@ -1253,12 +1262,21 @@ export async function roleRationale(
1253
1262
  section: string,
1254
1263
  serverTrustIdentity?: string,
1255
1264
  ): Promise<{ role: string; section: string; body: string }> {
1256
- void sessionToken;
1257
- void apiUrl;
1258
- void role;
1259
- void section;
1260
- void serverTrustIdentity;
1261
- localUnsupported('role rationale sections');
1265
+ const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
1266
+ const request = decodeRoleRationaleRequest({ role, section });
1267
+ const result = await localServerRequest<RoleRationaleResult>(
1268
+ local,
1269
+ `/api/cubes/${local.cubeId}/role-rationale`,
1270
+ 'POST',
1271
+ { ...request },
1272
+ { decodePayload: decodeRoleRationaleResult },
1273
+ );
1274
+ if (!result) throw new Error('Local Borg server returned an empty role rationale response');
1275
+ return {
1276
+ role: result.role_name,
1277
+ section: result.section.heading,
1278
+ body: result.section.body,
1279
+ };
1262
1280
  }
1263
1281
 
1264
1282
  /**
@@ -1662,8 +1680,26 @@ export async function patchRoleSection(
1662
1680
  * (reassign or evict those drones first).
1663
1681
  */
1664
1682
  export async function deleteRole(roleId: string): Promise<void> {
1665
- void roleId;
1666
- localUnsupported('role deletion');
1683
+ assertUuidShape(roleId, 'role_id');
1684
+ const active = await getActiveCube();
1685
+ if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
1686
+ assertUuidShape(active.cubeId, 'cube_id');
1687
+ const result = await localManageRequest<DeleteRoleResult>(
1688
+ active,
1689
+ `/api/cubes/${active.cubeId}/roles/${roleId}`,
1690
+ 'DELETE',
1691
+ {
1692
+ operation: `delete role ${manageCopyValue(roleId)} from cube ${manageCopyValue(active.name)}`,
1693
+ cubeName: active.name,
1694
+ noMutation: 'No role was deleted.',
1695
+ },
1696
+ decodeDeleteRoleRequest({}),
1697
+ decodeDeleteRoleResult,
1698
+ );
1699
+ if (!result) throw new Error('Local Borg server returned an empty role deletion response');
1700
+ if (result.role_id !== roleId) {
1701
+ throw new Error('Local Borg server returned a deletion response for an unexpected role');
1702
+ }
1667
1703
  }
1668
1704
 
1669
1705
  /**
@@ -214,7 +214,7 @@ export interface ServerAttachResult {
214
214
  }
215
215
 
216
216
  /**
217
- * Attach an enrolled client principal to one granted cube/role over protocol v7.
217
+ * Attach an enrolled client principal to one granted cube/role over protocol v8.
218
218
  * The client CSPRNG-generates the session bearer and persists it PENDING in the
219
219
  * OS keychain (keyed by the stable per-seat identity) BEFORE this request, so an
220
220
  * interrupted/lost response is recovered by re-sending the exact same bearer —
@@ -154,8 +154,8 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
154
154
  {
155
155
  name: 'borg_role-rationale',
156
156
  description:
157
- "Fetch an on-demand rationale/case-study section for a role playbook. " +
158
- "Pass a role name/id and a plain-label section key to read the rationale without expanding every context refresh.",
157
+ "Fetch exactly one named section from a role's detailed playbook using the current drone session. " +
158
+ "Pass a role name/UUID and a plain-label section key. Role names and section keys match case-insensitively; UUIDs match exactly. Malformed selectors, ambiguous role names, and unknown roles/sections refuse. Returns the server's canonical role name, role_id, section heading, and the section body in full, refusing rather than truncating when it exceeds the server's role-text size limit, so a drone can read one section on demand instead of carrying the whole playbook in every borg_regen.",
159
159
  inputSchema: {
160
160
  type: 'object',
161
161
  properties: {
@@ -481,7 +481,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
481
481
  },
482
482
  {
483
483
  name: 'borg_delete-role',
484
- description: 'Delete a role. Refuses if any drone is still assigned reassign them with borg_reassign-drone or remove them with borg_evict-drone first.',
484
+ description: 'Delete a role using the selected local client\'s cube-management grant. Unknown or inaccessible roles refuse. Also refuses for the default, mandatory, or human-seat role; a role referenced by message-taxonomy routing; or a role assigned to an active drone. Reassign active drones with borg_reassign-drone or remove them with borg_evict-drone first. Evicted drones that held the deleted role are reassigned to the cube\'s default role; their activity-log attribution is unaffected.',
485
485
  inputSchema: {
486
486
  type: 'object',
487
487
  properties: {
package/src/update-cmd.ts CHANGED
@@ -8,6 +8,7 @@ import { updateHelpText } from './cli-help.js';
8
8
  import { preflightBorgServerTag } from './server-handshake.js';
9
9
  import { loadBorgServerTrust } from './server-trust.js';
10
10
  import { shellEscape } from './shell-escape.js';
11
+ import { refreshManagedAgentMcpConfigs } from './config-utils.js';
11
12
 
12
13
  const CLIENT_PACKAGE = 'borgmcp';
13
14
  const SERVER_PACKAGE = 'borgmcp-server';
@@ -64,6 +65,7 @@ export interface UpdateDeps {
64
65
  reenter(binPath: string, args: readonly string[]): Promise<number>;
65
66
  serverJson(binPath: string, command: 'update' | 'status'): Promise<unknown>;
66
67
  verifyRunningProtocol(origin: string): Promise<void>;
68
+ refreshAgentMcpConfigs(): Promise<Array<'claude' | 'codex' | 'opencode'>>;
67
69
  confirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'>;
68
70
  isTTY(): boolean;
69
71
  stdout(text: string): void;
@@ -113,7 +115,8 @@ type ServerUpdateFailureStage =
113
115
  | 'final server state verification'
114
116
  | 'final package verification'
115
117
  | 'managed service continuity check'
116
- | 'running server protocol verification';
118
+ | 'running server protocol verification'
119
+ | 'agent MCP config refresh';
117
120
 
118
121
  interface NpmContext {
119
122
  commandPath: string;
@@ -695,6 +698,15 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
695
698
  }
696
699
 
697
700
  if (!serverWasPresent) {
701
+ try {
702
+ await deps.refreshAgentMcpConfigs();
703
+ } catch (error) {
704
+ deps.stderr(
705
+ `Client updated, but agent MCP config refresh failed: ${errorMessage(error, 'unknown failure')}.\n` +
706
+ `Run borg setup to repair Borg-written agent registrations. Configurations that use another command are not changed.\n`,
707
+ );
708
+ return 1;
709
+ }
698
710
  deps.stdout(
699
711
  `Updated ${CLIENT_PACKAGE}@${pair.client.version}. Local server: skipped (not installed).\n` +
700
712
  `Restart active agent sessions to load the updated client.\n`,
@@ -809,6 +821,9 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
809
821
  retryCommand = 'borg server status';
810
822
  await deps.verifyRunningProtocol(status.endpoint!);
811
823
  }
824
+ failureStage = 'agent MCP config refresh';
825
+ retryCommand = 'borg update --yes';
826
+ await deps.refreshAgentMcpConfigs();
812
827
  deps.stdout(
813
828
  state === 'stopped'
814
829
  ? (
@@ -1210,6 +1225,7 @@ export function buildDefaultUpdateDeps(): UpdateDeps {
1210
1225
  const trust = await loadBorgServerTrust(origin);
1211
1226
  await preflightBorgServerTag(origin, trust.fetchImpl);
1212
1227
  },
1228
+ refreshAgentMcpConfigs: async () => refreshManagedAgentMcpConfigs(),
1213
1229
  confirm: defaultConfirm,
1214
1230
  isTTY: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
1215
1231
  stdout: (text) => process.stdout.write(text),