borgmcp 2.0.11 → 2.1.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 (58) hide show
  1. package/README.md +11 -3
  2. package/dist/assimilate-cmd.d.ts +22 -4
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +109 -110
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts +7 -1
  7. package/dist/assimilate-deps.d.ts.map +1 -1
  8. package/dist/assimilate-deps.js +69 -25
  9. package/dist/assimilate-deps.js.map +1 -1
  10. package/dist/claude.d.ts +3 -1
  11. package/dist/claude.d.ts.map +1 -1
  12. package/dist/claude.js +26 -12
  13. package/dist/claude.js.map +1 -1
  14. package/dist/cli-help.d.ts +2 -0
  15. package/dist/cli-help.d.ts.map +1 -1
  16. package/dist/cli-help.js +25 -7
  17. package/dist/cli-help.js.map +1 -1
  18. package/dist/config.d.ts +7 -4
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +21 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/remote-client.js +1 -1
  23. package/dist/remote-client.js.map +1 -1
  24. package/dist/repository-cube-init.d.ts +72 -0
  25. package/dist/repository-cube-init.d.ts.map +1 -0
  26. package/dist/repository-cube-init.js +341 -0
  27. package/dist/repository-cube-init.js.map +1 -0
  28. package/dist/repository-identity.d.ts +29 -0
  29. package/dist/repository-identity.d.ts.map +1 -0
  30. package/dist/repository-identity.js +143 -0
  31. package/dist/repository-identity.js.map +1 -0
  32. package/dist/server-errors.d.ts +17 -0
  33. package/dist/server-errors.d.ts.map +1 -1
  34. package/dist/server-errors.js +32 -0
  35. package/dist/server-errors.js.map +1 -1
  36. package/dist/server-facade.d.ts +11 -1
  37. package/dist/server-facade.d.ts.map +1 -1
  38. package/dist/server-facade.js +33 -3
  39. package/dist/server-facade.js.map +1 -1
  40. package/dist/server-handshake.d.ts +38 -4
  41. package/dist/server-handshake.d.ts.map +1 -1
  42. package/dist/server-handshake.js +175 -13
  43. package/dist/server-handshake.js.map +1 -1
  44. package/docs/EXTRACTION_PROVENANCE.md +9 -7
  45. package/docs/LOCAL_SERVER.md +44 -9
  46. package/docs/RELEASING.md +21 -4
  47. package/package.json +2 -2
  48. package/src/assimilate-cmd.ts +179 -137
  49. package/src/assimilate-deps.ts +96 -24
  50. package/src/claude.ts +34 -14
  51. package/src/cli-help.ts +28 -7
  52. package/src/config.ts +30 -8
  53. package/src/remote-client.ts +1 -1
  54. package/src/repository-cube-init.ts +452 -0
  55. package/src/repository-identity.ts +199 -0
  56. package/src/server-errors.ts +41 -0
  57. package/src/server-facade.ts +47 -2
  58. package/src/server-handshake.ts +258 -14
@@ -8,7 +8,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
8
8
 
9
9
  ## Deliberate Transformations
10
10
 
11
- - Replaced the monorepo dependency on `borgmcp-shared` with the exact audited registry release `borgmcp-shared@0.6.2` and a fresh standalone lockfile.
11
+ - Replaced the monorepo dependency on `borgmcp-shared` with the exact audited registry release `borgmcp-shared@0.6.4` and a fresh standalone lockfile.
12
12
  - Replaced local template, role-section, drone-address, and log high-water-mark implementations with `borgmcp-shared` exports.
13
13
  - Removed monorepo-only website anti-drift tests and re-anchored remaining filesystem tests to this repository.
14
14
  - Removed consumer lifecycle hooks, parent-directory deployment scripts, minification, and private integration-environment configuration.
@@ -21,8 +21,8 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
21
21
  lightweight release tag failed before packaging. The reviewed `2.0.1`
22
22
  recovery, `2.0.2`, `2.0.3`, `2.0.4`, `2.0.5`, and `2.0.6` successors were
23
23
  published and registry-verified. The immutable `2.0.7` workflow failed before
