borgmcp-shared 0.10.1 → 0.12.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 (43) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/README.md +22 -15
  3. package/RELEASES.md +14 -0
  4. package/dist/conformance/adapter.d.ts +3 -0
  5. package/dist/conformance/adapter.d.ts.map +1 -1
  6. package/dist/conformance/adapter.js +91 -10
  7. package/dist/conformance/adapter.js.map +1 -1
  8. package/dist/conformance/index.d.ts +13 -0
  9. package/dist/conformance/index.d.ts.map +1 -1
  10. package/dist/conformance/index.js +83 -14
  11. package/dist/conformance/index.js.map +1 -1
  12. package/dist/protocol/contract.d.ts +2 -1
  13. package/dist/protocol/contract.d.ts.map +1 -1
  14. package/dist/protocol/contract.js +9 -4
  15. package/dist/protocol/contract.js.map +1 -1
  16. package/dist/protocol/coordination.d.ts.map +1 -1
  17. package/dist/protocol/coordination.js +8 -2
  18. package/dist/protocol/coordination.js.map +1 -1
  19. package/dist/protocol/errors.d.ts +1 -0
  20. package/dist/protocol/errors.d.ts.map +1 -1
  21. package/dist/protocol/errors.js +1 -0
  22. package/dist/protocol/errors.js.map +1 -1
  23. package/dist/protocol/types.d.ts +2 -0
  24. package/dist/protocol/types.d.ts.map +1 -1
  25. package/dist/protocol/version.d.ts +1 -1
  26. package/dist/protocol/version.js +1 -1
  27. package/dist/templates.d.ts +1 -1
  28. package/dist/templates.d.ts.map +1 -1
  29. package/dist/templates.js +178 -53
  30. package/dist/templates.js.map +1 -1
  31. package/docs/release-records.json +103 -0
  32. package/docs/releases/0.12.0.md +7 -0
  33. package/docs/releasing.md +104 -23
  34. package/docs/template-lifecycle.md +66 -0
  35. package/package.json +2 -2
  36. package/src/conformance/adapter.ts +110 -9
  37. package/src/conformance/index.ts +101 -15
  38. package/src/protocol/contract.ts +11 -5
  39. package/src/protocol/coordination.ts +8 -2
  40. package/src/protocol/errors.ts +1 -0
  41. package/src/protocol/types.ts +2 -0
  42. package/src/protocol/version.ts +2 -2
  43. package/src/templates.ts +185 -53
@@ -11,7 +11,7 @@ import {
11
11
  encodeInvitationArtifact,
12
12
  type InvitationArtifact,
13
13
  } from '../protocol/contract.js';
14
- import type { EnrichedStreamEntry } from '../protocol/types.js';
14
+ import type { AppendLogRequest, EnrichedStreamEntry } from '../protocol/types.js';
15
15
  import { ROLE_IN_USE_DELETE_MESSAGE } from '../protocol/coordination.js';
16
16
 
17
17
  export * from './adapter.js';
@@ -99,17 +99,53 @@ export interface AppendLogResultConformanceVector {
99
99
  accepts: boolean;
100
100
  }
101
101
 
