borgmcp 2.7.1 → 2.7.3

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 (60) hide show
  1. package/README.md +15 -10
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +83 -26
  4. package/dist/assimilate-cmd.js.map +1 -1
  5. package/dist/claude.js +6 -16
  6. package/dist/claude.js.map +1 -1
  7. package/dist/cli-help.d.ts +5 -0
  8. package/dist/cli-help.d.ts.map +1 -1
  9. package/dist/cli-help.js +49 -1
  10. package/dist/cli-help.js.map +1 -1
  11. package/dist/cubes.d.ts +10 -5
  12. package/dist/cubes.d.ts.map +1 -1
  13. package/dist/cubes.js +54 -3
  14. package/dist/cubes.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +60 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/log-stream.d.ts +3 -0
  19. package/dist/log-stream.d.ts.map +1 -1
  20. package/dist/log-stream.js +6 -1
  21. package/dist/log-stream.js.map +1 -1
  22. package/dist/opencode-seat-identity.d.ts +22 -0
  23. package/dist/opencode-seat-identity.d.ts.map +1 -0
  24. package/dist/opencode-seat-identity.js +69 -0
  25. package/dist/opencode-seat-identity.js.map +1 -0
  26. package/dist/server-facade.d.ts +4 -0
  27. package/dist/server-facade.d.ts.map +1 -1
  28. package/dist/server-facade.js +11 -0
  29. package/dist/server-facade.js.map +1 -1
  30. package/dist/setup.js +2 -4
  31. package/dist/setup.js.map +1 -1
  32. package/dist/startup-services.d.ts +3 -1
  33. package/dist/startup-services.d.ts.map +1 -1
  34. package/dist/startup-services.js +7 -2
  35. package/dist/startup-services.js.map +1 -1
  36. package/dist/stream-owner.d.ts +9 -0
  37. package/dist/stream-owner.d.ts.map +1 -1
  38. package/dist/stream-owner.js +14 -2
  39. package/dist/stream-owner.js.map +1 -1
  40. package/dist/stream-status.d.ts.map +1 -1
  41. package/dist/stream-status.js +8 -1
  42. package/dist/stream-status.js.map +1 -1
  43. package/dist/tool-manifest.js +11 -11
  44. package/dist/tool-manifest.js.map +1 -1
  45. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  46. package/docs/RELEASING.md +11 -1
  47. package/package.json +1 -1
  48. package/src/assimilate-cmd.ts +94 -26
  49. package/src/claude.ts +11 -16
  50. package/src/cli-help.ts +65 -1
  51. package/src/cubes.ts +70 -3
  52. package/src/index.ts +66 -7
  53. package/src/log-stream.ts +9 -1
  54. package/src/opencode-seat-identity.ts +111 -0
  55. package/src/server-facade.ts +15 -0
  56. package/src/setup.ts +2 -4
  57. package/src/startup-services.ts +8 -2
  58. package/src/stream-owner.ts +23 -2
  59. package/src/stream-status.ts +8 -1
  60. package/src/tool-manifest.ts +11 -11