24
- package creation or npm publication. The `2.0.8`, `2.0.9`, and `2.0.10` successors were
25
- published, so the next candidate identity is `2.0.11`. Extraction and
24
+ package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`, and
25
+ `2.1.0` successors were published, so the next candidate identity is `2.1.1`. Extraction and
26
26
  versioning do not authorize publication.
27
27
 
28
28
  ## Review Holds
@@ -38,9 +38,11 @@ artifact reaches no hosted authority.
38
38
  Local enrollment now uses the reviewed client-generated credential/retry
39
39
  contract, with a pre-request `PENDING` record in the local 0600-permission seat
40
40
  store, exact-tuple ambiguous retry, and verified activation. The contract now
41
- resolves to the audited registry `borgmcp-shared@0.6.2`. The matching
42
- `borgmcp-server@0.1.17` release is published and registry-verified. Client
43
- `borgmcp@2.0.10` is published. The immutable `v2.0.7`
41
+ resolves to the audited registry `borgmcp-shared@0.6.4` with integrity
42
+ `sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`.
43
+ The matching `borgmcp-server@0.1.21` release is published with integrity
44
+ `sha512-g0QH4cKcsKuDyZs1MvBmvCoCLaZ4wVI04yHCGSvcAeWghWxSBH0IIF8Q5OFi1pLB7TZXOVP6D3QAabUQ2eJTFg==`
45
+ and pins the same `borgmcp-shared@0.6.4` contract. Client `borgmcp@2.1.0` is published. The immutable `v2.0.7`
44
46
  attempt failed before publication and remains preserved. Publication of the next
45
- candidate remains gated by reviewed `v2.0.11` source, a fresh annotated tag, and
47
+ candidate remains gated by reviewed `v2.1.1` source, a fresh annotated tag, and
46
48
  the exact-artifact and protected-publication gates.
@@ -32,10 +32,28 @@ Commands:
32
32
  status Report verified runtime evidence.
33
33
  update Verify and activate a local server artifact.
34
34
  invite Create a single-use invitation in an interactive terminal.
35
+ cube init Initialize this Git repository's cube; does not create a drone.
35
36
 
36
37
  Run borg server <command> --help for server command options.
37
38
  ```
38
39
 
40
+ `borg server cube init --help` prints:
41
+
42
+ ```text
43
+ borg server cube init — initialize this Git repository's cube without creating a drone
44
+
45
+ Usage:
46
+ borg server cube init [options]
47
+
48
+ Options:
49
+ --host <host> Borg server host or URL (bare hosts default to HTTPS)
50
+ --enroll Prompt for a hidden enrollment invitation
51
+ --cube-name <name> Repository cube name (otherwise edit the proposed name)
52
+ --template software-dev|starter New-cube template (default: software-dev)
53
+ --yes, -y Accept new-cube defaults; never adopt by name
54
+ --help, -h Show this help
55
+ ```
56
+
39
57
  Local server client credentials are stored in the owner-controlled
40
58
  `~/.borg/credentials` file with mode 0600. Fresh same-machine setup provisions
41
59
  the first owner record there, so bare `borg assimilate` can use it without an
@@ -73,10 +91,25 @@ exactly; the credential becomes active only after the versioned response is
73
91
  decoded and the authenticated protocol handshake succeeds. A new process
74
92
  resumes that pending enrollment before displaying another invitation prompt.
75
93
 
76
- `--cube-name <name>` explicitly selects the repository cube name. Without an
77
- explicit name, Borg uses the `origin` repository name or, when `origin` is
78
- absent, proposes the sanitized repository-directory basename. Confirm
79
- interactively or pass `--yes`; 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.
107
+
108
+ For repositories with a canonical public `origin`, that origin is the stable
109
+ repository identity. Repositories without one receive an invisible UUID stored
110
+ in an owner-only local file. Its lookup key is an HMAC of the canonical Git
111
+ common directory, so linked worktrees share one identity and local paths are not
112
+ stored in cleartext. A malformed identity secret or state file fails closed.
80
113
 
81
114
  The connection is HTTPS-only. Borg validates the server trust material, stores
82
115
  parent enrollment credentials in `~/.borg/credentials` and session credentials
