borgmcp-shared 0.12.3 → 0.13.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 +14 -0
- package/RELEASES.md +24 -0
- package/dist/conformance/adapter.d.ts +7 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +157 -3
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +33 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +10 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +36 -2
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +19 -5
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +11 -2
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/documents.d.ts +78 -0
- package/dist/protocol/documents.d.ts.map +1 -0
- package/dist/protocol/documents.js +196 -0
- package/dist/protocol/documents.js.map +1 -0
- package/dist/protocol/errors.d.ts +5 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +5 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/sse.d.ts +2 -2
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +33 -4
- package/dist/protocol/sse.js.map +1 -1
- package/dist/protocol/types.d.ts +7 -0
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.d.ts.map +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/protocol/version.js.map +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +16 -9
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +9 -0
- package/docs/cube-documents.md +35 -0
- package/docs/release-records.json +30 -0
- package/docs/releases/0.13.0.md +7 -0
- package/docs/releases/0.13.1.md +7 -0
- package/package.json +1 -1
- package/src/conformance/adapter.ts +340 -1
- package/src/conformance/index.ts +11 -0
- package/src/protocol/contract.ts +19 -5
- package/src/protocol/coordination.ts +15 -1
- package/src/protocol/documents.ts +256 -0
- package/src/protocol/errors.ts +5 -0
- package/src/protocol/index.ts +1 -0
- package/src/protocol/sse.ts +38 -3
- package/src/protocol/types.ts +7 -0
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +17 -9
package/src/conformance/index.ts
CHANGED
|
@@ -23,6 +23,17 @@ export interface ConformanceVector<Input, Output> {
|
|
|
23
23
|
expected: Output;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const DOCUMENT_CONFORMANCE = [
|
|
27
|
+
{ name: 'accepts markdown UTF-8 content', fixture: 'markdown', expected: 'created' },
|
|
28
|
+
{ name: 'accepts plain UTF-8 content', fixture: 'plain', expected: 'created' },
|
|
29
|
+
{ name: 'rejects unsupported content types', fixture: 'unsupported-content-type', expected: 'DOCUMENT_CONTENT_TYPE_UNSUPPORTED' },
|
|
30
|
+
{ name: 'requires a title of at most 120 characters', fixture: 'oversize-title', expected: 'INVALID_INPUT' },
|
|
31
|
+
{ name: 'rejects an unknown or cross-cube supersedes id', fixture: 'foreign-supersedes', expected: 'DOCUMENT_SUPERSESSION_INVALID' },
|
|
32
|
+
{ name: 'rejects a branch in the linear supersession chain', fixture: 'branched-supersedes', expected: 'DOCUMENT_SUPERSESSION_INVALID' },
|
|
33
|
+
{ name: 'delists removed content while retaining exact-id resolution', fixture: 'removed', expected: 'audit-resolvable' },
|
|
34
|
+
{ name: 'allows only the author or a cube manager to remove', fixture: 'peer-remove', expected: 'DOCUMENT_REMOVE_DENIED' },
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
26
37
|
export interface BroadcastHwmComparisonInput {
|
|
27
38
|
a: BroadcastHwm;
|
|
28
39
|
b: BroadcastHwm;
|
package/src/protocol/contract.ts
CHANGED
|
@@ -13,11 +13,15 @@ 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.
|
|
16
|
+
export const SHARED_PACKAGE_VERSION = '0.13.1' 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. */
|
|
20
20
|
export const ROLE_TEXT_MAX_BYTES = 51_200 as const;
|
|
21
|
+
export const DEFAULT_LOG_ENTRY_ADVISORY_BYTES = 1024 as const;
|
|
22
|
+
export const DEFAULT_MAX_LOG_ENTRY_BYTES = 4096 as const;
|
|
23
|
+
export const LOG_ENTRY_ADVISORY_ENV = 'BORG_SERVER_LOG_ENTRY_ADVISORY_BYTES' as const;
|
|
24
|
+
export const MAX_LOG_ENTRY_ENV = 'BORG_SERVER_MAX_LOG_ENTRY_BYTES' as const;
|
|
21
25
|
|
|
22
26
|
export const HEALTH_PATH = '/healthz' as const;
|
|
23
27
|
export const PROTOCOL_INFO_PATH = '/api/protocol' as const;
|
|
@@ -30,12 +34,18 @@ export const REPOSITORY_CUBE_RESOLVE_PATH = '/api/repository-cubes/resolve' as c
|
|
|
30
34
|
export const REPOSITORY_CUBE_ASSOCIATION_PATH = '/api/repository-cubes/association' as const;
|
|
31
35
|
export const ATTACH_PATH = '/api/client/attach' as const;
|
|
32
36
|
export const SELF_RUNTIME_METADATA_PATH = '/api/cubes/:cubeId/drones/self/metadata' as const;
|
|
37
|
+
export const DOCUMENTS_PATH = '/api/cubes/:cubeId/documents' as const;
|
|
38
|
+
export const DOCUMENT_PATH = '/api/cubes/:cubeId/documents/:documentId' as const;
|
|
33
39
|
|
|
34
40
|
export const PROTOCOL_HTTP_CONTRACT = {
|
|
35
41
|
health: { method: 'GET', path: HEALTH_PATH, authenticated: false, success_status: 204, bodyless: true },
|
|
36
42
|
protocol: { method: 'GET', path: PROTOCOL_INFO_PATH, authenticated: false, success_status: 200 },
|
|
37
43
|
enrollment: { method: 'POST', path: ENROLLMENT_EXCHANGE_PATH, authenticated: 'invitation', success_status: 201 },
|
|
38
44
|
cubes: { method: 'POST', path: CUBES_PATH, authenticated: true, success_status: 201 },
|
|
45
|
+
document_put: { method: 'PUT', path: DOCUMENTS_PATH, authenticated: true, success_status: 201, mutation: true },
|
|
46
|
+
document_list: { method: 'GET', path: DOCUMENTS_PATH, authenticated: true, success_status: 200, mutation: false },
|
|
47
|
+
document_get: { method: 'GET', path: DOCUMENT_PATH, authenticated: true, success_status: 200, mutation: false },
|
|
48
|
+
document_remove: { method: 'DELETE', path: DOCUMENT_PATH, authenticated: true, success_status: 200, mutation: true },
|
|
39
49
|
cube_delete: {
|
|
40
50
|
method: 'DELETE',
|
|
41
51
|
path: CUBE_PATH,
|
|
@@ -104,7 +114,7 @@ export const PROTOCOL_HTTP_CONTRACT = {
|
|
|
104
114
|
|
|
105
115
|
export const PROTOCOL_LIMIT_CEILINGS = {
|
|
106
116
|
max_request_bytes: 10 * 1024 * 1024,
|
|
107
|
-
max_log_message_bytes:
|
|
117
|
+
max_log_message_bytes: 65_536,
|
|
108
118
|
max_read_page_size: 500,
|
|
109
119
|
max_replay_page_size: 1000,
|
|
110
120
|
} as const;
|
|
@@ -580,7 +590,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
|
|
|
580
590
|
exactKeys(input, ['protocol_version'], ['protocol_version']);
|
|
581
591
|
if (input.protocol_version !== PROTOCOL_VERSION) {
|
|
582
592
|
throw new ProtocolContractError(
|
|
583
|
-
'This client requires protocol
|
|
593
|
+
'This client requires protocol v10. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
|
|
584
594
|
ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
|
|
585
595
|
['protocol_version'],
|
|
586
596
|
);
|
|
@@ -997,12 +1007,12 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
|
|
|
997
1007
|
const input = record(value);
|
|
998
1008
|
exactKeys(
|
|
999
1009
|
input,
|
|
1000
|
-
['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to'],
|
|
1010
|
+
['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to', 'documents'],
|
|
1001
1011
|
['post_id', 'message'],
|
|
1002
1012
|
);
|
|
1003
1013
|
const output: import('./types.js').AppendLogRequest = {
|
|
1004
1014
|
post_id: decodeUuid(input.post_id, ['post_id']),
|
|
1005
|
-
message: boundedString(input.message, 1,
|
|
1015
|
+
message: boundedString(input.message, 1, PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes, ['message']),
|
|
1006
1016
|
};
|
|
1007
1017
|
if (input.visibility !== undefined) {
|
|
1008
1018
|
if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
|
|
@@ -1024,6 +1034,10 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
|
|
|
1024
1034
|
if (input.to !== undefined) {
|
|
1025
1035
|
output.to = decodeStringArray(input.to, 'to', 100, 120);
|
|
1026
1036
|
}
|
|
1037
|
+
if (input.documents !== undefined) {
|
|
1038
|
+
output.documents = decodeStringArray(input.documents, 'documents', 100, 128)
|
|
1039
|
+
.map((id, index) => decodeOpaqueIdentifier(id, ['documents', index]));
|
|
1040
|
+
}
|
|
1027
1041
|
return output;
|
|
1028
1042
|
}
|
|
1029
1043
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
PROTOCOL_LIMIT_CEILINGS,
|
|
2
3
|
ProtocolContractError,
|
|
3
4
|
compareLogCursor,
|
|
4
5
|
decodeCanonicalTimestamp,
|
|
@@ -376,7 +377,7 @@ function decodeUnreachableRecipient(
|
|
|
376
377
|
|
|
377
378
|
export function decodeAppendLogResult(value: unknown): AppendLogResult {
|
|
378
379
|
const input = object(value);
|
|
379
|
-
exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients'], ['entry', 'deduplicated']);
|
|
380
|
+
exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients', 'advisory'], ['entry', 'deduplicated']);
|
|
380
381
|
if (typeof input.deduplicated !== 'boolean') {
|
|
381
382
|
throw new ProtocolContractError('Invalid append-log deduplicated flag.');
|
|
382
383
|
}
|
|
@@ -393,6 +394,19 @@ export function decodeAppendLogResult(value: unknown): AppendLogResult {
|
|
|
393
394
|
}
|
|
394
395
|
output.unreachableRecipients = input.unreachableRecipients.map(decodeUnreachableRecipient);
|
|
395
396
|
}
|
|
397
|
+
if (input.advisory !== undefined) {
|
|
398
|
+
const advisory = object(input.advisory);
|
|
399
|
+
exact(advisory, ['code', 'threshold_bytes'], ['code', 'threshold_bytes']);
|
|
400
|
+
if (advisory.code !== 'STORE_AS_DOCUMENT') {
|
|
401
|
+
throw new ProtocolContractError('Invalid append-log document advisory.');
|
|
402
|
+
}
|
|
403
|
+
const threshold = positiveInteger(
|
|
404
|
+
advisory.threshold_bytes,
|
|
405
|
+
'advisory.threshold_bytes',
|
|
406
|
+
PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
|
|
407
|
+
);
|
|
408
|
+
output.advisory = { code: 'STORE_AS_DOCUMENT', threshold_bytes: threshold };
|
|
409
|
+
}
|
|
396
410
|
return output;
|
|
397
411
|
}
|
|
398
412
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { ErrorCode } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
ProtocolContractError,
|
|
4
|
+
decodeCanonicalTimestamp,
|
|
5
|
+
decodeOpaqueIdentifier,
|
|
6
|
+
decodeProtocolEnvelope,
|
|
7
|
+
decodeUuid,
|
|
8
|
+
utf8ByteLength,
|
|
9
|
+
type ProtocolEnvelope,
|
|
10
|
+
} from './contract.js';
|
|
11
|
+
|
|
12
|
+
export const DOCUMENT_CONTENT_TYPES = ['text/markdown', 'text/plain'] as const;
|
|
13
|
+
export const DOCUMENT_DEFAULT_MAX_BYTES = 65_536 as const;
|
|
14
|
+
export const DOCUMENT_DEFAULT_MAX_ACTIVE_BYTES_PER_CUBE = 524_288 as const;
|
|
15
|
+
export const DOCUMENT_MAX_BYTES_ENV = 'BORG_SERVER_MAX_DOCUMENT_BYTES' as const;
|
|
16
|
+
export const DOCUMENT_MAX_ACTIVE_BYTES_PER_CUBE_ENV =
|
|
17
|
+
'BORG_SERVER_MAX_ACTIVE_DOCUMENT_BYTES_PER_CUBE' as const;
|
|
18
|
+
export type DocumentContentType = (typeof DOCUMENT_CONTENT_TYPES)[number];
|
|
19
|
+
export type DocumentState = 'active' | 'superseded' | 'removed';
|
|
20
|
+
|
|
21
|
+
export interface DocumentActor {
|
|
22
|
+
drone_id: string | null;
|
|
23
|
+
label: string | null;
|
|
24
|
+
role: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DocumentCitation {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
size_bytes: number;
|
|
31
|
+
state: DocumentState;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CubeDocumentMetadata extends DocumentCitation {
|
|
35
|
+
content_type: DocumentContentType;
|
|
36
|
+
supersedes: string | null;
|
|
37
|
+
superseded_by: string | null;
|
|
38
|
+
author: DocumentActor;
|
|
39
|
+
created_at: string;
|
|
40
|
+
removed_by: DocumentActor | null;
|
|
41
|
+
removed_at: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CubeDocument extends CubeDocumentMetadata {
|
|
45
|
+
content: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PutDocumentRequest {
|
|
49
|
+
title: string;
|
|
50
|
+
content_type: DocumentContentType;
|
|
51
|
+
content: string;
|
|
52
|
+
supersedes?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PutDocumentResult { document: CubeDocument }
|
|
55
|
+
export interface GetDocumentRequest { id: string }
|
|
56
|
+
export interface GetDocumentResult { document: CubeDocument }
|
|
57
|
+
export type ListDocumentsRequest = Record<string, never>;
|
|
58
|
+
export interface ListDocumentsResult { documents: CubeDocumentMetadata[] }
|
|
59
|
+
export interface RemoveDocumentRequest { id: string }
|
|
60
|
+
export interface RemoveDocumentResult { document: CubeDocumentMetadata }
|
|
61
|
+
|
|
62
|
+
function object(value: unknown): Record<string, unknown> {
|
|
63
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
64
|
+
throw new ProtocolContractError('Expected a document object.');
|
|
65
|
+
}
|
|
66
|
+
return value as Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function exact(value: Record<string, unknown>, allowed: readonly string[], required: readonly string[]): void {
|
|
70
|
+
for (const key of Object.keys(value)) {
|
|
71
|
+
if (!allowed.includes(key)) throw new ProtocolContractError('Unknown document field.');
|
|
72
|
+
}
|
|
73
|
+
for (const key of required) {
|
|
74
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
75
|
+
throw new ProtocolContractError(`Missing document field "${key}".`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function text(value: unknown, field: string, maximumBytes: number, allowEmpty = false): string {
|
|
81
|
+
if (typeof value !== 'string' || (!allowEmpty && value.length === 0) || utf8ByteLength(value) > maximumBytes) {
|
|
82
|
+
throw new ProtocolContractError(`Invalid document field "${field}".`);
|
|
83
|
+
}
|
|
84
|
+
for (let index = 0; index < value.length; index++) {
|
|
85
|
+
const code = value.charCodeAt(index);
|
|
86
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
87
|
+
const next = value.charCodeAt(index + 1);
|
|
88
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) throw new ProtocolContractError(`Invalid UTF-8 document field "${field}".`);
|
|
89
|
+
index++;
|
|
90
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
91
|
+
throw new ProtocolContractError(`Invalid UTF-8 document field "${field}".`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function title(value: unknown): string {
|
|
98
|
+
const decoded = text(value, 'title', 480);
|
|
99
|
+
if (Array.from(decoded).length > 120 || decoded !== decoded.trim() || /[\u0000-\u001f\u007f-\u009f]/.test(decoded)) {
|
|
100
|
+
throw new ProtocolContractError('Invalid document field "title".');
|
|
101
|
+
}
|
|
102
|
+
return decoded;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function contentType(value: unknown): DocumentContentType {
|
|
106
|
+
if (!DOCUMENT_CONTENT_TYPES.includes(value as DocumentContentType)) {
|
|
107
|
+
throw new ProtocolContractError(
|
|
108
|
+
'Unsupported document content type.',
|
|
109
|
+
ErrorCode.DOCUMENT_CONTENT_TYPE_UNSUPPORTED,
|
|
110
|
+
['content_type'],
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return value as DocumentContentType;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function count(value: unknown, field: string): number {
|
|
117
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > 10 * 1024 * 1024) {
|
|
118
|
+
throw new ProtocolContractError(`Invalid document field "${field}".`);
|
|
119
|
+
}
|
|
120
|
+
return value as number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function nullableId(value: unknown, field: string): string | null {
|
|
124
|
+
return value === null ? null : decodeOpaqueIdentifier(value, [field]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function nullableText(value: unknown, field: string): string | null {
|
|
128
|
+
return value === null ? null : text(value, field, 120);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function decodeDocumentActor(value: unknown): DocumentActor {
|
|
132
|
+
const input = object(value);
|
|
133
|
+
exact(input, ['drone_id', 'label', 'role'], ['drone_id', 'label', 'role']);
|
|
134
|
+
return {
|
|
135
|
+
drone_id: input.drone_id === null ? null : decodeUuid(input.drone_id, ['drone_id']),
|
|
136
|
+
label: nullableText(input.label, 'label'),
|
|
137
|
+
role: nullableText(input.role, 'role'),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function decodeDocumentCitation(value: unknown): DocumentCitation {
|
|
142
|
+
const input = object(value);
|
|
143
|
+
exact(input, ['id', 'title', 'size_bytes', 'state'], ['id', 'title', 'size_bytes', 'state']);
|
|
144
|
+
if (!['active', 'superseded', 'removed'].includes(String(input.state))) {
|
|
145
|
+
throw new ProtocolContractError('Invalid document state.');
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
id: decodeOpaqueIdentifier(input.id, ['id']),
|
|
149
|
+
title: title(input.title),
|
|
150
|
+
size_bytes: count(input.size_bytes, 'size_bytes'),
|
|
151
|
+
state: input.state as DocumentState,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function decodeDocumentCitations(value: unknown): DocumentCitation[] {
|
|
156
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 100) {
|
|
157
|
+
throw new ProtocolContractError('Document citations must contain 1-100 entries.');
|
|
158
|
+
}
|
|
159
|
+
const citations = value.map(decodeDocumentCitation);
|
|
160
|
+
if (new Set(citations.map(({ id }) => id)).size !== citations.length) {
|
|
161
|
+
throw new ProtocolContractError('Document citation ids must be unique.');
|
|
162
|
+
}
|
|
163
|
+
return citations;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function decodeCubeDocumentMetadata(value: unknown): CubeDocumentMetadata {
|
|
167
|
+
const input = object(value);
|
|
168
|
+
exact(input, ['id', 'title', 'size_bytes', 'state', 'content_type', 'supersedes', 'superseded_by', 'author', 'created_at', 'removed_by', 'removed_at'], ['id', 'title', 'size_bytes', 'state', 'content_type', 'supersedes', 'superseded_by', 'author', 'created_at', 'removed_by', 'removed_at']);
|
|
169
|
+
const citation = decodeDocumentCitation({ id: input.id, title: input.title, size_bytes: input.size_bytes, state: input.state });
|
|
170
|
+
const removed = citation.state === 'removed';
|
|
171
|
+
const hasRemovedBy = input.removed_by !== null;
|
|
172
|
+
const hasRemovedAt = input.removed_at !== null;
|
|
173
|
+
if (hasRemovedBy !== hasRemovedAt || removed !== hasRemovedBy) {
|
|
174
|
+
throw new ProtocolContractError('Removed document audit fields do not match its state.');
|
|
175
|
+
}
|
|
176
|
+
if (citation.state === 'active' && input.superseded_by !== null) {
|
|
177
|
+
throw new ProtocolContractError('Active document cannot have a superseding revision.');
|
|
178
|
+
}
|
|
179
|
+
if (citation.state === 'superseded' && input.superseded_by === null) {
|
|
180
|
+
throw new ProtocolContractError('Superseded document must identify its next revision.');
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...citation,
|
|
184
|
+
content_type: contentType(input.content_type),
|
|
185
|
+
supersedes: nullableId(input.supersedes, 'supersedes'),
|
|
186
|
+
superseded_by: nullableId(input.superseded_by, 'superseded_by'),
|
|
187
|
+
author: decodeDocumentActor(input.author),
|
|
188
|
+
created_at: decodeCanonicalTimestamp(input.created_at, ['created_at']),
|
|
189
|
+
removed_by: input.removed_by === null ? null : decodeDocumentActor(input.removed_by),
|
|
190
|
+
removed_at: input.removed_at === null ? null : decodeCanonicalTimestamp(input.removed_at, ['removed_at']),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function decodeCubeDocument(value: unknown): CubeDocument {
|
|
195
|
+
const input = object(value);
|
|
196
|
+
const content = text(input.content, 'content', 10 * 1024 * 1024, true);
|
|
197
|
+
const { content: _content, ...metadataInput } = input;
|
|
198
|
+
const metadata = decodeCubeDocumentMetadata(metadataInput);
|
|
199
|
+
if (metadata.size_bytes !== utf8ByteLength(content)) throw new ProtocolContractError('Document size does not match its UTF-8 content.');
|
|
200
|
+
return { ...metadata, content };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function decodePutDocumentRequest(value: unknown): PutDocumentRequest {
|
|
204
|
+
const input = object(value);
|
|
205
|
+
exact(input, ['title', 'content_type', 'content', 'supersedes'], ['title', 'content_type', 'content']);
|
|
206
|
+
const output: PutDocumentRequest = {
|
|
207
|
+
title: title(input.title),
|
|
208
|
+
content_type: contentType(input.content_type),
|
|
209
|
+
content: text(input.content, 'content', 10 * 1024 * 1024, true),
|
|
210
|
+
};
|
|
211
|
+
if (input.supersedes !== undefined) output.supersedes = decodeOpaqueIdentifier(input.supersedes, ['supersedes']);
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function decodeGetDocumentRequest(value: unknown): GetDocumentRequest {
|
|
216
|
+
const input = object(value); exact(input, ['id'], ['id']);
|
|
217
|
+
return { id: decodeOpaqueIdentifier(input.id, ['id']) };
|
|
218
|
+
}
|
|
219
|
+
export function decodeListDocumentsRequest(value: unknown): ListDocumentsRequest {
|
|
220
|
+
const input = object(value); exact(input, [], []); return {};
|
|
221
|
+
}
|
|
222
|
+
export const decodeRemoveDocumentRequest = decodeGetDocumentRequest;
|
|
223
|
+
|
|
224
|
+
function oneDocument<T>(value: unknown, decode: (input: unknown) => T): { document: T } {
|
|
225
|
+
const input = object(value); exact(input, ['document'], ['document']);
|
|
226
|
+
return { document: decode(input.document) };
|
|
227
|
+
}
|
|
228
|
+
export const decodePutDocumentResult = (value: unknown): PutDocumentResult => {
|
|
229
|
+
const result = oneDocument(value, decodeCubeDocument);
|
|
230
|
+
if (result.document.state !== 'active' || result.document.removed_at !== null || result.document.removed_by !== null) {
|
|
231
|
+
throw new ProtocolContractError('New document result must be active.');
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
};
|
|
235
|
+
export const decodeGetDocumentResult = (value: unknown): GetDocumentResult => oneDocument(value, decodeCubeDocument);
|
|
236
|
+
export const decodeRemoveDocumentResult = (value: unknown): RemoveDocumentResult => {
|
|
237
|
+
const result = oneDocument(value, decodeCubeDocumentMetadata);
|
|
238
|
+
if (result.document.state !== 'removed') throw new ProtocolContractError('Removed document result must be removed.');
|
|
239
|
+
return result;
|
|
240
|
+
};
|
|
241
|
+
export function decodeListDocumentsResult(value: unknown): ListDocumentsResult {
|
|
242
|
+
const input = object(value); exact(input, ['documents'], ['documents']);
|
|
243
|
+
if (!Array.isArray(input.documents) || input.documents.length > 500) throw new ProtocolContractError('Invalid document list.');
|
|
244
|
+
const documents = input.documents.map(decodeCubeDocumentMetadata);
|
|
245
|
+
if (documents.some(({ state }) => state === 'removed')) throw new ProtocolContractError('Removed documents must be delisted.');
|
|
246
|
+
return { documents };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const decodePutDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<PutDocumentRequest> => decodeProtocolEnvelope(value, decodePutDocumentRequest);
|
|
250
|
+
export const decodePutDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<PutDocumentResult> => decodeProtocolEnvelope(value, decodePutDocumentResult);
|
|
251
|
+
export const decodeGetDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<GetDocumentRequest> => decodeProtocolEnvelope(value, decodeGetDocumentRequest);
|
|
252
|
+
export const decodeGetDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<GetDocumentResult> => decodeProtocolEnvelope(value, decodeGetDocumentResult);
|
|
253
|
+
export const decodeListDocumentsRequestEnvelope = (value: unknown): ProtocolEnvelope<ListDocumentsRequest> => decodeProtocolEnvelope(value, decodeListDocumentsRequest);
|
|
254
|
+
export const decodeListDocumentsResultEnvelope = (value: unknown): ProtocolEnvelope<ListDocumentsResult> => decodeProtocolEnvelope(value, decodeListDocumentsResult);
|
|
255
|
+
export const decodeRemoveDocumentRequestEnvelope = (value: unknown): ProtocolEnvelope<RemoveDocumentRequest> => decodeProtocolEnvelope(value, decodeRemoveDocumentRequest);
|
|
256
|
+
export const decodeRemoveDocumentResultEnvelope = (value: unknown): ProtocolEnvelope<RemoveDocumentResult> => decodeProtocolEnvelope(value, decodeRemoveDocumentResult);
|
package/src/protocol/errors.ts
CHANGED
|
@@ -22,6 +22,11 @@ export enum ErrorCode {
|
|
|
22
22
|
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
|
23
23
|
ROLE_SECTION_NOT_FOUND = 'ROLE_SECTION_NOT_FOUND',
|
|
24
24
|
ROLE_HAS_FROZEN_DRONES = 'ROLE_HAS_FROZEN_DRONES',
|
|
25
|
+
DOCUMENT_NOT_FOUND = 'DOCUMENT_NOT_FOUND',
|
|
26
|
+
DOCUMENT_CONTENT_TYPE_UNSUPPORTED = 'DOCUMENT_CONTENT_TYPE_UNSUPPORTED',
|
|
27
|
+
DOCUMENT_BUDGET_EXCEEDED = 'DOCUMENT_BUDGET_EXCEEDED',
|
|
28
|
+
DOCUMENT_SUPERSESSION_INVALID = 'DOCUMENT_SUPERSESSION_INVALID',
|
|
29
|
+
DOCUMENT_REMOVE_DENIED = 'DOCUMENT_REMOVE_DENIED',
|
|
25
30
|
CUBE_DELETED = 'CUBE_DELETED',
|
|
26
31
|
DRONE_EVICTED = 'DRONE_EVICTED',
|
|
27
32
|
DRONE_FROZEN = 'DRONE_FROZEN',
|
package/src/protocol/index.ts
CHANGED
package/src/protocol/sse.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EnrichedStreamEntry } from './types.js';
|
|
2
|
+
import { decodeDocumentCitations } from './documents.js';
|
|
2
3
|
import {
|
|
3
4
|
ProtocolContractError,
|
|
4
5
|
decodeCanonicalTimestamp,
|
|
@@ -6,15 +7,41 @@ import {
|
|
|
6
7
|
decodeLogCursor,
|
|
7
8
|
decodeOpaqueIdentifier,
|
|
8
9
|
decodeUuid,
|
|
10
|
+
PROTOCOL_LIMIT_CEILINGS,
|
|
9
11
|
utf8ByteLength,
|
|
10
12
|
type LogCursor,
|
|
11
13
|
type ProtocolErrorEnvelope,
|
|
12
14
|
} from './contract.js';
|
|
13
15
|
|
|
16
|
+
const MAX_UUID = '00000000-0000-4000-8000-000000000000';
|
|
17
|
+
const MAX_TIMESTAMP = '0000-00-00T00:00:00.000Z';
|
|
18
|
+
const MAX_LOG_DATA_BYTES = utf8ByteLength(JSON.stringify({
|
|
19
|
+
cursor: { created_at: MAX_TIMESTAMP, id: MAX_UUID },
|
|
20
|
+
entry: {
|
|
21
|
+
id: MAX_UUID,
|
|
22
|
+
cube_id: MAX_UUID,
|
|
23
|
+
drone_id: MAX_UUID,
|
|
24
|
+
message: '\0'.repeat(PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes),
|
|
25
|
+
visibility: 'broadcast',
|
|
26
|
+
created_at: MAX_TIMESTAMP,
|
|
27
|
+
drone_label: '\0'.repeat(120),
|
|
28
|
+
role_name: '\0'.repeat(120),
|
|
29
|
+
recipient_drone_ids: Array.from({ length: 100 }, () => MAX_UUID),
|
|
30
|
+
documents: Array.from({ length: 100 }, (_, index) => ({
|
|
31
|
+
id: `${index.toString().padStart(3, '0')}${'x'.repeat(125)}`,
|
|
32
|
+
title: '😀'.repeat(120),
|
|
33
|
+
size_bytes: 10 * 1024 * 1024,
|
|
34
|
+
state: 'superseded',
|
|
35
|
+
})),
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
const MAX_LOG_FRAME_BYTES = MAX_LOG_DATA_BYTES +
|
|
39
|
+
utf8ByteLength(`event: log\nid: ${MAX_UUID}\ndata: `);
|
|
40
|
+
|
|
14
41
|
export const SSE_LIMITS = {
|
|
15
42
|
total_bytes: 1024 * 1024,
|
|
16
|
-
frame_bytes:
|
|
17
|
-
data_bytes:
|
|
43
|
+
frame_bytes: MAX_LOG_FRAME_BYTES,
|
|
44
|
+
data_bytes: MAX_LOG_DATA_BYTES,
|
|
18
45
|
frame_count: 1000,
|
|
19
46
|
unknown_data_bytes: 4096,
|
|
20
47
|
} as const;
|
|
@@ -97,6 +124,7 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
|
|
|
97
124
|
'drone_label',
|
|
98
125
|
'role_name',
|
|
99
126
|
'recipient_drone_ids',
|
|
127
|
+
'documents',
|
|
100
128
|
],
|
|
101
129
|
[
|
|
102
130
|
'id',
|
|
@@ -120,7 +148,11 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
|
|
|
120
148
|
id: decodeUuid(entry.id, ['entry', 'id']),
|
|
121
149
|
cube_id: decodeUuid(entry.cube_id, ['entry', 'cube_id']),
|
|
122
150
|
drone_id: entry.drone_id === null ? null : decodeUuid(entry.drone_id, ['entry', 'drone_id']),
|
|
123
|
-
message: boundedString(
|
|
151
|
+
message: boundedString(
|
|
152
|
+
entry.message,
|
|
153
|
+
'message',
|
|
154
|
+
PROTOCOL_LIMIT_CEILINGS.max_log_message_bytes,
|
|
155
|
+
),
|
|
124
156
|
visibility: entry.visibility,
|
|
125
157
|
created_at: decodeCanonicalTimestamp(entry.created_at, ['entry', 'created_at']),
|
|
126
158
|
drone_label: nullableString(entry.drone_label, 'drone_label', 120),
|
|
@@ -128,6 +160,9 @@ export function decodeEnrichedStreamEntry(value: unknown): EnrichedStreamEntry {
|
|
|
128
160
|
recipient_drone_ids: entry.recipient_drone_ids.map((id, index) =>
|
|
129
161
|
decodeUuid(id, ['entry', 'recipient_drone_ids', index])
|
|
130
162
|
),
|
|
163
|
+
...(entry.documents === undefined ? {} : {
|
|
164
|
+
documents: decodeDocumentCitations(entry.documents),
|
|
165
|
+
}),
|
|
131
166
|
};
|
|
132
167
|
}
|
|
133
168
|
|
package/src/protocol/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MessageTaxonomy } from '../templates.js';
|
|
2
|
+
import type { DocumentCitation } from './documents.js';
|
|
2
3
|
|
|
3
4
|
export type AgentKind = 'claude' | 'codex' | 'opencode';
|
|
4
5
|
export type RoleClass = 'queen' | 'worker';
|
|
@@ -100,6 +101,7 @@ export interface ActivityLogEntry {
|
|
|
100
101
|
message: string;
|
|
101
102
|
visibility: LogVisibility;
|
|
102
103
|
created_at: string;
|
|
104
|
+
documents?: DocumentCitation[];
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export interface EnrichedStreamEntry extends ActivityLogEntry {
|
|
@@ -177,6 +179,7 @@ export interface AppendLogRequest {
|
|
|
177
179
|
recipientDroneIds?: string[];
|
|
178
180
|
class?: string;
|
|
179
181
|
to?: string[];
|
|
182
|
+
documents?: string[];
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
export interface AppendLogResponse {
|
|
@@ -184,6 +187,10 @@ export interface AppendLogResponse {
|
|
|
184
187
|
deduplicated: boolean;
|
|
185
188
|
routing?: RoutingEcho | null;
|
|
186
189
|
unreachableRecipients?: Array<{ id: string; label: string }>;
|
|
190
|
+
advisory?: {
|
|
191
|
+
code: 'STORE_AS_DOCUMENT';
|
|
192
|
+
threshold_bytes: number;
|
|
193
|
+
};
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
export interface Decision {
|
package/src/protocol/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Current Borg coordination protocol generation. Clean-slate
|
|
2
|
-
export const PROTOCOL_VERSION = '
|
|
1
|
+
/** Current Borg coordination protocol generation. Clean-slate v10. */
|
|
2
|
+
export const PROTOCOL_VERSION = '10' as const;
|
|
3
3
|
|
|
4
4
|
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|
package/src/templates.ts
CHANGED
|
@@ -192,6 +192,14 @@ Receipt and liveness:
|
|
|
192
192
|
- Send ACK with \`to:\` to the dispatcher only to confirm receipt; it does not start or complete work.
|
|
193
193
|
- Reply to a directed PING with PONG and \`to:\` to the sender.`;
|
|
194
194
|
|
|
195
|
+
const OPERATOR_CONTROLLED_OWNERSHIP_DISCIPLINE = `
|
|
196
|
+
|
|
197
|
+
Ownership and liveness:
|
|
198
|
+
- Follow active work through concrete milestones from the dispatch and acceptance evidence, not a fixed elapsed-time cadence.
|
|
199
|
+
- If silence or liveness evidence makes status uncertain, send one direct status request and report the evidence to the human.
|
|
200
|
+
- Silence, delay, stale or disconnected state, and missed milestones never authorize rerouting or reassignment.
|
|
201
|
+
- Coordinator, Queen, or Director rerouting or reassignment requires explicit human operator approval for the exact work item and recipient.`;
|
|
202
|
+
|
|
195
203
|
const SOFTWARE_DEV_DIRECTIVE = `## Scope and coordination
|
|
196
204
|
|
|
197
205
|
- The human-authorized outcome, repositories, acceptance criteria, and permitted mutations are the hard boundary.
|
|
@@ -300,9 +308,9 @@ Scope contract:
|
|
|
300
308
|
Activation:
|
|
301
309
|
- Order named drones to start exact authorized work with START NOW, RESUME NOW, REVIEW NOW, or HOLD; name the exact item and first concrete action.
|
|
302
310
|
- ACK and claim are receipt only; neither means work has started or a review is complete.
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
-
|
|
311
|
+
- Verify activation and progress against the concrete milestones from the dispatch and acceptance evidence.
|
|
312
|
+
- When a milestone is missing and status is uncertain, follow the ownership and liveness discipline. Do not interrupt slow local work merely to satisfy a reporting cadence.
|
|
313
|
+
- Require BLOCKED when safe work stops, naming the missing input while independent work continues.
|
|
306
314
|
- Waiting is valid when work is complete, blocked, under active review, or awaiting human authority. Never manufacture work to avoid idleness.
|
|
307
315
|
|
|
308
316
|
Review:
|
|
@@ -321,7 +329,7 @@ Communication:
|
|
|
321
329
|
- Send PING with \`to:\` only for a directed liveness check. Use DECISION or HALT only for an intentional cube-wide human-seat message. After an authorized merge, broadcast MERGED with the exact merge SHA.
|
|
322
330
|
- Keep the primary playbook operational and concise. Delete obsolete, redundant, historical, cautionary, and example-heavy prose; do not relocate it into new runbooks, decisions, contracts, rationale, or case-study archives unless it has a current operational consumer.
|
|
323
331
|
|
|
324
|
-
Builders implement; reviewers review; you coordinate. Integrate only when authorized.${COORDINATOR_FINDING_DISPATCH_DISCIPLINE}${SERIALIZED_REVIEW_ROUNDS_DISCIPLINE}${GIT_OPERATIONAL_DISCIPLINE_COORDINATOR}${PUSH_DISCIPLINE_COORDINATOR}${DRONE_ADDRESSING_CONVENTION}${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}${DIRECTED_DISCUSSION_DISCIPLINE}${RECEIPT_AND_LIVENESS_DISCIPLINE}`;
|
|
332
|
+
Builders implement; reviewers review; you coordinate. Integrate only when authorized.${COORDINATOR_FINDING_DISPATCH_DISCIPLINE}${SERIALIZED_REVIEW_ROUNDS_DISCIPLINE}${GIT_OPERATIONAL_DISCIPLINE_COORDINATOR}${PUSH_DISCIPLINE_COORDINATOR}${DRONE_ADDRESSING_CONVENTION}${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}${DIRECTED_DISCUSSION_DISCIPLINE}${RECEIPT_AND_LIVENESS_DISCIPLINE}${OPERATOR_CONTROLLED_OWNERSHIP_DISCIPLINE}`;
|
|
325
333
|
|
|
326
334
|
// Producer minimalism adapts principles from https://github.com/DietrichGebert/ponytail
|
|
327
335
|
// (MIT); this wording is original to Borg MCP.
|
|
@@ -343,7 +351,7 @@ Implementation discipline:
|
|
|
343
351
|
- Mark a deliberate corner-cut with a comment naming the known ceiling and the upgrade path.
|
|
344
352
|
|
|
345
353
|
While working:
|
|
346
|
-
- Post STARTING with the branch and first concrete action.
|
|
354
|
+
- Post STARTING with the branch and first concrete action. Post PROGRESS only when a substantive milestone changes what the Coordinator needs to know. Do not interrupt slow local work merely to satisfy a reporting cadence.
|
|
347
355
|
- Do not add cleanup, broad refactors, speculative hardening, documentation programs, or follow-up issues unless assigned.
|
|
348
356
|
- A discovered issue outside the slice is a finding, not permission to fix it.
|
|
349
357
|
- Add proportionate tests for behavior you change. Run focused verification required by the touched surface, and do not rerun green CI checks merely to duplicate exact-revision evidence.
|
|
@@ -593,13 +601,13 @@ const STARTER: Template = {
|
|
|
593
601
|
|
|
594
602
|
- State the exact work item, boundaries, first action, and completion evidence.
|
|
595
603
|
- Route START NOW, RESUME NOW, REVIEW NOW, or HOLD to a named drone.
|
|
596
|
-
- ACK is receipt only; verify
|
|
604
|
+
- ACK is receipt only; verify activation and progress against concrete milestones from the dispatch and acceptance evidence.
|
|
597
605
|
- Questions, findings, proposals, open queues, and spare capacity do not authorize new work.
|
|
598
606
|
- Route completed work to the Reviewer only when review is required.
|
|
599
607
|
- Send START NOW, RESUME NOW, REVIEW NOW, and HOLD with \`to:\` to the named Worker or Reviewer.
|
|
600
608
|
- Send PING with \`to:\` only for a directed liveness check. Use DECISION or HALT only for an intentional cube-wide human-seat message.
|
|
601
609
|
- Ask the human before rescoping, abandoning, waiving, merging, shipping, publishing, or taking an irreversible action unless already delegated.
|
|
602
|
-
- Waiting is valid when work is complete, blocked, under review, or awaiting authority.${COORDINATOR_FINDING_DISPATCH_DISCIPLINE}${ANTI_PASSIVE_STANDING_DISCIPLINE}${DRONE_ADDRESSING_CONVENTION}${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}${DIRECTED_DISCUSSION_DISCIPLINE}${RECEIPT_AND_LIVENESS_DISCIPLINE}`,
|
|
610
|
+
- Waiting is valid when work is complete, blocked, under review, or awaiting authority.${COORDINATOR_FINDING_DISPATCH_DISCIPLINE}${ANTI_PASSIVE_STANDING_DISCIPLINE}${DRONE_ADDRESSING_CONVENTION}${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}${DIRECTED_DISCUSSION_DISCIPLINE}${RECEIPT_AND_LIVENESS_DISCIPLINE}${OPERATOR_CONTROLLED_OWNERSHIP_DISCIPLINE}`,
|
|
603
611
|
},
|
|
604
612
|
{
|
|
605
613
|
name: 'Worker',
|
|
@@ -608,7 +616,7 @@ const STARTER: Template = {
|
|
|
608
616
|
detailed_description: `Execute only work explicitly dispatched to you.
|
|
609
617
|
|
|
610
618
|
- Confirm the exact item, boundaries, and expected evidence before changing anything.
|
|
611
|
-
- Post STARTING, perform the smallest coherent task, and report substantive
|
|
619
|
+
- Post STARTING, perform the smallest coherent task, and report PROGRESS when a substantive milestone changes what the Coordinator needs to know. Do not interrupt slow local work merely to satisfy a reporting cadence.
|
|
612
620
|
- Preserve unrelated state. Do not add cleanup, speculative improvements, or follow-up work.
|
|
613
621
|
- If blocked, state the missing input and stop affected mutation; do not silently change the goal.
|
|
614
622
|
- Send STARTING, PROGRESS, DONE, REVIEW-READY, and BLOCKED with \`to:\` to the Coordinator, with the result and verification evidence.
|
|
@@ -749,7 +757,7 @@ Continuity:
|
|
|
749
757
|
- DISPATCH, HOLD, and DECISION are not completion when they leave an authorized follow-on action.
|
|
750
758
|
- After answering an interruption, resume any Director action you can advance in the same turn.
|
|
751
759
|
- Waiting is valid only when no routed Director action or active outcome remains, or while a named Shaper/reviewer/human decision is outstanding and you have no independent action.
|
|
752
|
-
- An active Director outcome ends with APPROVED, or with BLOCKED naming the missing decision or the reason it cannot proceed. After DECISION, continue with any dispatch or verification that decision enables.${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}`;
|
|
760
|
+
- An active Director outcome ends with APPROVED, or with BLOCKED naming the missing decision or the reason it cannot proceed. After DECISION, continue with any dispatch or verification that decision enables.${STRUCTURED_MESSAGE_ROUTING_DISCIPLINE}${OPERATOR_CONTROLLED_OWNERSHIP_DISCIPLINE}`;
|
|
753
761
|
|
|
754
762
|
// Producer minimalism adapts principles from https://github.com/DietrichGebert/ponytail
|
|
755
763
|
// (MIT); this wording is original to Borg MCP.
|