borgmcp 2.1.0 → 2.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dist/assimilate-cmd.d.ts +11 -2
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +60 -85
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +26 -8
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/cli-help.d.ts.map +1 -1
  10. package/dist/cli-help.js +7 -5
  11. package/dist/cli-help.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/regen-format.d.ts +1 -6
  16. package/dist/regen-format.d.ts.map +1 -1
  17. package/dist/regen-format.js +9 -8
  18. package/dist/regen-format.js.map +1 -1
  19. package/dist/remote-client.js +1 -1
  20. package/dist/remote-client.js.map +1 -1
  21. package/dist/repository-cube-init.d.ts +16 -1
  22. package/dist/repository-cube-init.d.ts.map +1 -1
  23. package/dist/repository-cube-init.js +167 -22
  24. package/dist/repository-cube-init.js.map +1 -1
  25. package/dist/server-errors.d.ts +11 -0
  26. package/dist/server-errors.d.ts.map +1 -1
  27. package/dist/server-errors.js +20 -0
  28. package/dist/server-errors.js.map +1 -1
  29. package/dist/server-facade.d.ts +2 -1
  30. package/dist/server-facade.d.ts.map +1 -1
  31. package/dist/server-facade.js +16 -10
  32. package/dist/server-facade.js.map +1 -1
  33. package/dist/server-handshake.d.ts +32 -2
  34. package/dist/server-handshake.d.ts.map +1 -1
  35. package/dist/server-handshake.js +161 -8
  36. package/dist/server-handshake.js.map +1 -1
  37. package/docs/EXTRACTION_PROVENANCE.md +9 -7
  38. package/docs/LOCAL_SERVER.md +19 -13
  39. package/docs/RELEASING.md +21 -5
  40. package/package.json +2 -2
  41. package/src/assimilate-cmd.ts +85 -69
  42. package/src/assimilate-deps.ts +37 -7
  43. package/src/cli-help.ts +7 -5
  44. package/src/index.ts +5 -1
  45. package/src/regen-format.ts +11 -9
  46. package/src/remote-client.ts +1 -1
  47. package/src/repository-cube-init.ts +202 -23
  48. package/src/server-errors.ts +27 -0
  49. package/src/server-facade.ts +16 -10
  50. package/src/server-handshake.ts +226 -8
@@ -50,7 +50,7 @@ Options:
50
50
  --enroll Prompt for a hidden enrollment invitation
51
51
  --cube-name <name> Repository cube name (otherwise edit the proposed name)
52
52
  --template software-dev|starter New-cube template (default: software-dev)
53
- --yes, -y Skip confirmation prompts
53
+ --yes, -y Accept new-cube defaults; never adopt by name
54
54
  --help, -h Show this help
