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.
Files changed (108) hide show
  1. package/README.md +7 -0
  2. package/dist/assimilate-cmd.d.ts +8 -23
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +8 -5
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +1 -2
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/cli-help.d.ts.map +1 -1
  10. package/dist/cli-help.js +1 -0
  11. package/dist/cli-help.js.map +1 -1
  12. package/dist/codex-launch.d.ts +1 -1
  13. package/dist/codex-launch.d.ts.map +1 -1
  14. package/dist/codex-launch.js.map +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +55 -18
  17. package/dist/config.js.map +1 -1
  18. package/dist/cubes.d.ts +0 -7
  19. package/dist/cubes.d.ts.map +1 -1
  20. package/dist/cubes.js +0 -9
  21. package/dist/cubes.js.map +1 -1
  22. package/dist/docs-sections.js +1 -1
  23. package/dist/docs-sections.js.map +1 -1
  24. package/dist/gc-orphan-inboxes.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +36 -52
  27. package/dist/index.js.map +1 -1
  28. package/dist/log-provenance.d.ts +20 -0
  29. package/dist/log-provenance.d.ts.map +1 -0
  30. package/dist/log-provenance.js +96 -0
  31. package/dist/log-provenance.js.map +1 -0
  32. package/dist/log-stream.d.ts +6 -16
  33. package/dist/log-stream.d.ts.map +1 -1
  34. package/dist/log-stream.js.map +1 -1
  35. package/dist/quickstart-cmd.d.ts.map +1 -1
  36. package/dist/quickstart-cmd.js +9 -2
  37. package/dist/quickstart-cmd.js.map +1 -1
  38. package/dist/regen-format.d.ts +1 -2
  39. package/dist/regen-format.d.ts.map +1 -1
  40. package/dist/regen-format.js +4 -3
  41. package/dist/regen-format.js.map +1 -1
  42. package/dist/regen.js +1 -1
  43. package/dist/regen.js.map +1 -1
  44. package/dist/remote-client.d.ts.map +1 -1
  45. package/dist/remote-client.js +8 -8
  46. package/dist/remote-client.js.map +1 -1
  47. package/dist/repository-cube-init.d.ts +1 -1
  48. package/dist/repository-cube-init.d.ts.map +1 -1
  49. package/dist/repository-cube-init.js +2 -5
  50. package/dist/repository-cube-init.js.map +1 -1
  51. package/dist/repository-identity.d.ts +14 -4
  52. package/dist/repository-identity.d.ts.map +1 -1
  53. package/dist/repository-identity.js +86 -18
  54. package/dist/repository-identity.js.map +1 -1
  55. package/dist/self-path.d.ts.map +1 -1
  56. package/dist/self-path.js.map +1 -1
  57. package/dist/server-handshake.d.ts +1 -1
  58. package/dist/server-handshake.d.ts.map +1 -1
  59. package/dist/server-handshake.js +0 -1
  60. package/dist/server-handshake.js.map +1 -1
  61. package/dist/tool-manifest.d.ts.map +1 -1
  62. package/dist/tool-manifest.js +12 -4
  63. package/dist/tool-manifest.js.map +1 -1
  64. package/dist/unknown-subcommand.d.ts +1 -1
  65. package/dist/unknown-subcommand.d.ts.map +1 -1
  66. package/dist/unknown-subcommand.js +1 -0
  67. package/dist/unknown-subcommand.js.map +1 -1
  68. package/dist/update-cmd.js +1 -1
  69. package/dist/update-cmd.js.map +1 -1
  70. package/docs/RELEASING.md +5 -4
  71. package/package.json +2 -2
  72. package/src/assimilate-cmd.ts +28 -37
  73. package/src/assimilate-deps.ts +0 -4
  74. package/src/cli-help.ts +1 -0
  75. package/src/codex-launch.ts +2 -1
  76. package/src/config.ts +58 -22
  77. package/src/cubes.ts +0 -10
  78. package/src/docs-sections.ts +1 -1
  79. package/src/gc-orphan-inboxes.ts +1 -1
  80. package/src/index.ts +41 -56
  81. package/src/log-provenance.ts +119 -0
  82. package/src/log-stream.ts +12 -17
  83. package/src/quickstart-cmd.ts +10 -2
  84. package/src/regen-format.ts +5 -6
  85. package/src/regen.ts +2 -3
  86. package/src/remote-client.ts +12 -9
  87. package/src/repository-cube-init.ts +3 -6
  88. package/src/repository-identity.ts +125 -20
  89. package/src/self-path.ts +0 -1
  90. package/src/server-handshake.ts +1 -2
  91. package/src/tool-manifest.ts +12 -4
  92. package/src/unknown-subcommand.ts +1 -0
  93. package/src/update-cmd.ts +1 -1
  94. package/dist/postinstall-banner.d.ts +0 -2
  95. package/dist/postinstall-banner.d.ts.map +0 -1
  96. package/dist/postinstall-banner.js +0 -27
  97. package/dist/postinstall-banner.js.map +0 -1
  98. package/dist/postinstall.d.ts +0 -11
  99. package/dist/postinstall.d.ts.map +0 -1
  100. package/dist/postinstall.js +0 -33
  101. package/dist/postinstall.js.map +0 -1
  102. package/dist/types.d.ts +0 -5
  103. package/dist/types.d.ts.map +0 -1
  104. package/dist/types.js +0 -5
  105. package/dist/types.js.map +0 -1
  106. package/src/postinstall-banner.ts +0 -37
  107. package/src/postinstall.ts +0 -36
  108. 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` +
@@ -1,5 +1,6 @@
1
1
  import type { BorgCli } from './cubes.js';
2
- import { wakePathArming, type AgentKind } from './regen-format.js';
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 = 2 as const;
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(([account, value]) => {
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 record = JSON.parse(stored) as Partial<PendingServerCubeCreationRecord> & {
1111
- version?: unknown;
1112
- state?: unknown;
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
- if (
1115
- record.version !== SERVER_CUBE_RETRY_RECORD_VERSION ||
1116
- record.state !== 'pending' ||
1117
- record.origin !== input.origin ||
1118
- record.trustIdentity !== input.trustIdentity ||
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
- return {
1152
+ const replacement: PendingServerCubeCreationRecord = {
1129
1153
  origin: input.origin,
1130
1154
  trustIdentity: input.trustIdentity,
1131
1155
  clientId: input.clientId,
1132
1156
  repositoryBinding,
1133
- retryKey: record.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
- } catch {
1140
- throw new Error('pending Borg server cube creation does not match this repository');
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: {
@@ -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",
@@ -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, unlinkSync } from 'node:fs';
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(message);
1039
+ const lifecycleSignal = lifecycleSignalForMessage(finalMessage);
1045
1040
  if (lifecycleSignal) {
1046
- const decision = await shouldSuppressLifecycleLog(active, message);
1041
+ const decision = await shouldSuppressLifecycleLog(active, finalMessage);
1047
1042
  if (decision.suppress) {
1048
- await recordLifecycleLog(active, message);
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, message, appendOpts);
1079
- await recordLifecycleLog(active, message);
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 text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${routed}${unreachable}${citations}${advisory}`;
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
- let template = null;
1267
- if (templateName) {
1268
- template = getTemplate(templateName);
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
- // Apply template roles if requested. Merges by name: any role the
1306
- // server auto-seeded (e.g. "Drone") that the template doesn't
1307
- // also include stays put; templated roles upsert.
1308
- if (template) {
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: { cube, result: 'created', template: null, roles_created: null, roles_updated: null },
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: ActiveCube, entryId: string) => Promise<boolean>;
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
- export interface ActiveCube {
675
- cubeId: string;
676
- droneId: string;
677
- sessionToken: string;
678
- apiUrl: string;
679
- serverTrustIdentity?: string;
680
- localSessionCredentialRef?: string;
681
- localSessionExpiresAt?: string | null;
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: ActiveCube,
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: ActiveCube,
1269
+ active: StreamActiveCube,
1275
1270
  lastEventId: string | null,
1276
1271
  onEventId: (id: string) => void,
1277
1272
  deps: StreamDeps = {}
@@ -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
- context = null;
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');
@@ -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 a ref and exact commit SHA, never a filesystem path. A reviewing seat checks out the SHA in its own worktree with \`git checkout --detach <SHA>\` and never reads another seat's folder.
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>"` (optionally `template="software-dev"`)');
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
  }