borgmcp 2.16.1 → 3.0.2
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 +5 -3
- 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/bare-launch-menu.d.ts +5 -5
- package/dist/bare-launch-menu.d.ts.map +1 -1
- package/dist/bare-launch-menu.js +2 -2
- package/dist/bare-launch-menu.js.map +1 -1
- package/dist/claude.d.ts +0 -2
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +5 -18
- 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/cli-platform.d.ts +4 -0
- package/dist/cli-platform.d.ts.map +1 -1
- package/dist/cli-platform.js +29 -10
- package/dist/cli-platform.js.map +1 -1
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +30 -12
- 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/first-run-server.d.ts.map +1 -1
- package/dist/first-run-server.js +4 -1
- package/dist/first-run-server.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/lifecycle-log-guard.d.ts.map +1 -1
- package/dist/lifecycle-log-guard.js +12 -3
- package/dist/lifecycle-log-guard.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 +2 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +10 -5
- 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 +5 -3
- 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/bare-launch-menu.ts +6 -6
- package/src/claude.ts +8 -21
- package/src/cleanup-cmd.ts +4 -4
- package/src/cli-help.ts +15 -27
- package/src/cli-platform.ts +42 -10
- package/src/cubes.ts +29 -14
- 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/first-run-server.ts +4 -1
- 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/lifecycle-log-guard.ts +15 -3
- package/src/opencode-seat-identity.ts +5 -5
- package/src/regen-format.ts +2 -1
- package/src/remote-client.ts +12 -6
- 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/cli-help.ts
CHANGED
|
@@ -16,16 +16,6 @@ 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
19
|
export function cleanupHelpText(version: string): string {
|
|
30
20
|
return (
|
|
31
21
|
`borg cleanup (borgmcp ${version}) — review orphaned Borg-managed worktrees\n\n` +
|
|
@@ -63,9 +53,8 @@ export function clientSubcommandHelpText(
|
|
|
63
53
|
switch (command) {
|
|
64
54
|
case 'setup': return setupHelpText(version);
|
|
65
55
|
case 'assimilate': return assimilateHelpText(version);
|
|
66
|
-
case 'reset-local-
|
|
56
|
+
case 'reset-local-connection': return resetLocalSeatHelpText(version);
|
|
67
57
|
case 'recover-enrollment': return recoverEnrollmentHelpText(version);
|
|
68
|
-
case 'sync': return syncHelpText(version);
|
|
69
58
|
case 'cleanup': return cleanupHelpText(version);
|
|
70
59
|
case 'launch-all': return launchAllHelpText(version);
|
|
71
60
|
default: return null;
|
|
@@ -101,9 +90,8 @@ export function topLevelHelpText(version: string): string {
|
|
|
101
90
|
` borg assimilate --host <host> Join or create on an explicit server\n` +
|
|
102
91
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
|
103
92
|
` borg server cube init Initialize this repository's cube without creating a drone\n` +
|
|
104
|
-
` borg reset-local-
|
|
93
|
+
` borg reset-local-connection Clear ONLY this worktree's saved connection to its cube (offline; after a rejection)\n` +
|
|
105
94
|
` borg recover-enrollment Restore or clear ONLY one failed server enrollment transaction\n` +
|
|
106
|
-
` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
|
|
107
95
|
` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
|
|
108
96
|
` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
|
|
109
97
|
` borg launch-all [cube] --cli claude|codex|opencode\n` +
|
|
@@ -157,8 +145,8 @@ export function serverHelpText(): string {
|
|
|
157
145
|
` update Verify and activate a local server artifact.\n` +
|
|
158
146
|
` invite Create a single-use invitation in an interactive terminal.\n` +
|
|
159
147
|
` cert-reissue Widen the server certificate to cover another address without replacing the CA.\n` +
|
|
160
|
-
` client-list List enrolled clients, states, and cube grants while the server is
|
|
161
|
-
` client-grant Grant a client read, write, or manage access to a cube while the server is
|
|
148
|
+
` client-list List enrolled clients, states, and cube grants while the server is live; committed changes take effect on the next request.\n` +
|
|
149
|
+
` client-grant Grant a client read, write, or manage access to a cube while the server is live; committed changes take effect on the next request.\n` +
|
|
162
150
|
` dashboard View the running local server dashboard.\n` +
|
|
163
151
|
` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
|
|
164
152
|
`Run borg server <command> --help for server command options.\n`
|
|
@@ -203,7 +191,7 @@ export function assimilateHelpText(version: string): string {
|
|
|
203
191
|
`Flags:\n` +
|
|
204
192
|
` --worktree <name> Create + launch the drone in a sibling git worktree\n` +
|
|
205
193
|
` --here Stay in the current worktree (no sibling spawn)\n` +
|
|
206
|
-
` --force
|
|
194
|
+
` --force Reattach despite a still-live inbox monitor in this worktree\n` +
|
|
207
195
|
` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
|
|
208
196
|
` --host <host> Borg server host or URL (bare hosts default to HTTPS)\n` +
|
|
209
197
|
` --enroll Prompt for a hidden enrollment invitation in the operator terminal\n` +
|
|
@@ -216,7 +204,7 @@ export function assimilateHelpText(version: string): string {
|
|
|
216
204
|
`repository association skips all prompts. One accessible exact-name legacy cube requires\n` +
|
|
217
205
|
`explicit interactive adoption; ambiguous matches fail closed. An enrolled owner client may\n` +
|
|
218
206
|
`create an idempotent repository cube; ordinary clients require an explicit cube grant.\n` +
|
|
219
|
-
`
|
|
207
|
+
`A drone is created only after enrollment. Preview only.\n` +
|
|
220
208
|
`See docs/LOCAL_SERVER.md for self-hosted setup and current status.\n\n` +
|
|
221
209
|
`For local or provider-specific models, configure the selected agent CLI directly.\n` +
|
|
222
210
|
`OpenCode supports Ollama and other providers through its own model configuration.\n`
|
|
@@ -224,25 +212,25 @@ export function assimilateHelpText(version: string): string {
|
|
|
224
212
|
}
|
|
225
213
|
|
|
226
214
|
/**
|
|
227
|
-
* Help text for `borg reset-local-
|
|
215
|
+
* Help text for `borg reset-local-connection --help`. The offline, network-free seat
|
|
228
216
|
* reset recommended by the pin-matched SESSION_REJECTED diagnostic (#1082).
|
|
229
217
|
*/
|
|
230
218
|
export function resetLocalSeatHelpText(version: string): string {
|
|
231
219
|
return (
|
|
232
|
-
`borg reset-local-
|
|
220
|
+
`borg reset-local-connection (borgmcp ${version}) — clear ONLY this worktree's saved connection to its cube\n\n` +
|
|
233
221
|
`Offline and network-free: it contacts no server and revokes nothing server-side. It clears\n` +
|
|
234
|
-
`just this worktree's saved
|
|
235
|
-
`
|
|
222
|
+
`just this worktree's saved connection — its credential and cube binding together — from the\n` +
|
|
223
|
+
`private store on this machine. Server, trust anchor, cube, and every sibling worktree are\n` +
|
|
236
224
|
`left untouched.\n\n` +
|
|
237
225
|
`Use it after \`borg assimilate\` reports this worktree's session was revoked or superseded\n` +
|
|
238
226
|
`(a pin-matched rejection), then ask the operator for a new invitation and re-enroll.\n\n` +
|
|
239
227
|
`Usage:\n` +
|
|
240
|
-
` borg reset-local-
|
|
241
|
-
` borg reset-local-
|
|
242
|
-
` borg reset-local-
|
|
243
|
-
` borg reset-local-
|
|
228
|
+
` borg reset-local-connection Reset this worktree's saved connection (TTY confirms [y/N])\n` +
|
|
229
|
+
` borg reset-local-connection --host <host> No-op unless this worktree connects to <host>\n` +
|
|
230
|
+
` borg reset-local-connection --yes Reset without a prompt (required when non-interactive)\n` +
|
|
231
|
+
` borg reset-local-connection --help Show this help\n\n` +
|
|
244
232
|
`Flags:\n` +
|
|
245
|
-
` --host <host> Only act if this worktree
|
|
233
|
+
` --host <host> Only act if this worktree connects to <host> (else no-op)\n` +
|
|
246
234
|
` --yes, -y Skip the confirmation prompt (required in non-TTY contexts)\n`
|
|
247
235
|
);
|
|
248
236
|
}
|
package/src/cli-platform.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import which from 'which';
|
|
2
2
|
import type { BorgCli } from './cubes.js';
|
|
3
3
|
import { getProjectCliPreference, setProjectCliPreference } from './cubes.js';
|
|
4
|
+
import {
|
|
5
|
+
isCodexMcpServerConfigured,
|
|
6
|
+
isMcpServerConfigured,
|
|
7
|
+
isOpenCodeMcpServerConfigured,
|
|
8
|
+
} from './config-utils.js';
|
|
4
9
|
|
|
5
10
|
export interface CliAvailability {
|
|
6
11
|
claude: string | null;
|
|
@@ -8,8 +13,11 @@ export interface CliAvailability {
|
|
|
8
13
|
opencode: string | null;
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
export type CliConfiguration = Record<BorgCli, boolean>;
|
|
17
|
+
|
|
11
18
|
export interface CliChoiceDeps {
|
|
12
19
|
detectCli: () => CliAvailability;
|
|
20
|
+
detectConfigured: () => CliConfiguration;
|
|
13
21
|
getPreference: () => Promise<BorgCli | null>;
|
|
14
22
|
setPreference: (cli: BorgCli) => Promise<void>;
|
|
15
23
|
prompt: (message: string) => Promise<string>;
|
|
@@ -40,6 +48,21 @@ export function installedCliNames(availability: CliAvailability): BorgCli[] {
|
|
|
40
48
|
return out;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
export function detectCliConfiguration(): CliConfiguration {
|
|
52
|
+
return {
|
|
53
|
+
claude: isMcpServerConfigured(),
|
|
54
|
+
codex: isCodexMcpServerConfigured(),
|
|
55
|
+
opencode: isOpenCodeMcpServerConfigured(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function configuredCliNames(
|
|
60
|
+
availability: CliAvailability,
|
|
61
|
+
configuration: CliConfiguration,
|
|
62
|
+
): BorgCli[] {
|
|
63
|
+
return installedCliNames(availability).filter((cli) => configuration[cli]);
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
async function setPreferenceAndReturn(cli: BorgCli, deps: CliChoiceDeps): Promise<BorgCli> {
|
|
44
67
|
await deps.setPreference(cli);
|
|
45
68
|
return cli;
|
|
@@ -59,31 +82,39 @@ export async function resolveCliChoice(
|
|
|
59
82
|
if (!installed.includes(explicit)) {
|
|
60
83
|
throw new Error(`${explicit} CLI is not installed.`);
|
|
61
84
|
}
|
|
85
|
+
// An explicit --cli is an intentional request, so the caller may still
|
|
86
|
+
// configure that installed CLI on demand. Automatic selection below is
|
|
87
|
+
// restricted to registrations already present in the agent config.
|
|
62
88
|
await deps.setPreference(explicit);
|
|
63
89
|
return explicit;
|
|
64
90
|
}
|
|
65
91
|
|
|
92
|
+
const configured = configuredCliNames(availability, deps.detectConfigured());
|
|
93
|
+
if (configured.length === 0) {
|
|
94
|
+
throw new Error('No supported agent CLI is configured for Borg. Run `borg setup` to configure one, then run `borg` or `borg assimilate` again.');
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
const stored = await deps.getPreference();
|
|
67
|
-
if (stored &&
|
|
98
|
+
if (stored && configured.includes(stored)) return stored;
|
|
68
99
|
|
|
69
|
-
if (
|
|
70
|
-
await deps.setPreference(
|
|
71
|
-
return
|
|
100
|
+
if (configured.length === 1) {
|
|
101
|
+
await deps.setPreference(configured[0]);
|
|
102
|
+
return configured[0];
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
if (!deps.isTTY()) {
|
|
75
|
-
throw new Error('Multiple agent CLIs detected. Pass --cli claude, --cli codex, or --cli opencode to choose.');
|
|
106
|
+
throw new Error('Multiple configured agent CLIs detected. Pass --cli claude, --cli codex, or --cli opencode to choose.');
|
|
76
107
|
}
|
|
77
108
|
|
|
78
|
-
const promptLines =
|
|
109
|
+
const promptLines = configured.map((cli, i) => ` ${i + 1}) ${cli}`);
|
|
79
110
|
const answer = (await deps.prompt(`Use which CLI for this project?\n${promptLines.join('\n')}\n[1]: `)).trim();
|
|
80
|
-
if (answer === '' || answer === '1') return setPreferenceAndReturn(
|
|
111
|
+
if (answer === '' || answer === '1') return setPreferenceAndReturn(configured[0], deps);
|
|
81
112
|
const num = parseInt(answer, 10);
|
|
82
|
-
if (!Number.isNaN(num) && num >= 1 && num <=
|
|
83
|
-
return setPreferenceAndReturn(
|
|
113
|
+
if (!Number.isNaN(num) && num >= 1 && num <= configured.length) {
|
|
114
|
+
return setPreferenceAndReturn(configured[num - 1], deps);
|
|
84
115
|
}
|
|
85
116
|
const lower = answer.toLowerCase();
|
|
86
|
-
for (const cli of
|
|
117
|
+
for (const cli of configured) {
|
|
87
118
|
if (lower === cli) return setPreferenceAndReturn(cli, deps);
|
|
88
119
|
}
|
|
89
120
|
throw new Error(`invalid CLI choice "${answer}"`);
|
|
@@ -92,6 +123,7 @@ export async function resolveCliChoice(
|
|
|
92
123
|
export function defaultCliChoiceDeps(prompt: (message: string) => Promise<string>, isTTY: () => boolean): CliChoiceDeps {
|
|
93
124
|
return {
|
|
94
125
|
detectCli: detectCliAvailability,
|
|
126
|
+
detectConfigured: detectCliConfiguration,
|
|
95
127
|
getPreference: getProjectCliPreference,
|
|
96
128
|
setPreference: setProjectCliPreference,
|
|
97
129
|
prompt,
|
package/src/cubes.ts
CHANGED
|
@@ -84,6 +84,13 @@ interface LaunchFile {
|
|
|
84
84
|
projects: Record<string, { cli: BorgCli }>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
const UNREADABLE_STATE = Symbol('unreadable-state-file');
|
|
88
|
+
type StateFileRead<T> = T | null | typeof UNREADABLE_STATE;
|
|
89
|
+
|
|
90
|
+
function unreadableStateError(filePath: string): Error {
|
|
91
|
+
return new Error(`Borg state file is unreadable; refusing to overwrite it: ${filePath}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
export interface CodexWakeTargetRecord {
|
|
88
95
|
threadId: string;
|
|
89
96
|
socketPath: string;
|
|
@@ -110,7 +117,7 @@ let pinnedMcpSeatIdentity: {
|
|
|
110
117
|
export class McpSeatIdentityChangedError extends Error {
|
|
111
118
|
readonly code = 'SEAT_IDENTITY_CHANGED';
|
|
112
119
|
constructor() {
|
|
113
|
-
super('
|
|
120
|
+
super('This worktree\'s saved connection changed after this MCP session pinned its identity. Exit this session and relaunch from the intended worktree.');
|
|
114
121
|
this.name = 'McpSeatIdentityChangedError';
|
|
115
122
|
}
|
|
116
123
|
}
|
|
@@ -225,7 +232,7 @@ function isLaunchFile(data: any): data is LaunchFile {
|
|
|
225
232
|
);
|
|
226
233
|
}
|
|
227
234
|
|
|
228
|
-
async function readLaunchFile(): Promise<LaunchFile
|
|
235
|
+
async function readLaunchFile(): Promise<StateFileRead<LaunchFile>> {
|
|
229
236
|
let raw: string;
|
|
230
237
|
try {
|
|
231
238
|
raw = await readFile(LAUNCH_FILE, 'utf8');
|
|
@@ -235,9 +242,9 @@ async function readLaunchFile(): Promise<LaunchFile | null> {
|
|
|
235
242
|
}
|
|
236
243
|
try {
|
|
237
244
|
const parsed = JSON.parse(raw);
|
|
238
|
-
return isLaunchFile(parsed) ? parsed :
|
|
245
|
+
return isLaunchFile(parsed) ? parsed : UNREADABLE_STATE;
|
|
239
246
|
} catch {
|
|
240
|
-
return
|
|
247
|
+
return UNREADABLE_STATE;
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
|
|
@@ -263,7 +270,7 @@ function isCodexWakeTargetsFile(data: any): data is CodexWakeTargetsFile {
|
|
|
263
270
|
);
|
|
264
271
|
}
|
|
265
272
|
|
|
266
|
-
async function readCodexWakeTargetsFile(): Promise<CodexWakeTargetsFile
|
|
273
|
+
async function readCodexWakeTargetsFile(): Promise<StateFileRead<CodexWakeTargetsFile>> {
|
|
267
274
|
let raw: string;
|
|
268
275
|
try {
|
|
269
276
|
raw = await readFile(CODEX_WAKE_TARGETS_FILE, 'utf8');
|
|
@@ -273,9 +280,9 @@ async function readCodexWakeTargetsFile(): Promise<CodexWakeTargetsFile | null>
|
|
|
273
280
|
}
|
|
274
281
|
try {
|
|
275
282
|
const parsed = JSON.parse(raw);
|
|
276
|
-
return isCodexWakeTargetsFile(parsed) ? parsed :
|
|
283
|
+
return isCodexWakeTargetsFile(parsed) ? parsed : UNREADABLE_STATE;
|
|
277
284
|
} catch {
|
|
278
|
-
return
|
|
285
|
+
return UNREADABLE_STATE;
|
|
279
286
|
}
|
|
280
287
|
}
|
|
281
288
|
|
|
@@ -439,7 +446,7 @@ export async function snapshotLocalSeat(): Promise<LocalSeatSnapshot | null> {
|
|
|
439
446
|
// CR#4: discover an ACTIVE seat OR a bound-PENDING record (a sibling whose
|
|
440
447
|
// activation failed, bound to THIS worktree by the attach bind-pending step).
|
|
441
448
|
// getActiveSeatForWorktree would MISS the bound-pending record (it requires
|
|
442
|
-
// state==='active' + a drone id), so `reset-local-
|
|
449
|
+
// state==='active' + a drone id), so `reset-local-connection` would FALSELY report
|
|
443
450
|
// "nothing to reset" (exit 0) while a resumable, server-digest-bound bearer
|
|
444
451
|
// persists at rest — a FALSE-SUCCESS revocation failure. getSeatForWorktree sees
|
|
445
452
|
// both, and the offline reset's exact re-check + delete cover the bound-pending
|
|
@@ -574,6 +581,7 @@ export async function refreshActiveCubeMetadata(active: ActiveCubeInput): Promis
|
|
|
574
581
|
|
|
575
582
|
export async function getProjectCliPreference(): Promise<BorgCli | null> {
|
|
576
583
|
const data = await readLaunchFile();
|
|
584
|
+
if (data === UNREADABLE_STATE) throw unreadableStateError(LAUNCH_FILE);
|
|
577
585
|
if (!data) return null;
|
|
578
586
|
const entry = data.projects[findProjectRoot()];
|
|
579
587
|
return entry?.cli === 'claude' || entry?.cli === 'codex' || entry?.cli === 'opencode' ? entry.cli : null;
|
|
@@ -586,6 +594,7 @@ export async function getProjectCliPreference(): Promise<BorgCli | null> {
|
|
|
586
594
|
*/
|
|
587
595
|
export async function getProjectCliPreferenceForPath(dir: string): Promise<BorgCli | null> {
|
|
588
596
|
const data = await readLaunchFile();
|
|
597
|
+
if (data === UNREADABLE_STATE) throw unreadableStateError(LAUNCH_FILE);
|
|
589
598
|
if (!data) return null;
|
|
590
599
|
const entry = data.projects[findProjectRoot(dir)];
|
|
591
600
|
return entry?.cli === 'claude' || entry?.cli === 'codex' || entry?.cli === 'opencode' ? entry.cli : null;
|
|
@@ -617,9 +626,11 @@ export async function readAllProjectIdentities(): Promise<
|
|
|
617
626
|
* sibling worktree but the process still began in the invoking checkout.
|
|
618
627
|
*/
|
|
619
628
|
export async function setProjectCliPreference(cli: BorgCli, dir?: string): Promise<void> {
|
|
620
|
-
const existing =
|
|
621
|
-
existing
|
|
622
|
-
|
|
629
|
+
const existing = await readLaunchFile();
|
|
630
|
+
if (existing === UNREADABLE_STATE) throw unreadableStateError(LAUNCH_FILE);
|
|
631
|
+
const next = existing ?? { projects: {} };
|
|
632
|
+
next.projects[findProjectRoot(dir)] = { cli };
|
|
633
|
+
await writeLaunchFile(next);
|
|
623
634
|
}
|
|
624
635
|
|
|
625
636
|
export async function setCodexWakeTarget(
|
|
@@ -627,12 +638,14 @@ export async function setCodexWakeTarget(
|
|
|
627
638
|
droneId: string,
|
|
628
639
|
target: Omit<CodexWakeTargetRecord, 'updatedAt'>
|
|
629
640
|
): Promise<void> {
|
|
630
|
-
const existing =
|
|
631
|
-
existing
|
|
641
|
+
const existing = await readCodexWakeTargetsFile();
|
|
642
|
+
if (existing === UNREADABLE_STATE) throw unreadableStateError(CODEX_WAKE_TARGETS_FILE);
|
|
643
|
+
const next = existing ?? { targets: {} };
|
|
644
|
+
next.targets[codexWakeTargetKey(cubeId, droneId)] = {
|
|
632
645
|
...target,
|
|
633
646
|
updatedAt: new Date().toISOString(),
|
|
634
647
|
};
|
|
635
|
-
await writeCodexWakeTargetsFile(
|
|
648
|
+
await writeCodexWakeTargetsFile(next);
|
|
636
649
|
}
|
|
637
650
|
|
|
638
651
|
export async function getCodexWakeTarget(
|
|
@@ -640,6 +653,7 @@ export async function getCodexWakeTarget(
|
|
|
640
653
|
droneId: string
|
|
641
654
|
): Promise<CodexWakeTargetRecord | null> {
|
|
642
655
|
const existing = await readCodexWakeTargetsFile();
|
|
656
|
+
if (existing === UNREADABLE_STATE) throw unreadableStateError(CODEX_WAKE_TARGETS_FILE);
|
|
643
657
|
if (!existing) return null;
|
|
644
658
|
const target = existing.targets[codexWakeTargetKey(cubeId, droneId)];
|
|
645
659
|
if (!target || typeof target.threadId !== 'string' || typeof target.socketPath !== 'string') {
|
|
@@ -661,6 +675,7 @@ export async function pruneDeadCodexWakeTargets(
|
|
|
661
675
|
socketLiveness: (socketPath: string) => boolean | null
|
|
662
676
|
): Promise<void> {
|
|
663
677
|
const existing = await readCodexWakeTargetsFile();
|
|
678
|
+
if (existing === UNREADABLE_STATE) throw unreadableStateError(CODEX_WAKE_TARGETS_FILE);
|
|
664
679
|
if (!existing) return;
|
|
665
680
|
const { targets, changed } = pruneDeadWakeTargets(existing.targets, socketLiveness);
|
|
666
681
|
if (changed) await writeCodexWakeTargetsFile({ ...existing, targets });
|
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
|
}
|
package/src/first-run-server.ts
CHANGED
|
@@ -177,8 +177,11 @@ export async function offerFirstRunServerInstall(
|
|
|
177
177
|
: decision === 'eof'
|
|
178
178
|
? 'Installation was cancelled because confirmation input ended.'
|
|
179
179
|
: 'Installation was declined.';
|
|
180
|
+
const setupRecovery = options.initializeServer
|
|
181
|
+
? ', and no agent configuration was written. Run `borg setup` again to configure your agents.'
|
|
182
|
+
: '.';
|
|
180
183
|
deps.stderr(
|
|
181
|
-
`${reason} No server package or server state was changed
|
|
184
|
+
`${reason} No server package or server state was changed${setupRecovery}\n` +
|
|
182
185
|
`To install it later, run \`${command}\`. Then run \`borg server setup\` and \`borg server start\`.\n`,
|
|
183
186
|
);
|
|
184
187
|
return { kind: 'declined' };
|
|
@@ -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
|
}
|
|
@@ -28,6 +28,15 @@ interface LifecycleStateFile {
|
|
|
28
28
|
entries: Record<string, LifecycleStateEntry>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const UNREADABLE_STATE = Symbol('unreadable-lifecycle-state');
|
|
32
|
+
type LifecycleStateRead = LifecycleStateFile | typeof UNREADABLE_STATE;
|
|
33
|
+
|
|
34
|
+
function unreadableStateError(): Error {
|
|
35
|
+
return new Error(
|
|
36
|
+
`Lifecycle log state is unreadable; refusing to overwrite it: ${STATE_FILE}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
export function lifecycleSignalForMessage(message: string): LifecycleSignal | null {
|
|
32
41
|
if (message.startsWith('ARRIVAL: ')) return 'arrival';
|
|
33
42
|
if (
|
|
@@ -44,7 +53,7 @@ function stateKey(subject: LifecycleLogSubject): string {
|
|
|
44
53
|
return `${subject.cubeId}:${subject.droneId}`;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
async function readState(): Promise<
|
|
56
|
+
async function readState(): Promise<LifecycleStateRead> {
|
|
48
57
|
try {
|
|
49
58
|
const raw = await readFile(STATE_FILE, 'utf8');
|
|
50
59
|
const parsed = JSON.parse(raw);
|
|
@@ -58,9 +67,10 @@ async function readState(): Promise<LifecycleStateFile> {
|
|
|
58
67
|
return parsed as LifecycleStateFile;
|
|
59
68
|
}
|
|
60
69
|
} catch (err: any) {
|
|
61
|
-
if (err?.code
|
|
70
|
+
if (err?.code === 'ENOENT') return { entries: {} };
|
|
71
|
+
return UNREADABLE_STATE;
|
|
62
72
|
}
|
|
63
|
-
return
|
|
73
|
+
return UNREADABLE_STATE;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
async function writeState(state: LifecycleStateFile): Promise<void> {
|
|
@@ -100,6 +110,7 @@ export async function shouldSuppressLifecycleLog(
|
|
|
100
110
|
message: string
|
|
101
111
|
): Promise<{ suppress: boolean; signal: LifecycleSignal | null }> {
|
|
102
112
|
const state = await readState();
|
|
113
|
+
if (state === UNREADABLE_STATE) throw unreadableStateError();
|
|
103
114
|
return shouldSuppressLifecycleLogFromState(
|
|
104
115
|
message,
|
|
105
116
|
state.entries[stateKey(subject)]
|
|
@@ -138,6 +149,7 @@ export async function recordLifecycleLog(
|
|
|
138
149
|
message: string
|
|
139
150
|
): Promise<void> {
|
|
140
151
|
const state = await readState();
|
|
152
|
+
if (state === UNREADABLE_STATE) throw unreadableStateError();
|
|
141
153
|
const key = stateKey(subject);
|
|
142
154
|
state.entries[key] = nextLifecycleStateAfterLog(message, state.entries[key]);
|
|
143
155
|
await writeState(state);
|
|
@@ -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.
|