borgmcp 4.6.2 → 4.8.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 +7 -0
- package/dist/assimilate-cmd.d.ts +8 -23
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +8 -5
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +1 -2
- package/dist/assimilate-deps.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.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/docs-sections.js +1 -1
- package/dist/docs-sections.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 +36 -52
- package/dist/index.js.map +1 -1
- package/dist/log-provenance.d.ts +20 -0
- package/dist/log-provenance.d.ts.map +1 -0
- package/dist/log-provenance.js +96 -0
- package/dist/log-provenance.js.map +1 -0
- 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/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 +4 -3
- 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.d.ts.map +1 -1
- package/dist/tool-manifest.js +12 -4
- 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 +28 -37
- package/src/assimilate-deps.ts +0 -4
- package/src/cli-help.ts +1 -0
- package/src/codex-launch.ts +2 -1
- package/src/config.ts +58 -22
- package/src/cubes.ts +0 -10
- package/src/docs-sections.ts +1 -1
- package/src/gc-orphan-inboxes.ts +1 -1
- package/src/index.ts +41 -56
- package/src/log-provenance.ts +119 -0
- package/src/log-stream.ts +12 -17
- package/src/quickstart-cmd.ts +10 -2
- package/src/regen-format.ts +5 -6
- 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 +12 -4
- 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/cli-help.ts
CHANGED
|
@@ -172,6 +172,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
172
172
|
` borg Show the launch menu in a repository root; resume directly in a linked worktree\n` +
|
|
173
173
|
` borg setup Set up borg MCP server + agent CLI integration\n` +
|
|
174
174
|
` borg update Update the client and installed local server together\n` +
|
|
175
|
+
` borg upgrade Alias for borg update\n` +
|
|
175
176
|
` borg doctor Check agent hook commands, OpenCode integration, and startup diagnostics\n` +
|
|
176
177
|
` borg clone <url> [dir] Clone a repository, then create and launch its cube\n` +
|
|
177
178
|
` borg quickstart Create a cube and a drone for every role, then launch them\n` +
|
package/src/codex-launch.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BorgCli } from './cubes.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { AgentKind } from './agent-runtime.js';
|
|
3
|
+
import { wakePathArming } from './regen-format.js';
|
|
3
4
|
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
4
5
|
|
|
5
6
|
/**
|
package/src/config.ts
CHANGED
|
@@ -31,7 +31,8 @@ const SERVER_CREDENTIAL_RECORD_VERSION = 2 as const;
|
|
|
31
31
|
const SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 3 as const;
|
|
32
32
|
const LEGACY_SERVER_PENDING_ENROLLMENT_RECORD_VERSION = 1 as const;
|
|
33
33
|
const SERVER_ACCEPTED_ENROLLMENT_MARKER_VERSION = 1 as const;
|
|
34
|
-
const SERVER_CUBE_RETRY_RECORD_VERSION =
|
|
34
|
+
const SERVER_CUBE_RETRY_RECORD_VERSION = 3 as const;
|
|
35
|
+
const LEGACY_SERVER_CUBE_RETRY_RECORD_VERSION = 2 as const;
|
|
35
36
|
// The 0600 credential store (Queen rescope: replaces the OS keychain). A single
|
|
36
37
|
// file holds every parent credential/enrollment record; a single flock
|
|
37
38
|
// serializes every mutator + observer that must (SR-seven #4).
|
|
@@ -326,7 +327,7 @@ async function writeServerCredentialRecord(
|
|
|
326
327
|
}
|
|
327
328
|
if (backend.entries) {
|
|
328
329
|
const accounts = await backend.entries();
|
|
329
|
-
const conflicts = Object.entries(accounts).filter(([
|
|
330
|
+
const conflicts = Object.entries(accounts).filter(([, value]) => {
|
|
330
331
|
try {
|
|
331
332
|
const parsed = JSON.parse(value) as { version?: unknown; origin?: unknown };
|
|
332
333
|
return parsed.version === SERVER_CREDENTIAL_RECORD_VERSION && parsed.origin === record.origin;
|
|
@@ -1106,39 +1107,74 @@ export async function getOrCreatePendingServerCubeCreation(
|
|
|
1106
1107
|
return withCredentialStoreLock(async () => {
|
|
1107
1108
|
const stored = await backend.get(account);
|
|
1108
1109
|
if (stored) {
|
|
1110
|
+
let record: Omit<Partial<PendingServerCubeCreationRecord>, 'template'> & {
|
|
1111
|
+
version?: unknown;
|
|
1112
|
+
state?: unknown;
|
|
1113
|
+
template?: unknown;
|
|
1114
|
+
};
|
|
1109
1115
|
try {
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1116
|
+
const parsed: unknown = JSON.parse(stored);
|
|
1117
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid record');
|
|
1118
|
+
record = parsed as typeof record;
|
|
1119
|
+
} catch {
|
|
1120
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1121
|
+
}
|
|
1122
|
+
const retryKey = typeof record.retryKey === 'string' && UUID_RE.test(record.retryKey)
|
|
1123
|
+
? record.retryKey
|
|
1124
|
+
: null;
|
|
1125
|
+
const sameOperation =
|
|
1126
|
+
record.state === 'pending' &&
|
|
1127
|
+
record.origin === input.origin &&
|
|
1128
|
+
record.trustIdentity === input.trustIdentity &&
|
|
1129
|
+
record.clientId === input.clientId &&
|
|
1130
|
+
record.repositoryBinding === repositoryBinding &&
|
|
1131
|
+
record.name === input.name &&
|
|
1132
|
+
record.workingRepoName === input.workingRepoName &&
|
|
1133
|
+
retryKey !== null;
|
|
1134
|
+
if (sameOperation && record.version === SERVER_CUBE_RETRY_RECORD_VERSION && record.template === input.template) {
|
|
1135
|
+
return {
|
|
1136
|
+
origin: input.origin,
|
|
1137
|
+
trustIdentity: input.trustIdentity,
|
|
1138
|
+
clientId: input.clientId,
|
|
1139
|
+
repositoryBinding,
|
|
1140
|
+
retryKey,
|
|
1141
|
+
name: input.name,
|
|
1142
|
+
workingRepoName: input.workingRepoName,
|
|
1143
|
+
repository: input.repository,
|
|
1144
|
+
template: input.template,
|
|
1113
1145
|
};
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
record.clientId !== input.clientId ||
|
|
1120
|
-
record.repositoryBinding !== repositoryBinding ||
|
|
1121
|
-
record.name !== input.name ||
|
|
1122
|
-
record.workingRepoName !== input.workingRepoName ||
|
|
1123
|
-
record.template !== input.template ||
|
|
1124
|
-
typeof record.retryKey !== 'string' || !UUID_RE.test(record.retryKey)
|
|
1125
|
-
) {
|
|
1126
|
-
throw new Error('mismatch');
|
|
1146
|
+
}
|
|
1147
|
+
if (sameOperation && record.version === LEGACY_SERVER_CUBE_RETRY_RECORD_VERSION) {
|
|
1148
|
+
const preserveRetryKey = record.template === input.template;
|
|
1149
|
+
if (!preserveRetryKey && record.template !== 'default') {
|
|
1150
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1127
1151
|
}
|
|
1128
|
-
|
|
1152
|
+
const replacement: PendingServerCubeCreationRecord = {
|
|
1129
1153
|
origin: input.origin,
|
|
1130
1154
|
trustIdentity: input.trustIdentity,
|
|
1131
1155
|
clientId: input.clientId,
|
|
1132
1156
|
repositoryBinding,
|
|
1133
|
-
retryKey:
|
|
1157
|
+
retryKey: preserveRetryKey ? retryKey : randomUUID(),
|
|
1134
1158
|
name: input.name,
|
|
1135
1159
|
workingRepoName: input.workingRepoName,
|
|
1136
1160
|
repository: input.repository,
|
|
1137
1161
|
template: input.template,
|
|
1138
1162
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1163
|
+
await backend.set(account, JSON.stringify({
|
|
1164
|
+
version: SERVER_CUBE_RETRY_RECORD_VERSION,
|
|
1165
|
+
state: 'pending',
|
|
1166
|
+
origin: replacement.origin,
|
|
1167
|
+
trustIdentity: replacement.trustIdentity,
|
|
1168
|
+
clientId: replacement.clientId,
|
|
1169
|
+
repositoryBinding: replacement.repositoryBinding,
|
|
1170
|
+
retryKey: replacement.retryKey,
|
|
1171
|
+
name: replacement.name,
|
|
1172
|
+
workingRepoName: replacement.workingRepoName,
|
|
1173
|
+
template: replacement.template,
|
|
1174
|
+
}));
|
|
1175
|
+
return replacement;
|
|
1141
1176
|
}
|
|
1177
|
+
throw new Error('pending Borg server cube creation does not match this repository');
|
|
1142
1178
|
}
|
|
1143
1179
|
const record: PendingServerCubeCreationRecord = {
|
|
1144
1180
|
origin: input.origin,
|
package/src/cubes.ts
CHANGED
|
@@ -498,16 +498,6 @@ export async function findRemainingActiveSeatForWorktree(worktree: string): Prom
|
|
|
498
498
|
return record ? { apiUrl: record.origin, operation: record.operation } : null;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
/**
|
|
502
|
-
* Legacy binding-only writer. In the collapsed single-store model an ACTIVE seat is
|
|
503
|
-
* created ONLY by the atomic mint→activate+bind path in seats.ts (driven by the
|
|
504
|
-
* attach FINALIZE); there is no standalone binding write. Retained solely as a
|
|
505
|
-
* fail-closed guard for incomplete session metadata.
|
|
506
|
-
*/
|
|
507
|
-
export async function setActiveCube(_active: ActiveCubeInput): Promise<void> {
|
|
508
|
-
throw new Error('local Borg server session metadata is incomplete');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
501
|
export function activeCubeWithFreshRegenIdentity(
|
|
512
502
|
active: ActiveCube,
|
|
513
503
|
result: {
|
package/src/docs-sections.ts
CHANGED
|
@@ -40,7 +40,7 @@ export const DOCS_SECTIONS: DocsSection[] = [
|
|
|
40
40
|
title: "Core concepts",
|
|
41
41
|
url: `${SITE_URL}/docs/concepts/`,
|
|
42
42
|
summary: "Cubes, drones, roles, activity-log signals, explicit audiences, claims, and decisions.",
|
|
43
|
-
keywords: ["cube", "drone", "role", "log", "signal", "claim", "decision", "coordinate", "coordination", "routing", "recipient", "audience", "selector", "direct", "broadcast"],
|
|
43
|
+
keywords: ["cube", "drone", "role", "log", "signal", "claim", "decision", "coordinate", "coordination", "routing", "recipient", "audience", "selector", "direct", "broadcast", "refs", "sha", "provenance"],
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
slug: "install",
|
package/src/gc-orphan-inboxes.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* always err toward KEEPING.
|
|
15
15
|
*/
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
|
-
import { readFileSync, readdirSync, statSync
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import {
|
|
20
20
|
pidfilePathFor,
|
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,
|
|
@@ -117,6 +118,13 @@ import {
|
|
|
117
118
|
renderRuntimeMetadataLines,
|
|
118
119
|
} from './roster-render.js';
|
|
119
120
|
import { resolveWorkingRepo } from './working-repo.js';
|
|
121
|
+
import {
|
|
122
|
+
auditMessageShas,
|
|
123
|
+
renderProvenance,
|
|
124
|
+
requiresRefs,
|
|
125
|
+
resolveRefs,
|
|
126
|
+
validateRefs,
|
|
127
|
+
} from './log-provenance.js';
|
|
120
128
|
import {
|
|
121
129
|
CubeDeletedError,
|
|
122
130
|
DroneEvictedError,
|
|
@@ -176,27 +184,6 @@ import {
|
|
|
176
184
|
|
|
177
185
|
const OPEN_CODE_IDENTITY_HANDSHAKE_TIMEOUT_MS = 5_000;
|
|
178
186
|
|
|
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
187
|
export async function runApplyTemplateTool(
|
|
201
188
|
cubeId: string,
|
|
202
189
|
template: Template,
|
|
@@ -1039,13 +1026,21 @@ export async function main() {
|
|
|
1039
1026
|
const to = normalizeLogAudience(args?.to);
|
|
1040
1027
|
const active = await getActiveCube();
|
|
1041
1028
|
if (!active) throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
|
|
1029
|
+
const refs = validateRefs(args?.refs);
|
|
1030
|
+
if (requiresRefs(message) && refs.length === 0) {
|
|
1031
|
+
throw new Error('REVIEW-READY messages require refs. Pass refs: ["HEAD", "origin/<branch>", "origin/main"].');
|
|
1032
|
+
}
|
|
1033
|
+
const repository = active.worktree ?? findProjectRoot();
|
|
1034
|
+
const shaAudit = auditMessageShas(message, repository);
|
|
1035
|
+
if (shaAudit.refusal) throw new Error(shaAudit.refusal);
|
|
1036
|
+
const finalMessage = `${message}${renderProvenance(resolveRefs(refs, repository))}`;
|
|
1042
1037
|
seedDisplayIdentity(active);
|
|
1043
1038
|
const displayIdentity = renderDisplayIdentity(active);
|
|
1044
|
-
const lifecycleSignal = lifecycleSignalForMessage(
|
|
1039
|
+
const lifecycleSignal = lifecycleSignalForMessage(finalMessage);
|
|
1045
1040
|
if (lifecycleSignal) {
|
|
1046
|
-
const decision = await shouldSuppressLifecycleLog(active,
|
|
1041
|
+
const decision = await shouldSuppressLifecycleLog(active, finalMessage);
|
|
1047
1042
|
if (decision.suppress) {
|
|
1048
|
-
await recordLifecycleLog(active,
|
|
1043
|
+
await recordLifecycleLog(active, finalMessage);
|
|
1049
1044
|
if (lifecycleSignal === 'arrival') markArrivalAnnouncedThisProcess();
|
|
1050
1045
|
return {
|
|
1051
1046
|
content: [
|
|
@@ -1075,8 +1070,8 @@ export async function main() {
|
|
|
1075
1070
|
...(documents ? { documents } : {}),
|
|
1076
1071
|
serverTrustIdentity: active.serverTrustIdentity,
|
|
1077
1072
|
};
|
|
1078
|
-
const result = await appendLog(active.sessionToken, active.apiUrl,
|
|
1079
|
-
await recordLifecycleLog(active,
|
|
1073
|
+
const result = await appendLog(active.sessionToken, active.apiUrl, finalMessage, appendOpts);
|
|
1074
|
+
await recordLifecycleLog(active, finalMessage);
|
|
1080
1075
|
if (lifecycleSignal === 'arrival') markArrivalAnnouncedThisProcess();
|
|
1081
1076
|
let recipientDrones: any[] = [];
|
|
1082
1077
|
if (result.entry.visibility === 'direct' && result.entry.recipient_drone_ids.length > 0) {
|
|
@@ -1104,7 +1099,10 @@ export async function main() {
|
|
|
1104
1099
|
const advisory = result.advisory?.code === 'STORE_AS_DOCUMENT'
|
|
1105
1100
|
? `\nAdvisory: this message exceeded ${result.advisory.threshold_bytes} UTF-8 bytes. Store durable detail with borg_put-document, then cite its full id in borg_log.documents with an explicit borg_log.to audience.`
|
|
1106
1101
|
: '';
|
|
1107
|
-
const
|
|
1102
|
+
const shaAdvisory = shaAudit.unverified.length > 0
|
|
1103
|
+
? `\nAdvisory: unverified Git SHA(s): ${shaAudit.unverified.join(', ')}`
|
|
1104
|
+
: '';
|
|
1105
|
+
const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${routed}${unreachable}${citations}${advisory}${shaAdvisory}`;
|
|
1108
1106
|
return {
|
|
1109
1107
|
content: [{ type: 'text', text }],
|
|
1110
1108
|
structuredContent: {
|
|
@@ -1257,18 +1255,15 @@ export async function main() {
|
|
|
1257
1255
|
case 'borg_create-cube': {
|
|
1258
1256
|
const name = args?.name as string;
|
|
1259
1257
|
const cubeDirective = args?.cube_directive as string;
|
|
1260
|
-
const templateName = args?.template as string | undefined;
|
|
1258
|
+
const templateName = (args?.template as string | undefined) ?? NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
1261
1259
|
if (!name) throw new Error('name is required');
|
|
1262
1260
|
if (cubeDirective === undefined) throw new Error('cube_directive is required (pass empty string if none)');
|
|
1263
1261
|
|
|
1264
1262
|
// Resolve template (validates name early so the cube isn't
|
|
1265
1263
|
// created in a partial state if the template name is wrong).
|
|
1266
|
-
|
|
1267
|
-
if (
|
|
1268
|
-
template
|
|
1269
|
-
if (!template) {
|
|
1270
|
-
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1271
|
-
}
|
|
1264
|
+
const template = getTemplate(templateName);
|
|
1265
|
+
if (!template) {
|
|
1266
|
+
throw new Error(`Unknown template "${templateName}". Available: ${listTemplateNames().join(', ')}`);
|
|
1272
1267
|
}
|
|
1273
1268
|
|
|
1274
1269
|
// client#499: the cube binds to an EXPLICIT repository (no cwd
|
|
@@ -1286,6 +1281,7 @@ export async function main() {
|
|
|
1286
1281
|
const resolvedCubeDirective = resolveCubeDirectiveForCreate(cubeDirective, template);
|
|
1287
1282
|
const resolvedMessageTaxonomy = resolveMessageTaxonomyForCreate(undefined, template);
|
|
1288
1283
|
const { result, cube } = await createCube(name, resolvedCubeDirective, {
|
|
1284
|
+
template: template.name,
|
|
1289
1285
|
message_taxonomy: resolvedMessageTaxonomy,
|
|
1290
1286
|
repository,
|
|
1291
1287
|
workingRepoName,
|
|
@@ -1302,30 +1298,19 @@ export async function main() {
|
|
|
1302
1298
|
};
|
|
1303
1299
|
}
|
|
1304
1300
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const summary = await applyTemplateToCube(cube.id, template);
|
|
1310
|
-
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1311
|
-
? ' Template cube directive applied (operator passed empty).'
|
|
1312
|
-
: '';
|
|
1313
|
-
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.`;
|
|
1314
|
-
return {
|
|
1315
|
-
content: [{ type: 'text', text }],
|
|
1316
|
-
structuredContent: {
|
|
1317
|
-
cube,
|
|
1318
|
-
result: 'created',
|
|
1319
|
-
template: templateName,
|
|
1320
|
-
roles_created: summary.created,
|
|
1321
|
-
roles_updated: summary.updated,
|
|
1322
|
-
},
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
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.`;
|
|
1301
|
+
const cubeDirectiveNote = resolvedCubeDirective !== cubeDirective
|
|
1302
|
+
? ' Template cube directive applied (operator passed empty).'
|
|
1303
|
+
: '';
|
|
1304
|
+
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.`;
|
|
1326
1305
|
return {
|
|
1327
1306
|
content: [{ type: 'text', text }],
|
|
1328
|
-
structuredContent: {
|
|
1307
|
+
structuredContent: {
|
|
1308
|
+
cube,
|
|
1309
|
+
result: 'created',
|
|
1310
|
+
template: templateName,
|
|
1311
|
+
roles_created: cube.roles.length,
|
|
1312
|
+
roles_updated: 0,
|
|
1313
|
+
},
|
|
1329
1314
|
};
|
|
1330
1315
|
}
|
|
1331
1316
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface GitResult {
|
|
4
|
+
status: number | null;
|
|
5
|
+
stdout?: string | null;
|
|
6
|
+
stderr?: string | null;
|
|
7
|
+
error?: Error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type RunGit = (cwd: string, args: string[]) => GitResult;
|
|
11
|
+
|
|
12
|
+
export interface ResolvedRef {
|
|
13
|
+
ref: string;
|
|
14
|
+
sha: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FULL_SHA_RE = /^[0-9a-f]{40}$/u;
|
|
18
|
+
const MESSAGE_SHA_RE = /(?<![0-9a-f])[0-9a-f]{40}(?![0-9a-f])/gu;
|
|
19
|
+
|
|
20
|
+
function defaultRunGit(cwd: string, args: string[]): GitResult {
|
|
21
|
+
const result = spawnSync('git', args, {
|
|
22
|
+
cwd,
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
status: result.status,
|
|
28
|
+
stdout: result.stdout,
|
|
29
|
+
stderr: result.stderr,
|
|
30
|
+
error: result.error,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function gitFailure(result: GitResult, fallback: string): string {
|
|
35
|
+
if (result.error) {
|
|
36
|
+
if ((result.error as NodeJS.ErrnoException).code === 'ENOENT') return 'not a git repository';
|
|
37
|
+
return result.error.message.replace(/[\x00-\x1F\x7F]/gu, ' ').trim() || fallback;
|
|
38
|
+
}
|
|
39
|
+
return result.stderr?.replace(/[\x00-\x1F\x7F]/gu, ' ').trim() || fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveCommit(ref: string, cwd: string, runGit: RunGit): GitResult & { sha?: string } {
|
|
43
|
+
let result: GitResult;
|
|
44
|
+
try {
|
|
45
|
+
result = runGit(cwd, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${ref}^{commit}`]);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { status: null, error: error instanceof Error ? error : new Error(String(error)) };
|
|
48
|
+
}
|
|
49
|
+
const sha = result.status === 0 ? result.stdout?.trim() : undefined;
|
|
50
|
+
return FULL_SHA_RE.test(sha ?? '') ? { ...result, sha } : result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateRefs(value: unknown): string[] {
|
|
54
|
+
if (value === undefined) return [];
|
|
55
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
|
|
56
|
+
throw new Error('refs must be an array of 1-8 unique Git refs');
|
|
57
|
+
}
|
|
58
|
+
const refs: string[] = [];
|
|
59
|
+
for (const valueRef of value) {
|
|
60
|
+
if (typeof valueRef !== 'string' || valueRef.length === 0) {
|
|
61
|
+
throw new Error('each refs entry must be a non-empty string');
|
|
62
|
+
}
|
|
63
|
+
if (Buffer.byteLength(valueRef, 'utf8') > 200) {
|
|
64
|
+
throw new Error(`Git ref exceeds 200 UTF-8 bytes: ${valueRef.slice(0, 40)}`);
|
|
65
|
+
}
|
|
66
|
+
if (valueRef.startsWith('-')) throw new Error(`Git ref must not start with "-": ${valueRef}`);
|
|
67
|
+
if (/[\s\x00-\x1F\x7F]/u.test(valueRef)) {
|
|
68
|
+
throw new Error(`Git ref must not contain whitespace or control characters: ${JSON.stringify(valueRef)}`);
|
|
69
|
+
}
|
|
70
|
+
if (refs.includes(valueRef)) throw new Error(`Git ref is duplicated: ${valueRef}`);
|
|
71
|
+
refs.push(valueRef);
|
|
72
|
+
}
|
|
73
|
+
return refs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveRefs(
|
|
77
|
+
refs: string[],
|
|
78
|
+
cwd: string,
|
|
79
|
+
runGit: RunGit = defaultRunGit,
|
|
80
|
+
): ResolvedRef[] {
|
|
81
|
+
return refs.map((ref) => {
|
|
82
|
+
const result = resolveCommit(ref, cwd, runGit);
|
|
83
|
+
if (!result.sha) {
|
|
84
|
+
throw new Error(`Could not resolve Git ref "${ref}": ${gitFailure(result, 'ref did not resolve to a commit')}`);
|
|
85
|
+
}
|
|
86
|
+
return { ref, sha: result.sha };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function auditMessageShas(
|
|
91
|
+
message: string,
|
|
92
|
+
cwd: string,
|
|
93
|
+
runGit: RunGit = defaultRunGit,
|
|
94
|
+
): { refusal?: string; unverified: string[] } {
|
|
95
|
+
const shas = [...new Set(message.match(MESSAGE_SHA_RE) ?? [])];
|
|
96
|
+
const unverified: string[] = [];
|
|
97
|
+
for (const sha of shas) {
|
|
98
|
+
if (resolveCommit(sha, cwd, runGit).sha) continue;
|
|
99
|
+
const prefixResult = resolveCommit(sha.slice(0, 7), cwd, runGit);
|
|
100
|
+
if (prefixResult.sha && prefixResult.sha !== sha) {
|
|
101
|
+
return {
|
|
102
|
+
refusal: `Message SHA ${sha} does not exist, but its 7-character prefix resolves to ${prefixResult.sha}. Pass Git refs through the refs parameter instead of typing a full SHA.`,
|
|
103
|
+
unverified,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (!prefixResult.sha) unverified.push(sha);
|
|
107
|
+
}
|
|
108
|
+
return { unverified };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderProvenance(resolved: ResolvedRef[]): string {
|
|
112
|
+
return resolved.length === 0
|
|
113
|
+
? ''
|
|
114
|
+
: `\n\n${resolved.map(({ ref, sha }) => `${ref} = ${sha}`).join('\n')}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function requiresRefs(message: string): boolean {
|
|
118
|
+
return /^REVIEW-READY(?:\b|$)/u.test(message);
|
|
119
|
+
}
|
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/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
|
|
@@ -242,6 +240,7 @@ export function markArrivalAnnouncedThisProcess(): void {
|
|
|
242
240
|
// acknowledgement-status query; this marker pins its required entry id.
|
|
243
241
|
// copy-param-claim: borg_read-entry.entry_id
|
|
244
242
|
// copy-param-claim: borg_log.to
|
|
243
|
+
// copy-param-claim: borg_log.refs
|
|
245
244
|
// copy-param-claim: borg_docs.topic
|
|
246
245
|
// The playbook below points drones to `borg_docs {topic}` for user questions
|
|
247
246
|
// about how Borg MCP works; this marker pins the param so the #490/#529 guard
|
|
@@ -263,14 +262,14 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
|
263
262
|
- \`borg_roster\` — see who else is connected
|
|
264
263
|
- \`borg_read-log unread_only=true [limit]\` — drain unread log entries from your server-side cursor
|
|
265
264
|
- \`borg_read-entry entry_id=<id>\` — read one known complete entry without moving the unread cursor
|
|
266
|
-
- \`borg_log message="<message>" to="broadcast"|["<selector>"]\` — append with an explicit audience
|
|
265
|
+
- \`borg_log message="<message>" to="broadcast"|["<selector>"] refs=["HEAD","origin/<branch>","origin/main"]\` — append with an explicit audience and optional mechanically resolved Git provenance; REVIEW-READY requires \`refs\`
|
|
267
266
|
- \`borg_assimilate <cube>\` — switch to a different cube
|
|
268
267
|
|
|
269
268
|
**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. Every \`borg_log\` call must choose its audience with \`to: "broadcast"\` or a non-empty selector array; omission, message text, prefixes, and classes never choose recipients.
|
|
270
269
|
|
|
271
270
|
**Communication discipline for non-human seats:**
|
|
272
271
|
- **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.
|
|
273
|
-
- **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.
|
|
272
|
+
- **Log:** a post must change what another seat does. Otherwise, do not write it. Keep posts short: lifecycle signal + mechanically resolved SHA and nothing else; defect + location/evidence; correction to your live claim; or a genuine blocking question. Post REVIEW-READY with \`refs: ["HEAD","origin/<branch>","origin/main"]\`.
|
|
274
273
|
- **Do not post:** plans, work-in-progress/progress narration, method or reasoning, restatements/agreement/credit, self-examination, framing phrases, or coordination commentary.
|
|
275
274
|
- **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.
|
|
276
275
|
|
|
@@ -373,7 +372,7 @@ The discipline applies at FOUR surfaces. Catches at the surface closest to origi
|
|
|
373
372
|
- Start each new work item in that stable worktree with \`git checkout -b <branch>\`. Do not create a new worktree or folder for each item.
|
|
374
373
|
- Hand a branch to another seat only through an explicit log event.
|
|
375
374
|
- Treat branch history as shared: another seat may have the branch checked out or fetched, so never rebase or force-push it.
|
|
376
|
-
- Hand over
|
|
375
|
+
- Hand over refs and exact commit SHAs, never a filesystem path. Post REVIEW-READY through \`borg_log\` with \`refs: ["HEAD","origin/<branch>","origin/main"]\`; a reviewing seat checks out the resolved SHA in its own worktree with \`git checkout --detach <SHA>\` and never reads another seat's folder.
|
|
377
376
|
- With no hosted remote, the commit is the durable handover artifact because clone-family worktrees share refs; omit the push step. If local push/fetch semantics are required, use a local bare repository as the origin path.
|
|
378
377
|
- Put detached review checkouts, clean-environment rigs, fake HOMEs, unpacked artifacts, and throwaway worktrees under \`~/.borg/scratch/<your-seat-label>/\`. Never use \`/tmp\` or an ad-hoc path. Scratch contents are disposable and must be cleaned up with the work.
|
|
379
378
|
- When an origin exists, synchronize with merge-only history using \`git fetch origin && git merge origin/main\`.`;
|
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
|
}
|