55
55
  ```
56
56
 
@@ -91,14 +91,19 @@ exactly; the credential becomes active only after the versioned response is
91
91
  decoded and the authenticated protocol handshake succeeds. A new process
92
92
  resumes that pending enrollment before displaying another invitation prompt.
93
93
 
94
- `borg assimilate` and `borg server cube init` share one guided repository-cube
95
- flow. Borg shows the repository and server, proposes an editable name, offers
96
- the Software Development and Starter templates, and asks for one confirmation.
97
- `--cube-name <name>` and `--template software-dev|starter` supply those values
98
- directly; `--yes` accepts the repository default name and Software Development
99
- template. `borg server cube init` stops after authoritative cube readback and
100
- never creates a drone. Existing repository associations use zero prompts and
101
- report that creation flags were unused. Bare repositories fail closed.
94
+ `borg assimilate` and `borg server cube init` share one repository-cube flow.
95
+ An existing local or server repository association is resolved without prompts.
96
+ Otherwise Borg checks accessible cubes against the exact proposed name. One
97
+ match displays the cube, repository, and server and requires explicit interactive
98
+ confirmation before the server atomically associates it; multiple matches fail
99
+ closed. `--yes` never adopts a cube by name. With no match, an authorized owner
100
+ continues to the creation guide, which shows the repository and server, proposes
101
+ an editable name, offers the Software Development and Starter templates, and
102
+ asks for one confirmation. `--cube-name <name>` and `--template
103
+ software-dev|starter` supply those creation values directly; `--yes` accepts the
104
+ repository default name and Software Development template. `borg server cube
105
+ init` stops after authoritative cube readback and never creates a drone. Bare
106
+ repositories fail closed.
102
107
 
103
108
  For repositories with a canonical public `origin`, that origin is the stable
104
109
  repository identity. Repositories without one receive an invisible UUID stored
@@ -168,8 +173,9 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
168
173
 
169
174
  ## Release status
170
175
 
171
- This self-hosted path consumes the published `borgmcp-shared@0.6.3` v4 registry
172
- release. The matching server owner-enrollment, cube-create, attach, restart, log,
173
- and SSE implementation must also pass the full process-level local dogfood gate.
174
- Until that gate opens the self-hosted path remains preview-only, and the client
176
+ This self-hosted path pins published `borgmcp-shared@0.6.4` and requires the
177
+ matching published `borgmcp-server@0.1.21` for protocol v5 repository resolve
178
+ and association. The server owner-enrollment, repository resolve/association,
179
+ cube-create, attach, restart, log, and SSE implementation must also pass the full
180
+ process-level local dogfood gate. Until that gate opens the self-hosted path remains preview-only, and the client
175
181
  publish is deferred accordingly.
package/docs/RELEASING.md CHANGED
@@ -104,8 +104,22 @@ The annotated `v2.0.11` tag object
104
104
  `0676d93d3e8a64ba583e95f164d122b39687b53a`. Workflow run `30073844074`, attempt 1,
105
105
  successfully published that exact source as `borgmcp@2.0.11`; the registry records integrity
106
106
  `sha512-429njJ7HVmWhpm8E71RoLzG57Qehy3RSxEzs6fQofVoOIiZXnK4QQDwbQe9MPfhKifbvVN5lGcDKtKAjuC76ww==`.
107
+ Never move, replace, reuse, or rerun that tag or workflow.
108
+
109
+ The annotated `v2.1.0` tag object
110
+ `e0b189bb0dd1f1b12369ab81870113925e1afc41` peels to protected-main commit
111
+ `10f18e4010c9c518c0ea2184d42ed5388642a53f`. Workflow run `30166498938`, attempt 1,
112
+ successfully published that exact source as `borgmcp@2.1.0`; the same-run artifact report records integrity
113
+ `sha512-Gm9n69y3C0feNDYRPEOnShLCscQJ/BoylcRMH+sDDQJmdcbjY8+U6dDTcFv3w+uH5P/IVwJydB8TGRHPXL0FQA==`.
114
+ Never move, replace, reuse, or rerun that tag or workflow.
115
+
116
+ The annotated `v2.1.1` tag object
117
+ `aa8d772c4791b2da16d702d5d78e5b3c365796c4` peels to protected-main commit
118
+ `db0a6a76744928e0067eb03b8a236076c56ae758`. Workflow run `30174878044`, attempt 1,
119
+ successfully published that exact source as `borgmcp@2.1.1`; the same-run artifact report records integrity
120
+ `sha512-nZJ3i6LBIj/4743mqVRJTUH3YGmC8HfC/NuQb7eOv+/K/dyBogDnwZ3opiZiZSe4LE5CXuG72Sip7cPRoCoZrA==`.
107
121
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
108
- uses the unused `v2.1.0` identity from a fresh reviewed protected-main commit
122
+ uses the unused `v2.2.0` identity from a fresh reviewed protected-main commit
109
123
  and requires the complete release gate again.
110
124
 
111
125
  ## Release Prerequisites
@@ -117,10 +131,12 @@ Before creating the release tag, independently verify all of these conditions:
117
131
  - the extraction review confirms no private backend secrets, deployment
118
132
  configuration, customer data, local state, or duplicated shared contracts
119
133
  entered the public package;
120
- - the exact audited registry dependency `borgmcp-shared@0.6.3` remains locked to
121
- its canonical tarball and integrity;
122
- - the client and matching published `borgmcp-server@0.1.20`, which pins
123
- `borgmcp-shared@0.6.3`, pass the complete local dogfood gate;
134
+ - the exact audited registry dependency `borgmcp-shared@0.6.4` remains locked to
135
+ its canonical tarball and integrity
136
+ `sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`;
137
+ - the client and matching published `borgmcp-server@0.1.21`, integrity
138
+ `sha512-g0QH4cKcsKuDyZs1MvBmvCoCLaZ4wVI04yHCGSvcAeWghWxSBH0IIF8Q5OFi1pLB7TZXOVP6D3QAabUQ2eJTFg==`, which pins
139
+ `borgmcp-shared@0.6.4`, pass the complete local dogfood gate;
124
140
  - the selected stable client version is unused and the exact release commit is
125
141
  on protected `main`;
126
142
  - the repository and protected npm environment settings pass an operator audit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.1.0",
3
+ "version": "2.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",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "@modelcontextprotocol/sdk": "^1.0.4",
71
- "borgmcp-shared": "0.6.3",
71
+ "borgmcp-shared": "0.6.4",
72
72
  "chalk": "^5.3.0",
73
73
  "prompts": "^2.4.2",
74
74
  "which": "^4.0.0"
@@ -46,6 +46,9 @@ import {
46
46
  CubeCreationConfirmationError,
47
47
  CubeCreationOutcomeUnknownError,
48
48
  LegacySessionCredentialCollisionError,
49
+ RepositoryAssociationOperationError,
50
+ RepositoryAssociationOutcomeUnknownError,
51
+ RepositoryAssociationResolutionError,
49
52
  } from './server-errors.js';
50
53
  import type { SeatStatus } from './seat-probe.js';
51
54
  import type { ServerSessionOperation } from './config.js';
@@ -54,12 +57,18 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
54
57
  import { createHash } from 'node:crypto';
55
58
  import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
56
59
  import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
57
- import type { CreateCubeRepository, CubeTemplate } from 'borgmcp-shared/protocol';
60
+ import type {
61
+ AssociateRepositoryCubeResponse,
62
+ CreateCubeRepository,
63
+ CubeTemplate,
64
+ } from 'borgmcp-shared/protocol';
58
65
  import {
59
66
  initializeRepositoryCube,
67
+ RepositoryAssociationConfirmationError,
60
68
  RepositoryAssociationSaveError,
61
69
  validRepositoryCubeName,
62
70
  type RepositoryCubeCreation,
71
+ type RepositoryCubeResolution,
63
72
  } from './repository-cube-init.js';
64
73
  import type { GitRepositoryContext, RepositoryAssociation } from './repository-identity.js';
65
74
 
@@ -266,6 +275,18 @@ export interface AssimilateDeps {
266
275
  serverCapabilities?: readonly string[];
267
276
  } | null>;
268
277
 
278
+ resolveRepositoryCube: (
279
+ apiUrl: string,
280
+ token: string,
281
+ input: { workingRepoName: string; repository: CreateCubeRepository },
282
+ serverTrustIdentity?: string,
283
+ ) => Promise<RepositoryCubeResolution>;
284
+ associateRepositoryCube: (
285
+ apiUrl: string,
286
+ token: string,
287
+ input: { cubeId: string; workingRepoName: string; repository: CreateCubeRepository },
288
+ serverTrustIdentity?: string,
289
+ ) => Promise<AssociateRepositoryCubeResponse>;
269
290
  listCubes: (apiUrl: string, token: string, serverTrustIdentity?: string) => Promise<CubeSummary[]>;
270
291
  getCube: (apiUrl: string, token: string, cubeId: string, serverTrustIdentity?: string) => Promise<CubeDetail>;
271
292
  createCube: (
@@ -684,71 +705,12 @@ export async function runAssimilate(
684
705
 
685
706
  let initialized;
686
707
  try {
687
- if (!auth.serverCapabilities.includes('create_cube') && !existing) {
688
- const repository = await deps.getRepositoryIdentity(repositoryContext);
689
- const association = await deps.getRepositoryAssociation(auth.serverTrustIdentity, repository);
690
- if (association) {
691
- initialized = await initializeRepositoryCube({
692
- mode, context: repositoryContext, serverOrigin: auth.apiUrl, flags: args.flags,
693
- }, {
694
- isTTY: deps.isTTY,
695
- prompt: deps.prompt,
696
- write: deps.stderr,
697
- getIdentity: async () => repository,
698
- getAssociation: async () => association,
699
- saveAssociation: (identity, saved) =>
700
- deps.saveRepositoryAssociation(auth.serverTrustIdentity, identity, saved),
701
- getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
702
- createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
703
- });
704
- } else {
705
- const candidateName = args.flags.cubeName ?? repositoryContext.derivedName;
706
- const accessible = await deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity);
707
- const matches = accessible.filter((cube) => cube.name === candidateName);
708
- if (matches.length !== 1) {
709
- deps.stderr(matches.length === 0
710
- ? `This enrolled client cannot create a cube on ${auth.apiUrl}. Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`
711
- : `More than one accessible cube is named '${candidateName}'. Nothing was changed. Ask the server operator for an unambiguous repository cube grant.\n`);
712
- return 1;
713
- }
714
- const cube = await deps.getCube(auth.apiUrl, auth.token, matches[0].id, auth.serverTrustIdentity);
715
- const association = {
716
- cubeId: cube.id,
717
- name: cube.name,
718
- workingRepoName: repositoryContext.derivedName,
719
- template: 'default' as const,
720
- };
721
- try {
722
- await deps.saveRepositoryAssociation(auth.serverTrustIdentity, repository, association);
723
- } catch {
724
- throw new RepositoryAssociationSaveError();
725
- }
726
- const response = {
727
- result: 'resolved' as const,
728
- cube_id: cube.id,
729
- name: cube.name,
730
- working_repo_name: repositoryContext.derivedName,
731
- repository,
732
- template: 'default' as const,
733
- human_seat_role_id: cube.roles.find((role) => role.is_human_seat)?.id ?? '',
734
- default_worker_role_id: cube.roles.find((role) => role.is_default)?.id ?? '',
735
- access: 'manage' as const,
736
- };
737
- deps.stderr(
738
- `Cube already initialized.\n Name: ${cube.name}\n Template: Default (legacy)\n` +
739
- ` Repository: ${repositoryContext.root}\n Server: ${auth.apiUrl}\n` +
740
- (mode === 'cube-init'
741
- ? `No drone was created.\nNext: borg assimilate --host ${shellEscape(auth.apiUrl)}\n`
742
- : 'Continuing with role and seat setup...\n'),
743
- );
744
- initialized = { kind: 'success' as const, creation: { response, cube }, existing: true };
745
- }
746
- } else {
747
708
  initialized = await initializeRepositoryCube({
748
709
  mode,
749
710
  context: repositoryContext,
750
711
  serverOrigin: auth.apiUrl,
751
712
  flags: args.flags,
713
+ canCreate: auth.serverCapabilities.includes('create_cube'),
752
714
  }, {
753
715
  isTTY: deps.isTTY,
754
716
  prompt: deps.prompt,
@@ -757,11 +719,62 @@ export async function runAssimilate(
757
719
  getAssociation: (repository) => deps.getRepositoryAssociation(auth.serverTrustIdentity, repository),
758
720
  saveAssociation: (repository, association) =>
759
721
  deps.saveRepositoryAssociation(auth.serverTrustIdentity, repository, association),
722
+ resolveAssociation: (repository, workingRepoName) => deps.resolveRepositoryCube(
723
+ auth.apiUrl,
724
+ auth.token,
725
+ { repository, workingRepoName },
726
+ auth.serverTrustIdentity,
727
+ ),
728
+ listCubes: () => deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity),
729
+ associateCube: (params) => deps.associateRepositoryCube(
730
+ auth.apiUrl,
731
+ auth.token,
732
+ params,
733
+ auth.serverTrustIdentity,
734
+ ),
760
735
  getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
761
736
  createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
762
737
  });
763
- }
764
738
  } catch (error) {
739
+ if (error instanceof RepositoryAssociationOutcomeUnknownError) {
740
+ deps.stderr(
741
+ 'Repository cube association outcome is unknown.\n' +
742
+ 'The server may have created the repository binding; no local repository association was saved and no drone was created.\n' +
743
+ 'Run the same command again; Borg will first resolve the authoritative server association without creating a cube.\n',
744
+ );
745
+ return 1;
746
+ }
747
+ if (error instanceof RepositoryAssociationResolutionError) {
748
+ deps.stderr(
749
+ 'Repository cube association could not be resolved.\n' +
750
+ 'Verify that the server is reachable and the client and server versions match, then run the same command again.\n' +
751
+ 'No cube, repository binding, or drone was created.\n',
752
+ );
753
+ return 1;
754
+ }
755
+ if (error instanceof RepositoryAssociationOperationError) {
756
+ const recovery = error.failure === 'repository-already-associated'
757
+ ? 'This repository is already associated with another cube. Run the same command again to use the existing managed association, or ask the server operator to correct the repository binding.'
758
+ : error.failure === 'cube-already-associated'
759
+ ? 'The selected cube is already associated with another repository. Choose a different cube, or run the command from the repository already linked to that cube.'
760
+ : error.failure === 'access-denied'
761
+ ? 'This enrolled client does not have permission to manage the selected cube. Ask the server operator to grant this client management access, then run the same command again.'
762
+ : 'The selected cube does not have valid authoritative roles. Ask the server operator to repair its role configuration, or choose another cube.';
763
+ deps.stderr(
764
+ 'Repository cube association could not be completed.\n' +
765
+ `${recovery}\n` +
766
+ 'No cube, repository binding, or drone was created.\n',
767
+ );
768
+ return 1;
769
+ }
770
+ if (error instanceof RepositoryAssociationConfirmationError) {
771
+ deps.stderr(
772
+ 'Repository cube association could not be confirmed.\n' +
773
+ 'The server may have created the repository binding; no local repository association was saved and no drone was created.\n' +
774
+ 'Run the same command again; Borg will resolve authoritative server state before creating or associating a cube.\n',
775
+ );
776
+ return 1;
777
+ }
765
778
  if (error instanceof RepositoryAssociationSaveError) {
766
779
  deps.stderr(
767
780
  'The repository cube was confirmed, but Borg could not save its local repository association.\n' +
@@ -772,18 +785,17 @@ export async function runAssimilate(
772
785
  }
773
786
  if (error instanceof CubeCreationOutcomeUnknownError) {
774
787
  deps.stderr(
775
- 'Cube creation outcome is unknown.\n' +
776
- 'No local repository association was saved.\n' +
777
- 'Run the same command again; the server will resolve the original request or return a conflict.\n',
788
+ 'Cube creation outcome is unconfirmed.\n' +
789
+ 'The server may have created the cube and repository binding; no local repository association was saved and no drone was created.\n' +
790
+ 'Run the same command again; Borg will resolve authoritative server state before creating a cube.\n',
778
791
  );
779
792
  return 1;
780
793
  }
781
794
  if (error instanceof CubeCreationConfirmationError) {
782
795
  deps.stderr(
783
796
  'Cube creation could not be confirmed.\n' +
784
- `${error.message}\n` +
785
- 'No local repository association was saved.\n' +
786
- 'Resolve the conflict, then run the same command again.\n',
797
+ 'The server may have created the cube and repository binding; no local repository association was saved and no drone was created.\n' +
798
+ 'Run the same command again; Borg will resolve authoritative server state before creating a cube.\n',
787
799
  );
788
800
  return 1;
789
801
  }
@@ -797,7 +809,11 @@ export async function runAssimilate(
797
809
  if (error instanceof BorgServerError) {
798
810
  return reportServerFailure(deps, auth.apiUrl, error);
799
811
  }
800
- deps.stderr('Could not create cube.\nThe selected Borg server rejected the request.\nNothing was changed.\n');
812
+ deps.stderr(
813
+ 'Repository cube initialization failed.\n' +
814
+ 'The server may have created or associated a cube; local repository state may be incomplete and no drone was created.\n' +
815
+ 'Run the same command again; Borg will resolve authoritative server state before creating or associating a cube.\n',
816
+ );
801
817
  return 1;
802
818
  }
803
819
  if (initialized.kind === 'stop') return initialized.code;
@@ -25,10 +25,12 @@ import {
25
25
  } from './remote-client.js';
26
26
  import {
27
27
  DEFAULT_LOCAL_SERVER_ORIGIN,
28
+ associateLocalBorgServerRepositoryCube,
28
29
  connectLocalBorgServer,
29
30
  createLocalBorgServerCube,
30
31
  enrollLocalBorgServer,
31
32
  probeLocalBorgServer,
33
+ resolveLocalBorgServerRepositoryCube,
32
34
  resumeLocalBorgServerEnrollment,
33
35
  sendBorgServerAttach,
34
36
  } from './server-handshake.js';
@@ -45,7 +47,7 @@ import {
45
47
  } from './cubes.js';
46
48
  import { loadBorgServerTrust } from './server-trust.js';
47
49
  import { defaultProbeSeat } from './seat-probe.js';
48
- import { BorgServerError } from './server-errors.js';
50
+ import { BorgServerError, CubeCreationConfirmationError } from './server-errors.js';
49
51
  import {
50
52
  findProjectRoot as cubesFindProjectRoot,
51
53
  getActiveCube as cubesGetActive,
@@ -238,6 +240,29 @@ export function buildDefaultAssimilateDeps(
238
240
  ...(onPending === undefined ? {} : { onPending }),
239
241
  }),
240
242
 
243
+ resolveRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
244
+ if (serverTrustIdentity === undefined) {
245
+ throw new Error('Selected Borg server authority state is missing or unreadable');
246
+ }
247
+ return resolveLocalBorgServerRepositoryCube(
248
+ apiUrl,
249
+ serverTrustIdentity,
250
+ token,
251
+ input,
252
+ );
253
+ },
254
+ associateRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
255
+ if (serverTrustIdentity === undefined) {
256
+ throw new Error('Selected Borg server authority state is missing or unreadable');
257
+ }
258
+ return associateLocalBorgServerRepositoryCube(
259
+ apiUrl,
260
+ serverTrustIdentity,
261
+ token,
262
+ input,
263
+ );
264
+ },
265
+
241
266
  listCubes: async (apiUrl, token, serverTrustIdentity) => {
242
267
  if (serverTrustIdentity === undefined) {
243
268
  throw new Error('Selected Borg server authority state is missing or unreadable');
@@ -284,18 +309,23 @@ export function buildDefaultAssimilateDeps(
284
309
  template: params.template,
285
310
  },
286
311
  );
287
- const cube = await remoteGetCube(created.cube_id, {
288
- apiUrl,
289
- authToken: token,
290
- serverTrustIdentity,
291
- });
312
+ let cube;
313
+ try {
314
+ cube = await remoteGetCube(created.cube_id, {
315
+ apiUrl,
316
+ authToken: token,
317
+ serverTrustIdentity,
318
+ });
319
+ } catch {
320
+ throw new CubeCreationConfirmationError('The server returned a cube result that could not be read back.');
321
+ }
292
322
  if (
293
323
  cube.id !== created.cube_id ||
294
324
  cube.name !== created.name ||
295
325
  !Array.isArray(cube.roles) ||
296
326
  !cube.roles.some((role: { id?: string }) => role.id === created.default_worker_role_id)
297
327
  ) {
298
- throw new Error('Borg server returned cube details outside the creation result');
328
+ throw new CubeCreationConfirmationError('The server returned cube details outside the creation result.');
299
329
  }
300
330
  return {
301
331
  response: created,
package/src/cli-help.ts CHANGED
@@ -55,6 +55,7 @@ export function serverHelpText(): string {
55
55
  ` status Report verified runtime evidence.\n` +
56
56
  ` update Verify and activate a local server artifact.\n` +
57
57
  ` invite Create a single-use invitation in an interactive terminal.\n` +
58
+ ` dashboard View the running local server dashboard.\n` +
58
59
  ` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
