borgmcp 2.16.0 → 3.0.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.
- package/README.md +38 -12
- package/dist/assimilate-cmd.js +43 -43
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.js +2 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/assimilate-guard.js +4 -4
- package/dist/assimilate-guard.js.map +1 -1
- package/dist/assimilate-welcome.js +2 -2
- package/dist/assimilate-welcome.js.map +1 -1
- package/dist/claude.d.ts +0 -2
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +2 -15
- package/dist/claude.js.map +1 -1
- package/dist/cleanup-cmd.d.ts +1 -1
- package/dist/cleanup-cmd.d.ts.map +1 -1
- package/dist/cleanup-cmd.js +3 -3
- package/dist/cleanup-cmd.js.map +1 -1
- package/dist/cli-help.d.ts +1 -2
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +15 -24
- package/dist/cli-help.js.map +1 -1
- package/dist/cubes.js +2 -2
- package/dist/cubes.js.map +1 -1
- package/dist/docs-sections.js +1 -1
- package/dist/docs-sections.js.map +1 -1
- package/dist/drone-lifecycle.d.ts.map +1 -1
- package/dist/drone-lifecycle.js +3 -3
- package/dist/drone-lifecycle.js.map +1 -1
- package/dist/drone-management.js +1 -1
- package/dist/drone-management.js.map +1 -1
- package/dist/evict-drone.js +2 -2
- package/dist/evict-drone.js.map +1 -1
- package/dist/foreign-path-reminder.js +1 -1
- package/dist/foreign-path-reminder.js.map +1 -1
- package/dist/inbox-monitor.js +2 -2
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.js +12 -12
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/launch-all-discovery.js +1 -1
- package/dist/launch-all-discovery.js.map +1 -1
- package/dist/opencode-seat-identity.js +5 -5
- package/dist/opencode-seat-identity.js.map +1 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +8 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.js +1 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.js +1 -1
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/reset-local-seat-cmd.d.ts +2 -2
- package/dist/reset-local-seat-cmd.d.ts.map +1 -1
- package/dist/reset-local-seat-cmd.js +17 -17
- package/dist/reset-local-seat-cmd.js.map +1 -1
- package/dist/seat-reattach-guard.js +1 -1
- package/dist/seat-reattach-guard.js.map +1 -1
- package/dist/seat-store.d.ts +1 -1
- package/dist/seat-store.js +6 -6
- package/dist/seat-store.js.map +1 -1
- package/dist/seats.js +1 -1
- package/dist/seats.js.map +1 -1
- package/dist/server-handshake.js +1 -1
- package/dist/server-handshake.js.map +1 -1
- package/dist/stream-status.js +3 -3
- package/dist/stream-status.js.map +1 -1
- package/dist/tool-manifest.js +1 -1
- package/dist/tool-manifest.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -2
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/worktree-lifecycle.d.ts +0 -16
- package/dist/worktree-lifecycle.d.ts.map +1 -1
- package/dist/worktree-lifecycle.js +0 -38
- package/dist/worktree-lifecycle.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +23 -10
- package/docs/LOCAL_SERVER.md +2 -2
- package/docs/SEAT_LIFECYCLE.md +19 -19
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +43 -43
- package/src/assimilate-deps.ts +2 -2
- package/src/assimilate-guard.ts +4 -4
- package/src/assimilate-welcome.ts +2 -2
- package/src/claude.ts +2 -17
- package/src/cleanup-cmd.ts +4 -4
- package/src/cli-help.ts +15 -27
- package/src/cubes.ts +2 -2
- package/src/docs-sections.ts +1 -1
- package/src/drone-lifecycle.ts +3 -3
- package/src/drone-management.ts +1 -1
- package/src/evict-drone.ts +2 -2
- package/src/foreign-path-reminder.ts +1 -1
- package/src/inbox-monitor.ts +2 -2
- package/src/index.ts +1 -1
- package/src/launch-all-cmd.ts +12 -12
- package/src/launch-all-discovery.ts +1 -1
- package/src/opencode-seat-identity.ts +5 -5
- package/src/regen-format.ts +8 -1
- package/src/remote-client.ts +1 -1
- package/src/repository-cube-init.ts +1 -1
- package/src/reset-local-seat-cmd.ts +17 -17
- package/src/seat-reattach-guard.ts +1 -1
- package/src/seat-store.ts +6 -6
- package/src/seats.ts +1 -1
- package/src/server-handshake.ts +1 -1
- package/src/stream-status.ts +3 -3
- package/src/tool-manifest.ts +1 -1
- package/src/unknown-subcommand.ts +1 -2
- package/src/worktree-lifecycle.ts +0 -45
- package/dist/sync.d.ts +0 -129
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -286
- package/dist/sync.js.map +0 -1
- package/src/sync.ts +0 -363
package/src/cubes.ts
CHANGED
|
@@ -110,7 +110,7 @@ let pinnedMcpSeatIdentity: {
|
|
|
110
110
|
export class McpSeatIdentityChangedError extends Error {
|
|
111
111
|
readonly code = 'SEAT_IDENTITY_CHANGED';
|
|
112
112
|
constructor() {
|
|
113
|
-
super('
|
|
113
|
+
super('This worktree\'s saved connection changed after this MCP session pinned its identity. Exit this session and relaunch from the intended worktree.');
|
|
114
114
|
this.name = 'McpSeatIdentityChangedError';
|
|
115
115
|
}
|
|
116
116
|
}
|
|
@@ -439,7 +439,7 @@ export async function snapshotLocalSeat(): Promise<LocalSeatSnapshot | null> {
|
|
|
439
439
|
// CR#4: discover an ACTIVE seat OR a bound-PENDING record (a sibling whose
|
|
440
440
|
// activation failed, bound to THIS worktree by the attach bind-pending step).
|
|
441
441
|
// getActiveSeatForWorktree would MISS the bound-pending record (it requires
|
|
442
|
-
// state==='active' + a drone id), so `reset-local-
|
|
442
|
+
// state==='active' + a drone id), so `reset-local-connection` would FALSELY report
|
|
443
443
|
// "nothing to reset" (exit 0) while a resumable, server-digest-bound bearer
|
|
444
444
|
// persists at rest — a FALSE-SUCCESS revocation failure. getSeatForWorktree sees
|
|
445
445
|
// both, and the offline reset's exact re-check + delete cover the bound-pending
|
package/src/docs-sections.ts
CHANGED
|
@@ -78,7 +78,7 @@ export const DOCS_SECTIONS: DocsSection[] = [
|
|
|
78
78
|
url: SEAT_LIFECYCLE_URL,
|
|
79
79
|
page: "docs/SEAT_LIFECYCLE.md",
|
|
80
80
|
summary: "Saved-seat states, re-attach and reset recovery, duplicate-session guards, and deterministic multi-seat selection.",
|
|
81
|
-
keywords: ["seat", "lifecycle", "reattach", "re-attach", "reset-local-
|
|
81
|
+
keywords: ["seat", "lifecycle", "reattach", "re-attach", "reset-local-connection", "evicted", "revoked", "superseded", "inbox monitor", "multiple seats", "silent deafness"],
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
slug: "self-hosting",
|
package/src/drone-lifecycle.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const CUBE_DELETED_CODE = 'CUBE_DELETED';
|
|
|
16
16
|
|
|
17
17
|
export class DroneEvictedError extends Error {
|
|
18
18
|
constructor(
|
|
19
|
-
message = 'This
|
|
19
|
+
message = 'This drone was removed from the cube.'
|
|
20
20
|
) {
|
|
21
21
|
super(message);
|
|
22
22
|
this.name = 'DroneEvictedError';
|
|
@@ -45,8 +45,8 @@ export const CUBE_DELETED_RESULT_MARKER = '[CUBE-DELETED]';
|
|
|
45
45
|
export function formatEvictedToolResult(cubeName?: string): string {
|
|
46
46
|
const cube = cubeName ?? 'the selected cube';
|
|
47
47
|
return (
|
|
48
|
-
`${EVICTED_RESULT_MARKER} This
|
|
49
|
-
'Borg has stopped listening for activity for this
|
|
48
|
+
`${EVICTED_RESULT_MARKER} This drone was removed from cube ${cube}.\n\n` +
|
|
49
|
+
'Borg has stopped listening for activity for this drone. Do not retry this request or restart the loop.\n\n' +
|
|
50
50
|
'Your worktree and project files are unchanged. Finish any local file safety checks, then end this agent session.\n\n' +
|
|
51
51
|
'To rejoin later, start a new session and use a new invitation from the server operator. Do not re-assimilate from this evicted session.'
|
|
52
52
|
);
|
package/src/drone-management.ts
CHANGED
|
@@ -50,7 +50,7 @@ const defaultDeps: DroneManagementDeps = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export const STALE_ROLE_DISPLAY_WARNING =
|
|
53
|
-
'Local display warning: The server committed this change, but Borg could not refresh this worktree\'s saved
|
|
53
|
+
'Local display warning: The server committed this change, but Borg could not refresh this worktree\'s saved connection metadata. Local role details may be stale. Do not retry the reassignment.\n\n' +
|
|
54
54
|
'Run `borg_regen` to refresh the server-authoritative role. If this session still shows the previous role, restart this agent session once. Do not re-assimilate or repeat the management request.';
|
|
55
55
|
|
|
56
56
|
function opaqueNotFound(): never {
|
package/src/evict-drone.ts
CHANGED
|
@@ -75,8 +75,8 @@ export function formatReassignDroneSuccess(input: {
|
|
|
75
75
|
export function formatEvictDroneSuccess(droneLabel: string, cubeName: string): string {
|
|
76
76
|
return (
|
|
77
77
|
`Removed ${droneLabel} from cube ${cubeName}.\n` +
|
|
78
|
-
'The
|
|
79
|
-
'The worktree and project files were not deleted. Activity history remains attributed to the removed
|
|
78
|
+
'The drone\'s credential is revoked. The session will stop after its next Borg request.\n' +
|
|
79
|
+
'The worktree and project files were not deleted. Activity history remains attributed to the removed drone.\n' +
|
|
80
80
|
'After its work is merged, run `borg cleanup` to review whether the worktree can be pruned.'
|
|
81
81
|
);
|
|
82
82
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
BORG_LAUNCH_WORKTREE_ENV,
|
|
18
18
|
} from './launch-access.js';
|
|
19
19
|
|
|
20
|
-
const REMINDER = 'Reminder: this
|
|
20
|
+
const REMINDER = 'Reminder: this drone is scoped to its own worktree and scratch root; coordinate before working on a foreign path.';
|
|
21
21
|
|
|
22
22
|
const PATH_KEYS = new Set([
|
|
23
23
|
'cwd',
|
package/src/inbox-monitor.ts
CHANGED
|
@@ -797,8 +797,8 @@ function liveMonitorYieldLine(
|
|
|
797
797
|
: null;
|
|
798
798
|
const holder = livePid === null ? 'another monitor instance' : `a live instance (pid ${livePid})`;
|
|
799
799
|
return (
|
|
800
|
-
`borg-inbox-monitor:
|
|
801
|
-
'yielding — another session likely holds this
|
|
800
|
+
`borg-inbox-monitor: inbox ${JSON.stringify(inboxPath)} is already monitored by ${holder}; ` +
|
|
801
|
+
'yielding — another session likely holds this worktree\'s connection.'
|
|
802
802
|
);
|
|
803
803
|
}
|
|
804
804
|
|
package/src/index.ts
CHANGED
|
@@ -561,7 +561,7 @@ export async function main() {
|
|
|
561
561
|
`# Re-attached to cube: ${displayIdentity.cubeName}`,
|
|
562
562
|
``,
|
|
563
563
|
`**Drone label:** ${displayIdentity.droneLabel}`,
|
|
564
|
-
`**
|
|
564
|
+
`**Drone:** existing identity reused — no new drone minted (gh#780)`,
|
|
565
565
|
``,
|
|
566
566
|
``,
|
|
567
567
|
].join('\n');
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -53,7 +53,7 @@ async function resolveTargetCube(
|
|
|
53
53
|
const identities = await deps.readAllProjectIdentities();
|
|
54
54
|
const matches = identities.filter((e) => e.cube.name === args.cubeName);
|
|
55
55
|
if (matches.length === 0) {
|
|
56
|
-
return { error: `no cube named '${args.cubeName}' found among this machine's saved
|
|
56
|
+
return { error: `no cube named '${args.cubeName}' found among this machine's saved connections — has any drone assimilated into it?` };
|
|
57
57
|
}
|
|
58
58
|
// gh#850: distinct cubes can share a name (same name across accounts/
|
|
59
59
|
// environments, or a stale seat). Silently taking matches[0] could launch
|
|
@@ -61,13 +61,13 @@ async function resolveTargetCube(
|
|
|
61
61
|
// cubeId + the project that holds the seat and refuse to guess.
|
|
62
62
|
if (matches.length > 1) {
|
|
63
63
|
const list = matches
|
|
64
|
-
.map((m) => ` ${m.cube.cubeId} (
|
|
64
|
+
.map((m) => ` ${m.cube.cubeId} (drone in ${m.projectPath})`)
|
|
65
65
|
.join('\n');
|
|
66
66
|
return {
|
|
67
67
|
error:
|
|
68
|
-
`'${args.cubeName}' is ambiguous — ${matches.length} saved
|
|
68
|
+
`'${args.cubeName}' is ambiguous — ${matches.length} saved connections on this machine share that name:\n${list}\n` +
|
|
69
69
|
'cd into the intended project and re-run without --cube-name (resolves the active cube), ' +
|
|
70
|
-
'or clear the stale
|
|
70
|
+
'or clear the stale connection(s) by running `borg reset-local-connection` from the worktree that holds each.',
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
return { cubeId: matches[0].cube.cubeId, name: args.cubeName };
|
|
@@ -237,7 +237,7 @@ export async function runLaunchAll(
|
|
|
237
237
|
}
|
|
238
238
|
} else {
|
|
239
239
|
deps.stdout(
|
|
240
|
-
`No worktrees found for cube '${cubeName}' — have you run \`borg assimilate --worktree\` to create any
|
|
240
|
+
`No worktrees found for cube '${cubeName}' — have you run \`borg assimilate --worktree\` to create any drones?\n`
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
243
|
return 0;
|
|
@@ -290,8 +290,8 @@ export async function runLaunchAll(
|
|
|
290
290
|
if (status === 'evicted') {
|
|
291
291
|
evictedCount += 1;
|
|
292
292
|
deps.stderr(
|
|
293
|
-
`skipping ${c.droneLabel} (${c.worktreeDir}):
|
|
294
|
-
`run \`borg cleanup --prune\` to remove the worktree, or \`borg assimilate\` to
|
|
293
|
+
`skipping ${c.droneLabel} (${c.worktreeDir}): drone no longer in cube (evicted) — ` +
|
|
294
|
+
`run \`borg cleanup --prune\` to remove the worktree, or \`borg assimilate\` to start fresh.\n`
|
|
295
295
|
);
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
@@ -299,7 +299,7 @@ export async function runLaunchAll(
|
|
|
299
299
|
revokedCount += 1;
|
|
300
300
|
deps.stderr(
|
|
301
301
|
`Local session was revoked.\n` +
|
|
302
|
-
`Next: run borg reset-local-
|
|
302
|
+
`Next: run borg reset-local-connection, then borg assimilate --host ${c.apiUrl} --enroll.\n`
|
|
303
303
|
);
|
|
304
304
|
continue;
|
|
305
305
|
}
|
|
@@ -307,7 +307,7 @@ export async function runLaunchAll(
|
|
|
307
307
|
rejectedCount += 1;
|
|
308
308
|
deps.stderr(
|
|
309
309
|
`Local session was superseded by a newer enrollment.\n` +
|
|
310
|
-
`Next: run borg reset-local-
|
|
310
|
+
`Next: run borg reset-local-connection, then borg assimilate --host ${c.apiUrl} --enroll.\n`
|
|
311
311
|
);
|
|
312
312
|
continue;
|
|
313
313
|
}
|
|
@@ -336,7 +336,7 @@ export async function runLaunchAll(
|
|
|
336
336
|
// cause-accurate note. Only the authoritative/terminal causes above skip.
|
|
337
337
|
if (status === 'unreachable') {
|
|
338
338
|
deps.stderr(
|
|
339
|
-
`note: could not reach ${c.droneLabel}'s server to confirm
|
|
339
|
+
`note: could not reach ${c.droneLabel}'s server to confirm that drone (network/timeout) — launching anyway.\n`
|
|
340
340
|
);
|
|
341
341
|
} else if (status === 'endpoint-mismatch') {
|
|
342
342
|
deps.stderr(
|
|
@@ -344,11 +344,11 @@ export async function runLaunchAll(
|
|
|
344
344
|
);
|
|
345
345
|
} else if (status === 'server-failure') {
|
|
346
346
|
deps.stderr(
|
|
347
|
-
`note: ${c.droneLabel}'s server returned an error while confirming
|
|
347
|
+
`note: ${c.droneLabel}'s server returned an error while confirming that drone (transient) — launching anyway.\n`
|
|
348
348
|
);
|
|
349
349
|
} else if (status === 'indeterminate') {
|
|
350
350
|
deps.stderr(
|
|
351
|
-
`note: could not confirm ${c.droneLabel}
|
|
351
|
+
`note: could not confirm ${c.droneLabel} is live (network/transient) — launching anyway.\n`
|
|
352
352
|
);
|
|
353
353
|
}
|
|
354
354
|
launchable.push(c);
|
|
@@ -95,7 +95,7 @@ export async function discoverDroneCandidates(
|
|
|
95
95
|
// 4. malformed entry (cubeId/droneId not UUID) → warn + skip
|
|
96
96
|
if (!isUuid(cube.cubeId) || !isUuid(cube.droneId)) {
|
|
97
97
|
deps.stderr(
|
|
98
|
-
`skipping ${worktreeDir}: its saved
|
|
98
|
+
`skipping ${worktreeDir}: its saved connection has a malformed cubeId/droneId — re-assimilate to fix\n`
|
|
99
99
|
);
|
|
100
100
|
continue;
|
|
101
101
|
}
|
|
@@ -71,14 +71,14 @@ export async function resolveOpenCodeSeatIdentity(
|
|
|
71
71
|
if (!active) {
|
|
72
72
|
throw new OpenCodeSeatIdentityError(
|
|
73
73
|
'SEAT_NOT_FOUND',
|
|
74
|
-
'No
|
|
74
|
+
'No Borg drone is bound to the OpenCode session directory.',
|
|
75
75
|
sessionWorktree,
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
if (typeof active.worktree !== 'string' || resolve(active.worktree) !== resolve(sessionWorktree)) {
|
|
79
79
|
throw new OpenCodeSeatIdentityError(
|
|
80
80
|
'SEAT_WORKTREE_MISMATCH',
|
|
81
|
-
'The resolved Borg
|
|
81
|
+
'The resolved Borg drone belongs to a different worktree than the OpenCode session.',
|
|
82
82
|
sessionWorktree,
|
|
83
83
|
active,
|
|
84
84
|
);
|
|
@@ -93,19 +93,19 @@ export function formatOpenCodeSeatIdentityError(
|
|
|
93
93
|
childCwd: string,
|
|
94
94
|
): string {
|
|
95
95
|
const lines = [
|
|
96
|
-
`Borg OpenCode
|
|
96
|
+
`Borg OpenCode identity error [${error.code}]`,
|
|
97
97
|
'',
|
|
98
98
|
error.message,
|
|
99
99
|
`- OpenCode session directory: ${error.sessionDirectory ?? 'unavailable'}`,
|
|
100
100
|
`- Borg MCP child cwd: ${childCwd}`,
|
|
101
101
|
];
|
|
102
102
|
if (error.seat) {
|
|
103
|
-
lines.push(`- Resolved
|
|
103
|
+
lines.push(`- Resolved drone: ${error.seat.droneLabel} (${error.seat.worktree})`);
|
|
104
104
|
}
|
|
105
105
|
lines.push(
|
|
106
106
|
'',
|
|
107
107
|
'The Borg stream and OpenCode wake injection were not started.',
|
|
108
|
-
'Exit this session and run `borg --cli opencode` from the intended worktree. If that worktree
|
|
108
|
+
'Exit this session and run `borg --cli opencode` from the intended worktree. If that worktree\'s saved connection is stale, run `borg reset-local-connection` from that exact worktree before assimilating again.',
|
|
109
109
|
);
|
|
110
110
|
return lines.join('\n');
|
|
111
111
|
}
|
package/src/regen-format.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* index.ts's stdio MCP server bootstrap.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { hostname as osHostname } from 'node:os';
|
|
9
10
|
import {
|
|
10
11
|
ROLE_SCOPED_SAFETY_DISCIPLINES,
|
|
11
12
|
UNIVERSAL_SAFETY_DISCIPLINES,
|
|
@@ -243,7 +244,7 @@ export function markArrivalAnnouncedThisProcess(): void {
|
|
|
243
244
|
export function getDronePlaybook(): string {
|
|
244
245
|
const arrivalInstruction = arrivalAnnouncedThisProcess
|
|
245
246
|
? ''
|
|
246
|
-
:
|
|
247
|
+
: `\n**When this MCP session first starts:** post one \`ARRIVAL: <your-label> (<your-role>) online on ${osHostname()}\`. After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit \`/mcp\` reconnect may show it again.\n`;
|
|
247
248
|
return `## How to operate as a Drone
|
|
248
249
|
|
|
249
250
|
You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
@@ -261,6 +262,12 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
|
261
262
|
|
|
262
263
|
**How coordination works:** the Cube gives primitives, not workflows. Your role's \`detailed_description\` (above) is your playbook — its conventions + signals come from there, not the system. The log is the coordination channel. Different cubes, different conventions.
|
|
263
264
|
|
|
265
|
+
**Communication discipline for non-human seats:**
|
|
266
|
+
- **Console:** write nothing except harness-required output. Surface something to the operator only when blocked and needing unblocking; do not narrate plans, progress, method, or results.
|
|
267
|
+
- **Log:** a post must change what another seat does. Otherwise, do not write it. Keep posts short: lifecycle signal + SHA and nothing else; defect + location/evidence; correction to your live claim; or a genuine blocking question.
|
|
268
|
+
- **Do not post:** plans, work-in-progress/progress narration, method or reasoning, restatements/agreement/credit, self-examination, framing phrases, or coordination commentary.
|
|
269
|
+
- **Evidence boundary:** state what a verdict did not exercise and any unavailable control in the same short clause. The human seat is excluded so its dispatches can explain constraints without being misapplied.
|
|
270
|
+
|
|
264
271
|
**Default: act autonomously, coordinate through the log.** Don't wait for user input. Need input → post the question, continue other work, other drones respond. The human supervisor is reachable through your cube's coordinating / human-seat role (the role your cube designates for direction + integration), or the Queen role when the seat is delegated to a drone — one continuous seat. Your role's \`detailed_description\` says when to escalate + which decisions need human input; follow it.
|
|
265
272
|
|
|
266
273
|
**Operating loop — each wake, in order:**
|
package/src/remote-client.ts
CHANGED
|
@@ -474,7 +474,7 @@ async function localCubeComposition(active: ActiveCube): Promise<{
|
|
|
474
474
|
const drones = dronePayload.drones.map(withValidatedRuntimeMetadata);
|
|
475
475
|
const drone = drones.find((candidate) => candidate.id === active.droneId);
|
|
476
476
|
const role = rolePayload.roles.find((candidate) => candidate.id === drone?.role_id);
|
|
477
|
-
if (!drone || !role) throw new Error('Local Borg server no longer recognizes this drone
|
|
477
|
+
if (!drone || !role) throw new Error('Local Borg server no longer recognizes this drone');
|
|
478
478
|
return {
|
|
479
479
|
cube: cubePayload.cube,
|
|
480
480
|
roles: rolePayload.roles,
|
|
@@ -169,7 +169,7 @@ function renderResult(
|
|
|
169
169
|
`Next: borg assimilate --host ${shellEscape(input.serverOrigin)}`,
|
|
170
170
|
);
|
|
171
171
|
} else {
|
|
172
|
-
lines.push('Continuing with role and
|
|
172
|
+
lines.push('Continuing with role and connection setup…');
|
|
173
173
|
}
|
|
174
174
|
resultWriter(`${lines.join('\n')}\n`);
|
|
175
175
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `borg reset-local-
|
|
2
|
+
* `borg reset-local-connection [--host <host>] [--yes]` — the dedicated LOCAL/OFFLINE
|
|
3
3
|
* seat reset from the ratified client-seat-reset-state-model (Option W).
|
|
4
4
|
*
|
|
5
5
|
* Attach is PURE DIAGNOSIS on a pin-matched SESSION_REJECTED (it mutates
|
|
@@ -95,7 +95,7 @@ export async function runResetLocalSeat(
|
|
|
95
95
|
const snapshot = await deps.snapshotLocalSeat();
|
|
96
96
|
if (!snapshot) {
|
|
97
97
|
deps.stdout(
|
|
98
|
-
`No saved
|
|
98
|
+
`No saved connection was found for this worktree (${worktree}); nothing to reset.\n`,
|
|
99
99
|
);
|
|
100
100
|
return 0;
|
|
101
101
|
}
|
|
@@ -105,9 +105,9 @@ export async function runResetLocalSeat(
|
|
|
105
105
|
// operator asked to reset.
|
|
106
106
|
if (requestedHost !== undefined && requestedHost !== snapshot.apiUrl) {
|
|
107
107
|
deps.stdout(
|
|
108
|
-
`This worktree
|
|
109
|
-
`nothing was changed. Re-run \`borg reset-local-
|
|
110
|
-
"reset this worktree's
|
|
108
|
+
`This worktree connects to ${snapshot.apiUrl}, not ${requestedHost}; ` +
|
|
109
|
+
`nothing was changed. Re-run \`borg reset-local-connection --host ${snapshot.apiUrl}\` to ` +
|
|
110
|
+
"reset this worktree's saved connection.\n",
|
|
111
111
|
);
|
|
112
112
|
return 0;
|
|
113
113
|
}
|
|
@@ -117,7 +117,7 @@ export async function runResetLocalSeat(
|
|
|
117
117
|
? `a saved local session credential is present (${snapshot.observation.state})`
|
|
118
118
|
: 'the saved local session credential is already cleared (only the binding remains)';
|
|
119
119
|
deps.stderr(
|
|
120
|
-
`This will clear ONLY this worktree's saved
|
|
120
|
+
`This will clear ONLY this worktree's saved connection to ${snapshot.apiUrl} ` +
|
|
121
121
|
`(worktree ${worktree}) — ${observed}. Server, trust anchor, cube, and sibling ` +
|
|
122
122
|
'worktrees are left untouched. It makes no network call and revokes nothing server-side.\n',
|
|
123
123
|
);
|
|
@@ -125,17 +125,17 @@ export async function runResetLocalSeat(
|
|
|
125
125
|
// ----- S1: consent OUTSIDE any lock -----
|
|
126
126
|
if (deps.isTTY()) {
|
|
127
127
|
const answer = await deps.prompt(
|
|
128
|
-
`Reset this worktree's saved
|
|
128
|
+
`Reset this worktree's saved connection now? [y/N]: `,
|
|
129
129
|
);
|
|
130
130
|
const normalized = answer.trim().toLowerCase();
|
|
131
131
|
if (normalized !== 'y' && normalized !== 'yes') {
|
|
132
|
-
deps.stderr("audit: no changes made — this worktree's saved
|
|
132
|
+
deps.stderr("audit: no changes made — this worktree's saved connection was left in place.\n");
|
|
133
133
|
return 0;
|
|
134
134
|
}
|
|
135
135
|
} else if (flags.yes !== true) {
|
|
136
136
|
deps.stderr(
|
|
137
137
|
'audit: no changes made — stdin is non-interactive and --yes was not passed. Re-run ' +
|
|
138
|
-
'`borg reset-local-
|
|
138
|
+
'`borg reset-local-connection --yes` to clear this worktree\'s saved connection without a prompt.\n',
|
|
139
139
|
);
|
|
140
140
|
return 1;
|
|
141
141
|
}
|
|
@@ -146,7 +146,7 @@ export async function runResetLocalSeat(
|
|
|
146
146
|
outcome = await deps.resetLocalSeatBinding(snapshot);
|
|
147
147
|
} catch {
|
|
148
148
|
deps.stderr(
|
|
149
|
-
`audit: no changes made — the
|
|
149
|
+
`audit: no changes made — the connection reset for ${snapshot.apiUrl} (worktree ` +
|
|
150
150
|
`${worktree}) could not complete (local credential store error). Retry — it is ` +
|
|
151
151
|
'safe to re-run.\n',
|
|
152
152
|
);
|
|
@@ -155,7 +155,7 @@ export async function runResetLocalSeat(
|
|
|
155
155
|
|
|
156
156
|
if (outcome.outcome === 'reset') {
|
|
157
157
|
deps.stderr(
|
|
158
|
-
`audit: this worktree's saved
|
|
158
|
+
`audit: this worktree's saved connection to ${snapshot.apiUrl} (worktree ${worktree}) ` +
|
|
159
159
|
'was cleared; server, trust anchor, cube, and sibling worktrees unchanged.\n',
|
|
160
160
|
);
|
|
161
161
|
let remaining: Awaited<ReturnType<ResetLocalSeatDeps['findRemainingActiveSeat']>> = null;
|
|
@@ -167,9 +167,9 @@ export async function runResetLocalSeat(
|
|
|
167
167
|
}
|
|
168
168
|
if (remaining?.apiUrl === snapshot.apiUrl) {
|
|
169
169
|
deps.stdout(
|
|
170
|
-
`Another saved
|
|
170
|
+
`Another saved connection remains for this worktree on ${snapshot.apiUrl}. ` +
|
|
171
171
|
`Re-attach with ${reattachCommand(snapshot.apiUrl)}; Borg will revalidate that ` +
|
|
172
|
-
'
|
|
172
|
+
'it with the server before launch.\n',
|
|
173
173
|
);
|
|
174
174
|
} else {
|
|
175
175
|
deps.stdout(recoveryGuidance(snapshot.apiUrl));
|
|
@@ -179,7 +179,7 @@ export async function runResetLocalSeat(
|
|
|
179
179
|
|
|
180
180
|
if (outcome.outcome === 'no-binding') {
|
|
181
181
|
deps.stdout(
|
|
182
|
-
`No saved
|
|
182
|
+
`No saved connection remained for this worktree (${worktree}); nothing to reset.\n`,
|
|
183
183
|
);
|
|
184
184
|
return 0;
|
|
185
185
|
}
|
|
@@ -188,7 +188,7 @@ export async function runResetLocalSeat(
|
|
|
188
188
|
// (a concurrent re-enroll wrote a fresh bearer, or another process already
|
|
189
189
|
// reset it). Never clobber a replacement — report the honest no-op.
|
|
190
190
|
deps.stdout(
|
|
191
|
-
`This worktree's saved
|
|
191
|
+
`This worktree's saved connection to ${snapshot.apiUrl} changed since it was read ` +
|
|
192
192
|
"(a concurrent re-enroll or reset); nothing was changed. Re-run to observe the current " +
|
|
193
193
|
'state.\n',
|
|
194
194
|
);
|
|
@@ -221,7 +221,7 @@ export type ResetLocalSeatParseResult =
|
|
|
221
221
|
| { ok: true; flags: ResetLocalSeatFlags }
|
|
222
222
|
| { ok: false; error: string };
|
|
223
223
|
|
|
224
|
-
/** Parse args after `borg reset-local-
|
|
224
|
+
/** Parse args after `borg reset-local-connection`. Supports `--host <h>` / `--host=<h>` / `--yes` / `-y`. */
|
|
225
225
|
export function parseResetLocalSeatArgs(rawArgs: string[]): ResetLocalSeatParseResult {
|
|
226
226
|
const flags: ResetLocalSeatFlags = {};
|
|
227
227
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
@@ -244,7 +244,7 @@ export function parseResetLocalSeatArgs(rawArgs: string[]): ResetLocalSeatParseR
|
|
|
244
244
|
} else {
|
|
245
245
|
return {
|
|
246
246
|
ok: false,
|
|
247
|
-
error: `unexpected argument: ${arg}. Usage: borg reset-local-
|
|
247
|
+
error: `unexpected argument: ${arg}. Usage: borg reset-local-connection [--host <host>] [--yes]`,
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -76,7 +76,7 @@ export function formatSeatReattachRefusal(
|
|
|
76
76
|
? ''
|
|
77
77
|
: ` Its heartbeat is ${holder.heartbeat}, so the process may be wedged.`;
|
|
78
78
|
return (
|
|
79
|
-
`This worktree's Borg
|
|
79
|
+
`This worktree's Borg drone already has a live session (inbox monitor pid ${holder.pid}).${heartbeat}\n` +
|
|
80
80
|
'No agent was launched. Stop the existing session or use a fresh worktree with `borg assimilate --worktree <name>`. ' +
|
|
81
81
|
`If the live monitor is wedged, override once with \`${forcedCommand}\`.\n`
|
|
82
82
|
);
|
package/src/seat-store.ts
CHANGED
|
@@ -165,7 +165,7 @@ function staleLockError(lockPath: string, held: LockPayload | null): Error {
|
|
|
165
165
|
? `its recorded owner process (pid ${held.pid}, started ${held.startTime}) is no longer running`
|
|
166
166
|
: 'its lock file is missing a valid owner identity or is corrupt';
|
|
167
167
|
return new Error(
|
|
168
|
-
`Borg
|
|
168
|
+
`Borg private store lock file ${lockPath} is stale: ${who}. ` +
|
|
169
169
|
'Borg will NOT remove it automatically. If no borg process is running on this ' +
|
|
170
170
|
`machine, delete ${lockPath} and retry; otherwise wait for the other borg process to finish.`,
|
|
171
171
|
);
|
|
@@ -263,7 +263,7 @@ async function assertSecureStorePerms(
|
|
|
263
263
|
): Promise<void> {
|
|
264
264
|
if ((fileMode & 0o077) !== 0) {
|
|
265
265
|
throw new Error(
|
|
266
|
-
`Borg
|
|
266
|
+
`Borg private store file ${filePath} has insecure permissions ` +
|
|
267
267
|
`(0${(fileMode & 0o777).toString(8)}, expected 0600); refusing to read a credential from it`,
|
|
268
268
|
);
|
|
269
269
|
}
|
|
@@ -277,7 +277,7 @@ async function assertSecureStorePerms(
|
|
|
277
277
|
}
|
|
278
278
|
if ((dirStat.mode & 0o077) !== 0) {
|
|
279
279
|
throw new Error(
|
|
280
|
-
`Borg
|
|
280
|
+
`Borg private store directory ${dir} has insecure permissions ` +
|
|
281
281
|
`(0${(dirStat.mode & 0o777).toString(8)}, expected 0700); refusing to read a credential under it`,
|
|
282
282
|
);
|
|
283
283
|
}
|
|
@@ -350,7 +350,7 @@ export async function readStoreFile(
|
|
|
350
350
|
* release it on EVERY path (finally) by unlinking OUR OWN lock. Acquire is an atomic
|
|
351
351
|
* `open(lockPath,'wx',0o600)`. On EEXIST the lock is held:
|
|
352
352
|
* - holder PID ALIVE → bounded wait/retry (attempts×waitMs), then throw the truthful
|
|
353
|
-
* transient 'Borg
|
|
353
|
+
* transient 'Borg private store is busy' error;
|
|
354
354
|
* - holder PID DEAD, or the payload is missing/unparseable → FAIL CLOSED naming the
|
|
355
355
|
* exact lockfile path + the recorded dead pid/start-time. Borg NEVER auto-deletes
|
|
356
356
|
* or steals it (no reclaim, no rename-claim). The operator clears it by hand only
|
|
@@ -422,7 +422,7 @@ export async function withStoreLock<T>(
|
|
|
422
422
|
});
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
|
-
throw new Error('Borg
|
|
425
|
+
throw new Error('Borg private store is busy');
|
|
426
426
|
} finally {
|
|
427
427
|
await unlink(tmp).catch(() => {});
|
|
428
428
|
}
|
|
@@ -469,7 +469,7 @@ export async function withStore<S, T>(
|
|
|
469
469
|
}
|
|
470
470
|
if (loaded === null) {
|
|
471
471
|
throw new Error(
|
|
472
|
-
'Borg
|
|
472
|
+
'Borg private store is malformed or has an unsupported version; refusing to overwrite it',
|
|
473
473
|
);
|
|
474
474
|
}
|
|
475
475
|
data = loaded;
|
package/src/seats.ts
CHANGED
|
@@ -242,7 +242,7 @@ async function readStore(): Promise<SeatsFile> {
|
|
|
242
242
|
const parsed = parseStore(raw);
|
|
243
243
|
if (parsed === null) {
|
|
244
244
|
throw new Error(
|
|
245
|
-
'Borg
|
|
245
|
+
'Borg private store is malformed or has an unsupported version; refusing to read it',
|
|
246
246
|
);
|
|
247
247
|
}
|
|
248
248
|
return parsed;
|
package/src/server-handshake.ts
CHANGED
|
@@ -338,7 +338,7 @@ export async function sendBorgServerAttach(
|
|
|
338
338
|
if (rejectedCode === ErrorCode.SESSION_REJECTED) {
|
|
339
339
|
throw new BorgServerError(
|
|
340
340
|
'SESSION_REJECTED',
|
|
341
|
-
'Borg server rejected the session:
|
|
341
|
+
'Borg server rejected the session: this worktree\'s connection is already bound to another session',
|
|
342
342
|
);
|
|
343
343
|
}
|
|
344
344
|
if (rejectedCode === ErrorCode.SESSION_REVOKED) {
|
package/src/stream-status.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
163
163
|
} else if (ownedByOther) {
|
|
164
164
|
const owner = status.ownership!;
|
|
165
165
|
summary = owner.droneLabel && owner.worktree
|
|
166
|
-
? `**Stream owned by
|
|
166
|
+
? `**Stream owned by ${owner.droneLabel} in \`${owner.worktree}\`.**`
|
|
167
167
|
: '**Stream owned by another Borg MCP process.**';
|
|
168
168
|
} else if (isNotStarted) {
|
|
169
169
|
summary = '**Stream not started.**';
|
|
@@ -248,7 +248,7 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
248
248
|
|
|
249
249
|
if (ownedByOther) {
|
|
250
250
|
const owner = status.ownership!;
|
|
251
|
-
lines.push(`- **stream owner
|
|
251
|
+
lines.push(`- **stream owner**: ${owner.droneLabel ?? '_(unknown)_'}`);
|
|
252
252
|
lines.push(`- **stream owner worktree**: ${owner.worktree ?? '_(unknown)_'}`);
|
|
253
253
|
lines.push(`- **stream owner pid**: ${owner.pid ?? '_(unknown)_'}`);
|
|
254
254
|
lines.push(`- **stream owner cwd**: ${owner.cwd ?? '_(unknown)_'}`);
|
|
@@ -260,7 +260,7 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
260
260
|
}`
|
|
261
261
|
);
|
|
262
262
|
lines.push('');
|
|
263
|
-
lines.push('Continue
|
|
263
|
+
lines.push('Continue as the owning drone, or close its duplicate agent session before relaunching from the intended worktree. The live owner releases this lock on exit; a stale lock is reclaimed automatically.');
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
if (wakePath.agentKind === 'opencode' && wakePath.openCode) {
|
package/src/tool-manifest.ts
CHANGED
|
@@ -506,7 +506,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
506
506
|
{
|
|
507
507
|
name: 'borg_evict-drone',
|
|
508
508
|
description:
|
|
509
|
-
'Remove a drone using the selected local client\'s cube-management grant. The
|
|
509
|
+
'Remove a drone using the selected local client\'s cube-management grant. The drone\'s credential is revoked, project files remain, and activity history keeps the removed drone\'s attribution. Accepts drone_id for the current cube or label with cube_id.',
|
|
510
510
|
inputSchema: {
|
|
511
511
|
type: 'object',
|
|
512
512
|
properties: {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* Design spec: docs/superpowers/specs/2026-05-29-worktree-lifecycle-design.md
|
|
10
10
|
* Q-resolutions baked in (SPEC-APPROVED 3a80412d):
|
|
11
11
|
* Q1 branch naming — `wt-<suffix>` prefix-stripped, full-basename fallback.
|
|
12
|
-
* Q2 idle-sync — ff-only, clean-gated; never merge/rebase; never over dirty.
|
|
13
12
|
* Q3 post-merge — auto-return to wt-<basename>; ANNOUNCE the prunable
|
|
14
13
|
* merged branch, prune only when explicitly requested.
|
|
15
14
|
* Q4 uniform — no primary-worktree carve-out; main is never a working branch.
|
|
@@ -119,55 +118,11 @@ export function classifyDirty(runSync: RunSync, cwd: string): DirtyClassificatio
|
|
|
119
118
|
return out;
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/** True iff `branch` is an ancestor of `ref` — i.e. a clean fast-forward target. */
|
|
123
|
-
export function isFastForward(runSync: RunSync, cwd: string, branch: string, ref: string): boolean {
|
|
124
|
-
return runSync('git', ['merge-base', '--is-ancestor', branch, ref], cwd).status === 0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
121
|
/** True iff `branch`'s tip is an ancestor of `ref` — i.e. fully merged into it. */
|
|
128
122
|
export function isMerged(runSync: RunSync, cwd: string, branch: string, ref: string): boolean {
|
|
129
123
|
return runSync('git', ['merge-base', '--is-ancestor', branch, ref], cwd).status === 0;
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
export interface SyncResult {
|
|
133
|
-
action: 'fast-forwarded' | 'already-current' | 'skipped-dirty' | 'skipped-diverged';
|
|
134
|
-
message?: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Idle-sync the current per-worktree branch to `ref` (Q2). NEVER discards
|
|
139
|
-
* work: dirty -> skipped-dirty (no mutation). Only fast-forwards (no
|
|
140
|
-
* merge/rebase): diverged -> skipped-diverged. The caller fetches first.
|
|
141
|
-
*
|
|
142
|
-
* `already-current` when the branch tip already equals `ref` (the common
|
|
143
|
-
* no-op case on every launch).
|
|
144
|
-
*/
|
|
145
|
-
export function syncWorktree(runSync: RunSync, cwd: string, branch: string, ref: string): SyncResult {
|
|
146
|
-
if (!isCleanTree(runSync, cwd)) {
|
|
147
|
-
return {
|
|
148
|
-
action: 'skipped-dirty',
|
|
149
|
-
message: 'uncommitted changes present; sync skipped (nothing discarded)',
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
if (!isFastForward(runSync, cwd, branch, ref)) {
|
|
153
|
-
return {
|
|
154
|
-
action: 'skipped-diverged',
|
|
155
|
-
message: `${branch} has diverged from ${ref}; resolve manually (no auto-merge/rebase)`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
// Already at ref? merge --ff-only is a no-op but we report it distinctly
|
|
159
|
-
// so callers can stay quiet on the common case.
|
|
160
|
-
const ahead = runSync('git', ['rev-list', '--count', `${branch}..${ref}`], cwd);
|
|
161
|
-
if (ahead.status === 0 && ahead.stdout.trim() === '0') {
|
|
162
|
-
return { action: 'already-current' };
|
|
163
|
-
}
|
|
164
|
-
const ff = runSync('git', ['merge', '--ff-only', ref], cwd);
|
|
165
|
-
if (ff.status !== 0) {
|
|
166
|
-
return { action: 'skipped-diverged', message: 'ff-only merge unexpectedly failed' };
|
|
167
|
-
}
|
|
168
|
-
return { action: 'fast-forwarded' };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
126
|
export interface AdoptResult {
|
|
172
127
|
action: 'adopted' | 'blocked-unmerged' | 'blocked-target-unmerged' | 'skipped-dirty';
|
|
173
128
|
message?: string;
|