borgmcp 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -10
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +83 -26
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.js +6 -16
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +49 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cubes.d.ts +10 -5
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +54 -3
- package/dist/cubes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -7
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +3 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +6 -1
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-seat-identity.d.ts +22 -0
- package/dist/opencode-seat-identity.d.ts.map +1 -0
- package/dist/opencode-seat-identity.js +69 -0
- package/dist/opencode-seat-identity.js.map +1 -0
- package/dist/server-facade.d.ts +4 -0
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +11 -0
- package/dist/server-facade.js.map +1 -1
- package/dist/setup.js +2 -4
- package/dist/setup.js.map +1 -1
- package/dist/startup-services.d.ts +3 -1
- package/dist/startup-services.d.ts.map +1 -1
- package/dist/startup-services.js +7 -2
- package/dist/startup-services.js.map +1 -1
- package/dist/stream-owner.d.ts +9 -0
- package/dist/stream-owner.d.ts.map +1 -1
- package/dist/stream-owner.js +14 -2
- package/dist/stream-owner.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +8 -1
- package/dist/stream-status.js.map +1 -1
- package/dist/tool-manifest.js +11 -11
- package/dist/tool-manifest.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/RELEASING.md +11 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +94 -26
- package/src/claude.ts +11 -16
- package/src/cli-help.ts +65 -1
- package/src/cubes.ts +70 -3
- package/src/index.ts +66 -7
- package/src/log-stream.ts +9 -1
- package/src/opencode-seat-identity.ts +111 -0
- package/src/server-facade.ts +15 -0
- package/src/setup.ts +2 -4
- package/src/startup-services.ts +8 -2
- package/src/stream-owner.ts +23 -2
- package/src/stream-status.ts +8 -1
- package/src/tool-manifest.ts +11 -11
package/src/assimilate-cmd.ts
CHANGED
|
@@ -540,11 +540,22 @@ function reportServerFailure(
|
|
|
540
540
|
);
|
|
541
541
|
return 1;
|
|
542
542
|
}
|
|
543
|
+
if (/Borg server trust files were not found/i.test(message)) {
|
|
544
|
+
deps.stderr(
|
|
545
|
+
`This machine has no trust material for Borg server ${apiUrl}. ` +
|
|
546
|
+
'For a local server on this machine, run `borg server setup`, then run ' +
|
|
547
|
+
'`borg server start`, and rerun ' +
|
|
548
|
+
`${retryCommand}. A server on another machine requires a supported trust-bootstrap ` +
|
|
549
|
+
'step before enrollment; an invitation alone cannot establish server identity.\n',
|
|
550
|
+
);
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
543
553
|
if (/trust|certificate|\bCA\b|authority state|pinned identity|cross-authority/i.test(message)) {
|
|
544
554
|
deps.stderr(
|
|
545
555
|
`Borg could not verify the expected server identity for ${apiUrl}. ` +
|
|
546
|
-
'Verify that this is the expected server. If it was re-initialized,
|
|
547
|
-
'
|
|
556
|
+
'Verify that this is the expected server. If it was re-initialized, ask the server ' +
|
|
557
|
+
'operator to restore the expected identity. For a local server on this machine, use ' +
|
|
558
|
+
'`borg server setup` and `borg server start`, then rerun ' +
|
|
548
559
|
`${retryCommand}.\n`,
|
|
549
560
|
);
|
|
550
561
|
return 1;
|
|
@@ -1456,7 +1467,10 @@ export async function runAssimilate(
|
|
|
1456
1467
|
}
|
|
1457
1468
|
}
|
|
1458
1469
|
|
|
1459
|
-
|
|
1470
|
+
// The common Git directory identifies the repository across every linked
|
|
1471
|
+
// worktree. Using projectRoot here fragments one repository's siblings when
|
|
1472
|
+
// assimilation starts inside an existing sibling worktree.
|
|
1473
|
+
const repoBase = basename(dirname(repositoryContext.commonDir));
|
|
1460
1474
|
const suffix = args.flags.worktree ?? roleSlug(assignedRole.name);
|
|
1461
1475
|
// gh#556 Part 1: empty-suffix guard (CR-binding). roleSlug can yield '' for a
|
|
1462
1476
|
// pathological all-special-char role name; an empty leaf would let join() collapse
|
|
@@ -1473,6 +1487,15 @@ export async function runAssimilate(
|
|
|
1473
1487
|
// (was a sibling <parent>/<repo>-<name>). Existing siblings are untouched
|
|
1474
1488
|
// (absolute git-registered paths). Collision dedup KEPT (<name>-<n>).
|
|
1475
1489
|
const homeDir = deps.homedir();
|
|
1490
|
+
let registeredWorktrees = listRegisteredWorktrees(deps, projectRoot);
|
|
1491
|
+
if (registeredWorktrees === null) {
|
|
1492
|
+
deps.stderr(
|
|
1493
|
+
'Borg could not enumerate this repository’s existing worktrees, so it did not risk creating a colliding sibling.\n' +
|
|
1494
|
+
'Run `git worktree list` from this repository and resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1495
|
+
'A seat was reserved and remains pending; rerunning after fixing the worktree issue resumes that seat.\n',
|
|
1496
|
+
);
|
|
1497
|
+
return 1;
|
|
1498
|
+
}
|
|
1476
1499
|
let candidate = computeWorktreePath(homeDir, repoBase, suffix);
|
|
1477
1500
|
let wtBranch = perWorktreeBranchName(basename(candidate), repoBase);
|
|
1478
1501
|
let n = 2;
|
|
@@ -1485,7 +1508,8 @@ export async function runAssimilate(
|
|
|
1485
1508
|
// fresh suffix so we never reuse/clobber its work.
|
|
1486
1509
|
while (
|
|
1487
1510
|
deps.pathExists(candidate) ||
|
|
1488
|
-
|
|
1511
|
+
registeredWorktrees.names.has(basename(candidate)) ||
|
|
1512
|
+
registeredWorktrees.branches.has(wtBranch) ||
|
|
1489
1513
|
(localBranchExists(deps.runSync, projectRoot, wtBranch) &&
|
|
1490
1514
|
!isMerged(deps.runSync, projectRoot, wtBranch, startRef))
|
|
1491
1515
|
) {
|
|
@@ -1493,26 +1517,56 @@ export async function runAssimilate(
|
|
|
1493
1517
|
wtBranch = perWorktreeBranchName(basename(candidate), repoBase);
|
|
1494
1518
|
n++;
|
|
1495
1519
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1520
|
+
let wt: ReturnType<AssimilateDeps['runSync']>;
|
|
1521
|
+
let residualBranch: string | null = null;
|
|
1522
|
+
while (true) {
|
|
1523
|
+
// gh#556 Part 1: create the intermediate ~/.borg/worktrees/<repo>/ before
|
|
1524
|
+
// `git worktree add` (git creates the leaf, not the parent chain). Plain
|
|
1525
|
+
// recursive mkdir — NO chmod of the existing ~/.borg (credentials file).
|
|
1526
|
+
deps.mkdirp(dirname(candidate));
|
|
1527
|
+
const branchExisted = localBranchExists(deps.runSync, projectRoot, wtBranch);
|
|
1528
|
+
wt = branchExisted
|
|
1529
|
+
? deps.runSync('git', ['worktree', 'add', candidate, wtBranch], projectRoot)
|
|
1530
|
+
: deps.runSync('git', ['worktree', 'add', '-b', wtBranch, candidate, startRef], projectRoot);
|
|
1531
|
+
if (wt.status === 0) break;
|
|
1532
|
+
|
|
1533
|
+
// Another assimilate may claim the name or branch after our first list.
|
|
1534
|
+
// Refresh and suffix-bump instead of surfacing a collision to the operator.
|
|
1535
|
+
const refreshed = listRegisteredWorktrees(deps, projectRoot);
|
|
1536
|
+
const branchAppeared = !branchExisted && localBranchExists(deps.runSync, projectRoot, wtBranch);
|
|
1537
|
+
const collision =
|
|
1538
|
+
deps.pathExists(candidate) ||
|
|
1539
|
+
refreshed?.names.has(basename(candidate)) === true ||
|
|
1540
|
+
refreshed?.branches.has(wtBranch) === true ||
|
|
1541
|
+
(!branchExisted && worktreeAddReportedCollision(wt.stderr));
|
|
1542
|
+
if (!collision || refreshed === null) {
|
|
1543
|
+
if (branchAppeared && refreshed?.branches.has(wtBranch) !== true) {
|
|
1544
|
+
residualBranch = wtBranch;
|
|
1545
|
+
}
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
registeredWorktrees = refreshed;
|
|
1549
|
+
do {
|
|
1550
|
+
candidate = computeWorktreePath(homeDir, repoBase, suffix, n);
|
|
1551
|
+
wtBranch = perWorktreeBranchName(basename(candidate), repoBase);
|
|
1552
|
+
n++;
|
|
1553
|
+
} while (
|
|
1554
|
+
deps.pathExists(candidate) ||
|
|
1555
|
+
registeredWorktrees.names.has(basename(candidate)) ||
|
|
1556
|
+
registeredWorktrees.branches.has(wtBranch) ||
|
|
1557
|
+
localBranchExists(deps.runSync, projectRoot, wtBranch)
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1514
1560
|
if (wt.status !== 0) {
|
|
1515
|
-
deps.stderr(
|
|
1561
|
+
deps.stderr(
|
|
1562
|
+
`Borg could not create sibling worktree ${candidate} on branch ${wtBranch}. ` +
|
|
1563
|
+
`Git reported: ${safeStderr(wt.stderr)}\n` +
|
|
1564
|
+
(residualBranch
|
|
1565
|
+
? `Git left branch ${residualBranch} without a registered worktree; Borg preserved it.\n`
|
|
1566
|
+
: '') +
|
|
1567
|
+
'Run `git worktree list` and `git status` to inspect repository state, resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1568
|
+
'A seat was reserved and remains pending; rerunning after fixing the worktree issue resumes that seat.\n',
|
|
1569
|
+
);
|
|
1516
1570
|
return 1;
|
|
1517
1571
|
}
|
|
1518
1572
|
deps.stderr(
|
|
@@ -1958,10 +2012,24 @@ export function safeStderr(msg: string): string {
|
|
|
1958
2012
|
return msg.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1959
2013
|
}
|
|
1960
2014
|
|
|
1961
|
-
function
|
|
2015
|
+
function worktreeAddReportedCollision(stderr: string): boolean {
|
|
2016
|
+
const message = safeStderr(stderr);
|
|
2017
|
+
return /(?:branch named .* already exists|already checked out|already registered|already exists at)/i.test(message);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function listRegisteredWorktrees(
|
|
2021
|
+
deps: AssimilateDeps,
|
|
2022
|
+
projectRoot: string,
|
|
2023
|
+
): { names: Set<string>; branches: Set<string> } | null {
|
|
1962
2024
|
const res = deps.runSync('git', ['worktree', 'list', '--porcelain'], projectRoot);
|
|
1963
|
-
if (res.status !== 0) return
|
|
1964
|
-
|
|
2025
|
+
if (res.status !== 0) return null;
|
|
2026
|
+
const names = new Set<string>();
|
|
2027
|
+
const branches = new Set<string>();
|
|
2028
|
+
for (const line of res.stdout.split('\n')) {
|
|
2029
|
+
if (line.startsWith('worktree ')) names.add(basename(line.slice('worktree '.length)));
|
|
2030
|
+
if (line.startsWith('branch refs/heads/')) branches.add(line.slice('branch refs/heads/'.length));
|
|
2031
|
+
}
|
|
2032
|
+
return { names, branches };
|
|
1965
2033
|
}
|
|
1966
2034
|
|
|
1967
2035
|
/**
|
package/src/claude.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget,
|
|
|
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';
|
|
31
|
-
import {
|
|
31
|
+
import { clientSubcommandHelpText, topLevelHelpText } from './cli-help.js';
|
|
32
32
|
import { runSpawn } from './spawn.js';
|
|
33
33
|
import { buildClaudeLaunchArgs } from './claude-launch-args.js';
|
|
34
34
|
import { parseSyncArgs, runSync } from './sync.js';
|
|
@@ -142,31 +142,26 @@ async function main() {
|
|
|
142
142
|
process.exit(0);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
const subcommandHelp = clientSubcommandHelpText(
|
|
146
|
+
process.argv[2],
|
|
147
|
+
process.argv.slice(3),
|
|
148
|
+
getPackageVersion(),
|
|
149
|
+
);
|
|
150
|
+
if (subcommandHelp !== null) {
|
|
151
|
+
process.stdout.write(subcommandHelp);
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
// Re-route subcommands.
|
|
146
156
|
if (process.argv[2] === 'setup') {
|
|
147
|
-
// gh#520: `borg setup --help` must show help, not run the wizard.
|
|
148
|
-
if (isHelpFlag(process.argv[3])) {
|
|
149
|
-
process.stdout.write(setupHelpText(getPackageVersion()));
|
|
150
|
-
process.exit(0);
|
|
151
|
-
}
|
|
152
157
|
await import('./setup.js');
|
|
153
158
|
return;
|
|
154
159
|
}
|
|
155
160
|
if (process.argv[2] === 'assimilate') {
|
|
156
|
-
// `borg assimilate --help` (or `... <role> --help`) must show help, not be
|
|
157
|
-
// rejected by the parser as an unknown flag.
|
|
158
|
-
if (process.argv.slice(3).some(isHelpFlag)) {
|
|
159
|
-
process.stdout.write(assimilateHelpText(getPackageVersion()));
|
|
160
|
-
process.exit(0);
|
|
161
|
-
}
|
|
162
161
|
const code = await runAssimilateEntry(process.argv.slice(3));
|
|
163
162
|
process.exit(code);
|
|
164
163
|
}
|
|
165
164
|
if (process.argv[2] === 'reset-local-seat') {
|
|
166
|
-
if (process.argv.slice(3).some(isHelpFlag)) {
|
|
167
|
-
process.stdout.write(resetLocalSeatHelpText(getPackageVersion()));
|
|
168
|
-
process.exit(0);
|
|
169
|
-
}
|
|
170
165
|
const parsed = parseResetLocalSeatArgs(process.argv.slice(3));
|
|
171
166
|
if (!parsed.ok) {
|
|
172
167
|
process.stderr.write(
|
package/src/cli-help.ts
CHANGED
|
@@ -16,6 +16,70 @@ export function isHelpFlag(arg: string | undefined): boolean {
|
|
|
16
16
|
return arg === '--help' || arg === '-h';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function syncHelpText(version: string): string {
|
|
20
|
+
return (
|
|
21
|
+
`borg sync (borgmcp ${version}) — update this Borg worktree from origin/main\n\n` +
|
|
22
|
+
`Usage:\n` +
|
|
23
|
+
` borg sync Update safely; refuse if the worktree is dirty\n` +
|
|
24
|
+
` borg sync --prune Also delete a merged local feature branch\n` +
|
|
25
|
+
` borg sync --help Show this help\n`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function cleanupHelpText(version: string): string {
|
|
30
|
+
return (
|
|
31
|
+
`borg cleanup (borgmcp ${version}) — review orphaned Borg-managed worktrees\n\n` +
|
|
32
|
+
`Usage:\n` +
|
|
33
|
+
` borg cleanup Report what is safe to remove; change nothing\n` +
|
|
34
|
+
` borg cleanup --prune Remove only worktrees proven safe to prune\n` +
|
|
35
|
+
` borg cleanup --help Show this help\n`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function launchAllHelpText(version: string): string {
|
|
40
|
+
return (
|
|
41
|
+
`borg launch-all (borgmcp ${version}) — launch a cube's saved drone worktrees\n\n` +
|
|
42
|
+
`Usage:\n` +
|
|
43
|
+
` borg launch-all [cube] [options]\n\n` +
|
|
44
|
+
`Options:\n` +
|
|
45
|
+
` --mode <tmux|windows|pastelist> Select the launch backend\n` +
|
|
46
|
+
` --only <name> Launch one role or drone label\n` +
|
|
47
|
+
` --dry-run Show what would launch\n` +
|
|
48
|
+
` --cli <claude|codex|opencode> Select the agent CLI\n` +
|
|
49
|
+
` --no-attach Do not attach to the tmux session\n` +
|
|
50
|
+
` --yes, -y Skip the large-fleet confirmation\n` +
|
|
51
|
+
` --force Override live-session skips\n` +
|
|
52
|
+
` --launch-delay <ms> Wait between launches\n` +
|
|
53
|
+
` --help, -h Show this help\n`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clientSubcommandHelpText(
|
|
58
|
+
command: string | undefined,
|
|
59
|
+
args: readonly string[],
|
|
60
|
+
version: string,
|
|
61
|
+
): string | null {
|
|
62
|
+
if (!args.some(isHelpFlag)) return null;
|
|
63
|
+
switch (command) {
|
|
64
|
+
case 'setup': return setupHelpText(version);
|
|
65
|
+
case 'assimilate': return assimilateHelpText(version);
|
|
66
|
+
case 'reset-local-seat': return resetLocalSeatHelpText(version);
|
|
67
|
+
case 'sync': return syncHelpText(version);
|
|
68
|
+
case 'cleanup': return cleanupHelpText(version);
|
|
69
|
+
case 'launch-all': return launchAllHelpText(version);
|
|
70
|
+
default: return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setupNextStepsText(): string {
|
|
75
|
+
return (
|
|
76
|
+
`◼ Next steps:\n` +
|
|
77
|
+
`1. Run \`borg server setup\` to prepare the local server.\n` +
|
|
78
|
+
`2. Run \`borg server start\` and leave that terminal open.\n` +
|
|
79
|
+
`3. In a second terminal, cd into your project and run \`borg assimilate\`.\n`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
19
83
|
/**
|
|
20
84
|
* Help text for top-level `borg --help`.
|
|
21
85
|
*
|
|
@@ -130,7 +194,7 @@ export function assimilateHelpText(version: string): string {
|
|
|
130
194
|
` --enroll Prompt for a hidden enrollment invitation in the operator terminal\n` +
|
|
131
195
|
` --template ${NEW_CUBE_TEMPLATE_OPTIONS} New-cube template (default: ${DEFAULT_NEW_CUBE_TEMPLATE})\n` +
|
|
132
196
|
` --no-template Unsupported for repository cube creation\n` +
|
|
133
|
-
` --cli claude|codex|opencode Agent CLI to launch
|
|
197
|
+
` --cli claude|codex|opencode Agent CLI to launch\n` +
|
|
134
198
|
` --model claude:<model> Legacy Claude model override (configure models in the agent CLI)\n` +
|
|
135
199
|
` --yes, -y Accept new-cube defaults; never adopt by name\n\n` +
|
|
136
200
|
`Creation shows repository context, name, template, and one confirmation. An existing\n` +
|
package/src/cubes.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface ActiveCube {
|
|
|
72
72
|
roleName?: string;
|
|
73
73
|
roleClass?: 'queen' | 'worker';
|
|
74
74
|
isHumanSeat?: boolean;
|
|
75
|
+
/** Canonical worktree bound to this exact durable seat. */
|
|
76
|
+
worktree?: string;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export type ActiveCubeInput = Omit<ActiveCube, 'sessionToken'> & {
|
|
@@ -97,7 +99,53 @@ interface CodexWakeTargetsFile {
|
|
|
97
99
|
* directory. If not found by filesystem root, return the original cwd.
|
|
98
100
|
* The returned absolute path is the "project key" used to scope cube state.
|
|
99
101
|
*/
|
|
100
|
-
|
|
102
|
+
let pinnedMcpProjectRoot: string | null = null;
|
|
103
|
+
let pinnedMcpSeatIdentity: {
|
|
104
|
+
worktree: string;
|
|
105
|
+
cubeId: string;
|
|
106
|
+
droneId: string;
|
|
107
|
+
credentialRef?: string;
|
|
108
|
+
} | null = null;
|
|
109
|
+
|
|
110
|
+
export class McpSeatIdentityChangedError extends Error {
|
|
111
|
+
readonly code = 'SEAT_IDENTITY_CHANGED';
|
|
112
|
+
constructor() {
|
|
113
|
+
super('The saved Borg seat changed after this MCP session pinned its identity. Exit this session and relaunch from the intended worktree.');
|
|
114
|
+
this.name = 'McpSeatIdentityChangedError';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function pinMcpProjectRoot(worktree: string): void {
|
|
119
|
+
const normalized = resolve(worktree);
|
|
120
|
+
if (pinnedMcpProjectRoot !== null && pinnedMcpProjectRoot !== normalized) {
|
|
121
|
+
throw new Error(`Borg MCP session project root is already pinned to ${pinnedMcpProjectRoot}`);
|
|
122
|
+
}
|
|
123
|
+
pinnedMcpProjectRoot = normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function pinMcpSeatIdentity(active: ActiveCube): void {
|
|
127
|
+
if (!active.worktree) throw new McpSeatIdentityChangedError();
|
|
128
|
+
pinMcpProjectRoot(active.worktree);
|
|
129
|
+
const next = {
|
|
130
|
+
worktree: resolve(active.worktree),
|
|
131
|
+
cubeId: active.cubeId,
|
|
132
|
+
droneId: active.droneId,
|
|
133
|
+
...(active.localSessionCredentialRef
|
|
134
|
+
? { credentialRef: active.localSessionCredentialRef }
|
|
135
|
+
: {}),
|
|
136
|
+
};
|
|
137
|
+
if (pinnedMcpSeatIdentity && (
|
|
138
|
+
pinnedMcpSeatIdentity.worktree !== next.worktree ||
|
|
139
|
+
pinnedMcpSeatIdentity.cubeId !== next.cubeId ||
|
|
140
|
+
pinnedMcpSeatIdentity.droneId !== next.droneId ||
|
|
141
|
+
pinnedMcpSeatIdentity.credentialRef !== next.credentialRef
|
|
142
|
+
)) {
|
|
143
|
+
throw new McpSeatIdentityChangedError();
|
|
144
|
+
}
|
|
145
|
+
pinnedMcpSeatIdentity = next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function findProjectRoot(cwd: string = pinnedMcpProjectRoot ?? process.cwd()): string {
|
|
101
149
|
let dir = resolve(cwd);
|
|
102
150
|
while (true) {
|
|
103
151
|
if (existsSync(join(dir, '.git'))) return dir;
|
|
@@ -244,9 +292,22 @@ async function writeCodexWakeTargetsFile(data: CodexWakeTargetsFile): Promise<vo
|
|
|
244
292
|
* refresh.
|
|
245
293
|
*/
|
|
246
294
|
export async function getActiveCube(): Promise<ActiveCube | null> {
|
|
247
|
-
|
|
295
|
+
return getActiveCubeForWorktree(findProjectRoot());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function getActiveCubeForWorktree(worktree: string): Promise<ActiveCube | null> {
|
|
299
|
+
const record = await getActiveSeatForWorktree(findProjectRoot(worktree));
|
|
248
300
|
if (!record || !record.cubeId || !record.droneId) return null;
|
|
249
|
-
|
|
301
|
+
const active = await hydrateActiveCube(record);
|
|
302
|
+
if (active && pinnedMcpSeatIdentity && (
|
|
303
|
+
resolve(active.worktree ?? '') !== pinnedMcpSeatIdentity.worktree ||
|
|
304
|
+
active.cubeId !== pinnedMcpSeatIdentity.cubeId ||
|
|
305
|
+
active.droneId !== pinnedMcpSeatIdentity.droneId ||
|
|
306
|
+
active.localSessionCredentialRef !== pinnedMcpSeatIdentity.credentialRef
|
|
307
|
+
)) {
|
|
308
|
+
throw new McpSeatIdentityChangedError();
|
|
309
|
+
}
|
|
310
|
+
return active;
|
|
250
311
|
}
|
|
251
312
|
|
|
252
313
|
/**
|
|
@@ -282,12 +343,18 @@ async function hydrateActiveCube(record: SeatRecord): Promise<ActiveCube | null>
|
|
|
282
343
|
serverTrustIdentity: record.trustIdentity,
|
|
283
344
|
localSessionCredentialRef: ref,
|
|
284
345
|
operation: record.operation,
|
|
346
|
+
worktree: record.worktree,
|
|
285
347
|
...(record.roleName !== undefined ? { roleName: record.roleName } : {}),
|
|
286
348
|
...(record.roleClass !== undefined ? { roleClass: record.roleClass } : {}),
|
|
287
349
|
...(record.isHumanSeat !== undefined ? { isHumanSeat: record.isHumanSeat } : {}),
|
|
288
350
|
};
|
|
289
351
|
}
|
|
290
352
|
|
|
353
|
+
export function __resetPinnedMcpProjectRootForTests(): void {
|
|
354
|
+
pinnedMcpProjectRoot = null;
|
|
355
|
+
pinnedMcpSeatIdentity = null;
|
|
356
|
+
}
|
|
357
|
+
|
|
291
358
|
/**
|
|
292
359
|
* Token-free lookup used after an offline reset. A surviving seat is only
|
|
293
360
|
* described as saved local state; the caller must still revalidate it with the
|
package/src/index.ts
CHANGED
|
@@ -64,9 +64,11 @@ import {
|
|
|
64
64
|
import {
|
|
65
65
|
activeCubeWithFreshRegenIdentity,
|
|
66
66
|
getActiveCube,
|
|
67
|
+
getActiveCubeForWorktree,
|
|
67
68
|
refreshActiveCubeMetadata,
|
|
68
69
|
findProjectRoot,
|
|
69
70
|
inboxPathForDrone,
|
|
71
|
+
pinMcpSeatIdentity,
|
|
70
72
|
} from './cubes.js';
|
|
71
73
|
import { isEntryInvocation, monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
72
74
|
import { addSessionStartHook, addUserPromptSubmitHook } from './config-utils.js';
|
|
@@ -140,6 +142,11 @@ import {
|
|
|
140
142
|
normalizeDirectLogRecipients,
|
|
141
143
|
} from './direct-log.js';
|
|
142
144
|
import { formatLocalManageToolResult } from './local-manage-tool-result.js';
|
|
145
|
+
import {
|
|
146
|
+
OpenCodeSeatIdentityError,
|
|
147
|
+
formatOpenCodeSeatIdentityError,
|
|
148
|
+
resolveOpenCodeSeatIdentity,
|
|
149
|
+
} from './opencode-seat-identity.js';
|
|
143
150
|
import {
|
|
144
151
|
runEvictDroneTool,
|
|
145
152
|
runReassignDroneTool,
|
|
@@ -216,8 +223,8 @@ export async function main() {
|
|
|
216
223
|
// installed client version.
|
|
217
224
|
handleVersionFlag();
|
|
218
225
|
const readinessProbe = isMcpReadinessProbe();
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
const openCodeRuntime = resolveSessionAgentKind() === 'opencode';
|
|
227
|
+
const startupServices = {
|
|
221
228
|
// Auto-register the SessionStart hook so existing users get borg-regen
|
|
222
229
|
// auto-orientation on session start without re-running borg setup. Idempotent.
|
|
223
230
|
sessionStartHook: () => {
|
|
@@ -255,19 +262,22 @@ export async function main() {
|
|
|
255
262
|
openCode: async () => {
|
|
256
263
|
installBorgPlugin();
|
|
257
264
|
const active = await getActiveCube();
|
|
258
|
-
if (active &&
|
|
265
|
+
if (active && openCodeRuntime) {
|
|
259
266
|
const port = computeOpenCodePort(active.droneId);
|
|
260
267
|
const serverUrl = `http://127.0.0.1:${port}`;
|
|
261
268
|
await connectOpenCodeDrone({
|
|
262
269
|
serverUrl,
|
|
263
|
-
directory:
|
|
270
|
+
directory: active.worktree ?? findProjectRoot(),
|
|
264
271
|
droneLabel: active.droneLabel,
|
|
265
272
|
cubeName: active.name,
|
|
266
273
|
});
|
|
267
274
|
setModuleInjectOpenCode(injectOpenCodeEntry);
|
|
268
275
|
}
|
|
269
276
|
},
|
|
270
|
-
}
|
|
277
|
+
};
|
|
278
|
+
// Claude and Codex retain their existing pre-handshake startup behavior.
|
|
279
|
+
// OpenCode must first obtain the session-scoped MCP root from its client.
|
|
280
|
+
if (!openCodeRuntime) await runMcpStartupServices(readinessProbe, startupServices);
|
|
271
281
|
|
|
272
282
|
// Create MCP server. `version` is the installed borgmcp version
|
|
273
283
|
// (T1.4 of 0.6.0): read at runtime from package.json so Claude
|
|
@@ -286,6 +296,16 @@ export async function main() {
|
|
|
286
296
|
}
|
|
287
297
|
);
|
|
288
298
|
|
|
299
|
+
let openCodeIdentityFailure: OpenCodeSeatIdentityError | null = null;
|
|
300
|
+
let finishOpenCodeIdentity: (() => void) | null = null;
|
|
301
|
+
const openCodeIdentityReady = openCodeRuntime && !readinessProbe
|
|
302
|
+
? new Promise<void>((resolveIdentity) => { finishOpenCodeIdentity = resolveIdentity; })
|
|
303
|
+
: null;
|
|
304
|
+
const waitForOpenCodeIdentity = async (): Promise<OpenCodeSeatIdentityError | null> => {
|
|
305
|
+
if (openCodeIdentityReady) await openCodeIdentityReady;
|
|
306
|
+
return openCodeIdentityFailure;
|
|
307
|
+
};
|
|
308
|
+
|
|
289
309
|
// gh#899: tool definitions built once at setup, then role-scoped per caller
|
|
290
310
|
// in the ListTools handler below (the dispatcher reaches deferred tools).
|
|
291
311
|
const allToolDefs: ToolManifestEntry[] = TOOL_MANIFEST;
|
|
@@ -294,6 +314,7 @@ export async function main() {
|
|
|
294
314
|
// (old cubes.json / pre-assimilate) → full set; deferred tools stay reachable
|
|
295
315
|
// via borg_tool. Never an auth boundary — live per-client cube grants govern.
|
|
296
316
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
317
|
+
await waitForOpenCodeIdentity();
|
|
297
318
|
let scope: { roleName?: string; roleClass?: 'queen' | 'worker'; isHumanSeat?: boolean } | null = null;
|
|
298
319
|
try {
|
|
299
320
|
const active = await getActiveCube();
|
|
@@ -310,6 +331,17 @@ export async function main() {
|
|
|
310
331
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
311
332
|
let { name, arguments: args } = request.params;
|
|
312
333
|
|
|
334
|
+
const identityFailure = await waitForOpenCodeIdentity();
|
|
335
|
+
if (identityFailure) {
|
|
336
|
+
return {
|
|
337
|
+
content: [{
|
|
338
|
+
type: 'text',
|
|
339
|
+
text: formatOpenCodeSeatIdentityError(identityFailure, process.cwd()),
|
|
340
|
+
}],
|
|
341
|
+
isError: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
313
345
|
// gh#899: borg_describe-tool — schema-only, NEVER executes. Returns the
|
|
314
346
|
// named tool's def from allToolDefs so a role-scoped session can learn a
|
|
315
347
|
// deferred tool's arguments before invoking it via borg_tool.
|
|
@@ -386,7 +418,7 @@ export async function main() {
|
|
|
386
418
|
since,
|
|
387
419
|
reportedModel,
|
|
388
420
|
agentKind: resolveReportableSessionAgentKind(),
|
|
389
|
-
workingRepo: resolveWorkingRepo(),
|
|
421
|
+
workingRepo: resolveWorkingRepo(active.worktree),
|
|
390
422
|
serverTrustIdentity: active.serverTrustIdentity,
|
|
391
423
|
});
|
|
392
424
|
} catch (error) {
|
|
@@ -467,7 +499,7 @@ export async function main() {
|
|
|
467
499
|
seedDisplayIdentity(active!);
|
|
468
500
|
const result = await regen(active!.sessionToken, active!.apiUrl, {
|
|
469
501
|
agentKind: resolveReportableSessionAgentKind(),
|
|
470
|
-
workingRepo: resolveWorkingRepo(),
|
|
502
|
+
workingRepo: resolveWorkingRepo(active!.worktree),
|
|
471
503
|
serverTrustIdentity: active!.serverTrustIdentity,
|
|
472
504
|
});
|
|
473
505
|
const displayIdentity = confirmDisplayIdentity(active!, identityFromRegen(result));
|
|
@@ -1259,9 +1291,36 @@ export async function main() {
|
|
|
1259
1291
|
// Create stdio transport
|
|
1260
1292
|
const transport = new StdioServerTransport();
|
|
1261
1293
|
|
|
1294
|
+
if (openCodeIdentityReady) {
|
|
1295
|
+
server.oninitialized = () => {
|
|
1296
|
+
void resolveOpenCodeSeatIdentity({
|
|
1297
|
+
listRoots: () => server.listRoots(),
|
|
1298
|
+
findProjectRoot,
|
|
1299
|
+
getActiveCubeForWorktree,
|
|
1300
|
+
pinSeatIdentity: pinMcpSeatIdentity,
|
|
1301
|
+
childCwd: process.cwd(),
|
|
1302
|
+
}).catch((error) => {
|
|
1303
|
+
openCodeIdentityFailure = error instanceof OpenCodeSeatIdentityError
|
|
1304
|
+
? error
|
|
1305
|
+
: new OpenCodeSeatIdentityError('ROOTS_UNAVAILABLE', String(error));
|
|
1306
|
+
}).finally(() => {
|
|
1307
|
+
finishOpenCodeIdentity?.();
|
|
1308
|
+
});
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1262
1312
|
// Connect server to transport
|
|
1263
1313
|
await server.connect(transport);
|
|
1264
1314
|
|
|
1315
|
+
if (openCodeIdentityReady) {
|
|
1316
|
+
await openCodeIdentityReady;
|
|
1317
|
+
if (openCodeIdentityFailure) {
|
|
1318
|
+
console.error(formatOpenCodeSeatIdentityError(openCodeIdentityFailure, process.cwd()));
|
|
1319
|
+
} else {
|
|
1320
|
+
await runMcpStartupServices(false, startupServices, { openCodeFirst: true });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1265
1324
|
// Resolve drone self-identification prefix before any console output
|
|
1266
1325
|
// (gh#25). Falls back to `[unassimilated · <repo>]` if no cube cached.
|
|
1267
1326
|
await initConsolePrefix();
|
package/src/log-stream.ts
CHANGED
|
@@ -496,7 +496,12 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
|
|
|
496
496
|
active.cubeId,
|
|
497
497
|
active.droneId,
|
|
498
498
|
STREAM_OWNER_STALE_MS,
|
|
499
|
-
{
|
|
499
|
+
{
|
|
500
|
+
isPidAlive: isProcessAlive,
|
|
501
|
+
...(active.worktree ? { worktree: active.worktree } : {}),
|
|
502
|
+
...(active.droneLabel ? { droneLabel: active.droneLabel } : {}),
|
|
503
|
+
...(active.name ? { cubeName: active.name } : {}),
|
|
504
|
+
},
|
|
500
505
|
);
|
|
501
506
|
leaseKey = lease ? nextLeaseKey : null;
|
|
502
507
|
}
|
|
@@ -642,6 +647,9 @@ export interface ActiveCube {
|
|
|
642
647
|
serverTrustIdentity?: string;
|
|
643
648
|
localSessionCredentialRef?: string;
|
|
644
649
|
localSessionExpiresAt?: string | null;
|
|
650
|
+
worktree?: string;
|
|
651
|
+
droneLabel?: string;
|
|
652
|
+
name?: string;
|
|
645
653
|
}
|
|
646
654
|
|
|
647
655
|
export async function streamOnce(
|