59
60
  `Run borg server <command> --help for server command options.\n`
60
61
  );
@@ -71,7 +72,7 @@ export function cubeInitHelpText(): string {
71
72
  ` --enroll Prompt for a hidden enrollment invitation\n` +
72
73
  ` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
73
74
  ` --template software-dev|starter New-cube template (default: software-dev)\n` +
74
- ` --yes, -y Skip confirmation prompts\n` +
75
+ ` --yes, -y Accept new-cube defaults; never adopt by name\n` +
75
76
  ` --help, -h Show this help\n`
76
77
  );
77
78
  }
@@ -101,11 +102,12 @@ export function assimilateHelpText(version: string): string {
101
102
  ` --no-template Unsupported for repository cube creation\n` +
102
103
  ` --cli claude|codex|opencode Agent CLI to launch (default: claude)\n` +
103
104
  ` --model claude:<model> Legacy Claude model override (configure models in the agent CLI)\n` +
104
- ` --yes, -y Skip confirmation prompts\n\n` +
105
+ ` --yes, -y Accept new-cube defaults; never adopt by name\n\n` +
105
106
  `Creation shows repository context, name, template, and one confirmation. An existing\n` +
106
- `repository association skips all prompts. An enrolled owner client may create an\n` +
107
- `idempotent repository cube; ordinary clients\n` +
108
- `require an explicit cube grant. Agent seats begin only after enrollment. Preview only.\n` +
107
+ `repository association skips all prompts. One accessible exact-name legacy cube requires\n` +
108
+ `explicit interactive adoption; ambiguous matches fail closed. An enrolled owner client may\n` +
109
+ `create an idempotent repository cube; ordinary clients require an explicit cube grant.\n` +
110
+ `Agent seats begin only after enrollment. Preview only.\n` +
109
111
  `See docs/LOCAL_SERVER.md for self-hosted setup and current status.\n\n` +
110
112
  `For local or provider-specific models, configure the selected agent CLI directly.\n` +
111
113
  `OpenCode supports Ollama and other providers through its own model configuration.\n`
package/src/index.ts CHANGED
@@ -76,6 +76,7 @@ import {
76
76
  formatRegenMarkdown,
77
77
  getDronePlaybook,
78
78
  getDronePlaybookChapter,
79
+ markArrivalAnnouncedThisProcess,
79
80
  nullTaxonomyTip,
80
81
  regenWakePathDroneLabel,
81
82
  } from './regen-format.js';
@@ -728,10 +729,12 @@ export async function main() {
728
729
  if (!message || typeof message !== 'string') throw new Error('message is required');
729
730
  const active = await getActiveCube();
730
731
  if (!active) throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
731
- if (lifecycleSignalForMessage(message)) {
732
+ const lifecycleSignal = lifecycleSignalForMessage(message);
733
+ if (lifecycleSignal) {
732
734
  const decision = await shouldSuppressLifecycleLog(active, message);
733
735
  if (decision.suppress) {
734
736
  await recordLifecycleLog(active, message);
737
+ if (lifecycleSignal === 'arrival') markArrivalAnnouncedThisProcess();
735
738
  return {
736
739
  content: [
737
740
  {
@@ -760,6 +763,7 @@ export async function main() {
760
763
  };
761
764
  const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
762
765
  await recordLifecycleLog(active, message);
766
+ if (lifecycleSignal === 'arrival') markArrivalAnnouncedThisProcess();
763
767
  const echo = result.routing?.message ? `\n${result.routing.message}` : '';
764
768
  // gh#534: surface to the SENDER which directed recipients are
765
769
  // currently unreachable via the wake path. The message is delivered
@@ -191,6 +191,12 @@ export function formatLeanOrientation(args: {
191
191
  // formatLeanOrientation({ ..., source: 'clear' }), so the per-surface
192
192
  // /clear block is gone (one place, not three).
193
193
 
194
+ let arrivalAnnouncedThisProcess = false;
195
+
196
+ export function markArrivalAnnouncedThisProcess(): void {
197
+ arrivalAnnouncedThisProcess = true;
198
+ }
199
+
194
200
  /**
195
201
  * Build the universal drone playbook.
196
202
  *
@@ -215,6 +221,9 @@ export function formatLeanOrientation(args: {
215
221
  // about how Borg MCP works; this marker pins the param so the #490/#529 guard
216
222
  // verifies borg_docs exposes `topic`.
217
223
  export function getDronePlaybook(): string {
224
+ const arrivalInstruction = arrivalAnnouncedThisProcess
225
+ ? ''
226
+ : '\n**When this MCP session first starts:** post one `ARRIVAL: <your-label> (<your-role>) online on <hostname> at <project-path>` (run `hostname`; use cwd for the path). After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit `/mcp` reconnect may show it again.\n';
218
227
  return `## How to operate as a Drone
219
228
 
220
229
  You're a Drone in a Cube. Coordinate with other drones through the activity log.
@@ -242,8 +251,7 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
242
251
  5. Nothing actionable + no prompt → done; wait for next wake.
243
252
 
244
253
  **On a \`<task-notification>\` wake:** the payload is a truncatable preview; the full entry is in the DB. Drain: \`borg_read-log unread_only=true limit=20\`, repeat until \`behind_by=0\`. Do NOT triage with \`since=<notification timestamp>\` (strict-after — skips the boundary entry) or a bare window (skips older-unread during bursts).
245
-
246
- **On first wake this session:** post one \`ARRIVAL: <your-label> (<your-role>) online on <hostname> at <project-path>\` (run \`hostname\`; use cwd for the path). One-time per session — don't repeat on later wakes; skip if already posted this session (e.g. after a \`/mcp\` reconnect).
254
+ ${arrivalInstruction}
247
255
 
248
256
  **When a log entry routes work to you** (a routing/assignment-class entry per your cube's conventions that names your label + asks for action, or a direct \`<your-label>:\` mention): call \`borg_ack entry_id=<id>\` within ~60s. Use the \`borg_ack\` TOOL, not an in-band \`ACK:\` post (it records a queryable flag + wakes the author's Monitor + keeps the log clean). Ack = receipt, not completion (\`STARTING\` / \`DONE\` still apply). Ack only routing-class signals — not every mention.
249
257
 
@@ -269,13 +277,6 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
269
277
  Any drone that commits code: run \`git diff --staged --stat\` before \`git commit\` to verify file count + LOC direction + paths match your intent. Catches deleted files / anomalous -LOC / wrong paths pre-push. Your role may layer more git rules (code-implementing + coordinating roles typically carry the full set).`;
270
278
  }