@@ -0,0 +1,111 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { resolve } from 'node:path';
3
+ import type { ActiveCube } from './cubes.js';
4
+
5
+ export type OpenCodeSeatIdentityErrorCode =
6
+ | 'ROOTS_UNAVAILABLE'
7
+ | 'ROOTS_INVALID'
8
+ | 'SEAT_NOT_FOUND'
9
+ | 'SEAT_WORKTREE_MISMATCH';
10
+
11
+ export class OpenCodeSeatIdentityError extends Error {
12
+ constructor(
13
+ public readonly code: OpenCodeSeatIdentityErrorCode,
14
+ message: string,
15
+ public readonly sessionDirectory?: string,
16
+ public readonly seat?: Pick<ActiveCube, 'droneLabel' | 'worktree'>,
17
+ ) {
18
+ super(message);
19
+ this.name = 'OpenCodeSeatIdentityError';
20
+ }
21
+ }
22
+
23
+ export interface OpenCodeSeatIdentityDeps {
24
+ listRoots: () => Promise<{ roots?: Array<{ uri?: string }> }>;
25
+ findProjectRoot: (directory: string) => string;
26
+ getActiveCubeForWorktree: (worktree: string) => Promise<ActiveCube | null>;
27
+ pinSeatIdentity: (active: ActiveCube) => void;
28
+ childCwd: string;
29
+ }
30
+
31
+ export async function resolveOpenCodeSeatIdentity(
32
+ deps: OpenCodeSeatIdentityDeps,
33
+ ): Promise<ActiveCube> {
34
+ let roots: { roots?: Array<{ uri?: string }> };
35
+ try {
36
+ roots = await deps.listRoots();
37
+ } catch {
38
+ throw new OpenCodeSeatIdentityError(
39
+ 'ROOTS_UNAVAILABLE',
40
+ 'OpenCode did not provide its session directory.',
41
+ );
42
+ }
43
+ if (!Array.isArray(roots.roots) || roots.roots.length !== 1) {
44
+ throw new OpenCodeSeatIdentityError(
45
+ 'ROOTS_INVALID',
46
+ 'OpenCode must provide exactly one local session directory.',
47
+ );
48
+ }
49
+ const uri = roots.roots[0]?.uri;
50
+ let sessionDirectory: string;
51
+ try {
52
+ const parsed = new URL(typeof uri === 'string' ? uri : '');
53
+ if (parsed.protocol !== 'file:' || parsed.hostname || parsed.search || parsed.hash) throw new Error();
54
+ sessionDirectory = resolve(fileURLToPath(parsed));
55
+ } catch {
56
+ throw new OpenCodeSeatIdentityError(
57
+ 'ROOTS_INVALID',
58
+ 'OpenCode provided an invalid or non-local session directory.',
59
+ );
60
+ }
61
+
62
+ const sessionWorktree = deps.findProjectRoot(sessionDirectory);
63
+ const cwdWorktree = deps.findProjectRoot(deps.childCwd);
64
+ // Direct `borg` launch keeps cwd as its seat source. When a shared OpenCode
65
+ // server spawns the MCP child elsewhere, the session-scoped root is the
66
+ // launcher-conferred pin and replaces that ambient cwd inference.
67
+ const identityWorktree = cwdWorktree === sessionWorktree
68
+ ? cwdWorktree
69
+ : sessionWorktree;
70
+ const active = await deps.getActiveCubeForWorktree(identityWorktree);
71
+ if (!active) {
72
+ throw new OpenCodeSeatIdentityError(
73
+ 'SEAT_NOT_FOUND',
74
+ 'No active Borg seat is bound to the OpenCode session directory.',
75
+ sessionWorktree,
76
+ );
77
+ }
78
+ if (typeof active.worktree !== 'string' || resolve(active.worktree) !== resolve(sessionWorktree)) {
79
+ throw new OpenCodeSeatIdentityError(
80
+ 'SEAT_WORKTREE_MISMATCH',
81
+ 'The resolved Borg seat belongs to a different worktree than the OpenCode session.',
82
+ sessionWorktree,
83
+ active,
84
+ );
85
+ }
86
+
87
+ deps.pinSeatIdentity(active);
88
+ return active;
89
+ }
90
+
91
+ export function formatOpenCodeSeatIdentityError(
92
+ error: OpenCodeSeatIdentityError,
93
+ childCwd: string,
94
+ ): string {
95
+ const lines = [
96
+ `Borg OpenCode seat identity error [${error.code}]`,
97
+ '',
98
+ error.message,
99
+ `- OpenCode session directory: ${error.sessionDirectory ?? 'unavailable'}`,
100
+ `- Borg MCP child cwd: ${childCwd}`,
101
+ ];
102
+ if (error.seat) {
103
+ lines.push(`- Resolved seat: ${error.seat.droneLabel} (${error.seat.worktree})`);
104
+ }
105
+ lines.push(
106
+ '',
107
+ 'The Borg stream and OpenCode wake injection were not started.',
108
+ 'Exit this session and run `borg --cli opencode` from the intended worktree. If that worktree’s saved seat is stale, run `borg reset-local-seat` from that exact worktree before assimilating again.',
109
+ );
110
+ return lines.join('\n');
111
+ }
@@ -12,6 +12,7 @@ export type ParsedServerFacadeArgs =
12
12
  | { kind: 'help' }