@@ -89,7 +122,8 @@ The lifecycle facade invokes the separately installed `borgmcp-server`; it does
89
122
  not bundle the server into the client. The server must be running and trusted
90
123
  before assimilation. An owner enrollment carrying the persisted `create_cube`
91
124
  capability creates one idempotent cube per repository during normal
92
- assimilation, using the server-owned `default` role template; repeating an
125
+ assimilation or `borg server cube init`, using Software Development or Starter;
126
+ repeating an
93
127
  ambiguous request does not duplicate the cube, and distinct repositories can
94
128
  create distinct bounded cubes. An ordinary enrolled client is denied before a
95
129
  create request is sent. Cloud-only capabilities fail explicitly rather than
@@ -139,8 +173,9 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
139
173
 
140
174
  ## Release status
141
175
 
142
- This self-hosted path consumes the published `borgmcp-shared@0.6.2` v3 registry
143
- release. The matching server owner-enrollment, cube-create, attach, restart, log,
144
- and SSE implementation must also pass the full process-level local dogfood gate.
145
- 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
146
181
  publish is deferred accordingly.
package/docs/RELEASING.md CHANGED
@@ -97,8 +97,22 @@ The annotated `v2.0.10` tag object
97
97
  `3f7003b65eadc74cd949857e831e1181a1aad2ff`. Workflow run `30063538271`, attempt 1,
98
98
  successfully published that exact source as `borgmcp@2.0.10`; the registry records integrity
99
99
  `sha512-BkFrq75mF7ih0/7Z7RsXsJYo43qNs4ng7tSnvcFCl3k6r+ex1FvTkah0+KQNinDSrv6a+HLWahUe8875so8zew==`.
