borgmcp 4.6.1 → 4.7.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/dist/assimilate-cmd.d.ts +8 -23
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +9 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +3 -16
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +5 -3
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +82 -30
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +1 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-launch.d.ts +1 -1
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js.map +1 -1
- package/dist/config-utils.d.ts +3 -0
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +141 -4
- package/dist/config-utils.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -18
- package/dist/config.js.map +1 -1
- package/dist/cubes.d.ts +0 -7
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -9
- package/dist/cubes.js.map +1 -1
- package/dist/gc-orphan-inboxes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -47
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +6 -16
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +2 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +101 -28
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-launch-trust.d.ts +1 -0
- package/dist/opencode-launch-trust.d.ts.map +1 -1
- package/dist/opencode-launch-trust.js +4 -0
- package/dist/opencode-launch-trust.js.map +1 -1
- package/dist/quickstart-cmd.d.ts.map +1 -1
- package/dist/quickstart-cmd.js +9 -2
- package/dist/quickstart-cmd.js.map +1 -1
- package/dist/regen-format.d.ts +1 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +1 -1
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -8
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.d.ts +1 -1
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +2 -5
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/repository-identity.d.ts +14 -4
- package/dist/repository-identity.d.ts.map +1 -1
- package/dist/repository-identity.js +86 -18
- package/dist/repository-identity.js.map +1 -1
- package/dist/self-path.d.ts.map +1 -1
- package/dist/self-path.js.map +1 -1
- package/dist/server-handshake.d.ts +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +0 -1
- package/dist/server-handshake.js.map +1 -1
- package/dist/tool-manifest.js +3 -3
- 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 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.js +1 -1
- package/dist/update-cmd.js.map +1 -1
- package/docs/RELEASING.md +5 -4
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +29 -37
- package/src/assimilate-deps.ts +2 -18
- package/src/claude.ts +91 -31
- package/src/cli-help.ts +1 -0
- package/src/codex-launch.ts +2 -1
- package/src/config-utils.ts +156 -4
- package/src/config.ts +58 -22
- package/src/cubes.ts +0 -10
- package/src/gc-orphan-inboxes.ts +1 -1
- package/src/index.ts +24 -51
- package/src/log-stream.ts +12 -17
- package/src/opencode-drone.ts +112 -30
- package/src/opencode-launch-trust.ts +5 -0
- package/src/quickstart-cmd.ts +10 -2
- package/src/regen-format.ts +1 -3
- package/src/regen.ts +2 -3
- package/src/remote-client.ts +12 -9
- package/src/repository-cube-init.ts +3 -6
- package/src/repository-identity.ts +125 -20
- package/src/self-path.ts +0 -1
- package/src/server-handshake.ts +1 -2
- package/src/tool-manifest.ts +3 -3
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1 -1
- package/dist/postinstall-banner.d.ts +0 -2
- package/dist/postinstall-banner.d.ts.map +0 -1
- package/dist/postinstall-banner.js +0 -27
- package/dist/postinstall-banner.js.map +0 -1
- package/dist/postinstall.d.ts +0 -11
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js +0 -33
- package/dist/postinstall.js.map +0 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/postinstall-banner.ts +0 -37
- package/src/postinstall.ts +0 -36
- package/src/types.ts +0 -5
package/src/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
formatDocumentMetadata,
|
|
68
68
|
} from './document-render.js';
|
|
69
69
|
import {
|
|
70
|
+
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
70
71
|
getTemplate,
|
|
71
72
|
listTemplateNames,
|
|
72
73
|
resolveCubeDirectiveForCreate,
|
|
@@ -153,7 +154,7 @@ import {
|
|
|
153
154
|
writeOpenCodeStartupDiagnostic,
|
|
154
155
|
} from './opencode-drone.js';
|
|
155
156
|
import { installBorgPlugin } from './opencode-plugin.js';
|
|
156
|
-
import { openCodeApiPasswordFromEnv } from './opencode-launch-trust.js';
|
|
157
|
+
import { openCodeApiPasswordFromEnv, openCodeLaunchCorrelationFromEnv } from './opencode-launch-trust.js';
|
|
157
158
|
import { setModuleInjectOpenCode } from './log-stream.js';
|
|
158
159
|
import {
|
|
159
160
|
lifecycleSignalForMessage,
|
|
@@ -176,27 +177,6 @@ import {
|
|
|
176
177
|
|
|
177
178
|
const OPEN_CODE_IDENTITY_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
178
179
|
|
|
179
|
-
/**
|
|
180
|
-
* Apply a template's roles + message_taxonomy to a cube.
|
|
181
|
-
*
|
|
182
|
-
* Client-orchestrated through the server's non-clobbering role and taxonomy
|
|
183
|
-
* primitives. New roles are inserted; existing template-named roles get ADD
|
|
184
|
-
* fragments (template sections/classes the cube lacks) auto-applied, but
|
|
185
|
-
* EVOLVED (conflicting) fragments are kept — never silently overwritten. The
|
|
186
|
-
* old per-role blanket `updateRole`/whole-taxonomy `updateCube` overwrite path
|
|
187
|
-
* is removed. Operators who want to take the template version of a conflicting
|
|
188
|
-
* fragment use `borg_sync-roles` with a `decisions` map. Primitive operations
|
|
189
|
-
* are sequential, so a later failure can leave earlier changes committed.
|
|
190
|
-
* Returns `{ created, updated }` for the caller's toast.
|
|
191
|
-
*/
|
|
192
|
-
async function applyTemplateToCube(
|
|
193
|
-
cubeId: string,
|
|
194
|
-
template: Template,
|
|
195
|
-
authority?: LocalManageAuthority,
|
|
196
|
-
): Promise<{ created: number; updated: number }> {
|
|
197
|
-
return await applyTemplate(cubeId, template.name, authority);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
180
|
export async function runApplyTemplateTool(
|
|
201
181
|
cubeId: string,
|
|
202
182
|
template: Template,
|
|
@@ -312,6 +292,11 @@ export async function connectOpenCodeRuntime(
|
|
|
312
292
|
console.error('OpenCode API credential is missing or unverifiable; skipping OpenCode entry injection. Relaunch through borg.');
|
|
313
293
|
return false;
|
|
314
294
|
}
|
|
295
|
+
const launchIdentity = openCodeLaunchCorrelationFromEnv(env);
|
|
296
|
+
if (launchIdentity === null) {
|
|
297
|
+
console.error('OpenCode launch identity is missing or unverifiable; skipping OpenCode entry injection. Relaunch through borg.');
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
315
300
|
const binding = openCodeLaunchBinding(configuredPort);
|
|
316
301
|
await (deps.connect ?? connectOpenCodeDrone)({
|
|
317
302
|
serverUrl: binding.serverUrl,
|
|
@@ -319,6 +304,7 @@ export async function connectOpenCodeRuntime(
|
|
|
319
304
|
directory: active.worktree ?? findProjectRoot(),
|
|
320
305
|
droneLabel: active.droneLabel,
|
|
321
306
|
cubeName: active.name,
|
|
307
|
+
launchIdentity,
|
|
322
308
|
});
|
|
323
309
|
return true;
|
|
324
310
|
}
|
|
@@ -1251,18 +1237,15 @@ export async function main() {
|
|
|
1251
1237
|
case 'borg_create-cube': {
|
|
1252
1238
|
const name = args?.name as string;
|
|
1253
1239
|
const cubeDirective = args?.cube_directive as string;
|
|
1254
|
-
const templateName = args?.template as string | undefined;
|
|
1240
|
+
const templateName = (args?.template as string | undefined) ?? NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
1255
1241
|
if (!name) throw new Error('name is required');
|
|
1256
1242
|
if (cubeDirective === undefined) throw new Error('cube_directive is required (pass empty string if none)');
|
|
1257
1243
|
|
|
1258
1244
|
// Resolve template (validates name early so the cube isn't
|
|
1259
1245
|
// created in a partial state if the template name is wrong).
|
|
1260
|
-
|
|
1261
|
-
if (
|
|
1262
|
-
template
|
|
1263
|
-
if (!template) {
|
|
1264
|
-
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1265
|
-
}
|
|
1246
|
+
const template = getTemplate(templateName);
|
|
1247
|
+
if (!template) {
|
|
1248
|
+
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1266
1249
|
}
|
|
1267
1250
|
|
|
1268
1251
|
// client#499: the cube binds to an EXPLICIT repository (no cwd
|
|
@@ -1280,6 +1263,7 @@ export async function main() {
|
|
|
1280
1263
|
const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
|
|
1281
1264
|
const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
|
|
1282
1265
|
const { result, cube } = await createCube(name, resolvedCubeDirective, {
|
|
1266
|
+
template: template.name,
|
|
1283
1267
|
message_taxonomy: resolvedMessageTaxonomy,
|
|
1284
1268
|
repository,
|
|
1285
1269
|
workingRepoName,
|
|
@@ -1296,30 +1280,19 @@ export async function main() {
|
|
|
1296
1280
|
};
|
|
1297
1281
|
}
|
|
1298
1282
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const summary = await applyTemplateToCube(cube.id, template);
|
|
1304
|
-
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1305
|
-
? ' Template cube directive applied (operator passed empty).'
|
|
1306
|
-
: '';
|
|
1307
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied — ${summary.created} role(s) created, ${summary.updated} updated.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1308
|
-
return {
|
|
1309
|
-
content: [{ type: 'text', text }],
|
|
1310
|
-
structuredContent: {
|
|
1311
|
-
cube,
|
|
1312
|
-
result: 'created',
|
|
1313
|
-
template: templateName,
|
|
1314
|
-
roles_created: summary.created,
|
|
1315
|
-
roles_updated: summary.updated,
|
|
1316
|
-
},
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
const text = `Created cube **${cube.name}** (id: ${cube.id}). A default "Drone" role was seeded — rename or replace it via borg_update-role / borg_create-role / borg_delete-role. Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1283
|
+
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1284
|
+
? ' Template cube directive applied (operator passed empty).'
|
|
1285
|
+
: '';
|
|
1286
|
+
const text = `Created cube **${cube.name}** (id: ${cube.id}) with template **${templateName}** applied atomically — ${cube.roles.length} role(s) created.${cubeDirectiveNote} Use borg_assimilate ${cube.name} to join as a drone.`;
|
|
1320
1287
|
return {
|
|
1321
1288
|
content: [{ type: 'text', text }],
|
|
1322
|
-
structuredContent: {
|
|
1289
|
+
structuredContent: {
|
|
1290
|
+
cube,
|
|
1291
|
+
result: 'created',
|
|
1292
|
+
template: templateName,
|
|
1293
|
+
roles_created: cube.roles.length,
|
|
1294
|
+
roles_updated: 0,
|
|
1295
|
+
},
|
|
1323
1296
|
};
|
|
1324
1297
|
}
|
|
1325
1298
|
|
package/src/log-stream.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
ErrorCode,
|
|
35
35
|
type DocumentCitation,
|
|
36
36
|
} from 'borgmcp-shared/protocol';
|
|
37
|
-
import { getActiveCube, inboxPathForDrone } from './cubes.js';
|
|
37
|
+
import { getActiveCube, inboxPathForDrone, type ActiveCube } from './cubes.js';
|
|
38
38
|
import { assertUuidShape } from './evict-drone.js';
|
|
39
39
|
import { loadBorgServerTrust } from './server-trust.js';
|
|
40
40
|
import {
|
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
CODEX_HEARTBEAT_CADENCE_MS,
|
|
57
57
|
fireCodexHeartbeatTick,
|
|
58
58
|
formatCodexWakePrompt,
|
|
59
|
-
resolveSessionAgentKind,
|
|
60
59
|
startCodexHeartbeat,
|
|
61
60
|
wakeCodexViaAppServer,
|
|
62
61
|
} from './codex-app-wake.js';
|
|
@@ -420,7 +419,7 @@ export interface StreamDeps {
|
|
|
420
419
|
isSourcePending?: () => Promise<boolean>,
|
|
421
420
|
) => Promise<boolean>;
|
|
422
421
|
/** Inspect whether one retry source remains beyond the durable unread cursor. */
|
|
423
|
-
hasPendingWakeEntry?: (active:
|
|
422
|
+
hasPendingWakeEntry?: (active: StreamActiveCube, entryId: string) => Promise<boolean>;
|
|
424
423
|
/** Clear in-process retry diagnostics after the durable entry is consumed. */
|
|
425
424
|
settleOpenCodeEntry?: (sourceEntryId: string) => void;
|
|
426
425
|
}
|
|
@@ -671,21 +670,17 @@ export function __runLoopForTest(testDeps: RunLoopTestDeps): Promise<void> {
|
|
|
671
670
|
// Single SSE session (exported for tests)
|
|
672
671
|
// ------------------------------------------------------------------
|
|
673
672
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
sessionToken
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
localSessionCredentialRef
|
|
681
|
-
|
|
682
|
-
worktree?: string;
|
|
683
|
-
droneLabel?: string;
|
|
684
|
-
name?: string;
|
|
685
|
-
}
|
|
673
|
+
/** Direct stream controls may omit metadata or omit trust to exercise fail-closed validation. */
|
|
674
|
+
export type StreamActiveCube = Pick<
|
|
675
|
+
ActiveCube,
|
|
676
|
+
'cubeId' | 'droneId' | 'sessionToken' | 'apiUrl'
|
|
677
|
+
> & Partial<Pick<
|
|
678
|
+
ActiveCube,
|
|
679
|
+
'serverTrustIdentity' | 'localSessionCredentialRef' | 'worktree' | 'droneLabel' | 'name'
|
|
680
|
+
>>;
|
|
686
681
|
|
|
687
682
|
export async function streamOnce(
|
|
688
|
-
active:
|
|
683
|
+
active: StreamActiveCube,
|
|
689
684
|
lastEventId: string | null,
|
|
690
685
|
onEventId: (id: string) => void,
|
|
691
686
|
deps: StreamDeps = {}
|
|
@@ -1271,7 +1266,7 @@ export async function streamOnce(
|
|
|
1271
1266
|
}
|
|
1272
1267
|
|
|
1273
1268
|
export async function streamOnceIfOwner(
|
|
1274
|
-
active:
|
|
1269
|
+
active: StreamActiveCube,
|
|
1275
1270
|
lastEventId: string | null,
|
|
1276
1271
|
onEventId: (id: string) => void,
|
|
1277
1272
|
deps: StreamDeps = {}
|
package/src/opencode-drone.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from 'fs';
|
|
15
15
|
import { createHash, randomUUID } from 'crypto';
|
|
16
16
|
import { createServer } from 'node:net';
|
|
17
|
-
import { join } from 'path';
|
|
17
|
+
import { join, resolve } from 'path';
|
|
18
18
|
import { tmpdir } from 'os';
|
|
19
19
|
import {
|
|
20
20
|
borgConfigRoot,
|
|
@@ -44,8 +44,9 @@ const OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES = 64 * 1024;
|
|
|
44
44
|
const diagnosticLogPathsForTests = new Set<string>();
|
|
45
45
|
|
|
46
46
|
function stateIdentityDigest(current: OpenCodeDroneState): string {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Diagnostic files follow the stable worktree location; launch placeholders
|
|
48
|
+
// must not select a second log before session identity resolves.
|
|
49
|
+
return createHash('sha256').update(resolve(current.directory)).digest('hex').slice(0, 24);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function openCodeStartupDiagnosticLogPath(): string {
|
|
@@ -166,6 +167,7 @@ interface OpenCodeDroneState {
|
|
|
166
167
|
directory: string;
|
|
167
168
|
droneLabel: string;
|
|
168
169
|
cubeName: string;
|
|
170
|
+
launchIdentity: string;
|
|
169
171
|
connected: boolean;
|
|
170
172
|
totalEntriesInjected: number;
|
|
171
173
|
totalEntriesRetried: number;
|
|
@@ -204,6 +206,7 @@ interface ConnectDeps {
|
|
|
204
206
|
directory: string;
|
|
205
207
|
droneLabel: string;
|
|
206
208
|
cubeName: string;
|
|
209
|
+
launchIdentity: string;
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
interface OCSession {
|
|
@@ -270,7 +273,7 @@ const OPEN_CODE_RECONCILIATION_ATTEMPTS = 20;
|
|
|
270
273
|
const OPEN_CODE_DELIVERY_HISTORY_LIMIT = 256;
|
|
271
274
|
|
|
272
275
|
interface SessionBinding {
|
|
273
|
-
version:
|
|
276
|
+
version: 5;
|
|
274
277
|
sessionId: string;
|
|
275
278
|
sessionCreatedAt: number;
|
|
276
279
|
knownRootSessionIds: string[];
|
|
@@ -278,6 +281,7 @@ interface SessionBinding {
|
|
|
278
281
|
directory: string;
|
|
279
282
|
droneLabel: string;
|
|
280
283
|
cubeName: string;
|
|
284
|
+
launchIdentity: string;
|
|
281
285
|
pendingSubmissions: Array<{
|
|
282
286
|
entryId: string;
|
|
283
287
|
sourceEntryId: string;
|
|
@@ -285,6 +289,13 @@ interface SessionBinding {
|
|
|
285
289
|
}>;
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
interface SessionBindingClaim {
|
|
293
|
+
version: 1;
|
|
294
|
+
directory: string;
|
|
295
|
+
droneLabel: string;
|
|
296
|
+
cubeName: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
export interface OpenCodeLaunchKickoff {
|
|
289
300
|
prompt: string;
|
|
290
301
|
apiPassword: string;
|
|
@@ -323,6 +334,9 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
323
334
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
324
335
|
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
325
336
|
}
|
|
337
|
+
if (!isOpenCode256BitIdentity(deps.launchIdentity)) {
|
|
338
|
+
throw new OpenCodeAuthenticationError('OpenCode launch identity is missing or unverifiable');
|
|
339
|
+
}
|
|
326
340
|
await ensurePrivateBorgConfigRoot(borgConfigRoot());
|
|
327
341
|
abandonOpenCodeDeliveries(state);
|
|
328
342
|
state = {
|
|
@@ -334,6 +348,7 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
334
348
|
directory: deps.directory,
|
|
335
349
|
droneLabel: deps.droneLabel,
|
|
336
350
|
cubeName: deps.cubeName,
|
|
351
|
+
launchIdentity: deps.launchIdentity,
|
|
337
352
|
connected: true,
|
|
338
353
|
totalEntriesInjected: 0,
|
|
339
354
|
totalEntriesRetried: 0,
|
|
@@ -546,17 +561,28 @@ async function promptSession(id: string, bodyObj: Record<string, unknown>): Prom
|
|
|
546
561
|
|
|
547
562
|
function bindingPath(): string {
|
|
548
563
|
const current = state!;
|
|
549
|
-
const
|
|
564
|
+
const identity = createHash('sha256').update(current.launchIdentity).digest('hex').slice(0, 24);
|
|
565
|
+
const path = join(tmpdir(), `borg-opencode-session-${identity}.json`);
|
|
550
566
|
bindingPathsForTests.add(path);
|
|
551
567
|
return path;
|
|
552
568
|
}
|
|
553
569
|
|
|
570
|
+
function bindingClaimPath(): string {
|
|
571
|
+
return `${bindingPath()}.claim`;
|
|
572
|
+
}
|
|
573
|
+
|
|
554
574
|
function bindingMatchesState(binding: SessionBinding): boolean {
|
|
555
575
|
const current = state!;
|
|
556
|
-
|
|
576
|
+
const sameResolvedSeat = binding.droneLabel === current.droneLabel
|
|
577
|
+
&& binding.cubeName === current.cubeName;
|
|
578
|
+
// The 256-bit launch identity lets the resolved MCP child claim only its
|
|
579
|
+
// launcher's placeholder binding; resolved ownership still requires labels.
|
|
580
|
+
const unclaimedLaunch = binding.droneLabel === 'opencode'
|
|
581
|
+
&& binding.cubeName === 'borg';
|
|
582
|
+
return binding.version === 5
|
|
557
583
|
&& binding.directory === current.directory
|
|
558
|
-
&& binding.
|
|
559
|
-
&&
|
|
584
|
+
&& binding.launchIdentity === current.launchIdentity
|
|
585
|
+
&& (sameResolvedSeat || unclaimedLaunch)
|
|
560
586
|
&& typeof binding.sessionId === 'string'
|
|
561
587
|
&& typeof binding.sessionCreatedAt === 'number'
|
|
562
588
|
&& Array.isArray(binding.knownRootSessionIds)
|
|
@@ -599,15 +625,14 @@ function clearBinding(): void {
|
|
|
599
625
|
|
|
600
626
|
function writeBinding(binding: SessionBinding): boolean {
|
|
601
627
|
const current = state!;
|
|
602
|
-
current.sessionId = binding.sessionId;
|
|
603
|
-
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
604
|
-
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
605
|
-
|
|
606
628
|
try {
|
|
607
629
|
const path = bindingPath();
|
|
608
630
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
609
631
|
writeFileSync(temporary, JSON.stringify(binding), { mode: 0o600 });
|
|
610
632
|
renameSync(temporary, path);
|
|
633
|
+
current.sessionId = binding.sessionId;
|
|
634
|
+
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
635
|
+
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
611
636
|
return true;
|
|
612
637
|
} catch (err) {
|
|
613
638
|
log(`session binding write failed: ${err}`);
|
|
@@ -615,10 +640,46 @@ function writeBinding(binding: SessionBinding): boolean {
|
|
|
615
640
|
}
|
|
616
641
|
}
|
|
617
642
|
|
|
643
|
+
function claimPlaceholderBinding(): boolean {
|
|
644
|
+
const current = state!;
|
|
645
|
+
const claim: SessionBindingClaim = {
|
|
646
|
+
version: 1,
|
|
647
|
+
directory: current.directory,
|
|
648
|
+
droneLabel: current.droneLabel,
|
|
649
|
+
cubeName: current.cubeName,
|
|
650
|
+
};
|
|
651
|
+
const path = bindingClaimPath();
|
|
652
|
+
try {
|
|
653
|
+
writeFileSync(path, JSON.stringify(claim), { mode: 0o600, flag: 'wx' });
|
|
654
|
+
return true;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
657
|
+
log(`session binding claim failed: ${error}`);
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const existing = JSON.parse(readFileSync(path, 'utf8')) as SessionBindingClaim;
|
|
663
|
+
return existing.version === 1
|
|
664
|
+
&& existing.directory === claim.directory
|
|
665
|
+
&& existing.droneLabel === claim.droneLabel
|
|
666
|
+
&& existing.cubeName === claim.cubeName;
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function dropCachedBinding(): void {
|
|
673
|
+
if (!state) return;
|
|
674
|
+
state.sessionId = null;
|
|
675
|
+
state.sessionCreatedAt = null;
|
|
676
|
+
state.knownRootSessionIds = [];
|
|
677
|
+
}
|
|
678
|
+
|
|
618
679
|
function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
619
680
|
const current = state!;
|
|
620
681
|
const binding: SessionBinding = {
|
|
621
|
-
version:
|
|
682
|
+
version: 5,
|
|
622
683
|
sessionId: session.id,
|
|
623
684
|
sessionCreatedAt: session.time.created,
|
|
624
685
|
knownRootSessionIds,
|
|
@@ -626,6 +687,7 @@ function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
|
626
687
|
directory: current.directory,
|
|
627
688
|
droneLabel: current.droneLabel,
|
|
628
689
|
cubeName: current.cubeName,
|
|
690
|
+
launchIdentity: current.launchIdentity,
|
|
629
691
|
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
630
692
|
entryId,
|
|
631
693
|
sourceEntryId: pending.sourceEntryId,
|
|
@@ -639,7 +701,7 @@ function persistCurrentBinding(): boolean {
|
|
|
639
701
|
const current = state;
|
|
640
702
|
if (!current?.sessionId || current.sessionCreatedAt === null) return false;
|
|
641
703
|
return writeBinding({
|
|
642
|
-
version:
|
|
704
|
+
version: 5,
|
|
643
705
|
sessionId: current.sessionId,
|
|
644
706
|
sessionCreatedAt: current.sessionCreatedAt,
|
|
645
707
|
knownRootSessionIds: current.knownRootSessionIds,
|
|
@@ -647,6 +709,7 @@ function persistCurrentBinding(): boolean {
|
|
|
647
709
|
directory: current.directory,
|
|
648
710
|
droneLabel: current.droneLabel,
|
|
649
711
|
cubeName: current.cubeName,
|
|
712
|
+
launchIdentity: current.launchIdentity,
|
|
650
713
|
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
651
714
|
entryId,
|
|
652
715
|
sourceEntryId: pending.sourceEntryId,
|
|
@@ -658,25 +721,34 @@ function persistCurrentBinding(): boolean {
|
|
|
658
721
|
function restoreBinding(): SessionBinding | null {
|
|
659
722
|
if (!state) return null;
|
|
660
723
|
if (state.sessionId && state.sessionCreatedAt !== null) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
droneLabel: state.droneLabel,
|
|
669
|
-
cubeName: state.cubeName,
|
|
670
|
-
pendingSubmissions: [...state.pendingSubmissions].map(([entryId, pending]) => ({
|
|
671
|
-
entryId,
|
|
672
|
-
sourceEntryId: pending.sourceEntryId,
|
|
673
|
-
sessionId: pending.sessionId,
|
|
674
|
-
})),
|
|
675
|
-
};
|
|
724
|
+
const persisted = readBinding();
|
|
725
|
+
if (
|
|
726
|
+
persisted?.droneLabel === state.droneLabel
|
|
727
|
+
&& persisted.cubeName === state.cubeName
|
|
728
|
+
) return persisted;
|
|
729
|
+
dropCachedBinding();
|
|
730
|
+
return null;
|
|
676
731
|
}
|
|
677
732
|
|
|
678
|
-
|
|
733
|
+
let binding = readBinding();
|
|
679
734
|
if (!binding) return null;
|
|
735
|
+
if (binding.droneLabel === 'opencode' && binding.cubeName === 'borg') {
|
|
736
|
+
if (!claimPlaceholderBinding()) return null;
|
|
737
|
+
if (!writeBinding({
|
|
738
|
+
...binding,
|
|
739
|
+
droneLabel: state.droneLabel,
|
|
740
|
+
cubeName: state.cubeName,
|
|
741
|
+
})) return null;
|
|
742
|
+
binding = readBinding();
|
|
743
|
+
if (
|
|
744
|
+
!binding
|
|
745
|
+
|| binding.droneLabel !== state.droneLabel
|
|
746
|
+
|| binding.cubeName !== state.cubeName
|
|
747
|
+
) {
|
|
748
|
+
dropCachedBinding();
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
680
752
|
state.sessionId = binding.sessionId;
|
|
681
753
|
state.sessionCreatedAt = binding.sessionCreatedAt;
|
|
682
754
|
state.knownRootSessionIds = binding.knownRootSessionIds;
|
|
@@ -1560,6 +1632,11 @@ export function __getOpenCodeDiagnosticLogPathForTests(): string {
|
|
|
1560
1632
|
return diagnosticLogPath(state);
|
|
1561
1633
|
}
|
|
1562
1634
|
|
|
1635
|
+
export function __getOpenCodeBindingPathForTests(): string {
|
|
1636
|
+
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1637
|
+
return bindingPath();
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1563
1640
|
export function __getOpenCodeLastObservationForTests(): OpenCodeLastObservation {
|
|
1564
1641
|
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1565
1642
|
return { ...state.lastObservation };
|
|
@@ -1649,6 +1726,11 @@ export function __resetOpenCodeDroneForTests(): void {
|
|
|
1649
1726
|
} catch {
|
|
1650
1727
|
// Already removed.
|
|
1651
1728
|
}
|
|
1729
|
+
try {
|
|
1730
|
+
unlinkSync(`${path}.claim`);
|
|
1731
|
+
} catch {
|
|
1732
|
+
// Already removed.
|
|
1733
|
+
}
|
|
1652
1734
|
}
|
|
1653
1735
|
bindingPathsForTests.clear();
|
|
1654
1736
|
for (const path of diagnosticLogPathsForTests) {
|
|
@@ -44,3 +44,8 @@ export function openCodeApiPasswordFromEnv(env: NodeJS.ProcessEnv): string | nul
|
|
|
44
44
|
const password = env[OPENCODE_SERVER_PASSWORD_ENV];
|
|
45
45
|
return isOpenCode256BitIdentity(password) ? password : null;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export function openCodeLaunchCorrelationFromEnv(env: NodeJS.ProcessEnv): string | null {
|
|
49
|
+
const identity = env[BORG_OPENCODE_LAUNCH_CORRELATION_ENV];
|
|
50
|
+
return isOpenCode256BitIdentity(identity) ? identity : null;
|
|
51
|
+
}
|
package/src/quickstart-cmd.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { LAUNCH_ALL_NO_DISPATCH_EXIT_CODE, runLaunchAll } from './launch-all-cmd
|
|
|
13
13
|
import type { QuickstartArgs } from './parse-quickstart-args.js';
|
|
14
14
|
import { roleSlug, type Role } from './role-resolver.js';
|
|
15
15
|
import { DEFAULT_LOCAL_SERVER_ORIGIN } from './server-handshake.js';
|
|
16
|
+
import { repositoryDiscoveryFailureMessage } from './repository-identity.js';
|
|
16
17
|
|
|
17
18
|
interface PlannedRole {
|
|
18
19
|
name: string;
|
|
@@ -178,8 +179,15 @@ export async function runQuickstart(
|
|
|
178
179
|
let context;
|
|
179
180
|
try {
|
|
180
181
|
context = await assimilate.resolveRepositoryContext(assimilate.cwd());
|
|
181
|
-
} catch {
|
|
182
|
-
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
|
|
184
|
+
context = null;
|
|
185
|
+
} else {
|
|
186
|
+
deps.stderr(
|
|
187
|
+
`borg quickstart: could not inspect this Git repository: ${repositoryDiscoveryFailureMessage(error)}\n`,
|
|
188
|
+
);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
183
191
|
}
|
|
184
192
|
if (!context) {
|
|
185
193
|
deps.stderr('borg quickstart: run this command inside a non-bare Git repository.\n');
|
package/src/regen-format.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { formatDocumentCitations } from './document-render.js';
|
|
|
21
21
|
import { shellEscape } from './shell-escape.js';
|
|
22
22
|
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
23
23
|
import { isBorgSession } from './launch-gate.js';
|
|
24
|
+
import type { AgentKind } from './agent-runtime.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Extract the SessionStart `source` from a Claude Code hook payload (gh#926).
|
|
@@ -72,9 +73,6 @@ export function shouldRelayPlainSessionReminder(args: {
|
|
|
72
73
|
);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/** The agent runtime a session runs under — drives the wake-path branch. */
|
|
76
|
-
export type AgentKind = 'claude' | 'codex' | 'opencode';
|
|
77
|
-
|
|
78
76
|
/**
|
|
79
77
|
* The agent-branched WAKE-PATH ARMING sub-block (gh#929/gh#927) — the single
|
|
80
78
|
* shared "re-establish your wake path" instruction reused by the launch
|
package/src/regen.ts
CHANGED
|
@@ -24,10 +24,9 @@ import {
|
|
|
24
24
|
formatLeanOrientation,
|
|
25
25
|
formatPlainSessionReminder,
|
|
26
26
|
shouldRelayPlainSessionReminder,
|
|
27
|
-
type AgentKind,
|
|
28
27
|
} from './regen-format.js';
|
|
29
28
|
import { resolveSessionAgentKind } from './codex-app-wake.js';
|
|
30
|
-
import { resolveReportableSessionAgentKind } from './agent-runtime.js';
|
|
29
|
+
import { resolveReportableSessionAgentKind, type AgentKind } from './agent-runtime.js';
|
|
31
30
|
import { handleVersionFlag } from './version.js';
|
|
32
31
|
import {
|
|
33
32
|
BORG_LAUNCH_REMINDER_DISABLED_ENV,
|
|
@@ -177,7 +176,7 @@ async function emitUnassimilatedNotice(): Promise<void> {
|
|
|
177
176
|
lines.push('');
|
|
178
177
|
lines.push('## Tools you can call once the user has chosen');
|
|
179
178
|
lines.push('- Join an existing cube: `borg_assimilate cube_name="<their choice>"`');
|
|
180
|
-
lines.push('- Create a new cube: `borg_create-cube name="<name>" cube_directive="<markdown>"` (
|
|
179
|
+
lines.push('- Create a new cube: `borg_create-cube name="<name>" cube_directive="<markdown>"` (defaults to `template="software-dev"`)');
|
|
181
180
|
lines.push('- See available templates: `borg_list-templates`');
|
|
182
181
|
process.stdout.write(lines.join('\n') + '\n');
|
|
183
182
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
} from 'borgmcp-shared/protocol';
|
|
61
61
|
import { Buffer } from 'node:buffer';
|
|
62
62
|
import { canonicalizeWorkingRepoIdentity } from './working-repo.js';
|
|
63
|
-
import { consolePrefix } from './console-prefix.js';
|
|
64
63
|
import { debugLog } from './debug.js';
|
|
65
64
|
import { assertUuidShape } from './evict-drone.js';
|
|
66
65
|
import {
|
|
@@ -70,7 +69,11 @@ import {
|
|
|
70
69
|
DRONE_EVICTED_CODE,
|
|
71
70
|
} from './drone-lifecycle.js';
|
|
72
71
|
import type { MessageTaxonomy, MessageTaxonomyClass } from 'borgmcp-shared/templates';
|
|
73
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
74
|
+
getTemplate,
|
|
75
|
+
type TemplateRole,
|
|
76
|
+
} from 'borgmcp-shared/templates';
|
|
74
77
|
import { parseRoleSections } from 'borgmcp-shared/role-section';
|
|
75
78
|
import type { FragmentView, NonClobberSyncResult } from './sync-roles-render.js';
|
|
76
79
|
import type { WorkingRepo } from './working-repo.js';
|
|
@@ -1521,10 +1524,8 @@ export async function listCubes(connection?: RemoteConnection): Promise<{ cubes:
|
|
|
1521
1524
|
* to surface the interactive template prompt on first-drone bootstrap.
|
|
1522
1525
|
*/
|
|
1523
1526
|
/**
|
|
1524
|
-
* Create a new cube. Server-side seeds
|
|
1525
|
-
* so the cube is assimilatable immediately
|
|
1526
|
-
* atomically when `opts.template` is set (single-withUserId transaction —
|
|
1527
|
-
* skips the auto-Drone insert to avoid is_default partial-index conflict).
|
|
1527
|
+
* Create a new cube. Server-side seeds the selected named template atomically
|
|
1528
|
+
* so the cube is assimilatable immediately.
|
|
1528
1529
|
*
|
|
1529
1530
|
* Returns `{ cube, roles }` — the roles array lets the assimilate
|
|
1530
1531
|
* orchestrator pick a default role without a follow-up `getCube` call.
|
|
@@ -1612,8 +1613,10 @@ export async function createCube(
|
|
|
1612
1613
|
connection?: RemoteConnection,
|
|
1613
1614
|
): Promise<{ result: 'created' | 'resolved'; cube: { id: string; name: string; cube_directive?: string; roles: any[]; drones?: any[]; [k: string]: any } }> {
|
|
1614
1615
|
if (!name?.trim()) throw new Error('Local Borg server cube creation requires a cube name');
|
|
1615
|
-
|
|
1616
|
-
|
|
1616
|
+
const templateName = opts?.template ?? NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
1617
|
+
const template = getTemplate(templateName);
|
|
1618
|
+
if (!template) {
|
|
1619
|
+
throw new Error(`Unknown template "${templateName}". Available: ${NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name).join(', ')}`);
|
|
1617
1620
|
}
|
|
1618
1621
|
if (!opts?.repository || !opts?.workingRepoName) {
|
|
1619
1622
|
throw new Error('Local Borg server cube creation requires an explicit repository identity');
|
|
@@ -1628,7 +1631,7 @@ export async function createCube(
|
|
|
1628
1631
|
name: name.trim(),
|
|
1629
1632
|
working_repo_name: opts.workingRepoName,
|
|
1630
1633
|
repository: opts.repository,
|
|
1631
|
-
template:
|
|
1634
|
+
template: template.name,
|
|
1632
1635
|
}));
|
|
1633
1636
|
// client#499: the server homes one cube per repository. A 'resolved' result
|
|
1634
1637
|
// means this repository already has a cube — report it honestly and DO NOT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
3
|
-
|
|
3
|
+
getTemplate,
|
|
4
4
|
} from 'borgmcp-shared/templates';
|
|
5
5
|
import type {
|
|
6
6
|
AssociateRepositoryCubeResponse,
|
|
@@ -33,7 +33,7 @@ export interface RepositoryCubeCreation {
|
|
|
33
33
|
cube: RepositoryCubeDetail;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type NewCubeTemplate =
|
|
36
|
+
type NewCubeTemplate = CubeTemplate;
|
|
37
37
|
|
|
38
38
|
const NEW_CUBE_TEMPLATE_NAMES: readonly NewCubeTemplate[] =
|
|
39
39
|
NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name);
|
|
@@ -110,10 +110,7 @@ export function validRepositoryCubeName(value: string): boolean {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function presentation(template: CubeTemplate) {
|
|
113
|
-
|
|
114
|
-
return { name: template, label: LEGACY_DEFAULT_TEMPLATE_LABEL };
|
|
115
|
-
}
|
|
116
|
-
return NEW_CUBE_TEMPLATE_PRESENTATIONS.find((candidate) => candidate.name === template)!;
|
|
113
|
+
return getTemplate(template)!;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
async function ask(
|