13
13
  | { kind: 'cube-init-help' }
14
14
  | { kind: 'cube-init'; args: string[] }
15
+ | { kind: 'command-help'; command: ServerLifecycleCommand }
15
16
  | { kind: 'command'; command: ServerLifecycleCommand; args: string[] }
16
17
  | { kind: 'error'; reason: 'unknown-command'; command: string };
17
18
 
@@ -33,6 +34,9 @@ export function parseServerFacadeArgs(args: readonly string[]): ParsedServerFaca
33
34
  if (!(SERVER_LIFECYCLE_COMMANDS as readonly string[]).includes(command)) {
34
35
  return { kind: 'error', reason: 'unknown-command', command };
35
36
  }
37
+ if (rest.some(isHelpFlag)) {
38
+ return { kind: 'command-help', command: command as ServerLifecycleCommand };
39
+ }
36
40
  return {
37
41
  kind: 'command',
38
42
  command: command as ServerLifecycleCommand,
@@ -163,6 +167,13 @@ export function unknownServerCommandText(command: string): string {
163
167
  );
164
168
  }
165
169
 
170
+ export function serverLifecycleHelpText(command: ServerLifecycleCommand): string {
171
+ return (
172
+ `Usage: borg server ${command} [arguments]\n\n` +
173
+ `Command arguments are server-owned and pass to the verified borg-mcp-server executable when run.\n`
174
+ );
175
+ }
176
+
166
177
  export function missingServerExecutableText(command: ServerLifecycleCommand): string {
167
178
  return (
168
179
  `Local server command is unavailable: borg-mcp-server was not found.\n` +
@@ -256,6 +267,10 @@ export async function runEarlyServerFacade(
256
267
  output.writeStdout(cubeInitHelpText(getPackageVersion()));
257
268
  return 0;
258
269
  }
270
+ if (parsed.kind === 'command-help') {
271
+ output.writeStdout(serverLifecycleHelpText(parsed.command));
272
+ return 0;
273
+ }
259
274
  if (parsed.kind === 'error') {
260
275
  output.writeStderr(unknownServerCommandText(parsed.command));
261
276
  return 1;
package/src/setup.ts CHANGED
@@ -38,6 +38,7 @@ import { handleVersionFlag } from './version.js';
38
38
  import { initDebugFromArgv } from './debug.js';
39
39
  import { defaultApprovalIo, setupApprovalWarnings } from './cli-tool-approval.js';
40
40
  import { offerFirstRunServerInstall } from './first-run-server.js';
41
+ import { setupNextStepsText } from './cli-help.js';
41
42
 
42
43
  /**
43
44
  * Main setup wizard
@@ -184,10 +185,7 @@ async function main() {
184
185
  // Success message
185
186
  console.log(chalk.green.bold('\nSetup complete!\n'));
186
187
  console.log(chalk.yellow('🔄 Restart Claude Code / Codex / OpenCode (or open a new session) for the changes to take effect.\n'));
187
- console.log(chalk.gray('◼ Next steps:'));
188
- console.log(chalk.gray('1. cd into your project, then run "borg assimilate --host <host>" to join a cube'));
189
- console.log(chalk.gray(' (this connects to your local server and launches your agent)'));
190
- console.log(chalk.gray('2. Use `borg assimilate --host <host> --enroll` from the operator terminal to enroll a new client\n'));
188
+ console.log(chalk.gray(setupNextStepsText()));
191
189
  }
192
190
 
193
191
  // Run wizard
@@ -16,10 +16,16 @@ export interface McpStartupServices {
16
16
  */
17
17
  export async function runMcpStartupServices(
18
18
  readinessProbe: boolean,
19
- services: McpStartupServices
19
+ services: McpStartupServices,
20
+ options: { openCodeFirst?: boolean } = {},
20
21
  ): Promise<void> {
21
22
  if (readinessProbe) return;
22
- const tasks = [
23
+ const tasks = options.openCodeFirst ? [
24
+ services.sessionStartHook,
25
+ services.auditHook,
26
+ services.openCode,
27
+ services.sseStream,
28
+ ] : [
23
29
  services.sessionStartHook,
24
30
  services.auditHook,
25
31
  services.sseStream,
@@ -24,6 +24,9 @@ export interface StreamOwnerRecord {
24
24
  cwd: string;
25
25
  startedAt: string;
26
26
  heartbeatAt: string;
27
+ worktree?: string;
28
+ droneLabel?: string;
29
+ cubeName?: string;
27
30
  }
28
31
 
29
32
  export interface StreamOwnershipSnapshot {
@@ -33,6 +36,9 @@ export interface StreamOwnershipSnapshot {
33
36
  cwd?: string;
34
37
  startedAt?: string;
35
38
  heartbeatAt?: string;
39
+ worktree?: string;
40
+ droneLabel?: string;
41
+ cubeName?: string;
36
42
  ageMs?: number;
37
43
  lockPath?: string;
38
44
  /** Opened-directory identity used to bind inspection to later takeover. */
@@ -54,6 +60,9 @@ export interface StreamOwnerDeps {
54
60
  locksDir?: string;
55
61
  processNonce?: string;
56
62
  processStartedAt?: string;
63
+ worktree?: string;
64
+ droneLabel?: string;
65
+ cubeName?: string;
57
66
  isPidAlive?: (pid: number) => boolean;
58
67
  beforeTakeoverVerify?: (takeoverPath: string) => Promise<void>;
59
68
  beforeLeaseRefreshMutation?: (lockPath: string) => Promise<void>;
@@ -177,6 +186,9 @@ export async function readOwnershipSnapshot(
177
186
  cwd: parsed.cwd,
178
187
  startedAt: parsed.startedAt,
179
188
  heartbeatAt: parsed.heartbeatAt,
189
+ worktree: parsed.worktree,
190
+ droneLabel: parsed.droneLabel,
191
+ cubeName: parsed.cubeName,
180
192
  ageMs,
181
193
  lockPath,
182
194
  lockDev: lockStat.dev,
@@ -572,7 +584,10 @@ function sameOwner(left: StreamOwnerRecord, right: StreamOwnerRecord): boolean {
572
584
  left.processNonce === right.processNonce &&
573
585
  left.cwd === right.cwd &&
574
586
  left.startedAt === right.startedAt &&
575
- left.heartbeatAt === right.heartbeatAt;
587
+ left.heartbeatAt === right.heartbeatAt &&
588
+ left.worktree === right.worktree &&
589
+ left.droneLabel === right.droneLabel &&
590
+ left.cubeName === right.cubeName;
576
591
  }
577
592
 
578
593
  async function readOwnershipRecord(lockPath: string): Promise<StreamOwnerRecord | null> {
@@ -655,6 +670,9 @@ function makeRecord(deps: StreamOwnerDeps): StreamOwnerRecord {
655
670
  cwd: deps.cwd ?? process.cwd(),
656
671
  startedAt: deps.processStartedAt ?? processStartedAt,
657
672
  heartbeatAt: now().toISOString(),
673
+ ...(deps.worktree ? { worktree: deps.worktree } : {}),
674
+ ...(deps.droneLabel ? { droneLabel: deps.droneLabel } : {}),
675
+ ...(deps.cubeName ? { cubeName: deps.cubeName } : {}),
658
676
  };
659
677
  }
660
678
 
@@ -668,7 +686,10 @@ function isRecord(value: any): value is StreamOwnerRecord {
668
686
  isSafeLeaseText(value.processNonce, 128) &&
669
687
  isSafeLeaseText(value.cwd, 4096) &&
670
688
  isIsoTimestamp(value.startedAt) &&
671
- isIsoTimestamp(value.heartbeatAt)
689
+ isIsoTimestamp(value.heartbeatAt) &&
690
+ (value.worktree === undefined || isSafeLeaseText(value.worktree, 4096)) &&
691
+ (value.droneLabel === undefined || isSafeLeaseText(value.droneLabel, 256)) &&
692
+ (value.cubeName === undefined || isSafeLeaseText(value.cubeName, 256))
672
693
  );
673
694
  }
674
695
 
@@ -161,7 +161,10 @@ export function renderStreamStatus(inputs: RenderInputs): string {
161
161
  if (orphanedInitialization) {
162
162
  summary = '**Stream blocked by an orphaned initialization lock.**';
163
163
  } else if (ownedByOther) {
164
- summary = '**Stream owned by another Borg MCP process.**';
164
+ const owner = status.ownership!;
165
+ summary = owner.droneLabel && owner.worktree
166
+ ? `**Stream owned by seat ${owner.droneLabel} in \`${owner.worktree}\`.**`
167
+ : '**Stream owned by another Borg MCP process.**';
165
168
  } else if (isNotStarted) {
166
169
  summary = '**Stream not started.**';
167
170
  } else if (!status.connected) {
@@ -245,6 +248,8 @@ export function renderStreamStatus(inputs: RenderInputs): string {
245
248
 
246
249
  if (ownedByOther) {
247
250
  const owner = status.ownership!;
251
+ lines.push(`- **stream owner seat**: ${owner.droneLabel ?? '_(unknown)_'}`);
252
+ lines.push(`- **stream owner worktree**: ${owner.worktree ?? '_(unknown)_'}`);
248
253
  lines.push(`- **stream owner pid**: ${owner.pid ?? '_(unknown)_'}`);
249
254
  lines.push(`- **stream owner cwd**: ${owner.cwd ?? '_(unknown)_'}`);
250
255
  lines.push(
@@ -254,6 +259,8 @@ export function renderStreamStatus(inputs: RenderInputs): string {
254
259
  : '_(unknown)_'
255
260
  }`
256
261
  );
262
+ lines.push('');
263
+ lines.push('Continue in the owning seat, or close its duplicate agent session before relaunching from the intended worktree. The live owner releases this lock on exit; a stale lock is reclaimed automatically.');
257
264
  }
258
265
 
259
266
  if (wakePath.agentKind === 'opencode' && wakePath.openCode) {
@@ -58,10 +58,10 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
58
58
  {
59
59
  name: 'borg_assimilate',
60
60
  description:
61
- "RE-ATTACH this session to the drone seat already saved for this worktree (gh#780: " +
62
- "this tool never creates seats). Provide the cube's name; on a match it returns the " +
63
- "cube directive, your role's instructions, and recent activity for the EXISTING seat. " +
64
- 'To create a seat or switch cubes, run `borg assimilate` in a terminal instead.',
61
+ "Reconnect this session to the existing drone saved for this worktree. This tool " +
62
+ "never creates drones. Provide the cube's name; on a match it returns the cube " +
63
+ "directive, your role's instructions, and recent activity for that drone. To create " +
64
+ 'a drone or switch cubes, run `borg assimilate` in a terminal instead.',
65
65
  inputSchema: {
66
66
  type: 'object',
67
67
  properties: {
@@ -116,7 +116,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
116
116
  {
117
117
  name: 'borg_playbook',
118
118
  description:
119
- 'Load the full operating-playbook chapter — the detailed disciplines, rationale, and examples behind the rule-spine in your regen (verification discipline v1/v2/v3, concrete source-of-truth surfaces, four-surface propagation). This detail is kept OUT of the regen bootstrap to keep it light; fetch it ONCE per session when doing review/verify-class work. Static text — do NOT re-fetch on every wake.',
119
+ 'Load the full operating-playbook chapter — the detailed disciplines, rationale, and examples behind the abbreviated session instructions (verification discipline v1/v2/v3, concrete source-of-truth surfaces, four-surface propagation). This detail is omitted from the initial context to keep it light; fetch it ONCE per session when doing review/verify-class work. Static text — do NOT re-fetch on every wake.',
120
120
  inputSchema: {
121
121
  type: 'object',
122
122
  properties: {},
@@ -153,7 +153,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
153
153
  name: 'borg_role-rationale',
154
154
  description:
155
155
  "Fetch an on-demand rationale/case-study section for a role playbook. " +
156
- "Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",
156
+ "Pass a role name/id and a plain-label section key to read the rationale without expanding every context refresh.",
157
157
  inputSchema: {
158
158
  type: 'object',
159
159
  properties: {
@@ -249,7 +249,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
249
249
  {
250
250
  name: 'borg_decide',
251
251
  description:
252
- 'Record a RATIFIED cube decision in the durable decision registry (gh#740) so drones cite it by topic instead of restating from memory. Coordinator/Queen are workflow-eligible to ratify, but their labels grant no server permission; the selected local client must have a live cube-manage grant. Recording IS the ratification act; a decision is not ratified until it is in the registry. Topic-keyed: recording a new decision on an existing topic supersedes the prior (one active per topic). Surfaces in borg_regen + borg_decisions.',
252
+ 'Record a RATIFIED cube decision in the durable decision registry so drones cite it by topic instead of restating from memory. Coordinator and Queen roles are workflow-eligible to ratify, but role labels grant no server permission; the selected local client must have a live cube-manage grant. Recording IS the ratification act; a decision is not ratified until it is in the registry. Topic-keyed: recording a new decision on an existing topic supersedes the prior (one active per topic). The decision appears in borg_regen and borg_decisions.',
253
253
  inputSchema: {
254
254
  type: 'object',
255
255
  required: ['topic', 'decision'],
@@ -272,7 +272,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
272
272
  {
273
273
  name: 'borg_decisions',
274
274
  description:
275
- 'List the active ratified decisions for the cube (gh#740) — the source of truth to CITE instead of restating a decision from memory. Any member may read. Pass `topic` to fetch one topic\'s active decision; omit for all active decisions.',
275
+ 'List the active ratified decisions for the cube — the source of truth to CITE instead of restating a decision from memory. Any member may read. Pass `topic` to fetch one topic\'s active decision; omit for all active decisions.',
276
276
  inputSchema: {
277
277
  type: 'object',
278
278
  properties: {
@@ -350,7 +350,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
350
350
  pattern: '^[a-z0-9-]+$',
351
351
  maxLength: 64,
352
352
  },
353
- cube_directive: { type: 'string', description: 'Markdown text every drone in this cube will see in regen. Anything project-specific.' },
353
+ cube_directive: { type: 'string', description: 'Project-specific Markdown shown to every drone when it refreshes cube context.' },
354
354
  template: {
355
355
  type: 'string',
356
356
  description: 'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.',
@@ -439,7 +439,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
439
439
  short_description: { type: 'string', description: 'One-line summary, shown to every drone in the cube.' },
440
440
  detailed_description: { type: 'string', description: 'Full playbook for drones in this role — workflow, conventions, log signals to post.' },
441
441
  is_default: { type: 'boolean', description: 'If true, new drones assimilating into this cube are assigned this role. Demotes the previous default.' },
442
- is_mandatory: { type: 'boolean', description: 'If true, role-less assimilate fills this unoccupied non-queen role before ordinary worker roles. A mandatory human-seat role is therefore selected first until occupied.' },
442
+ is_mandatory: { type: 'boolean', description: 'If true, role-less assimilation prioritizes this unoccupied role before ordinary worker roles. Platform-wide management roles are never auto-assigned; a mandatory human-operator role is selected first until occupied.' },
443
443
  is_human_seat: { type: 'boolean', description: 'If true, this role represents the cube\'s human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected.' },
444
444
  can_broadcast: { type: 'boolean', description: 'If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled.' },
445
445
  receives_all_direct: { type: 'boolean', description: 'If true, drones in this role can see direct log entries as observer/audit recipients.' },
@@ -458,7 +458,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
458
458
  short_description: { type: 'string', description: 'New short description (optional).' },
459
459
  detailed_description: { type: 'string', description: 'New detailed playbook (optional).' },
460
460
  is_default: { type: 'boolean', description: 'Set true to make this the cube\'s default role (optional).' },
461
- is_mandatory: { type: 'boolean', description: 'Set true/false to prioritize this unoccupied non-queen role during role-less assimilate.' },
461
+ is_mandatory: { type: 'boolean', description: 'Set true/false to prioritize this unoccupied role during role-less assimilation. Platform-wide management roles are never auto-assigned.' },
462
462
  is_human_seat: { type: 'boolean', description: 'Set true/false to mark/unmark this as the cube\'s human-occupied seat (the elevation source for the platform Queen role).' },
463
463
  can_broadcast: { type: 'boolean', description: 'Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled.' },
464
464
  receives_all_direct: { type: 'boolean', description: 'Set true/false to grant or remove observer visibility into direct log entries.' },