100
+ Never move, replace, reuse, or rerun that tag or workflow.
101
+
102
+ The annotated `v2.0.11` tag object
103
+ `fb95f4278137d1d7871d945c467eb33984abe1c4` peels to protected-main commit
104
+ `0676d93d3e8a64ba583e95f164d122b39687b53a`. Workflow run `30073844074`, attempt 1,
105
+ successfully published that exact source as `borgmcp@2.0.11`; the registry records integrity
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==`.
100
114
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
101
- uses the unused `v2.0.11` identity from a fresh reviewed protected-main commit
115
+ uses the unused `v2.1.1` identity from a fresh reviewed protected-main commit
102
116
  and requires the complete release gate again.
103
117
 
104
118
  ## Release Prerequisites
@@ -110,9 +124,12 @@ Before creating the release tag, independently verify all of these conditions:
110
124
  - the extraction review confirms no private backend secrets, deployment
111
125
  configuration, customer data, local state, or duplicated shared contracts
112
126
  entered the public package;
113
- - the exact audited registry dependency `borgmcp-shared@0.6.2` remains locked to
114
- its canonical tarball and integrity;
115
- - the client and matching server pass the complete local dogfood gate;
127
+ - the exact audited registry dependency `borgmcp-shared@0.6.4` remains locked to
128
+ its canonical tarball and integrity
129
+ `sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`;
130
+ - the client and matching published `borgmcp-server@0.1.21`, integrity
131
+ `sha512-g0QH4cKcsKuDyZs1MvBmvCoCLaZ4wVI04yHCGSvcAeWghWxSBH0IIF8Q5OFi1pLB7TZXOVP6D3QAabUQ2eJTFg==`, which pins
132
+ `borgmcp-shared@0.6.4`, pass the complete local dogfood gate;
116
133
  - the selected stable client version is unused and the exact release commit is
117
134
  on protected `main`;
118
135
  - 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.0.11",
3
+ "version": "2.1.1",
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.2",
71
+ "borgmcp-shared": "0.6.4",
72
72
  "chalk": "^5.3.0",
73
73
  "prompts": "^2.4.2",
74
74
  "which": "^4.0.0"
@@ -7,7 +7,6 @@ import {
7
7
  occupiedRoleIdsForAutoRole,
8
8
  pickDefaultRole,
9
9
  } from './role-resolver.js';
10
- import { deriveCubeName, parseGitRemote, sanitizeRemoteUrl } from './cube-name.js';
11
10
  import { validateName } from './name-validator.js';
12
11
  import { renderAssimilationWelcome } from './assimilate-welcome.js';
13
12
  import { shellEscape } from './shell-escape.js';
@@ -42,7 +41,15 @@ import { installBorgPlugin } from './opencode-plugin.js';
42
41
  import { computeOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
43
42
  import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
44
43
  import { normalizeServerEndpoint } from './server-endpoint.js';
45
- import { BorgServerError, LegacySessionCredentialCollisionError } from './server-errors.js';
44
+ import {
45
+ BorgServerError,
46
+ CubeCreationConfirmationError,
47
+ CubeCreationOutcomeUnknownError,
48
+ LegacySessionCredentialCollisionError,
49
+ RepositoryAssociationOperationError,
50
+ RepositoryAssociationOutcomeUnknownError,
51
+ RepositoryAssociationResolutionError,
52
+ } from './server-errors.js';
46
53
  import type { SeatStatus } from './seat-probe.js';
47
54
  import type { ServerSessionOperation } from './config.js';
48
55
  import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
@@ -50,6 +57,20 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
50
57
  import { createHash } from 'node:crypto';
51
58
  import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
52
59
  import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
60
+ import type {
61
+ AssociateRepositoryCubeResponse,
62
+ CreateCubeRepository,
63
+ CubeTemplate,
64
+ } from 'borgmcp-shared/protocol';
65
+ import {
66
+ initializeRepositoryCube,
67
+ RepositoryAssociationConfirmationError,
68
+ RepositoryAssociationSaveError,
69
+ validRepositoryCubeName,
70
+ type RepositoryCubeCreation,
71
+ type RepositoryCubeResolution,
72
+ } from './repository-cube-init.js';
73
+ import type { GitRepositoryContext, RepositoryAssociation } from './repository-identity.js';
53
74
 
54
75
  const PRIVATE_STATE_UNAVAILABLE_COPY = [
55
76
  'Borg could not safely prepare its private local state.',
@@ -73,6 +94,7 @@ export interface AssimilateFlags {
73
94
  export interface AssimilateArgs {
74
95
  role: string | undefined;
75
96
  flags: AssimilateFlags;
97
+ mode?: 'assimilate' | 'cube-init';
76
98
  }
77
99
 
78
100
  export interface CubeSummary {
@@ -222,6 +244,10 @@ export interface AssimilateDeps {
222
244
  scrubPending: () => Promise<unknown>;
223
245
  }) => Promise<FinalizeServerSeatOutcome>;
224
246
  findProjectRoot: (cwd: string) => string;
247
+ resolveRepositoryContext: (cwd: string) => Promise<GitRepositoryContext | null>;
248
+ getRepositoryIdentity: (context: GitRepositoryContext) => Promise<CreateCubeRepository>;
249
+ getRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository) => Promise<RepositoryAssociation | null>;
250
+ saveRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository, association: RepositoryAssociation) => Promise<void>;
225
251
 
226
252
  // gh#673 P2 (WI-1): write the borg-regen SessionStart hook into the
227
253
  // launch root's .claude/settings.local.json (project-local; idempotent).
@@ -249,14 +275,31 @@ export interface AssimilateDeps {
249
275
  serverCapabilities?: readonly string[];
250
276
  } | null>;
251
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>;
252
290
  listCubes: (apiUrl: string, token: string, serverTrustIdentity?: string) => Promise<CubeSummary[]>;
253
291
  getCube: (apiUrl: string, token: string, cubeId: string, serverTrustIdentity?: string) => Promise<CubeDetail>;
254
292
  createCube: (
255
293
  apiUrl: string,
256
294
  token: string,
257
- params: { name?: string; template?: string; projectRoot?: string },
295
+ params: {
296
+ name: string;
297
+ workingRepoName: string;
298
+ repository: CreateCubeRepository;
299
+ template: Exclude<CubeTemplate, 'default'>;
300
+ },
258
301
  serverTrustIdentity?: string,
259
- ) => Promise<CubeDetail>;
302
+ ) => Promise<RepositoryCubeCreation>;
260
303
  assimilate: (
261
304
  apiUrl: string,
262
305
  token: string,
@@ -301,11 +344,6 @@ function affirmative(answer: string): boolean {
301
344
  return normalized === '' || normalized === 'y' || normalized === 'yes';
302
345
  }
303
346
 
304
- function isLocalCubePresentationName(name: string): boolean {
305
- return name.length >= 1 && name.length <= 120 &&
306
- /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name);
307
- }
308
-
309
347
  async function selectAssimilationAuthority(
310
348
  flags: AssimilateFlags,
311
349
  deps: AssimilateDeps,
@@ -509,6 +547,7 @@ export async function runAssimilate(
509
547
  args: AssimilateArgs,
510
548
  deps: AssimilateDeps
511
549
  ): Promise<number> {
550
+ const mode = args.mode ?? 'assimilate';
512
551
  // ----- Input validation (before any subprocess work) -----
513
552
  if (args.role !== undefined) {
514
553
  const v = validateName(args.role);
@@ -524,6 +563,29 @@ export async function runAssimilate(
524
563
  return 1;
525
564
  }
526
565
  }
566
+ if (args.flags.cubeName !== undefined && !validRepositoryCubeName(args.flags.cubeName.trim())) {
567
+ deps.stderr('Invalid cube name. Use 1-120 letters, digits, spaces, dots, underscores, or hyphens, starting with a letter or digit.\n');
568
+ return 1;
569
+ }
570
+
571
+ let repositoryContext: GitRepositoryContext | null;
572
+ try {
573
+ repositoryContext = await deps.resolveRepositoryContext(deps.cwd());
574
+ } catch (error) {
575
+ if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
576
+ deps.stderr('borg assimilate requires a non-bare repository worktree. Clone or check out the repository, then retry.\n');
577
+ return 1;
578
+ }
579
+ repositoryContext = null;
580
+ }
581
+ if (!repositoryContext) {
582
+ deps.stderr(
583
+ 'No Git repository was found for this directory.\n' +
584
+ 'Nothing was changed.\n' +
585
+ 'Run this command inside a Git repository.\n',
586
+ );
587
+ return 1;
588
+ }
527
589
 
528
590
  try {
529
591
  await deps.preparePrivateRoot();
@@ -555,86 +617,9 @@ export async function runAssimilate(
555
617
  return reportServerFailure(deps, authority.apiUrl, localSeatReadError);
556
618
  }
557
619
 
558
- // ----- Repository + cube-name preflight -----
559
- // Resolve and, where necessary, confirm local presentation data before an
560
- // owner invitation can be consumed. A declined/underivable basename must
561
- // not leave a successfully enrolled client behind.
562
- const projectRoot = deps.findProjectRoot(deps.cwd());
563
- let cubeName: string | null;
564
- if (args.flags.cubeName) {
565
- cubeName = args.flags.cubeName;
566
- } else {
567
- const remoteResult = deps.runSync('git', ['remote', 'get-url', 'origin'], projectRoot);
568
- const remoteUrl = remoteResult.status === 0 ? remoteResult.stdout : null;
569
- const sanitizedRemote = remoteUrl ? sanitizeRemoteUrl(remoteUrl) : null;
570
- const parsedRepo = sanitizedRemote ? parseGitRemote(sanitizedRemote) : null;
571
-
572
- if (!parsedRepo) {
573
- const bareResult = deps.runSync('git', ['rev-parse', '--is-bare-repository'], projectRoot);
574
- if (bareResult.status === 0 && bareResult.stdout.trim() === 'true') {
575
- deps.stderr(
576
- 'borg assimilate requires a non-bare repository worktree. ' +
577
- (authority.kind === 'server'
578
- ? `Clone or check out the repository, then rerun ${localAssimilateCommand(authority.apiUrl)}.\n`
579
- : 'Clone or check out the repository, then retry.\n'),
580
- );
581
- return 1;
582
- }
583
- }
584
-
585
- cubeName = deriveCubeName(projectRoot, remoteUrl);
586
- if (!cubeName) {
587
- deps.stderr(
588
- 'Could not derive a cube name from this repository. ' +
589
- (authority.kind === 'server'
590
- ? `Rerun ${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`
591
- : 'Pass --cube-name <name> and retry.\n'),
592
- );
593
- return 1;
594
- }
620
+ const projectRoot = repositoryContext.root;
595
621
 
596
- if (!parsedRepo) {
597
- if (sanitizedRemote) {
598
- deps.stderr(
599
- `Could not parse the origin remote; using directory name '${cubeName}' as the cube name.\n`,
600
- );
601
- }
602
- if (!args.flags.yes) {
603
- if (!deps.isTTY()) {
604
- deps.stderr(
605
- `Using directory name '${cubeName}' as the cube name requires confirmation. ` +
606
- (authority.kind === 'server'
607
- ? `Rerun ${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\` or \`--yes\`.\n`
608
- : 'Re-run with --cube-name <name> or --yes.\n'),
609
- );
610
- return 1;
611
- }
612
- const confirmed = await deps.prompt(
613
- `No usable origin remote was found. Use directory name '${cubeName}' as the cube name? [Y/n]: `,
614
- );
615
- if (!affirmative(confirmed)) {
616
- deps.stderr(
617
- authority.kind === 'server'
618
- ? `Cube creation for ${authority.apiUrl} was cancelled. Rerun ` +
619
- `${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`
620
- : 'Cube creation cancelled. Re-run with --cube-name <name> to choose a name.\n',
621
- );
622
- return 1;
623
- }
624
- }
625
- }
626
- }
627
-
628
- if (authority.kind === 'server' && !isLocalCubePresentationName(cubeName)) {
629
- deps.stderr(
630
- `Invalid cube name for ${authority.apiUrl}. Use 1–120 letters, digits, spaces, dots, ` +
631
- 'underscores, or hyphens, starting with a letter or digit. Rerun ' +
632
- `${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`,
633
- );
634
- return 1;
635
- }
636
-
637
- let auth: { token: string; apiUrl: string; serverTrustIdentity: string };
622
+ let auth: { token: string; apiUrl: string; serverTrustIdentity: string; serverCapabilities: readonly string[] };
638
623
  {
639
624
  try {
640
625
  let serverAuth: {
@@ -696,6 +681,7 @@ export async function runAssimilate(
696
681
  token: serverAuth.token,
697
682
  apiUrl: authority.apiUrl,
698
683
  serverTrustIdentity: serverAuth.trustIdentity,
684
+ serverCapabilities: serverAuth.serverCapabilities ?? [],
699
685
  };
700
686
  } catch (error) {
701
687
  return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true);
@@ -717,67 +703,123 @@ export async function runAssimilate(
717
703
  // (no chdir has happened yet; this is a stable starting point).
718
704
  const originalCwd = deps.cwd();
719
705
 
720
- // ----- Step 3: Cube existence check (with auto-refresh on auth failure) -----
721
- // gh#653 B4: announce each network step. These calls take 2–5s and were
722
- // previously silent, so a user read the wait as a hang and Ctrl-C'd mid-run.
723
- deps.stderr('Checking your cubes…\n');
724
- let allCubes: CubeSummary[];
706
+ let initialized;
725
707
  try {
726
- allCubes = await deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity);
727
- } catch (err) {
728
- return reportServerFailure(deps, authority.apiUrl, err);
729
- }
730
- const existingCube = allCubes.find((c) => c.name === cubeName);
731
-
732
- // ----- Step 4: Fetch detail OR create cube -----
733
- let cubeDetail: CubeDetail;
734
- let isFirstDrone: boolean;
735
- if (existingCube) {
736
- try {
737
- cubeDetail = await deps.getCube(
708
+ initialized = await initializeRepositoryCube({
709
+ mode,
710
+ context: repositoryContext,
711
+ serverOrigin: auth.apiUrl,
712
+ flags: args.flags,
713
+ canCreate: auth.serverCapabilities.includes('create_cube'),
714
+ }, {
715
+ isTTY: deps.isTTY,
716
+ prompt: deps.prompt,
717
+ write: deps.stderr,
718
+ getIdentity: deps.getRepositoryIdentity,
719
+ getAssociation: (repository) => deps.getRepositoryAssociation(auth.serverTrustIdentity, repository),
720
+ saveAssociation: (repository, association) =>
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(
738
730
  auth.apiUrl,
739
731
  auth.token,
740
- existingCube.id,
732
+ params,
741
733
  auth.serverTrustIdentity,
734
+ ),
735
+ getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
736
+ createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
737
+ });
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',
742
744
  );
743
- } catch (error) {
744
- return reportServerFailure(deps, authority.apiUrl, error);
745
+ return 1;
745
746
  }
746
- isFirstDrone = (cubeDetail.drones?.length ?? 0) === 0;
747
- } else {
748
- // ----- Step 4a: First-drone bootstrap (template selection) -----
749
- let chosenTemplate: string | undefined;
750
- if (args.flags.noTemplate ||
751
- (args.flags.template !== undefined && args.flags.template !== 'default')) {
747
+ if (error instanceof RepositoryAssociationResolutionError) {
752
748
  deps.stderr(
753
- `Borg server ${authority.apiUrl} supports its default cube template only. ` +
754
- `Rerun ${localAssimilateCommand(authority.apiUrl)} without \`--template\` or \`--no-template\`.\n`,
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',
755
752
  );
756
753
  return 1;
757
754
  }
758
- chosenTemplate = 'default';
759
-
760
- // gh#653 B4: progress for the create round-trip (silent-window stall).
761
- deps.stderr(cubeName ? `Creating cube '${cubeName}'…\n` : 'Creating your cube…\n');
762
- try {
763
- const createParams = chosenTemplate
764
- ? {
765
- name: cubeName ?? undefined,
766
- template: chosenTemplate,
767
- projectRoot,
768
- }
769
- : { name: cubeName ?? undefined };
770
- cubeDetail = await deps.createCube(
771
- auth.apiUrl,
772
- auth.token,
773
- createParams,
774
- auth.serverTrustIdentity,
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',
775
767
  );
776
- } catch (error) {
777
- return reportServerFailure(deps, authority.apiUrl, error);
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
+ }
778
+ if (error instanceof RepositoryAssociationSaveError) {
779
+ deps.stderr(
780
+ 'The repository cube was confirmed, but Borg could not save its local repository association.\n' +
781
+ 'No drone was created.\n' +
782
+ 'Run the same command again; the server will resolve the existing cube and restore the local association.\n',
783
+ );
784
+ return 1;
785
+ }
786
+ if (error instanceof CubeCreationOutcomeUnknownError) {
787
+ deps.stderr(
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',
791
+ );
792
+ return 1;
778
793
  }
779
- isFirstDrone = true;
794
+ if (error instanceof CubeCreationConfirmationError) {
795
+ deps.stderr(
796
+ 'Cube creation could not be confirmed.\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',
799
+ );
800
+ return 1;
801
+ }
802
+ if (error instanceof BorgServerError && error.code === 'CREATE_CUBE_DENIED') {
803
+ deps.stderr(
804
+ `This enrolled client cannot create a cube on ${auth.apiUrl}. ` +
805
+ `Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`,
806
+ );
807
+ return 1;
808
+ }
809
+ if (error instanceof BorgServerError) {
810
+ return reportServerFailure(deps, auth.apiUrl, error);
811
+ }
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
+ );
817
+ return 1;
780
818
  }
819
+ if (initialized.kind === 'stop') return initialized.code;
820
+ const cubeDetail = initialized.creation.cube as CubeDetail;
821
+ const isFirstDrone = (cubeDetail.drones?.length ?? 0) === 0;
822
+ if (mode === 'cube-init') return 0;
781
823
 
782
824
  // Read the worktree identity before role selection. A live local seat must
783
825
  // retain its original role so the attach request reuses the exact durable
@@ -1754,7 +1796,7 @@ export async function runAssimilate(
1754
1796
  serverUrl,
1755
1797
  directory: agentCwd,
1756
1798
  droneLabel: result.drone_label,
1757
- cubeName: cubeName ?? 'borg',
1799
+ cubeName: cubeDetail.name,
1758
1800
  })
1759
1801
  .then(() => injectInitialKickoff(launchKickoff))
1760
1802
  .catch(() => {});