borgmcp 2.14.2 → 2.16.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 (124) hide show
  1. package/dist/agent-runtime.d.ts +5 -0
  2. package/dist/agent-runtime.d.ts.map +1 -1
  3. package/dist/agent-runtime.js +14 -0
  4. package/dist/agent-runtime.js.map +1 -1
  5. package/dist/assimilate-cmd.d.ts +5 -0
  6. package/dist/assimilate-cmd.d.ts.map +1 -1
  7. package/dist/assimilate-cmd.js +53 -14
  8. package/dist/assimilate-cmd.js.map +1 -1
  9. package/dist/assimilate-deps.d.ts.map +1 -1
  10. package/dist/assimilate-deps.js +19 -4
  11. package/dist/assimilate-deps.js.map +1 -1
  12. package/dist/bare-launch-menu.d.ts +8 -0
  13. package/dist/bare-launch-menu.d.ts.map +1 -1
  14. package/dist/bare-launch-menu.js +12 -0
  15. package/dist/bare-launch-menu.js.map +1 -1
  16. package/dist/claude.d.ts.map +1 -1
  17. package/dist/claude.js +28 -40
  18. package/dist/claude.js.map +1 -1
  19. package/dist/cleanup-cmd.d.ts.map +1 -1
  20. package/dist/cleanup-cmd.js +2 -2
  21. package/dist/cleanup-cmd.js.map +1 -1
  22. package/dist/cli-tool-approval.d.ts.map +1 -1
  23. package/dist/cli-tool-approval.js +3 -2
  24. package/dist/cli-tool-approval.js.map +1 -1
  25. package/dist/codex-remote.d.ts.map +1 -1
  26. package/dist/codex-remote.js +4 -3
  27. package/dist/codex-remote.js.map +1 -1
  28. package/dist/config-utils.d.ts +16 -1
  29. package/dist/config-utils.d.ts.map +1 -1
  30. package/dist/config-utils.js +176 -17
  31. package/dist/config-utils.js.map +1 -1
  32. package/dist/credential-paths.d.ts.map +1 -1
  33. package/dist/credential-paths.js +2 -3
  34. package/dist/credential-paths.js.map +1 -1
  35. package/dist/cubes.d.ts +6 -1
  36. package/dist/cubes.d.ts.map +1 -1
  37. package/dist/cubes.js +9 -4
  38. package/dist/cubes.js.map +1 -1
  39. package/dist/foreign-path-reminder.d.ts +12 -0
  40. package/dist/foreign-path-reminder.d.ts.map +1 -0
  41. package/dist/foreign-path-reminder.js +122 -0
  42. package/dist/foreign-path-reminder.js.map +1 -0
  43. package/dist/launch-access.d.ts +22 -0
  44. package/dist/launch-access.d.ts.map +1 -0
  45. package/dist/launch-access.js +27 -0
  46. package/dist/launch-access.js.map +1 -0
  47. package/dist/launch-all-deps.d.ts.map +1 -1
  48. package/dist/launch-all-deps.js +2 -2
  49. package/dist/launch-all-deps.js.map +1 -1
  50. package/dist/lifecycle-log-guard.js +2 -2
  51. package/dist/lifecycle-log-guard.js.map +1 -1
  52. package/dist/local-server-cursor.js +2 -2
  53. package/dist/local-server-cursor.js.map +1 -1
  54. package/dist/opencode-plugin.js +2 -2
  55. package/dist/opencode-plugin.js.map +1 -1
  56. package/dist/parse-assimilate-args.d.ts.map +1 -1
  57. package/dist/parse-assimilate-args.js +6 -1
  58. package/dist/parse-assimilate-args.js.map +1 -1
  59. package/dist/private-root.d.ts +17 -0
  60. package/dist/private-root.d.ts.map +1 -1
  61. package/dist/private-root.js +62 -2
  62. package/dist/private-root.js.map +1 -1
  63. package/dist/regen-format.js +1 -1
  64. package/dist/regen-format.js.map +1 -1
  65. package/dist/resolved-cli-config.d.ts +16 -0
  66. package/dist/resolved-cli-config.d.ts.map +1 -0
  67. package/dist/resolved-cli-config.js +26 -0
  68. package/dist/resolved-cli-config.js.map +1 -0
  69. package/dist/seat-store.d.ts.map +1 -1
  70. package/dist/seat-store.js +4 -3
  71. package/dist/seat-store.js.map +1 -1
  72. package/dist/seats.d.ts.map +1 -1
  73. package/dist/seats.js +2 -2
  74. package/dist/seats.js.map +1 -1
  75. package/dist/self-path.d.ts +2 -0
  76. package/dist/self-path.d.ts.map +1 -1
  77. package/dist/self-path.js +4 -0
  78. package/dist/self-path.js.map +1 -1
  79. package/dist/server-trust.d.ts.map +1 -1
  80. package/dist/server-trust.js +3 -3
  81. package/dist/server-trust.js.map +1 -1
  82. package/dist/setup-confirm.d.ts +1 -1
  83. package/dist/setup-confirm.js +1 -1
  84. package/dist/setup-selection.d.ts +38 -0
  85. package/dist/setup-selection.d.ts.map +1 -0
  86. package/dist/setup-selection.js +51 -0
  87. package/dist/setup-selection.js.map +1 -0
  88. package/dist/setup.js +80 -21
  89. package/dist/setup.js.map +1 -1
  90. package/dist/stream-owner.js +2 -2
  91. package/dist/stream-owner.js.map +1 -1
  92. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  93. package/docs/LOCAL_SERVER.md +23 -0
  94. package/docs/RELEASING.md +130 -282
  95. package/package.json +9 -9
  96. package/src/agent-runtime.ts +17 -0
  97. package/src/assimilate-cmd.ts +73 -14
  98. package/src/assimilate-deps.ts +23 -3
  99. package/src/bare-launch-menu.ts +17 -0
  100. package/src/claude.ts +31 -37
  101. package/src/cleanup-cmd.ts +2 -2
  102. package/src/cli-tool-approval.ts +3 -2
  103. package/src/codex-remote.ts +4 -2
  104. package/src/config-utils.ts +186 -18
  105. package/src/credential-paths.ts +2 -3
  106. package/src/cubes.ts +9 -4
  107. package/src/foreign-path-reminder.ts +141 -0
  108. package/src/launch-access.ts +40 -0
  109. package/src/launch-all-deps.ts +2 -2
  110. package/src/lifecycle-log-guard.ts +2 -2
  111. package/src/local-server-cursor.ts +2 -2
  112. package/src/opencode-plugin.ts +2 -2
  113. package/src/parse-assimilate-args.ts +7 -1
  114. package/src/private-root.ts +62 -2
  115. package/src/regen-format.ts +1 -1
  116. package/src/resolved-cli-config.ts +40 -0
  117. package/src/seat-store.ts +4 -3
  118. package/src/seats.ts +2 -2
  119. package/src/self-path.ts +5 -0
  120. package/src/server-trust.ts +3 -3
  121. package/src/setup-confirm.ts +1 -1
  122. package/src/setup-selection.ts +87 -0
  123. package/src/setup.ts +98 -24
  124. package/src/stream-owner.ts +2 -2