102
+ export interface AppendLogRequestConformanceVector {
103
+ name: string;
104
+ request: unknown;
105
+ accepts: boolean;
106
+ }
107
+
108
+ export interface AppendLogIdempotencyConformanceVector {
109
+ name: string;
110
+ actor: 'same' | 'different';
111
+ mutation: 'none' | 'message' | 'visibility' | 'recipient_set' | 'resolved_class_routing';
112
+ expected: 'deduplicated' | 'POST_ID_CONFLICT' | 'created';
113
+ }
114
+
115
+ /** Author-scoped post identity and exact resolved-routing retry outcomes. */
116
+ export const APPEND_LOG_IDEMPOTENCY_CONFORMANCE: readonly AppendLogIdempotencyConformanceVector[] = [
117
+ { name: 'deduplicates an exact same-author retry', actor: 'same', mutation: 'none', expected: 'deduplicated' },
118
+ { name: 'rejects a same-author message collision', actor: 'same', mutation: 'message', expected: 'POST_ID_CONFLICT' },
119
+ { name: 'rejects a same-author visibility collision', actor: 'same', mutation: 'visibility', expected: 'POST_ID_CONFLICT' },
120
+ { name: 'rejects a same-author recipient-set collision', actor: 'same', mutation: 'recipient_set', expected: 'POST_ID_CONFLICT' },
121
+ { name: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
122
+ { name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
123
+ ];
124
+
125
+ const APPEND_LOG_REQUEST = {
126
+ post_id: '00000000-0000-4000-8000-000000000205',
127
+ message: 'REVIEW-READY: example',
128
+ } satisfies AppendLogRequest;
129
+
130
+ export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
131
+ { name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
132
+ { name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
133
+ { name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
134
+ { name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
135
+ ];
136
+
102
137
  /** Runtime response vectors keep the append-log decoder aligned with its public type. */
103
138
  export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
104
139
  {
105
140
  name: 'accepts a response without optional routing metadata',
106
- response: { entry: APPEND_LOG_ENTRY },
141
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false },
107
142
  accepts: true,
108
143
  },
109
144
  {
110
145
  name: 'accepts exact routing metadata and unreachable recipients',
111
146
  response: {
112
147
  entry: APPEND_LOG_ENTRY,
148
+ deduplicated: false,
113
149
  routing: APPEND_LOG_ROUTING,
114
150
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
115
151
  },
@@ -117,73 +153,84 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
117
153
  },
118
154
  {
119
155
  name: 'accepts a null routing echo and an empty unreachable-recipient list',
120
- response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
156
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: true, routing: null, unreachableRecipients: [] },
121
157
  accepts: true,
122
158
  },
159
+ {
160
+ name: 'rejects an omitted deduplication result',
161
+ response: { entry: APPEND_LOG_ENTRY },
162
+ accepts: false,
163
+ },
164
+ {
165
+ name: 'rejects a non-boolean deduplication result',
166
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: 'false' },
167
+ accepts: false,
168
+ },
123
169
  {
124
170
  name: 'rejects unknown top-level response fields',
125
- response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
171
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: APPEND_LOG_ROUTING, extra: true },
126
172
  accepts: false,
127
173
  },
128
174
  {
129
175
  name: 'rejects a non-object routing echo',
130
- response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
176
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: 'review' },
131
177
  accepts: false,
132
178
  },
133
179
  {
134
180
  name: 'rejects missing routing fields',
135
- response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
181
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { class: 'review' } },
136
182
  accepts: false,
137
183
  },
138
184
  {
139
185
  name: 'rejects unknown routing fields',
140
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
186
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, extra: true } },
141
187
  accepts: false,
142
188
  },
143
189
  {
144
190
  name: 'rejects an invalid routing class',
145
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
191
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
146
192
  accepts: false,
147
193
  },
148
194
  {
149
195
  name: 'rejects an invalid routing recipient collection',
150
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
196
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
151
197
  accepts: false,
152
198
  },
153
199
  {
154
200
  name: 'rejects invalid routing recipient members',
155
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
201
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
156
202
  accepts: false,
157
203
  },
158
204
  {
159
205
  name: 'rejects an invalid routing fell-open flag',
160
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
206
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
161
207
  accepts: false,
162
208
  },
163
209
  {
164
210
  name: 'rejects an invalid routing message',
165
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
211
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
166
212
  accepts: false,
167
213
  },
168
214
  {
169
215
  name: 'rejects a non-array unreachable-recipient list',
170
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
216
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: {} },
171
217
  accepts: false,
172
218
  },
173
219
  {
174
220
  name: 'rejects a non-object unreachable recipient',
175
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
221
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: ['missing-reviewer'] },
176
222
  accepts: false,
177
223
  },
178
224
  {
179
225
  name: 'rejects missing unreachable-recipient fields',
180
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
226
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: [{ id: 'missing-reviewer' }] },
181
227
  accepts: false,
182
228
  },