271
279
 
272
- /**
273
- * Eager export of the playbook text. Cheap to compute (string concat);
274
- * exporting as a constant lets callers splice it directly without a
275
- * function call site.
276
- */
277
- export const DRONE_PLAYBOOK = getDronePlaybook();
278
-
279
280
  /**
280
281
  * gh#912: the verbose operating-discipline DETAIL externalized out of the
281
282
  * bootstrap regen into an on-demand chapter (fetched via the borg_playbook
@@ -387,6 +388,7 @@ let cachedRoleTextHash: string | null = null;
387
388
  export function __resetRegenSessionState(): void {
388
389
  boilerplateEmittedThisSession = false;
389
390
  cachedRoleTextHash = null;
391
+ arrivalAnnouncedThisProcess = false;
390
392
  }
391
393
 
392
394
  function safetyDisciplinesForRole(detailedDescription: string | null | undefined): string[] {
@@ -712,7 +712,7 @@ async function authedFetch(
712
712
  parsed.error !== null && typeof parsed.error === 'object' &&
713
713
  parsed.error.code === ROLE_SECTION_CONFLICT_CODE
714
714
  ) {
715
- // Shared 0.6.3 intentionally omits this server-local code. Re-validate the whole
715
+ // The shared protocol intentionally omits this server-local code. Re-validate the whole
716
716
  // envelope through the strict shared decoder with only the recognized
717
717
  // code substituted; no server-provided diagnostic is ever surfaced.
718
718
  decodeProtocolErrorEnvelope({