@@ -23,6 +23,7 @@ import { codexBorgSessionConfigArgs } from './launch-gate.js';
23
23
  import {
24
24
  codexAgentKindConfigArgs,
25
25
  codexRemoteWakeConfigArgs,
26
+ codexStateRootConfigArgs,
26
27
  withAgentRuntimeEnv,
27
28
  } from './agent-runtime.js';
28
29
  import type { BorgCli } from './cubes.js';
@@ -74,6 +75,14 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
74
75
  import { createHash } from 'node:crypto';
75
76
  import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
76
77
  import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
78
+ import {
79
+ BORG_LAUNCH_CLI_ENV,
80
+ BORG_LAUNCH_SCRATCH_ENV,
81
+ BORG_LAUNCH_WORKTREE_ENV,
82
+ codexLaunchDirectoryArgs,
83
+ scratchRootForSeat,
84
+ type LaunchAccessPaths,
85
+ } from './launch-access.js';
77
86
  import type {
78
87
  AssociateRepositoryCubeResponse,
79
88
  CreateCubeRepository,
@@ -287,6 +296,12 @@ export interface AssimilateDeps {
287
296
  // launch root's .claude/settings.local.json (project-local; idempotent).
288
297
  // Real wiring = config-utils addProjectSessionStartHook.
289
298
  installProjectSessionHook: (projectRoot: string) => void;
299
+ /** Pre-authorize only the launched seat's worktree + scratch paths. */
300
+ provisionLaunchAccess?: (
301
+ cli: BorgCli,
302
+ projectRoot: string,
303
+ paths: LaunchAccessPaths,
304
+ ) => void;
290
305
 
291
306
  /** gh#27: optional test seam — when set, selectAssimilationAuthority uses
292
307
  * this instead of prompting/failing. Not wired in production. */
@@ -373,6 +388,8 @@ export interface AssimilateDeps {
373
388
  // on healthy hosts; degraded-path surfaces a stderr warning and
374
389
  // still launches claude (never blocks).
375
390
  probeMcpReady: () => Promise<boolean>;
391
+ /** Save the resolved CLI for a newly-created sibling worktree. */
392
+ setCliPreferenceForWorktree: (cli: BorgCli, worktree: string) => Promise<void>;
376
393
  resolveCli: (explicit?: BorgCli) => Promise<BorgCli>;
377
394
  prepareCodexRemoteLaunch: () => Promise<CodexRemoteLaunch>;
378
395
  setCodexWakeTarget: (cubeId: string, droneId: string, target: { threadId: string; socketPath: string }) => Promise<void>;
@@ -693,13 +710,10 @@ export async function runAssimilate(
693
710
  ): Promise<number> {
694
711
  const mode = args.mode ?? 'assimilate';
695
712
  // ----- Input validation (before any subprocess work) -----
696
- if (args.role !== undefined) {
697
- const v = validateName(args.role);
698
- if (!v.ok) {
699
- deps.stderr(v.error + '\n');
700
- return 1;
701
- }
702
- }
713
+ // A role is a lookup key, not a path component. matchRoleByName() below
714
+ // applies the shared roleSlug() normalization, so displayed names such as
715
+ // "Builder" and "Code Reviewer" must reach that resolver. Keep the strict
716
+ // identifier validator for worktree names, which do become path components.
703
717
  if (args.flags.worktree !== undefined) {
704
718
  const v = validateName(args.flags.worktree);
705
719
  if (!v.ok) {
@@ -1807,6 +1821,22 @@ export async function runAssimilate(
1807
1821
  spawnedWorktreePath = deps.cwd();
1808
1822
  }
1809
1823
 
1824
+ // ----- Step 7b: provision launch access before persisting/launching -----
1825
+ // The launched process gets exactly its current worktree plus a stable,
1826
+ // disposable per-seat scratch root. Provision this before FINALIZE so a
1827
+ // failed config write cannot produce a saved seat that launches without
1828
+ // its promised path grants.
1829
+ const agentCwd = deps.cwd(); // post-chdir if step 3 spawned a worktree
1830
+ const seatWorktree = deps.findProjectRoot(agentCwd);
1831
+ const monitorStateRoot = monitorStateRootForWorktree(seatWorktree);
1832
+ const scratchRoot = scratchRootForSeat(deps.homedir(), result.drone_label, result.drone_id);
1833
+ const launchAccessPaths: LaunchAccessPaths = {
1834
+ // Access is granted at the repository root even when the harness starts in
1835
+ // a nested package. The launch cwd remains the operator's chosen subdir.
1836
+ worktree: seatWorktree,
1837
+ scratch: scratchRoot,
1838
+ };
1839
+
1810
1840
  // ----- Step 8: persist the binding (narrow rollback — worktree exists if spawned) -----
1811
1841
  const activeCube: ActiveCube = {
1812
1842
  cubeId: result.cube_id,
@@ -1836,6 +1866,37 @@ export async function runAssimilate(
1836
1866
  }
1837
1867
  };
1838
1868
 
1869
+ // CLI resolution happens before the server attach because agent_kind is part
1870
+ // of that request. The resolver therefore saved the preference against the
1871
+ // invoking checkout. Once a sibling exists, save the same choice under its
1872
+ // own project key so a later --here launch in that worktree can read it.
1873
+ if (spawnedWorktreePath) {
1874
+ try {
1875
+ await deps.setCliPreferenceForWorktree(cli, spawnedWorktreePath);
1876
+ } catch (err) {
1877
+ const message = err instanceof Error ? err.message : String(err);
1878
+ deps.stderr(
1879
+ `Borg could not save the ${cli} preference for sibling worktree ${spawnedWorktreePath}: ${message}. ` +
1880
+ 'The worktree was removed; correct the local configuration and retry.\n',
1881
+ );
1882
+ rollbackWorktree();
1883
+ return 1;
1884
+ }
1885
+ }
1886
+
1887
+ try {
1888
+ deps.mkdirp(scratchRoot);
1889
+ deps.provisionLaunchAccess?.(cli, seatWorktree, launchAccessPaths);
1890
+ } catch (err) {
1891
+ const message = err instanceof Error ? err.message : String(err);
1892
+ deps.stderr(
1893
+ `Borg could not pre-authorize the ${cli} launch paths: ${message}. ` +
1894
+ 'No agent was launched; correct the local configuration and retry.\n',
1895
+ );
1896
+ rollbackWorktree();
1897
+ return 1;
1898
+ }
1899
+
1839
1900
  // Local-server authority: drive the COMPOSITE cube-owned FINALIZE (Race 2).
1840
1901
  // The cube lock is held OUTER across revalidate → binding-write → activate; the
1841
1902
  // typed expectation is declared HERE at the orchestration layer (reattach =
@@ -1945,13 +2006,6 @@ export async function runAssimilate(
1945
2006
  }
1946
2007
  }
1947
2008
 
1948
- // The worktree, not a reminted drone UUID, is the stable local seat identity
1949
- // for monitor runtime state. Capture it once before the lazy GC and launch
1950
- // paths so both use the exact same explicit root.
1951
- const agentCwd = deps.cwd(); // post-chdir if step 3 spawned a worktree
1952
- const seatWorktree = deps.findProjectRoot(agentCwd);
1953
- const monitorStateRoot = monitorStateRootForWorktree(seatWorktree);
1954
-
1955
2009
  // gh#793: best-effort GC of orphaned inbox files (evicted/dead drones) in the
1956
2010
  // cube just joined — lazy-on-assimilate, no cron/new command. NEVER blocks or
1957
2011
  // fails the assimilate (whole call swallowed). Local-only signal (CubeDetail
@@ -2082,6 +2136,9 @@ export async function runAssimilate(
2082
2136
  ...(withAgentRuntimeEnv(process.env, cli) as Record<string, string>),
2083
2137
  ...modelEnv.set,
2084
2138
  BORG_SESSION: '1',
2139
+ [BORG_LAUNCH_CLI_ENV]: cli,
2140
+ [BORG_LAUNCH_WORKTREE_ENV]: seatWorktree,
2141
+ [BORG_LAUNCH_SCRATCH_ENV]: scratchRoot,
2085
2142
  };
2086
2143
  if (cli === 'opencode' && launchApproval.openCodePermission) {
2087
2144
  childEnv.OPENCODE_PERMISSION = launchApproval.openCodePermission;
@@ -2127,10 +2184,12 @@ export async function runAssimilate(
2127
2184
  // off when no socket is available, overriding legacy static configs that
2128
2185
  // formerly used this transport marker as Codex identity.
2129
2186
  launchArgs = [
2187
+ ...codexLaunchDirectoryArgs(launchAccessPaths),
2130
2188
  ...launchApproval.codexArgs,
2131
2189
  ...codexBorgSessionConfigArgs(),
2132
2190
  ...codexAgentKindConfigArgs(),
2133
2191
  ...codexRemoteWakeConfigArgs(codexSocketPath !== null),
2192
+ ...codexStateRootConfigArgs(),
2134
2193
  ...remoteArgs,
2135
2194
  ...withCodexCwdArg(launchArgs, agentCwd),
2136
2195
  ];
@@ -11,7 +11,7 @@
11
11
  import { spawnSync, spawn as spawnChild } from 'node:child_process';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { existsSync, mkdirSync } from 'node:fs';
14
- import { hostname as osHostname, homedir as osHomedir } from 'node:os';
14
+ import { hostname as osHostname } from 'node:os';
15
15
  import { createInterface } from 'node:readline/promises';
16
16
  import prompts from 'prompts';
17
17
  import { readinessProbeEnv } from './readiness-probe.js';
@@ -60,9 +60,16 @@ import {
60
60
  hasPersistedActiveCube as cubesHasPersistedActive,
61
61
  setActiveCube as cubesSetActive,
62
62
  inboxPathForDrone,
63
+ setProjectCliPreference,
63
64
  setCodexWakeTarget,
64
65
  } from './cubes.js';
65
- import { addProjectSessionStartHook } from './config-utils.js';
66
+ import {
67
+ addClaudeLaunchAccess,
68
+ addCodexForeignPathReminderHook,
69
+ addOpenCodeLaunchAccess,
70
+ addProjectSessionStartHook,
71
+ } from './config-utils.js';
72
+ import type { LaunchAccessPaths } from './launch-access.js';
66
73
  import { findPendingServerEnrollment } from './config.js';
67
74
  import { setTerminalTitle as setTitle } from './terminal-title.js';
68
75
  import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
@@ -77,6 +84,7 @@ import {
77
84
  saveRepositoryAssociation,
78
85
  } from './repository-identity.js';
79
86
  import { PromptInterruptedError } from './repository-cube-init.js';
87
+ import { borgHomeRoot } from './private-root.js';
80
88
 
81
89
  /**
82
90
  * Wraps the readline question operation with the production interruption
@@ -126,7 +134,7 @@ export function buildDefaultAssimilateDeps(
126
134
  // gh#556 Part 1: ~/.borg/worktrees relocation. homedir seams $HOME;
127
135
  // mkdirp is a plain recursive create (NO chmod of existing parents — the
128
136
  // ~/.borg credentials file's perms stay untouched).
129
- homedir: () => osHomedir(),
137
+ homedir: () => borgHomeRoot(),
130
138
  mkdirp: (dir) => mkdirSync(dir, { recursive: true }),
131
139
  preparePrivateRoot: () => ensurePrivateBorgConfigRoot(),
132
140
  exec: (cmd, args, cwd, env) =>
@@ -235,6 +243,17 @@ export function buildDefaultAssimilateDeps(
235
243
  installProjectSessionHook: (projectRoot) => {
236
244
  addProjectSessionStartHook(projectRoot);
237
245
  },
246
+ provisionLaunchAccess: (cli, projectRoot, paths: LaunchAccessPaths) => {
247
+ if (cli === 'claude') {
248
+ addClaudeLaunchAccess(projectRoot, paths);
249
+ } else if (cli === 'codex') {
250
+ // Codex receives the path grant on the launch command itself. Its
251
+ // native PreToolUse hook is global but reads this launch's scoped env.
252
+ addCodexForeignPathReminderHook();
253
+ } else {
254
+ addOpenCodeLaunchAccess(projectRoot, paths);
255
+ }
256
+ },
238
257
 
239
258
  // #1015: discovery is advisory but still verifies the server-owned CA.
240
259
  detectLocalServer: async () =>
@@ -557,6 +576,7 @@ export function buildDefaultAssimilateDeps(
557
576
  }
558
577
  }, () => process.stdin.isTTY === true)
559
578
  ),
579
+ setCliPreferenceForWorktree: (cli, worktree) => setProjectCliPreference(cli, worktree),
560
580
  prepareCodexRemoteLaunch: () => prepareCodexRemoteLaunch(defaultCodexRemoteDeps()),
561
581
  setCodexWakeTarget,
562
582
  findLoadedCodexThread,
@@ -39,6 +39,23 @@ export interface LaunchMenuInputs {
39
39
  hasLaunchAllTargets: boolean;
40
40
  }
41
41
 
42
+ /**
43
+ * Resolve and configure the CLI that will actually launch.
44
+ *
45
+ * This callback deliberately runs after the one-shot menu choice. A bare
46
+ * launch may resolve one default CLI first and then launch a different
47
+ * installed CLI for this invocation; only the final CLI may be self-healed.
48
+ */
49
+ export function configureSelectedLaunchCli(
50
+ defaultCli: BorgCli,
51
+ action: LaunchMenuAction | undefined,
52
+ configure: (cli: BorgCli) => void,
53
+ ): BorgCli {
54
+ const cli = action?.kind === 'launch' ? action.cli : defaultCli;
55
+ configure(cli);
56
+ return cli;
57
+ }
58
+
42
59
  const PRETTY: Record<BorgCli, string> = { claude: 'Claude', codex: 'Codex', opencode: 'OpenCode' };
43
60
 
44
61
  /**
package/src/claude.ts CHANGED
@@ -24,7 +24,7 @@ import { basename } from 'node:path';
24
24
  import { createInterface } from 'node:readline/promises';
25
25
  import { fileURLToPath } from 'node:url';
26
26
  import chalk from 'chalk';
27
- import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets } from './cubes.js';
27
+ import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets, type BorgCli } from './cubes.js';
28
28
  import { monitorStateRootForWorktree } from './inbox-monitor.js';
29
29
  import { formatSeatReattachRefusal, inspectLiveInboxMonitor } from './seat-reattach-guard.js';
30
30
  import { handleVersionFlag, getPackageVersion } from './version.js';
@@ -48,10 +48,12 @@ import { runLaunchAll } from './launch-all-cmd.js';
48
48
  import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
49
49
  import { discoverDroneCandidates } from './launch-all-discovery.js';
50
50
  import {
51
+ configureSelectedLaunchCli,
51
52
  explicitCliLaunchHint,
52
53
  runBareLaunchMenu,
53
54
  shouldResolveExplicitCliLaunchHintTargets,
54
55
  shouldShowLaunchMenu,
56
+ type LaunchMenuAction,
55
57
  } from './bare-launch-menu.js';
56
58
  import { setTerminalTitle } from './terminal-title.js';
57
59
  import { initConsolePrefix, consolePrefix } from './console-prefix.js';
@@ -62,6 +64,7 @@ import {
62
64
  BORG_CODEX_REMOTE_WAKE_ENV,
63
65
  codexAgentKindConfigArgs,
64
66
  codexRemoteWakeConfigArgs,
67
+ codexStateRootConfigArgs,
65
68
  withAgentRuntimeEnv,
66
69
  } from './agent-runtime.js';
67
70
  import { findLoadedCodexThread } from './codex-app-server.js';
@@ -80,6 +83,7 @@ import {
80
83
  removeSessionStartHook,
81
84
  } from './config-utils.js';
82
85
  import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
86
+ import { configureResolvedCli } from './resolved-cli-config.js';
83
87
  import { installBorgPlugin } from './opencode-plugin.js';
84
88
  import { allocateOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff, openCodeLaunchBinding } from './opencode-drone.js';
85
89
  import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
@@ -321,7 +325,7 @@ async function main() {
321
325
  parsedCli.cli,
322
326
  defaultCliChoiceDeps(prompt, () => process.stdin.isTTY === true)
323
327
  );
324
- ensureDetectedCliConfigured();
328
+ let launchAction: LaunchMenuAction | undefined;
325
329
 
326
330
  // Active cube for this directory — needed for the launch menu's option-3
327
331
  // availability, the terminal title, and the inbox-Monitor clause below.
@@ -382,9 +386,13 @@ async function main() {
382
386
  // option 1 → configured default; option 2 → the other agent, ONE-SHOT
383
387
  // (we deliberately do NOT call setProjectCliPreference — the saved
384
388
  // preference is changed only via `borg --cli <agent>`).
385
- cli = action.cli;
389
+ launchAction = action;
386
390
  }
387
391
 
392
+ // Configure only the CLI that will actually launch. This must follow the
393
+ // one-shot menu: the resolved default can differ from the menu selection.
394
+ cli = configureSelectedLaunchCli(cli, launchAction, ensureResolvedCliConfigured);
395
+
388
396
  if (active && !parsedCli.force) {
389
397
  const inboxPath = inboxPathForDrone(active.cubeId, active.droneId);
390
398
  const stateRoot = monitorStateRootForWorktree(findProjectRoot(process.cwd()));
@@ -531,6 +539,7 @@ async function main() {
531
539
  ...codexBorgSessionConfigArgs(),
532
540
  ...codexAgentKindConfigArgs(),
533
541
  ...codexRemoteWakeConfigArgs(codexSocketPath !== null),
542
+ ...codexStateRootConfigArgs(),
534
543
  ...remoteArgs,
535
544
  ...withCodexCwdArg([...passthroughArgs, kickoff], process.cwd()),
536
545
  ];
@@ -614,40 +623,25 @@ async function main() {
614
623
  });
615
624
  }
616
625
 
617
- function ensureDetectedCliConfigured(): void {
618
- const found = detectCliAvailability();
619
- if (found.claude) {
620
- try {
621
- ensureCliMcpConfigured('claude');
622
- // gh#673 P2 (WI-1): the orientation hook lives PROJECT-LOCAL in
623
- // <root>/.claude/settings.local.json ensured on every bare
624
- // `borg` launch so pre-P2 worktrees self-heal. The legacy GLOBAL
625
- // hook is then removed: safe because this ensure precedes every
626
- // borg-launched agent spawn (other projects get their local hook
627
- // at their own next launch/assimilate), and P1's BORG_SESSION
628
- // gate already no-ops the global hook in non-borg sessions.
629
- addProjectSessionStartHook(findProjectRoot(process.cwd()));
630
- removeSessionStartHook();
631
- addUserPromptSubmitHook();
632
- } catch (err: any) {
633
- console.error(`${consolePrefix()}${chalk.yellow(`warning: Claude Code integration check failed: ${err?.message ?? err}`)}`);
634
- }
635
- }
636
- if (found.codex) {
637
- try {
638
- ensureCliMcpConfigured('codex');
639
- addCodexSessionStartHook();
640
- addCodexUserPromptSubmitHook();
641
- } catch (err: any) {
642
- console.error(`${consolePrefix()}${chalk.yellow(`warning: Codex integration check failed: ${err?.message ?? err}`)}`);
643
- }
644
- }
645
- if (found.opencode) {
646
- try {
647
- ensureCliMcpConfigured('opencode');
648
- } catch (err: any) {
649
- console.error(`${consolePrefix()}${chalk.yellow(`warning: OpenCode integration check failed: ${err?.message ?? err}`)}`);
650
- }
626
+ function ensureResolvedCliConfigured(cli: BorgCli): void {
627
+ const label = cli === 'claude' ? 'Claude Code' : cli === 'codex' ? 'Codex' : 'OpenCode';
628
+ try {
629
+ configureResolvedCli(cli, {
630
+ ensureMcp: ensureCliMcpConfigured,
631
+ addClaudeProjectSessionStartHook: () => {
632
+ // gh#673 P2 (WI-1): the orientation hook lives PROJECT-LOCAL in
633
+ // <root>/.claude/settings.local.json ensured on every bare
634
+ // `borg` launch so pre-P2 worktrees self-heal. The legacy GLOBAL
635
+ // hook is then removed after the local hook is in place.
636
+ addProjectSessionStartHook(findProjectRoot(process.cwd()));
637
+ },
638
+ removeClaudeGlobalSessionStartHook: removeSessionStartHook,
639
+ addClaudeUserPromptSubmitHook: addUserPromptSubmitHook,
640
+ addCodexSessionStartHook,
641
+ addCodexUserPromptSubmitHook,
642
+ });
643
+ } catch (err: any) {
644
+ console.error(`${consolePrefix()}${chalk.yellow(`warning: ${label} integration check failed: ${err?.message ?? err}`)}`);
651
645
  }
652
646
  }
653
647
 
@@ -36,7 +36,6 @@
36
36
  import { spawnSync } from 'node:child_process';
37
37
  import { realpathSync } from 'node:fs';
38
38
  import { sep, basename, dirname } from 'node:path';
39
- import { homedir } from 'node:os';
40
39
  import chalk from 'chalk';
41
40
  import {
42
41
  classifyDirty,
@@ -51,6 +50,7 @@ import {
51
50
  type ActiveCube,
52
51
  } from './cubes.js';
53
52
  import { defaultProbeSeat, type SeatStatus } from './seat-probe.js';
53
+ import { borgHomeRoot } from './private-root.js';
54
54
 
55
55
  // ------------------------------------------------------------------
56
56
  // Types
@@ -208,7 +208,7 @@ const defaultDeps: Required<CleanupDeps> = {
208
208
  const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
209
209
  return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
210
210
  },
211
- homeDir: () => homedir(),
211
+ homeDir: () => borgHomeRoot(),
212
212
  cwd: () => process.cwd(),
213
213
  listSeats: () => readAllProjectIdentities(),
214
214
  probeSeat: defaultProbeSeat,
@@ -1,8 +1,9 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
- import { homedir, tmpdir } from 'node:os';
3
+ import { tmpdir } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import type { BorgCli } from './cubes.js';
6
+ import { borgHomeRoot } from './private-root.js';
6
7
 
7
8
  /**
8
9
  * Client #20: the narrow coordination surface a borg-launched agent needs
@@ -360,7 +361,7 @@ async function withNativeCodexProfileLayer<T>(
360
361
  env: NodeJS.ProcessEnv,
361
362
  query: (profileCwd: string, trustOverride: string) => Promise<CodexConfigSnapshot>
362
363
  ): Promise<T> {
363
- const codexHome = resolve(env.CODEX_HOME || join(homedir(), '.codex'));
364
+ const codexHome = resolve(env.CODEX_HOME || join(borgHomeRoot(), '.codex'));
364
365
  const profilePath = resolve(codexHome, `${profile}.config.toml`);
365
366
  if (dirname(profilePath) !== codexHome) {
366
367
  throw new Error('Codex profile path was invalid');
@@ -1,5 +1,4 @@
1
1
  import { mkdirSync, chmodSync, readdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
2
  import { join, resolve } from 'node:path';
4
3
  import { randomBytes } from 'node:crypto';
5
4
  import { spawn } from 'node:child_process';
@@ -11,7 +10,9 @@ import {
11
10
  BORG_CODEX_REMOTE_WAKE_ENV,
12
11
  codexAgentKindConfigArgs,
13
12
  codexRemoteWakeConfigArgs,
13
+ codexStateRootConfigArgs,
14
14
  } from './agent-runtime.js';
15
+ import { borgConfigRoot } from './private-root.js';
15
16
 
16
17
  /**
17
18
  * gh#528 — npm-managed Codex remote-wake.
@@ -75,7 +76,7 @@ export interface PrepareCodexRemoteDeps {
75
76
  isAlive?: (pid: number) => boolean;
76
77
  }
77
78
 
78
- export const DEFAULT_CODEX_REMOTE_DIR = join(homedir(), '.config', 'borgmcp', 'codex-remote');
79
+ export const DEFAULT_CODEX_REMOTE_DIR = join(borgConfigRoot(), 'codex-remote');
79
80
  const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS = 30_000;
80
81
  const MAX_CODEX_APP_SERVER_STDERR_CHARS = 16_384;
81
82
 
@@ -426,6 +427,7 @@ export function defaultCodexRemoteDeps(): Pick<
426
427
  ...codexBorgSessionConfigArgs(),
427
428
  ...codexAgentKindConfigArgs(),
428
429
  ...codexRemoteWakeConfigArgs(),
430
+ ...codexStateRootConfigArgs(),
429
431
  ...codexAppServerSocketConfigArgs(socketPath),
430
432
  '--listen',
431
433
  `unix://${socketPath}`,