borgmcp-shared 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conformance/adapter.d.ts +6 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +62 -1
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +24 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +22 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +1 -1
- package/dist/protocol/contract.js +2 -2
- package/dist/protocol/coordination.d.ts +26 -1
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +52 -0
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1 -0
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +6 -0
- package/docs/release-records.json +14 -0
- package/docs/releases/0.14.0.md +8 -0
- package/package.json +1 -1
- package/src/conformance/adapter.ts +139 -0
- package/src/conformance/index.ts +34 -0
- package/src/protocol/contract.ts +2 -2
- package/src/protocol/coordination.ts +100 -0
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +1 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
decodeCreateCubeResponseEnvelope,
|
|
7
7
|
decodeDeleteCubeResponseEnvelope,
|
|
8
8
|
decodeAppendLogResultEnvelope,
|
|
9
|
+
decodeAckStatusResultEnvelope,
|
|
9
10
|
decodeDecisionResultEnvelope,
|
|
10
11
|
decodeDecisionsResultEnvelope,
|
|
11
12
|
decodeEnrollmentExchangeResponseEnvelope,
|
|
@@ -119,6 +120,8 @@ export interface ConformanceCubeManagementState {
|
|
|
119
120
|
export interface ConformanceAuthorityState {
|
|
120
121
|
enrolled_clients: number;
|
|
121
122
|
enrollment_claims: number;
|
|
123
|
+
activity_acknowledgements: number;
|
|
124
|
+
activity_claims: number;
|
|
122
125
|
cubes: number;
|
|
123
126
|
roles: number;
|
|
124
127
|
grants: number;
|
|
@@ -304,6 +307,11 @@ export interface ConformanceOperations {
|
|
|
304
307
|
cube: ConformanceCube,
|
|
305
308
|
request: unknown,
|
|
306
309
|
): Promise<ConformanceHttpResponse>;
|
|
310
|
+
ackStatus(
|
|
311
|
+
credential: string,
|
|
312
|
+
cube: ConformanceCube,
|
|
313
|
+
request: unknown,
|
|
314
|
+
): Promise<ConformanceHttpResponse>;
|
|
307
315
|
updateCube(
|
|
308
316
|
credential: string,
|
|
309
317
|
cube: ConformanceCube,
|
|
@@ -383,6 +391,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
383
391
|
{ id: 'sse.replay-live-transition', area: 'sse' },
|
|
384
392
|
{ id: 'cursor.explicit-expiry', area: 'cursor' },
|
|
385
393
|
{ id: 'acks.idempotent', area: 'acks' },
|
|
394
|
+
{ id: 'acks.status-query', area: 'acks' },
|
|
386
395
|
{ id: 'claims.durable-noncursor', area: 'claims' },
|
|
387
396
|
{ id: 'decisions.topic-supersession', area: 'decisions' },
|
|
388
397
|
{ id: 'security.drone-management-authorization', area: 'security' },
|
|
@@ -1994,6 +2003,136 @@ export async function runAdapterConformance(
|
|
|
1994
2003
|
return { first_status: 204, repeated_status: 204, bodyless: true };
|
|
1995
2004
|
});
|
|
1996
2005
|
|
|
2006
|
+
await record('acks.status-query', async () => {
|
|
2007
|
+
const cube = await environment.admin.createCube('ack-status-cube');
|
|
2008
|
+
await environment.admin.grantCube(principalA, cube);
|
|
2009
|
+
const role = await environment.admin.createRole(cube, {
|
|
2010
|
+
roleClass: 'worker',
|
|
2011
|
+
isHumanSeat: false,
|
|
2012
|
+
name: 'Ack Status Worker',
|
|
2013
|
+
});
|
|
2014
|
+
const acknowledgedRecipient = await environment.admin.createDrone(principalA, cube, role);
|
|
2015
|
+
const claimingRecipient = await environment.admin.createDrone(principalA, cube, role);
|
|
2016
|
+
const acknowledgedCredential = await environment.admin.issueManagedDroneSession(acknowledgedRecipient);
|
|
2017
|
+
const claimingCredential = await environment.admin.issueManagedDroneSession(claimingRecipient);
|
|
2018
|
+
const append = await environment.operations.append(
|
|
2019
|
+
credentialA,
|
|
2020
|
+
cube,
|
|
2021
|
+
createProtocolEnvelope('ack-status-append', {
|
|
2022
|
+
post_id: '00000000-0000-4000-8000-000000000310',
|
|
2023
|
+
message: 'START NOW acknowledgement status fixture',
|
|
2024
|
+
visibility: 'direct',
|
|
2025
|
+
recipientDroneIds: [acknowledgedRecipient.id, claimingRecipient.id],
|
|
2026
|
+
}),
|
|
2027
|
+
);
|
|
2028
|
+
expectStatus(append, 201, 'Acknowledgement-status fixture append');
|
|
2029
|
+
const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
|
|
2030
|
+
|
|
2031
|
+
const beforeMissing = await environment.admin.observeAuthorityState();
|
|
2032
|
+
const missingResponse = await environment.operations.ackStatus(
|
|
2033
|
+
credentialA,
|
|
2034
|
+
cube,
|
|
2035
|
+
createProtocolEnvelope('ack-status-missing', { entry_id: entry.id }),
|
|
2036
|
+
);
|
|
2037
|
+
expectStatus(missingResponse, 200, 'Missing acknowledgement status');
|
|
2038
|
+
const missing = decodeAckStatusResultEnvelope(missingResponse.body).payload;
|
|
2039
|
+
invariant(
|
|
2040
|
+
missing.visibility === 'direct' &&
|
|
2041
|
+
missing.recipients.length === 2 &&
|
|
2042
|
+
missing.recipients.every((recipient) => recipient.acknowledged_at === null) &&
|
|
2043
|
+
missing.claims.length === 0,
|
|
2044
|
+
'Missing acknowledgement was not represented as nullable per-recipient state.',
|
|
2045
|
+
);
|
|
2046
|
+
assertStateDelta(
|
|
2047
|
+
beforeMissing,
|
|
2048
|
+
await environment.admin.observeAuthorityState(),
|
|
2049
|
+
{},
|
|
2050
|
+
'Missing acknowledgement status query',
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
expectStatus(
|
|
2054
|
+
await environment.operations.ack(
|
|
2055
|
+
acknowledgedCredential,
|
|
2056
|
+
cube,
|
|
2057
|
+
createProtocolEnvelope('ack-status-ack', { entry_id: entry.id, kind: 'ack' }),
|
|
2058
|
+
),
|
|
2059
|
+
204,
|
|
2060
|
+
'Acknowledgement-status acknowledgement',
|
|
2061
|
+
);
|
|
2062
|
+
expectStatus(
|
|
2063
|
+
await environment.operations.ack(
|
|
2064
|
+
claimingCredential,
|
|
2065
|
+
cube,
|
|
2066
|
+
createProtocolEnvelope('ack-status-claim', { entry_id: entry.id, kind: 'claim' }),
|
|
2067
|
+
),
|
|
2068
|
+
204,
|
|
2069
|
+
'Acknowledgement-status claim',
|
|
2070
|
+
);
|
|
2071
|
+
const beforeDistinct = await environment.admin.observeAuthorityState();
|
|
2072
|
+
const distinctResponse = await environment.operations.ackStatus(
|
|
2073
|
+
credentialA,
|
|
2074
|
+
cube,
|
|
2075
|
+
createProtocolEnvelope('ack-status-distinct', { entry_id: entry.id }),
|
|
2076
|
+
);
|
|
2077
|
+
expectStatus(distinctResponse, 200, 'Distinct acknowledgement and claim status');
|
|
2078
|
+
const distinct = decodeAckStatusResultEnvelope(distinctResponse.body).payload;
|
|
2079
|
+
const acknowledged = distinct.recipients.filter((recipient) => recipient.acknowledged_at !== null);
|
|
2080
|
+
invariant(
|
|
2081
|
+
acknowledged.length === 1 &&
|
|
2082
|
+
acknowledged[0].drone_id === acknowledgedRecipient.id &&
|
|
2083
|
+
distinct.claims.length === 1 &&
|
|
2084
|
+
distinct.claims[0].drone_id === claimingRecipient.id &&
|
|
2085
|
+
distinct.recipients.find((recipient) => recipient.drone_id === claimingRecipient.id)?.acknowledged_at === null,
|
|
2086
|
+
'Acknowledgement status conflated acknowledgement and claim records.',
|
|
2087
|
+
);
|
|
2088
|
+
assertStateDelta(
|
|
2089
|
+
beforeDistinct,
|
|
2090
|
+
await environment.admin.observeAuthorityState(),
|
|
2091
|
+
{},
|
|
2092
|
+
'Distinct acknowledgement and claim status query',
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
const unreadResponse = await environment.operations.read(
|
|
2096
|
+
credentialA,
|
|
2097
|
+
cube,
|
|
2098
|
+
createProtocolEnvelope('ack-status-unread', { cursor: null, limit: 10 }),
|
|
2099
|
+
);
|
|
2100
|
+
expectStatus(unreadResponse, 200, 'Post-query unread read');
|
|
2101
|
+
const unread = decodeReadLogResultEnvelope(unreadResponse.body).payload;
|
|
2102
|
+
invariant(
|
|
2103
|
+
unread.entries.length === 1 && unread.entries[0].id === entry.id,
|
|
2104
|
+
'Acknowledgement-status query advanced unread delivery state.',
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
const beforeUnknown = await environment.admin.observeAuthorityState();
|
|
2108
|
+
expectError(
|
|
2109
|
+
await environment.operations.ackStatus(
|
|
2110
|
+
credentialA,
|
|
2111
|
+
cube,
|
|
2112
|
+
createProtocolEnvelope('ack-status-unknown', {
|
|
2113
|
+
entry_id: '00000000-0000-4000-8000-000000000311',
|
|
2114
|
+
}),
|
|
2115
|
+
),
|
|
2116
|
+
404,
|
|
2117
|
+
ErrorCode.NOT_FOUND,
|
|
2118
|
+
'Unknown acknowledgement-status entry',
|
|
2119
|
+
);
|
|
2120
|
+
assertStateDelta(
|
|
2121
|
+
beforeUnknown,
|
|
2122
|
+
await environment.admin.observeAuthorityState(),
|
|
2123
|
+
{},
|
|
2124
|
+
'Unknown acknowledgement status query',
|
|
2125
|
+
);
|
|
2126
|
+
return {
|
|
2127
|
+
missing_acknowledgement: null,
|
|
2128
|
+
acknowledgements: 1,
|
|
2129
|
+
claims: 1,
|
|
2130
|
+
distinct: true,
|
|
2131
|
+
unread_entry_available: true,
|
|
2132
|
+
unknown_entry_code: ErrorCode.NOT_FOUND,
|
|
2133
|
+
};
|
|
2134
|
+
});
|
|
2135
|
+
|
|
1997
2136
|
await record('claims.durable-noncursor', async () => {
|
|
1998
2137
|
invariant(entries[1] && liveCursor, 'Log fixtures did not produce claim state.');
|
|
1999
2138
|
const claim = await environment.operations.ack(
|
package/src/conformance/index.ts
CHANGED
|
@@ -1012,6 +1012,40 @@ export const ROLE_RATIONALE_CONFORMANCE: readonly RoleRationaleConformanceVector
|
|
|
1012
1012
|
},
|
|
1013
1013
|
];
|
|
1014
1014
|
|
|
1015
|
+
export interface AckStatusConformanceVector {
|
|
1016
|
+
name: string;
|
|
1017
|
+
fixture: 'missing-ack' | 'ack-and-claim' | 'unread-preserved' | 'unknown-entry';
|
|
1018
|
+
expected:
|
|
1019
|
+
| { status: 200; acknowledged_at: null; mutation: 'none' }
|
|
1020
|
+
| { status: 200; acknowledgements: 1; claims: 1; distinct: true; mutation: 'none' }
|
|
1021
|
+
| { status: 200; unread_entry_available: true; mutation: 'none' }
|
|
1022
|
+
| { status: 404; error: 'NOT_FOUND'; mutation: 'none' };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Portable acknowledgement-status outcomes for one exact log entry. */
|
|
1026
|
+
export const ACK_STATUS_CONFORMANCE: readonly AckStatusConformanceVector[] = [
|
|
1027
|
+
{
|
|
1028
|
+
name: 'reports an intended recipient with no acknowledgement',
|
|
1029
|
+
fixture: 'missing-ack',
|
|
1030
|
+
expected: { status: 200, acknowledged_at: null, mutation: 'none' },
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: 'keeps acknowledgements and claims in separate result collections',
|
|
1034
|
+
fixture: 'ack-and-claim',
|
|
1035
|
+
expected: { status: 200, acknowledgements: 1, claims: 1, distinct: true, mutation: 'none' },
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: 'does not consume the queried entry from unread delivery',
|
|
1039
|
+
fixture: 'unread-preserved',
|
|
1040
|
+
expected: { status: 200, unread_entry_available: true, mutation: 'none' },
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: 'returns not found for an unknown entry instead of missing acknowledgement state',
|
|
1044
|
+
fixture: 'unknown-entry',
|
|
1045
|
+
expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
|
|
1046
|
+
},
|
|
1047
|
+
];
|
|
1048
|
+
|
|
1015
1049
|
/** One canonical corpus consumed unchanged by shared, server, and client tests. */
|
|
1016
1050
|
export const RUNTIME_METADATA_REPOSITORY_CONFORMANCE:
|
|
1017
1051
|
readonly RuntimeMetadataRepositoryConformanceVector[] = [
|
package/src/protocol/contract.ts
CHANGED
|
@@ -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.
|
|
16
|
+
export const SHARED_PACKAGE_VERSION = '0.14.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. */
|
|
@@ -590,7 +590,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
|
|
|
590
590
|
exactKeys(input, ['protocol_version'], ['protocol_version']);
|
|
591
591
|
if (input.protocol_version !== PROTOCOL_VERSION) {
|
|
592
592
|
throw new ProtocolContractError(
|
|
593
|
-
'This client requires protocol
|
|
593
|
+
'This client requires protocol v11. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
|
|
594
594
|
ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
|
|
595
595
|
['protocol_version'],
|
|
596
596
|
);
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
AppendLogResponse,
|
|
17
17
|
Decision,
|
|
18
18
|
EnrichedStreamEntry,
|
|
19
|
+
LogVisibility,
|
|
19
20
|
RoutingEcho,
|
|
20
21
|
} from './types.js';
|
|
21
22
|
import { isLabelLine, parseRoleSections } from '../role-section.js';
|
|
@@ -46,6 +47,31 @@ export interface ReadLogResult {
|
|
|
46
47
|
claims: ClaimRecord[];
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export interface AckStatusRequest {
|
|
51
|
+
entry_id: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AckStatusRecipient {
|
|
55
|
+
drone_id: string;
|
|
56
|
+
drone_label: string | null;
|
|
57
|
+
drone_role: string | null;
|
|
58
|
+
acknowledged_at: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AckStatusClaim {
|
|
62
|
+
drone_id: string;
|
|
63
|
+
drone_label: string | null;
|
|
64
|
+
drone_role: string | null;
|
|
65
|
+
claimed_at: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AckStatusResult {
|
|
69
|
+
entry_id: string;
|
|
70
|
+
visibility: LogVisibility;
|
|
71
|
+
recipients: AckStatusRecipient[];
|
|
72
|
+
claims: AckStatusClaim[];
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
export interface ReassignDroneRequest {
|
|
50
76
|
role_id: string;
|
|
51
77
|
}
|
|
@@ -309,6 +335,80 @@ export function decodeReadLogRequestEnvelope(
|
|
|
309
335
|
return decodeProtocolEnvelope(value, decodeReadLogRequest);
|
|
310
336
|
}
|
|
311
337
|
|
|
338
|
+
export function decodeAckStatusRequest(value: unknown): AckStatusRequest {
|
|
339
|
+
const input = object(value);
|
|
340
|
+
exact(input, ['entry_id'], ['entry_id']);
|
|
341
|
+
return { entry_id: decodeUuid(input.entry_id, ['entry_id']) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function decodeAckStatusRequestEnvelope(
|
|
345
|
+
value: unknown,
|
|
346
|
+
): ProtocolEnvelope<AckStatusRequest> {
|
|
347
|
+
return decodeProtocolEnvelope(value, decodeAckStatusRequest);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function decodeAckStatusRecipient(value: unknown): AckStatusRecipient {
|
|
351
|
+
const input = object(value);
|
|
352
|
+
exact(
|
|
353
|
+
input,
|
|
354
|
+
['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
|
|
355
|
+
['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
|
|
356
|
+
);
|
|
357
|
+
return {
|
|
358
|
+
drone_id: decodeUuid(input.drone_id, ['drone_id']),
|
|
359
|
+
drone_label: nullableString(input.drone_label, 'drone_label', 120),
|
|
360
|
+
drone_role: nullableString(input.drone_role, 'drone_role', 120),
|
|
361
|
+
acknowledged_at: input.acknowledged_at === null
|
|
362
|
+
? null
|
|
363
|
+
: decodeCanonicalTimestamp(input.acknowledged_at, ['acknowledged_at']),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function decodeAckStatusClaim(value: unknown): AckStatusClaim {
|
|
368
|
+
const input = object(value);
|
|
369
|
+
exact(
|
|
370
|
+
input,
|
|
371
|
+
['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
|
|
372
|
+
['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
|
|
373
|
+
);
|
|
374
|
+
return {
|
|
375
|
+
drone_id: decodeUuid(input.drone_id, ['drone_id']),
|
|
376
|
+
drone_label: nullableString(input.drone_label, 'drone_label', 120),
|
|
377
|
+
drone_role: nullableString(input.drone_role, 'drone_role', 120),
|
|
378
|
+
claimed_at: decodeCanonicalTimestamp(input.claimed_at, ['claimed_at']),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function decodeAckStatusResult(value: unknown): AckStatusResult {
|
|
383
|
+
const input = object(value);
|
|
384
|
+
exact(
|
|
385
|
+
input,
|
|
386
|
+
['entry_id', 'visibility', 'recipients', 'claims'],
|
|
387
|
+
['entry_id', 'visibility', 'recipients', 'claims'],
|
|
388
|
+
);
|
|
389
|
+
if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
|
|
390
|
+
throw new ProtocolContractError('Invalid acknowledgement-status visibility.');
|
|
391
|
+
}
|
|
392
|
+
if (!Array.isArray(input.recipients) || input.recipients.length > 500) {
|
|
393
|
+
throw new ProtocolContractError('Invalid acknowledgement-status recipients.');
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(input.claims) || input.claims.length > 500) {
|
|
396
|
+
throw new ProtocolContractError('Invalid acknowledgement-status claims.');
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
entry_id: decodeUuid(input.entry_id, ['entry_id']),
|
|
400
|
+
visibility: input.visibility,
|
|
401
|
+
recipients: input.recipients.map(decodeAckStatusRecipient),
|
|
402
|
+
claims: input.claims.map(decodeAckStatusClaim),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function decodeAckStatusResultEnvelope(
|
|
407
|
+
value: unknown,
|
|
408
|
+
): ProtocolEnvelope<AckStatusResult> {
|
|
409
|
+
return decodeProtocolEnvelope(value, decodeAckStatusResult);
|
|
410
|
+
}
|
|
411
|
+
|
|
312
412
|
function decodeClaimRecord(value: unknown): ClaimRecord {
|
|
313
413
|
const input = object(value);
|
|
314
414
|
exact(
|
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 v11. */
|
|
2
|
+
export const PROTOCOL_VERSION = '11' as const;
|
|
3
3
|
|
|
4
4
|
export type ProtocolVersion = typeof PROTOCOL_VERSION;
|
package/src/templates.ts
CHANGED
|
@@ -308,6 +308,7 @@ Scope contract:
|
|
|
308
308
|
Activation:
|
|
309
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.
|
|
310
310
|
- ACK and claim are receipt only; neither means work has started or a review is complete.
|
|
311
|
+
- When receipt is uncertain, use \`borg_ack-status\` for the routed entry; it reports acknowledgements and claims without advancing unread cursors.
|
|
311
312
|
- Verify activation and progress against the concrete milestones from the dispatch and acceptance evidence.
|
|
312
313
|
- 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
314
|
- Require BLOCKED when safe work stops, naming the missing input while independent work continues.
|