borgmcp 3.17.0 → 4.0.1
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 +10 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/direct-log.d.ts +2 -1
- package/dist/direct-log.d.ts.map +1 -1
- package/dist/direct-log.js +23 -9
- package/dist/direct-log.js.map +1 -1
- package/dist/docs-sections.js +3 -3
- package/dist/docs-sections.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -13
- package/dist/index.js.map +1 -1
- package/dist/log-audit-core.d.ts.map +1 -1
- package/dist/log-audit-core.js +4 -2
- package/dist/log-audit-core.js.map +1 -1
- package/dist/regen-format.d.ts +6 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +33 -14
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.d.ts +10 -5
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +20 -33
- package/dist/remote-client.js.map +1 -1
- package/dist/server-handshake.d.ts +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +40 -25
- package/dist/tool-manifest.js.map +1 -1
- package/dist/tool-scope.d.ts +1 -1
- package/dist/tool-scope.d.ts.map +1 -1
- package/dist/tool-scope.js +1 -0
- package/dist/tool-scope.js.map +1 -1
- package/docs/DOCUMENTS.md +4 -2
- package/docs/RELEASING.md +4 -2
- package/package.json +2 -2
- package/src/cli-help.ts +4 -1
- package/src/direct-log.ts +25 -8
- package/src/docs-sections.ts +3 -3
- package/src/index.ts +38 -12
- package/src/log-audit-core.ts +4 -2
- package/src/regen-format.ts +35 -14
- package/src/remote-client.ts +48 -54
- package/src/server-handshake.ts +1 -1
- package/src/tool-manifest.ts +41 -25
- package/src/tool-scope.ts +1 -0
- package/dist/local-log-routing.d.ts +0 -19
- package/dist/local-log-routing.d.ts.map +0 -1
- package/dist/local-log-routing.js +0 -67
- package/dist/local-log-routing.js.map +0 -1
- package/src/local-log-routing.ts +0 -106
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
75
|
-
"borgmcp-shared": "0.
|
|
75
|
+
"borgmcp-shared": "1.0.0",
|
|
76
76
|
"chalk": "^5.3.0",
|
|
77
77
|
"prompts": "^2.4.2",
|
|
78
78
|
"which": "^4.0.0"
|
package/src/cli-help.ts
CHANGED
|
@@ -164,7 +164,10 @@ export function topLevelHelpText(version: string): string {
|
|
|
164
164
|
` They coordinate through a shared log (a "cube"). For Claude Code, Codex & OpenCode.\n\n` +
|
|
165
165
|
`Docs & quickstart: https://github.com/Byte-Ventures/borg-mcp-client#readme\n\n` +
|
|
166
166
|
`Install Claude Code, Codex, or OpenCode first. Type \`borg ...\` in your terminal;\n` +
|
|
167
|
-
`type \`borg_...\` inside your agent session once you've joined a cube ("assimilate").\n
|
|
167
|
+
`type \`borg_...\` inside your agent session once you've joined a cube ("assimilate").\n` +
|
|
168
|
+
`Inside a cube, every log message requires \`to: "broadcast"\` or a non-empty selector list.\n` +
|
|
169
|
+
`There is no omitted or taxonomy-selected audience.\n` +
|
|
170
|
+
`Direct routing controls delivery and wakes, not secrecy from other cube members.\n\n` +
|
|
168
171
|
`Usage:\n` +
|
|
169
172
|
` borg Show the launch menu in a repository root; resume directly in a linked worktree\n` +
|
|
170
173
|
` borg setup Set up borg MCP server + agent CLI integration\n` +
|
package/src/direct-log.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
export type LogAudience = 'broadcast' | string[];
|
|
4
|
+
|
|
5
|
+
export function normalizeLogAudience(value: unknown): LogAudience {
|
|
6
|
+
if (value === 'broadcast') return value;
|
|
7
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 100) {
|
|
8
|
+
throw new Error('to is required and must be "broadcast" or contain 1-100 recipient selectors');
|
|
9
|
+
}
|
|
10
|
+
for (const selector of value) {
|
|
11
|
+
if (typeof selector !== 'string') {
|
|
12
|
+
throw new Error('to recipient selectors must be strings containing 1-120 UTF-8 bytes');
|
|
13
|
+
}
|
|
14
|
+
const bytes = Buffer.byteLength(selector, 'utf8');
|
|
15
|
+
if (bytes < 1 || bytes > 120) {
|
|
16
|
+
throw new Error('to recipient selectors must be strings containing 1-120 UTF-8 bytes');
|
|
17
|
+
}
|
|
18
|
+
if (selector !== selector.trim() || /[\u0000-\u001f\u007f-\u009f]/.test(selector)) {
|
|
19
|
+
throw new Error('to recipient selectors must be trimmed and control-free');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (new Set(value).size !== value.length) {
|
|
23
|
+
throw new Error('to recipient selectors must be unique');
|
|
24
|
+
}
|
|
25
|
+
return [...value];
|
|
9
26
|
}
|
package/src/docs-sections.ts
CHANGED
|
@@ -39,8 +39,8 @@ export const DOCS_SECTIONS: DocsSection[] = [
|
|
|
39
39
|
slug: "concepts",
|
|
40
40
|
title: "Core concepts",
|
|
41
41
|
url: `${SITE_URL}/docs/concepts/`,
|
|
42
|
-
summary: "Cubes, drones, roles,
|
|
43
|
-
keywords: ["cube", "drone", "role", "log", "signal", "claim", "decision", "coordinate", "coordination"],
|
|
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"],
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
slug: "install",
|
|
@@ -96,7 +96,7 @@ export const DOCS_SECTIONS: DocsSection[] = [
|
|
|
96
96
|
title: "Tool reference",
|
|
97
97
|
url: `${SITE_URL}/docs/tools/`,
|
|
98
98
|
summary: "Every borg_* tool — name, description, params.",
|
|
99
|
-
keywords: ["tool", "tools", "api", "reference", "param", "borg_"],
|
|
99
|
+
keywords: ["tool", "tools", "api", "reference", "param", "borg_", "borg_read-entry", "entry_id", "exact entry"],
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
slug: "faq",
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getRoleInfoByName,
|
|
29
29
|
getRoster,
|
|
30
30
|
readLog,
|
|
31
|
+
readLogEntry,
|
|
31
32
|
appendLog,
|
|
32
33
|
ackLogEntry,
|
|
33
34
|
getAckStatus,
|
|
@@ -87,6 +88,7 @@ import { addUserPromptSubmitHook } from './config-utils.js';
|
|
|
87
88
|
import {
|
|
88
89
|
humanAgo,
|
|
89
90
|
formatLogEntryMarkdown,
|
|
91
|
+
formatLogRecipients,
|
|
90
92
|
formatRegenMarkdown,
|
|
91
93
|
getDronePlaybook,
|
|
92
94
|
getDronePlaybookChapter,
|
|
@@ -157,7 +159,7 @@ import {
|
|
|
157
159
|
shouldSuppressLifecycleLog,
|
|
158
160
|
} from './lifecycle-log-guard.js';
|
|
159
161
|
import {
|
|
160
|
-
|
|
162
|
+
normalizeLogAudience,
|
|
161
163
|
} from './direct-log.js';
|
|
162
164
|
import { formatLocalManageToolResult } from './local-manage-tool-result.js';
|
|
163
165
|
import {
|
|
@@ -850,6 +852,24 @@ export async function main() {
|
|
|
850
852
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
851
853
|
}
|
|
852
854
|
|
|
855
|
+
case 'borg_read-entry': {
|
|
856
|
+
const active = await requireActiveCube();
|
|
857
|
+
const { entry, drones, roles } = await readLogEntry(
|
|
858
|
+
active.sessionToken,
|
|
859
|
+
active.apiUrl,
|
|
860
|
+
args ?? {},
|
|
861
|
+
active.serverTrustIdentity,
|
|
862
|
+
);
|
|
863
|
+
const droneById = new Map(drones.map((drone) => [drone.id, drone]));
|
|
864
|
+
const roleById = new Map(roles.map((role) => [role.id, role]));
|
|
865
|
+
const text = [
|
|
866
|
+
`# Activity log: ${active.name}`,
|
|
867
|
+
'',
|
|
868
|
+
formatLogEntryMarkdown(entry, droneById, roleById),
|
|
869
|
+
].join('\n');
|
|
870
|
+
return { content: [{ type: 'text', text }] };
|
|
871
|
+
}
|
|
872
|
+
|
|
853
873
|
case 'borg_put-document': {
|
|
854
874
|
const active = await requireActiveCube();
|
|
855
875
|
const result = await putDocument(active.sessionToken, active.apiUrl, args ?? {}, active.serverTrustIdentity);
|
|
@@ -880,6 +900,7 @@ export async function main() {
|
|
|
880
900
|
case 'borg_log': {
|
|
881
901
|
const message = args?.message as string;
|
|
882
902
|
if (!message || typeof message !== 'string') throw new Error('message is required');
|
|
903
|
+
const to = normalizeLogAudience(args?.to);
|
|
883
904
|
const active = await getActiveCube();
|
|
884
905
|
if (!active) throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
|
|
885
906
|
seedDisplayIdentity(active);
|
|
@@ -900,28 +921,33 @@ export async function main() {
|
|
|
900
921
|
};
|
|
901
922
|
}
|
|
902
923
|
}
|
|
903
|
-
const hasTo = Object.prototype.hasOwnProperty.call(args ?? {}, 'to');
|
|
904
|
-
const recipients = hasTo ? normalizeDirectLogRecipients(args?.to) : undefined;
|
|
905
924
|
const explicitClass = typeof args?.class === 'string' ? args.class : undefined;
|
|
906
|
-
const visibility: 'broadcast' | 'direct' | undefined =
|
|
907
|
-
args?.visibility === 'broadcast' || args?.visibility === 'direct'
|
|
908
|
-
? args.visibility
|
|
909
|
-
: undefined;
|
|
910
925
|
const documents = args?.documents as string[] | undefined;
|
|
911
926
|
if (!active.serverTrustIdentity) {
|
|
912
927
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
913
928
|
}
|
|
914
929
|
const appendOpts = {
|
|
930
|
+
to,
|
|
915
931
|
...(explicitClass ? { class: explicitClass } : {}),
|
|
916
|
-
...(hasTo ? { to: recipients ?? [] } : {}),
|
|
917
|
-
...(visibility ? { visibility } : {}),
|
|
918
932
|
...(documents ? { documents } : {}),
|
|
919
933
|
serverTrustIdentity: active.serverTrustIdentity,
|
|
920
934
|
};
|
|
921
935
|
const result = await appendLog(active.sessionToken, active.apiUrl, message, appendOpts);
|
|
922
936
|
await recordLifecycleLog(active, message);
|
|
923
937
|
if (lifecycleSignal === 'arrival') markArrivalAnnouncedThisProcess();
|
|
924
|
-
|
|
938
|
+
let recipientDrones: any[] = [];
|
|
939
|
+
if (result.entry.visibility === 'direct' && result.entry.recipient_drone_ids.length > 0) {
|
|
940
|
+
try {
|
|
941
|
+
recipientDrones = (await getRoster(active)).drones;
|
|
942
|
+
} catch {
|
|
943
|
+
// The log is already persisted; keep success truthful with stable id fallbacks.
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const recipientById = new Map(recipientDrones.map((drone) => [drone.id, drone]));
|
|
947
|
+
const routedRecipients = formatLogRecipients(result.entry, recipientById);
|
|
948
|
+
const routed = routedRecipients.length > 0
|
|
949
|
+
? `\nRecipients: ${routedRecipients.join(', ')}`
|
|
950
|
+
: '';
|
|
925
951
|
// gh#534: surface to the SENDER which directed recipients are
|
|
926
952
|
// currently unreachable via the wake path. The message is delivered
|
|
927
953
|
// regardless (persisted server-side); they read it when they return.
|
|
@@ -933,9 +959,9 @@ export async function main() {
|
|
|
933
959
|
const cited = formatDocumentCitations(result.entry.documents);
|
|
934
960
|
const citations = cited.length > 0 ? `\nDocuments: ${cited.join('; ')}` : '';
|
|
935
961
|
const advisory = result.advisory?.code === 'STORE_AS_DOCUMENT'
|
|
936
|
-
? `\nAdvisory: this message exceeded ${result.advisory.threshold_bytes} UTF-8 bytes. Store durable detail with borg_put-document
|
|
962
|
+
? `\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.`
|
|
937
963
|
: '';
|
|
938
|
-
const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${
|
|
964
|
+
const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${routed}${unreachable}${citations}${advisory}`;
|
|
939
965
|
return { content: [{ type: 'text', text }] };
|
|
940
966
|
}
|
|
941
967
|
|
package/src/log-audit-core.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const LOG_AUDIT_NUDGE = (count: number): string =>
|
|
2
2
|
`Heads up: ${count}+ state-changing tool calls since the last \`borg_log\` post. ` +
|
|
3
3
|
'If that work was a substantive unit (a change that ships, a blocker hit, a finding ' +
|
|
4
|
-
"worth sharing), post to the cube log per your role's conventions before continuing."
|
|
4
|
+
"worth sharing), post to the cube log per your role's conventions before continuing. " +
|
|
5
|
+
'Choose `to: "broadcast"` or a non-empty selector array; never omit `to`.';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Pure transcript scan shared by the Claude hook and the OpenCode plugin.
|
|
@@ -12,7 +13,8 @@ export function evaluateLogAudit(
|
|
|
12
13
|
renderNudge: (count: number) => string = (count) =>
|
|
13
14
|
`Heads up: ${count}+ state-changing tool calls since the last \`borg_log\` post. ` +
|
|
14
15
|
'If that work was a substantive unit (a change that ships, a blocker hit, a finding ' +
|
|
15
|
-
"worth sharing), post to the cube log per your role's conventions before continuing."
|
|
16
|
+
"worth sharing), post to the cube log per your role's conventions before continuing. " +
|
|
17
|
+
'Choose `to: "broadcast"` or a non-empty selector array; never omit `to`.',
|
|
16
18
|
): string | null {
|
|
17
19
|
const materialTools = new Set([
|
|
18
20
|
'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash',
|
package/src/regen-format.ts
CHANGED
|
@@ -240,6 +240,8 @@ export function markArrivalAnnouncedThisProcess(): void {
|
|
|
240
240
|
// copy-param-claim: borg_ack-status.entry_id
|
|
241
241
|
// The playbook below directs uncertain receipt checks through the read-only
|
|
242
242
|
// acknowledgement-status query; this marker pins its required entry id.
|
|
243
|
+
// copy-param-claim: borg_read-entry.entry_id
|
|
244
|
+
// copy-param-claim: borg_log.to
|
|
243
245
|
// copy-param-claim: borg_docs.topic
|
|
244
246
|
// The playbook below points drones to `borg_docs {topic}` for user questions
|
|
245
247
|
// about how Borg MCP works; this marker pins the param so the #490/#529 guard
|
|
@@ -247,7 +249,7 @@ export function markArrivalAnnouncedThisProcess(): void {
|
|
|
247
249
|
export function getDronePlaybook(): string {
|
|
248
250
|
const arrivalInstruction = arrivalAnnouncedThisProcess
|
|
249
251
|
? ''
|
|
250
|
-
: `\n**When this MCP session first starts:**
|
|
252
|
+
: `\n**When this MCP session first starts:** call \`borg_log message="ARRIVAL: <your-label> (<your-role>) online on ${osHostname()}" to="broadcast"\` once. After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit \`/mcp\` reconnect may show it again.\n`;
|
|
251
253
|
return `## How to operate as a Drone
|
|
252
254
|
|
|
253
255
|
You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
@@ -260,10 +262,11 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
|
260
262
|
- \`borg_role\` — re-read your role's detailed playbook
|
|
261
263
|
- \`borg_roster\` — see who else is connected
|
|
262
264
|
- \`borg_read-log unread_only=true [limit]\` — drain unread log entries from your server-side cursor
|
|
263
|
-
- \`
|
|
265
|
+
- \`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
|
|
264
267
|
- \`borg_assimilate <cube>\` — switch to a different cube
|
|
265
268
|
|
|
266
|
-
**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.
|
|
269
|
+
**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.
|
|
267
270
|
|
|
268
271
|
**Communication discipline for non-human seats:**
|
|
269
272
|
- **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.
|
|
@@ -282,10 +285,10 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
|
|
|
282
285
|
or a status reply must never be the last action of a turn while work is outstanding.
|
|
283
286
|
6. Nothing actionable, no prompt, and no work of your own outstanding → done; wait for next wake.
|
|
284
287
|
|
|
285
|
-
**On a \`<task-notification>\` wake:** the payload is a truncatable preview; the full entry is in the DB. Drain: \`borg_read-log unread_only=true limit=20\`, repeat until \`behind_by=0\`. Do NOT triage with \`since=<notification timestamp>\` (strict-after — skips the boundary entry) or a bare window (skips older-unread during bursts).
|
|
288
|
+
**On a \`<task-notification>\` wake:** the payload is a truncatable preview; the full entry is in the DB. Drain: \`borg_read-log unread_only=true limit=20\`, repeat until \`behind_by=0\`. If you later need one known entry's complete body, call \`borg_read-entry entry_id=<id>\`. Do NOT triage with \`since=<notification timestamp>\` (strict-after — skips the boundary entry) or a bare window (skips older-unread during bursts).
|
|
286
289
|
${arrivalInstruction}
|
|
287
290
|
|
|
288
|
-
**When a log entry
|
|
291
|
+
**When a log entry asks you to act:** if its explicit \`to\` audience includes your drone and its message assigns you work or directly asks you to act, call \`borg_ack entry_id=<id>\` within ~60s. Use the \`borg_ack\` TOOL, not an in-band \`ACK:\` post (it records a queryable flag + wakes the author's Monitor + keeps the log clean). Ack = receipt, not completion (\`STARTING\` / \`DONE\` still apply). Ack actionable assignments and direct action requests only — not every addressed entry or mention.
|
|
289
292
|
|
|
290
293
|
**Claim a work item before you start it (\`borg_ack ... kind=claim\`):** \`borg_ack\` has two kinds — \`ack\` (receipt, the default) and \`claim\` (advisory ownership of a routed work item you are about to take). When a routed entry could be picked up by more than one drone, \`borg_ack entry_id=<id> kind=claim\` BEFORE starting — it announces you are taking it so peers skip the duplicate work, and wakes the rest of the entry's audience. If a live peer already holds the claim, skip it; if the claim is STALE (the claimant went silent past the wake-path SLA), re-claim and proceed. A claim is ADVISORY only — it NEVER substitutes for the completion or approval signal your role's conventions require; a bogus or abandoned claim can at most delay a work item, never bypass its real gate.
|
|
291
294
|
|
|
@@ -303,9 +306,9 @@ ${arrivalInstruction}
|
|
|
303
306
|
|
|
304
307
|
**Posting to the log:** post per your role's conventions whenever you start/finish a task, get stuck, answer a drone, or learn something others need — regardless of who initiated (a log signal, your own scan, or a user prompt). Conventions live in your role detail; the system is vocabulary-agnostic.
|
|
305
308
|
|
|
306
|
-
**
|
|
307
|
-
- Posting a verdict / decision / result a specific drone is waiting on:
|
|
308
|
-
- Any drone posting a multi-seat DELIVERABLE (spec / security classification / review artifact 3+ seats build or gate against):
|
|
309
|
+
**Address every post explicitly:** use \`to: ["<selector>"]\` for one or more intended recipients and \`to: "broadcast"\` for every drone. Prefixes and optional \`class\` values classify and lifecycle-tag entries only; they never route or supply a default audience.
|
|
310
|
+
- Posting a verdict / decision / result a specific drone is waiting on: include that drone in a non-empty \`to\` selector array so they're WOKEN. Direct addressing governs delivery and the WAKE; it is NOT read-confidentiality: every member can read every entry — the cube is the trust boundary, so never post secrets relying on direct routing.
|
|
311
|
+
- Any drone posting a multi-seat DELIVERABLE (spec / security classification / review artifact 3+ seats build or gate against): use \`to: "broadcast"\` or explicitly list every intended selector. Never rely on the signal prefix or class to choose recipients.
|
|
309
312
|
|
|
310
313
|
**Pre-commit git hygiene (universal):**
|
|
311
314
|
|
|
@@ -361,7 +364,7 @@ The discipline applies at FOUR surfaces. Catches at the surface closest to origi
|
|
|
361
364
|
- **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep's the referenced file BEFORE composing the proposal. If the proposal cites current \`origin/main\` or a branch/SHA, grep that ref via \`git show <ref>:<path> | grep\`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.
|
|
362
365
|
- **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep's the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via \`git show <ref>:<path> | grep\`; don't let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.
|
|
363
366
|
- **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.
|
|
364
|
-
- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking issue from a cube event payload, the FILING drone fetches the originating entry's full body
|
|
367
|
+
- **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking issue from a cube event payload, the FILING drone fetches the originating entry's full body with \`borg_read-entry entry_id=<id>\` BEFORE composing the issue body. For routine wake triage, use \`borg_read-log unread_only=true\` and drain until caught up; do not rely on a truncated event preview or a \`since=<same timestamp>\` read, which can skip the boundary entry. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface — the filed issue becomes the cube's durable cross-cycle memory; correcting it requires a follow-up correction post, and later pickup drones inherit the incomplete framing if the correction is missed.
|
|
365
368
|
|
|
366
369
|
**Ratified-decision drift is a four-surface drift-class.** A ratified cube decision restated from memory drifts exactly like a code-identifier claim — it propagates dispatch (Surface 1, brainstorm) → copy (Surface 2, comment) → gate (Surface 3, review), and the cheapest catch is at the brainstorm surface. At each surface, a drone restating a ratified decision source-reads \`borg_decisions {topic}\` FIRST: the active registry entry is the source of truth; your memory is a derivative artifact. Core rule — **cite ratified decisions by topic; never restate one from memory.**
|
|
367
370
|
|
|
@@ -403,9 +406,9 @@ export function humanAgo(date: Date | string): string {
|
|
|
403
406
|
* include — robustness wins.
|
|
404
407
|
*/
|
|
405
408
|
/**
|
|
406
|
-
* gh#479 — discoverability tip for
|
|
409
|
+
* gh#479 — discoverability tip for message classification. When a
|
|
407
410
|
* cube has no `message_taxonomy` declared, borg_regen + borg_cube append
|
|
408
|
-
* this tip so operators discover how to
|
|
411
|
+
* this tip so operators discover how to classify signals and lifecycle. Self-
|
|
409
412
|
* removing: returns '' once a taxonomy exists. Copy is UX-locked
|
|
410
413
|
* (design d45098c1) — keep verbatim.
|
|
411
414
|
*/
|
|
@@ -419,7 +422,7 @@ export function nullTaxonomyTip(messageTaxonomy: unknown): string {
|
|
|
419
422
|
// this inline marker pins the real inputSchema param so the #490/#529 guard
|
|
420
423
|
// (client/__tests__/copy-mechanism-guard.test.ts) verifies the tool actually
|
|
421
424
|
// exposes it — the #479 miss class, now caught co-located with the copy.
|
|
422
|
-
return 'Tip: no message taxonomy declared — set one to
|
|
425
|
+
return 'Tip: no message taxonomy declared — set one to classify signal prefixes and dispatch/completion lifecycle. Every borg_log call still requires an explicit to audience. Use borg_update-cube with a taxonomy array, or add classes with borg_patch-taxonomy-class.';
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
export function regenWakePathDroneLabel(
|
|
@@ -574,7 +577,7 @@ export function formatRegenMarkdown(
|
|
|
574
577
|
? [
|
|
575
578
|
'## Getting started',
|
|
576
579
|
'',
|
|
577
|
-
'**You (this agent):** post `borg_log message="<task>"`; check `borg_roster`.',
|
|
580
|
+
'**You (this agent):** post `borg_log message="<task>" to="broadcast"`; check `borg_roster`.',
|
|
578
581
|
'**Your user:** in a new terminal in the repository, add a teammate: `borg assimilate <role>`; optional `--worktree <name>` names its worktree.',
|
|
579
582
|
'For "what do I do next?", use `borg_docs`.',
|
|
580
583
|
'',
|
|
@@ -704,8 +707,26 @@ export function formatLogEntryMarkdown(
|
|
|
704
707
|
? ` ${formatDroneAddressToken(entry.drone_id)}`
|
|
705
708
|
: '';
|
|
706
709
|
const citations = formatDocumentCitations(entry.documents);
|
|
710
|
+
const recipients = formatLogRecipients(entry, droneById);
|
|
711
|
+
const routed = recipients.length > 0
|
|
712
|
+
? `\n Recipients: ${recipients.join(', ')}`
|
|
713
|
+
: '';
|
|
707
714
|
const documents = citations.length > 0
|
|
708
715
|
? `\n Documents:\n${citations.map((citation) => ` - ${citation}`).join('\n')}`
|
|
709
716
|
: '';
|
|
710
|
-
return `**[${ts}]**${entryId}${addr} ${d?.label ?? '?'} (${r?.name ?? '?'}): ${entry.message}${documents}`;
|
|
717
|
+
return `**[${ts}]**${entryId}${addr} ${d?.label ?? '?'} (${r?.name ?? '?'}): ${entry.message}${routed}${documents}`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function formatLogRecipients(
|
|
721
|
+
entry: { visibility?: unknown; recipient_drone_ids?: unknown },
|
|
722
|
+
droneById: Map<string, any>,
|
|
723
|
+
): string[] {
|
|
724
|
+
if (entry.visibility !== 'direct' || !Array.isArray(entry.recipient_drone_ids)) return [];
|
|
725
|
+
return entry.recipient_drone_ids.map((droneId) => {
|
|
726
|
+
if (typeof droneId !== 'string') return '?';
|
|
727
|
+
const label = droneById.get(droneId)?.label;
|
|
728
|
+
return typeof label === 'string' && label.length > 0
|
|
729
|
+
? label
|
|
730
|
+
: formatDroneAddressToken(droneId);
|
|
731
|
+
});
|
|
711
732
|
}
|
package/src/remote-client.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
decodeDeleteRoleRequest,
|
|
25
25
|
decodeDeleteRoleResult,
|
|
26
26
|
decodeDroneRuntimeMetadataState,
|
|
27
|
+
decodeEntryQueryRequest,
|
|
28
|
+
decodeEntryQueryResult,
|
|
27
29
|
decodeEvictDroneResult,
|
|
28
30
|
decodeProtocolEnvelope,
|
|
29
31
|
decodeProtocolErrorEnvelope,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
type AckStatusResult,
|
|
46
48
|
type AgentKind,
|
|
47
49
|
type DeleteRoleResult,
|
|
50
|
+
type EntryQueryResult,
|
|
48
51
|
type EvictDroneResult,
|
|
49
52
|
type ReassignDroneResult,
|
|
50
53
|
type RoleRationaleResult,
|
|
@@ -89,7 +92,7 @@ import {
|
|
|
89
92
|
type LocalServerCursor,
|
|
90
93
|
} from './local-server-cursor.js';
|
|
91
94
|
import { readBoundedResponseBody } from './server-response.js';
|
|
92
|
-
import {
|
|
95
|
+
import { normalizeLogAudience, type LogAudience } from './direct-log.js';
|
|
93
96
|
import { RoleSectionConflictError } from './local-manage-tool-result.js';
|
|
94
97
|
|
|
95
98
|
export interface RemoteConnection {
|
|
@@ -1112,6 +1115,32 @@ export async function readLog(
|
|
|
1112
1115
|
};
|
|
1113
1116
|
}
|
|
1114
1117
|
|
|
1118
|
+
/** Read one complete log entry without consulting or advancing the unread cursor. */
|
|
1119
|
+
export async function readLogEntry(
|
|
1120
|
+
sessionToken: string,
|
|
1121
|
+
apiUrl: string,
|
|
1122
|
+
input: unknown,
|
|
1123
|
+
serverTrustIdentity?: string,
|
|
1124
|
+
): Promise<{ entry: EntryQueryResult['entry']; drones: any[]; roles: any[] }> {
|
|
1125
|
+
const request = decodeEntryQueryRequest(input);
|
|
1126
|
+
const local = await localAuthorityContext(sessionToken, apiUrl, serverTrustIdentity);
|
|
1127
|
+
const result = await localServerRequest<EntryQueryResult>(
|
|
1128
|
+
local,
|
|
1129
|
+
`/api/cubes/${local.cubeId}/logs/${encodeURIComponent(request.entry_id)}`,
|
|
1130
|
+
'GET',
|
|
1131
|
+
undefined,
|
|
1132
|
+
{ decodePayload: decodeEntryQueryResult },
|
|
1133
|
+
);
|
|
1134
|
+
if (!result) throw new Error('Local Borg server returned an empty log-entry response');
|
|
1135
|
+
if (request.entry_id.length === 36
|
|
1136
|
+
? result.entry.id !== request.entry_id
|
|
1137
|
+
: !result.entry.id.startsWith(request.entry_id)) {
|
|
1138
|
+
throw new ProtocolContractError('Log-entry response id does not match the requested selector.');
|
|
1139
|
+
}
|
|
1140
|
+
const composed = await localCubeComposition(local);
|
|
1141
|
+
return { entry: result.entry, drones: composed.drones, roles: composed.roles };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1115
1144
|
/**
|
|
1116
1145
|
* Sprint 25 log substrate refactor: explicit ack on a log entry.
|
|
1117
1146
|
*
|
|
@@ -1445,69 +1474,34 @@ export async function appendLog(
|
|
|
1445
1474
|
apiUrl: string,
|
|
1446
1475
|
message: string,
|
|
1447
1476
|
opts: {
|
|
1448
|
-
|
|
1449
|
-
recipientDroneIds?: string[];
|
|
1477
|
+
to: LogAudience;
|
|
1450
1478
|
class?: string;
|
|
1451
|
-
to?: string[];
|
|
1452
1479
|
documents?: string[];
|
|
1453
1480
|
serverTrustIdentity?: string;
|
|
1454
|
-
}
|
|
1481
|
+
},
|
|
1455
1482
|
): Promise<ReturnType<typeof decodeAppendLogResult>> {
|
|
1456
|
-
|
|
1457
|
-
throw new Error(
|
|
1458
|
-
"Invalid input: visibility:'broadcast' cannot be combined with non-empty to:. " +
|
|
1459
|
-
'Remove visibility to direct to recipients, or remove to: to broadcast.',
|
|
1460
|
-
);
|
|
1461
|
-
}
|
|
1462
|
-
if (opts.to?.length === 0) {
|
|
1463
|
-
throw new Error('Direct log recipient list must contain at least one recipient');
|
|
1464
|
-
}
|
|
1483
|
+
const to = normalizeLogAudience(opts?.to);
|
|
1465
1484
|
const postId = randomUUID();
|
|
1466
1485
|
const local = await localAuthorityContext(
|
|
1467
1486
|
sessionToken,
|
|
1468
1487
|
apiUrl,
|
|
1469
1488
|
opts.serverTrustIdentity,
|
|
1470
1489
|
);
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
dronePayload.drones,
|
|
1487
|
-
rolePayload.roles,
|
|
1488
|
-
);
|
|
1489
|
-
visibility = 'direct';
|
|
1490
|
-
} else if (visibility === undefined && recipientDroneIds !== undefined) {
|
|
1491
|
-
visibility = 'direct';
|
|
1492
|
-
}
|
|
1493
|
-
const request = decodeAppendLogRequest({
|
|
1494
|
-
post_id: postId,
|
|
1495
|
-
message,
|
|
1496
|
-
...(visibility ? { visibility } : {}),
|
|
1497
|
-
...(visibility === 'direct' && recipientDroneIds
|
|
1498
|
-
? { recipientDroneIds }
|
|
1499
|
-
: {}),
|
|
1500
|
-
...(opts.class ? { class: opts.class } : {}),
|
|
1501
|
-
...(opts.documents ? { documents: opts.documents } : {}),
|
|
1502
|
-
});
|
|
1503
|
-
const payload = await localServerRequest<ReturnType<typeof decodeAppendLogResult>>(
|
|
1504
|
-
local,
|
|
1505
|
-
`/api/cubes/${local.cubeId}/logs`,
|
|
1506
|
-
'POST',
|
|
1507
|
-
{ ...request },
|
|
1508
|
-
{ retryMode: 'append-log', decodePayload: decodeAppendLogResult },
|
|
1509
|
-
);
|
|
1510
|
-
if (!payload) throw new Error('Local Borg server returned an empty log response');
|
|
1490
|
+
const request = decodeAppendLogRequest({
|
|
1491
|
+
post_id: postId,
|
|
1492
|
+
message,
|
|
1493
|
+
to,
|
|
1494
|
+
...(opts.class ? { class: opts.class } : {}),
|
|
1495
|
+
...(opts.documents ? { documents: opts.documents } : {}),
|
|
1496
|
+
});
|
|
1497
|
+
const payload = await localServerRequest<ReturnType<typeof decodeAppendLogResult>>(
|
|
1498
|
+
local,
|
|
1499
|
+
`/api/cubes/${local.cubeId}/logs`,
|
|
1500
|
+
'POST',
|
|
1501
|
+
{ ...request },
|
|
1502
|
+
{ retryMode: 'append-log', decodePayload: decodeAppendLogResult },
|
|
1503
|
+
);
|
|
1504
|
+
if (!payload) throw new Error('Local Borg server returned an empty log response');
|
|
1511
1505
|
return payload;
|
|
1512
1506
|
}
|
|
1513
1507
|
|
package/src/server-handshake.ts
CHANGED
|
@@ -215,7 +215,7 @@ export interface ServerAttachResult {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
|
-
* Attach an enrolled client principal to one granted cube/role over protocol
|
|
218
|
+
* Attach an enrolled client principal to one granted cube/role over protocol v12.
|
|
219
219
|
* The client CSPRNG-generates the session bearer and persists it PENDING in the
|
|
220
220
|
* local 0600 credential store (keyed by the stable per-seat identity) BEFORE
|
|
221
221
|
* this request, so an interrupted/lost response is recovered by re-sending the
|