183
229
  {
184
230
  name: 'rejects unknown unreachable-recipient fields',
185
231
  response: {
186
232
  entry: APPEND_LOG_ENTRY,
233
+ deduplicated: false,
187
234
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
188
235
  },
189
236
  accepts: false,
@@ -192,6 +239,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
192
239
  name: 'rejects an invalid unreachable-recipient id',
193
240
  response: {
194
241
  entry: APPEND_LOG_ENTRY,
242
+ deduplicated: false,
195
243
  unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
196
244
  },
197
245
  accepts: false,
@@ -200,6 +248,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
200
248
  name: 'rejects an invalid unreachable-recipient label',
201
249
  response: {
202
250
  entry: APPEND_LOG_ENTRY,
251
+ deduplicated: false,
203
252
  unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
204
253
  },
205
254
  accepts: false,
@@ -750,11 +799,48 @@ const ATTACH_RESPONSE = {
750
799
  runtime_metadata_reported: false,
751
800
  },
752
801
  session: { id: '40000000-0000-4000-8000-000000000001' },
802
+ initial_log_cursor: null,
753
803
  } satisfies AttachResponse;
754
804
 
755
805
  /** Wire vectors for the v3 non-expiring attach-session response. */
756
806
  export const ATTACH_SESSION_CONFORMANCE: readonly AttachSessionConformanceVector[] = [
757
807
  { name: 'accepts exact non-expiring session id', response: ATTACH_RESPONSE, accepts: true },
808
+ {
809
+ name: 'accepts a canonical initial log cursor',
810
+ response: {
811
+ ...ATTACH_RESPONSE,
812
+ initial_log_cursor: {
813
+ id: '50000000-0000-4000-8000-000000000001',
814
+ created_at: '2026-07-14T10:00:00.000Z',
815
+ },
816
+ },
817
+ accepts: true,
818
+ },
819
+ {
820
+ name: 'rejects an omitted initial log cursor',
821
+ response: (({ initial_log_cursor: _, ...response }) => response)(ATTACH_RESPONSE),
822
+ accepts: false,
823
+ },
824
+ {
825
+ name: 'rejects an invalid initial log cursor UUID',
826
+ response: {
827
+ ...ATTACH_RESPONSE,
828
+ initial_log_cursor: { id: 'not-a-uuid', created_at: '2026-07-14T10:00:00.000Z' },
829
+ },
830
+ accepts: false,
831
+ },
832
+ {
833
+ name: 'rejects unknown initial log cursor fields',
834
+ response: {
835
+ ...ATTACH_RESPONSE,
836
+ initial_log_cursor: {
837
+ id: '50000000-0000-4000-8000-000000000001',
838
+ created_at: '2026-07-14T10:00:00.000Z',
839
+ extra: true,
840
+ },
841
+ },
842
+ accepts: false,
843
+ },
758
844
  {
759
845
  name: 'rejects retired expires_at field',
760
846
  response: {
@@ -13,7 +13,7 @@ import type {
13
13
  } from './types.js';
14
14
 
15
15
  export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
16
- export const SHARED_PACKAGE_VERSION = '0.10.1' as const;
16
+ export const SHARED_PACKAGE_VERSION = '0.12.0' as const;
17
17
  /** Maximum UTF-8 payload for each newly recorded decision text field. */
18
18
  export const DECISION_TEXT_MAX_BYTES = 512 as const;
19
19
  /** Maximum UTF-8 size of role detailed-description text and any returned section slice. */
@@ -580,7 +580,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
580
580
  exactKeys(input, ['protocol_version'], ['protocol_version']);
581
581
  if (input.protocol_version !== PROTOCOL_VERSION) {
582
582
  throw new ProtocolContractError(
583
- 'This client requires protocol v8. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
583
+ 'This client requires protocol v9. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
584
584
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
585
585
  ['protocol_version'],
586
586
  );
@@ -997,10 +997,11 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
997
997
  const input = record(value);
998
998
  exactKeys(
999
999
  input,
1000
- ['message', 'visibility', 'recipientDroneIds', 'class', 'to'],
1001
- ['message'],
1000
+ ['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to'],
1001
+ ['post_id', 'message'],
1002
1002
  );
1003
1003
  const output: import('./types.js').AppendLogRequest = {
1004
+ post_id: decodeUuid(input.post_id, ['post_id']),
1004
1005
  message: boundedString(input.message, 1, 10_240, ['message']),
1005
1006
  };
1006
1007
  if (input.visibility !== undefined) {
@@ -1259,6 +1260,7 @@ export interface AttachResponse {
1259
1260
  role: AttachRole;
1260
1261
  drone: AttachDrone;
1261
1262
  session: AttachSession;
1263
+ initial_log_cursor: LogCursor | null;
1262
1264
  }
1263
1265
 
1264
1266
  function decodeAttachCube(value: unknown, path: readonly (string | number)[]): AttachCube {
@@ -1393,12 +1395,13 @@ export function decodeAttachRequestEnvelope(
1393
1395
  */
1394
1396
  export function decodeAttachResponse(value: unknown): AttachResponse {
1395
1397
  const input = record(value);
1396
- exactKeys(input, ['result', 'cube', 'role', 'drone', 'session'], [
1398
+ exactKeys(input, ['result', 'cube', 'role', 'drone', 'session', 'initial_log_cursor'], [
1397
1399
  'result',
1398
1400
  'cube',
1399
1401
  'role',
1400
1402
  'drone',
1401
1403
  'session',
1404
+ 'initial_log_cursor',
1402
1405
  ]);
1403
1406
  if (input.result !== 'created' && input.result !== 'reused') {
1404
1407
  fail('Expected result "created" or "reused".', ['result']);
@@ -1409,6 +1412,9 @@ export function decodeAttachResponse(value: unknown): AttachResponse {
1409
1412
  role: decodeAttachRole(input.role, ['role']),
1410
1413
  drone: decodeAttachDrone(input.drone, ['drone']),
1411
1414
  session: decodeAttachSession(input.session, ['session']),
1415
+ initial_log_cursor: input.initial_log_cursor === null
1416
+ ? null
1417
+ : decodeLogCursor(input.initial_log_cursor, ['initial_log_cursor']),
1412
1418
  };
1413
1419
  }
1414
1420
 
@@ -376,8 +376,14 @@ function decodeUnreachableRecipient(
376
376
 
377
377
  export function decodeAppendLogResult(value: unknown): AppendLogResult {
378
378
  const input = object(value);
379
- exact(input, ['entry', 'routing', 'unreachableRecipients'], ['entry']);
380
- const output: AppendLogResult = { entry: decodeEnrichedStreamEntry(input.entry) };
379
+ exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients'], ['entry', 'deduplicated']);
380
+ if (typeof input.deduplicated !== 'boolean') {
381
+ throw new ProtocolContractError('Invalid append-log deduplicated flag.');
382
+ }
383
+ const output: AppendLogResult = {
384
+ entry: decodeEnrichedStreamEntry(input.entry),
385
+ deduplicated: input.deduplicated,
386
+ };
381
387
  if (input.routing !== undefined) {
382
388
  output.routing = input.routing === null ? null : decodeRoutingEcho(input.routing);
383
389
  }
@@ -14,6 +14,7 @@ export enum ErrorCode {
14
14
  NOT_FOUND = 'NOT_FOUND',
15
15
  REPOSITORY_ALREADY_ASSOCIATED = 'REPOSITORY_ALREADY_ASSOCIATED',
16
16
  CUBE_ALREADY_ASSOCIATED = 'CUBE_ALREADY_ASSOCIATED',
17
+ POST_ID_CONFLICT = 'POST_ID_CONFLICT',
17
18
  ROLE_IN_USE = 'ROLE_IN_USE',
18
19
  DEFAULT_ROLE_REQUIRED = 'DEFAULT_ROLE_REQUIRED',
19
20
  ROLE_REQUIRED = 'ROLE_REQUIRED',
@@ -171,6 +171,7 @@ export interface RoutingEcho {
171
171
  }
172
172
 
173
173
  export interface AppendLogRequest {
174
+ post_id: string;
174
175
  message: string;
175
176
  visibility?: LogVisibility;
176
177
  recipientDroneIds?: string[];
@@ -180,6 +181,7 @@ export interface AppendLogRequest {
180
181
 
181
182
  export interface AppendLogResponse {
182
183
  entry: ActivityLogEntry;
184
+ deduplicated: boolean;
183
185
  routing?: RoutingEcho | null;
184
186
  unreachableRecipients?: Array<{ id: string; label: string }>;
185
187
  }
@@ -1,4 +1,4 @@
1
- /** Current Borg coordination protocol generation. Clean-slate v8. */
2
- export const PROTOCOL_VERSION = '8' as const;
1
+ /** Current Borg coordination protocol generation. Clean-slate v9. */
2
+ export const PROTOCOL_VERSION = '9' as const;
3
3
 
4
4
  export type ProtocolVersion = typeof PROTOCOL_VERSION;