@etiquekit/etq 1.0.12 → 1.0.13

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 (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -56
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +7 -6
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +11 -11
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4686 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +831 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +14 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,632 @@
1
+ type JsonRecord = Record<string, unknown>;
2
+
3
+ export const RIG_CONDUCTOR_SCHEMA = 'rig_conductor.v0';
4
+ export const RIG_CONDUCTOR_VALIDATION_SCHEMA = 'rig-conductor-validation.v0';
5
+
6
+ const EXECUTION_STRATEGIES = ['solo', 'tango', 'rig', 'shadow'] as const;
7
+ const RIG_EVENT_TYPES = ['rig_opened', 'child_spawned', 'child_returned', 'rig_closed'] as const;
8
+ const STRUCTURED_CHECK_STATUSES = ['pass', 'fail', 'not_run', 'not_applicable'] as const;
9
+ const RESERVED_STRATEGIES = ['shadow'] as const;
10
+ const CANONICAL_WRITE_PREFIXES = [
11
+ '.etiquette/events',
12
+ '.etiquette/notify',
13
+ 'docs/work/ledger',
14
+ 'docs/work/packets',
15
+ 'docs/work/lanes',
16
+ 'artifacts/',
17
+ ] as const;
18
+ const CANONICAL_TOOLS = [
19
+ 'bus_write',
20
+ 'ledger_write',
21
+ 'merge',
22
+ 'push',
23
+ 'protected_branch_push',
24
+ 'close_lane',
25
+ 'grant',
26
+ 'sign',
27
+ 'rotate_secret',
28
+ 'mutate_authority',
29
+ ] as const;
30
+
31
+ export type RigExecutionStrategy = (typeof EXECUTION_STRATEGIES)[number];
32
+ export type RigLifecycleEventType = (typeof RIG_EVENT_TYPES)[number];
33
+ export type RigConductorStructuredCheckStatus = (typeof STRUCTURED_CHECK_STATUSES)[number];
34
+
35
+ export interface RigConductorAuthorityBoundary {
36
+ topology_grants_authority: false;
37
+ parenthood_grants_authority: false;
38
+ child_mints_authority: false;
39
+ child_can_write_parent_ledger: false;
40
+ station_role_grants_authority: false;
41
+ validation_is_authorization: false;
42
+ }
43
+
44
+ export interface RigConductorStations {
45
+ orchestration_lead: string;
46
+ rig_lead: string;
47
+ synthesis_owner: string;
48
+ reviewer: string;
49
+ }
50
+
51
+ export interface RigConductorStationEdge {
52
+ from: keyof RigConductorStations;
53
+ to: keyof RigConductorStations;
54
+ }
55
+
56
+ export interface RigConductorReturnContract {
57
+ return_to: string;
58
+ required_refs: string[];
59
+ disposal_confirmed_required: true;
60
+ }
61
+
62
+ export interface RigConductorTaskSpec {
63
+ task_id: string;
64
+ lane: string;
65
+ assigned_to: string;
66
+ allowed_writes: string[];
67
+ validation: string[];
68
+ return_contract: RigConductorReturnContract;
69
+ }
70
+
71
+ export interface RigConductorChildNode {
72
+ node_id: string;
73
+ parent_node: string;
74
+ return_to: string;
75
+ lane: string;
76
+ assigned_to: string;
77
+ scratch_ref?: string;
78
+ allowed_writes: string[];
79
+ allowed_tools: string[];
80
+ forbidden_actions: string[];
81
+ child_spawn_budget: number;
82
+ task_spec: RigConductorTaskSpec;
83
+ }
84
+
85
+ export interface RigConductorStructuredCheck {
86
+ name: string;
87
+ compile_status?: RigConductorStructuredCheckStatus;
88
+ gate_status?: RigConductorStructuredCheckStatus;
89
+ tests_failed?: number;
90
+ blocking: boolean;
91
+ }
92
+
93
+ export interface RigConductorLifecycleEvent {
94
+ type: RigLifecycleEventType;
95
+ node_id?: string;
96
+ ref?: string;
97
+ disposal_confirmed?: boolean;
98
+ checks?: RigConductorStructuredCheck[];
99
+ }
100
+
101
+ export interface RigConductorSynthesis {
102
+ owner: string;
103
+ return_to: string;
104
+ child_return_refs: string[];
105
+ disposal_confirmed: boolean;
106
+ }
107
+
108
+ export interface RigConductorEnvelope {
109
+ schema: typeof RIG_CONDUCTOR_SCHEMA;
110
+ rig_id: string;
111
+ workflow_id: string;
112
+ lane: string;
113
+ execution_strategy: RigExecutionStrategy;
114
+ parent_node: string;
115
+ return_to: string;
116
+ synthesis_owner: string;
117
+ allowed_writes: string[];
118
+ allowed_tools: string[];
119
+ forbidden_actions: string[];
120
+ child_spawn_budget: number;
121
+ max_depth: number;
122
+ total_descendant_budget: number;
123
+ ephemeral_children: true;
124
+ stations: RigConductorStations;
125
+ station_edges: RigConductorStationEdge[];
126
+ child_nodes: RigConductorChildNode[];
127
+ lifecycle_events: RigConductorLifecycleEvent[];
128
+ synthesis?: RigConductorSynthesis;
129
+ authority_boundary: RigConductorAuthorityBoundary;
130
+ }
131
+
132
+ export interface RigConductorValidation {
133
+ schema: typeof RIG_CONDUCTOR_VALIDATION_SCHEMA;
134
+ rig_ref: string;
135
+ verdict: 'legitimate' | 'blocked';
136
+ reasons: string[];
137
+ checks: {
138
+ strategy_supported: boolean;
139
+ station_graph_acyclic: boolean;
140
+ reviewer_not_implementer: boolean;
141
+ child_envelopes_subset_parent: boolean;
142
+ child_task_specs_subset_envelopes: boolean;
143
+ child_scratch_isolated: boolean;
144
+ children_not_canonical_writers: boolean;
145
+ child_budget_within_parent: boolean;
146
+ depth_within_max: boolean;
147
+ synthesis_has_child_refs: boolean;
148
+ child_return_checks_present: boolean;
149
+ child_return_checks_nonblocking: boolean;
150
+ disposal_confirmed: boolean;
151
+ lifecycle_events_valid: boolean;
152
+ authority_boundary_false: boolean;
153
+ };
154
+ derived: {
155
+ max_observed_depth: number;
156
+ child_count: number;
157
+ descendant_budget_requested: number;
158
+ };
159
+ authority_boundary: RigConductorAuthorityBoundary & {
160
+ validation_grants_authority: false;
161
+ topology_is_routing_only: true;
162
+ };
163
+ }
164
+
165
+ function isRecord(value: unknown): value is JsonRecord {
166
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
167
+ }
168
+
169
+ function asRecord(value: unknown, field: string): JsonRecord {
170
+ if (!isRecord(value)) throw new Error(`${field} must be an object`);
171
+ return value;
172
+ }
173
+
174
+ function asString(record: JsonRecord, field: string): string {
175
+ const value = record[field];
176
+ if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`${field} must be a non-empty string`);
177
+ return value.trim();
178
+ }
179
+
180
+ function optionalString(record: JsonRecord, field: string): string | undefined {
181
+ const value = record[field];
182
+ if (value === undefined) return undefined;
183
+ if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`${field} must be a non-empty string when present`);
184
+ return value.trim();
185
+ }
186
+
187
+ function asStringArray(record: JsonRecord, field: string): string[] {
188
+ const value = record[field];
189
+ if (!Array.isArray(value)) throw new Error(`${field} must be a string array`);
190
+ return value.map((item) => {
191
+ if (typeof item !== 'string' || item.trim().length === 0) throw new Error(`${field} must contain only non-empty strings`);
192
+ return item.trim();
193
+ });
194
+ }
195
+
196
+ function asNonNegativeInteger(record: JsonRecord, field: string): number {
197
+ const value = record[field];
198
+ if (!Number.isInteger(value) || Number(value) < 0) throw new Error(`${field} must be a non-negative integer`);
199
+ return Number(value);
200
+ }
201
+
202
+ function optionalNonNegativeInteger(record: JsonRecord, field: string): number | undefined {
203
+ if (record[field] === undefined) return undefined;
204
+ return asNonNegativeInteger(record, field);
205
+ }
206
+
207
+ function asBoolean(record: JsonRecord, field: string): boolean {
208
+ const value = record[field];
209
+ if (typeof value !== 'boolean') throw new Error(`${field} must be a boolean`);
210
+ return value;
211
+ }
212
+
213
+ function asTrue(record: JsonRecord, field: string): true {
214
+ const value = record[field];
215
+ if (value === undefined) return true;
216
+ if (value !== true) throw new Error(`${field} must be true`);
217
+ return true;
218
+ }
219
+
220
+ function asFalse(record: JsonRecord, field: keyof RigConductorAuthorityBoundary): false {
221
+ if (record[field] !== false) throw new Error(`authority_boundary.${field} must be false`);
222
+ return false;
223
+ }
224
+
225
+ function asEnum<T extends readonly string[]>(values: T, value: unknown, field: string): T[number] {
226
+ if (typeof value !== 'string' || !values.includes(value)) throw new Error(`${field} must be one of: ${values.join(', ')}`);
227
+ return value as T[number];
228
+ }
229
+
230
+ function assertPortableRef(value: string, field: string): void {
231
+ if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
232
+ throw new Error(`${field} must be an opaque or repo-relative ref, not a local absolute path`);
233
+ }
234
+ if (/[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
235
+ throw new Error(`${field} must be an opaque or repo-relative ref, not a URL`);
236
+ }
237
+ }
238
+
239
+ function parseAuthorityBoundary(value: unknown): RigConductorAuthorityBoundary {
240
+ const record = asRecord(value, 'authority_boundary');
241
+ return {
242
+ topology_grants_authority: asFalse(record, 'topology_grants_authority'),
243
+ parenthood_grants_authority: asFalse(record, 'parenthood_grants_authority'),
244
+ child_mints_authority: asFalse(record, 'child_mints_authority'),
245
+ child_can_write_parent_ledger: asFalse(record, 'child_can_write_parent_ledger'),
246
+ station_role_grants_authority: asFalse(record, 'station_role_grants_authority'),
247
+ validation_is_authorization: asFalse(record, 'validation_is_authorization'),
248
+ };
249
+ }
250
+
251
+ function parseStations(value: unknown): RigConductorStations {
252
+ const record = asRecord(value, 'stations');
253
+ return {
254
+ orchestration_lead: asString(record, 'orchestration_lead'),
255
+ rig_lead: asString(record, 'rig_lead'),
256
+ synthesis_owner: asString(record, 'synthesis_owner'),
257
+ reviewer: asString(record, 'reviewer'),
258
+ };
259
+ }
260
+
261
+ function parseStationEdge(value: unknown): RigConductorStationEdge {
262
+ const record = asRecord(value, 'station edge');
263
+ return {
264
+ from: asEnum(['orchestration_lead', 'rig_lead', 'synthesis_owner', 'reviewer'] as const, record.from, 'station_edges.from'),
265
+ to: asEnum(['orchestration_lead', 'rig_lead', 'synthesis_owner', 'reviewer'] as const, record.to, 'station_edges.to'),
266
+ };
267
+ }
268
+
269
+ function parseReturnContract(value: unknown): RigConductorReturnContract {
270
+ const record = asRecord(value, 'return_contract');
271
+ return {
272
+ return_to: asString(record, 'return_to'),
273
+ required_refs: asStringArray(record, 'required_refs'),
274
+ disposal_confirmed_required: asTrue(record, 'disposal_confirmed_required'),
275
+ };
276
+ }
277
+
278
+ function parseTaskSpec(value: unknown): RigConductorTaskSpec {
279
+ const record = asRecord(value, 'task_spec');
280
+ const taskId = asString(record, 'task_id');
281
+ const lane = asString(record, 'lane');
282
+ assertPortableRef(taskId, 'task_spec.task_id');
283
+ assertPortableRef(lane, 'task_spec.lane');
284
+ const allowedWrites = asStringArray(record, 'allowed_writes');
285
+ for (const ref of allowedWrites) assertPortableRef(ref, 'task_spec.allowed_writes');
286
+ return {
287
+ task_id: taskId,
288
+ lane,
289
+ assigned_to: asString(record, 'assigned_to'),
290
+ allowed_writes: allowedWrites,
291
+ validation: asStringArray(record, 'validation'),
292
+ return_contract: parseReturnContract(record.return_contract),
293
+ };
294
+ }
295
+
296
+ function parseStructuredCheck(value: unknown): RigConductorStructuredCheck {
297
+ const record = asRecord(value, 'structured check');
298
+ return {
299
+ name: asString(record, 'name'),
300
+ compile_status: record.compile_status === undefined
301
+ ? undefined
302
+ : asEnum(STRUCTURED_CHECK_STATUSES, record.compile_status, 'checks.compile_status'),
303
+ gate_status: record.gate_status === undefined
304
+ ? undefined
305
+ : asEnum(STRUCTURED_CHECK_STATUSES, record.gate_status, 'checks.gate_status'),
306
+ tests_failed: optionalNonNegativeInteger(record, 'tests_failed'),
307
+ blocking: asBoolean(record, 'blocking'),
308
+ };
309
+ }
310
+
311
+ function parseChildNode(value: unknown): RigConductorChildNode {
312
+ const record = asRecord(value, 'child node');
313
+ const nodeId = asString(record, 'node_id');
314
+ const lane = asString(record, 'lane');
315
+ assertPortableRef(nodeId, 'child_nodes.node_id');
316
+ assertPortableRef(lane, 'child_nodes.lane');
317
+ if ('depth' in record) throw new Error('child_nodes.depth is derived from parent_node chain and must not be declared');
318
+ const scratchRef = optionalString(record, 'scratch_ref');
319
+ if (scratchRef !== undefined) assertPortableRef(scratchRef, 'child_nodes.scratch_ref');
320
+ const allowedWrites = asStringArray(record, 'allowed_writes');
321
+ for (const ref of allowedWrites) assertPortableRef(ref, 'child_nodes.allowed_writes');
322
+ return {
323
+ node_id: nodeId,
324
+ parent_node: asString(record, 'parent_node'),
325
+ return_to: asString(record, 'return_to'),
326
+ lane,
327
+ assigned_to: asString(record, 'assigned_to'),
328
+ scratch_ref: scratchRef,
329
+ allowed_writes: allowedWrites,
330
+ allowed_tools: asStringArray(record, 'allowed_tools'),
331
+ forbidden_actions: asStringArray(record, 'forbidden_actions'),
332
+ child_spawn_budget: asNonNegativeInteger(record, 'child_spawn_budget'),
333
+ task_spec: parseTaskSpec(record.task_spec),
334
+ };
335
+ }
336
+
337
+ function parseLifecycleEvent(value: unknown): RigConductorLifecycleEvent {
338
+ const record = asRecord(value, 'lifecycle event');
339
+ const checks = record.checks === undefined ? undefined : record.checks;
340
+ if (checks !== undefined && !Array.isArray(checks)) throw new Error('lifecycle_events.checks must be an array when present');
341
+ return {
342
+ type: asEnum(RIG_EVENT_TYPES, record.type, 'lifecycle_events.type') as RigLifecycleEventType,
343
+ node_id: optionalString(record, 'node_id'),
344
+ ref: optionalString(record, 'ref'),
345
+ disposal_confirmed: typeof record.disposal_confirmed === 'boolean' ? record.disposal_confirmed : undefined,
346
+ checks: Array.isArray(checks) ? checks.map(parseStructuredCheck) : undefined,
347
+ };
348
+ }
349
+
350
+ function parseSynthesis(value: unknown): RigConductorSynthesis | undefined {
351
+ if (value === undefined) return undefined;
352
+ const record = asRecord(value, 'synthesis');
353
+ return {
354
+ owner: asString(record, 'owner'),
355
+ return_to: asString(record, 'return_to'),
356
+ child_return_refs: asStringArray(record, 'child_return_refs'),
357
+ disposal_confirmed: record.disposal_confirmed === true,
358
+ };
359
+ }
360
+
361
+ export function parseRigConductorEnvelope(value: unknown): RigConductorEnvelope {
362
+ const record = asRecord(value, 'rig conductor envelope');
363
+ if (record.schema !== RIG_CONDUCTOR_SCHEMA) throw new Error(`schema must be ${RIG_CONDUCTOR_SCHEMA}`);
364
+ const rigId = asString(record, 'rig_id');
365
+ const lane = asString(record, 'lane');
366
+ assertPortableRef(rigId, 'rig_id');
367
+ assertPortableRef(lane, 'lane');
368
+ const allowedWrites = asStringArray(record, 'allowed_writes');
369
+ for (const ref of allowedWrites) assertPortableRef(ref, 'allowed_writes');
370
+ const childNodes = (record.child_nodes ?? []) as unknown;
371
+ if (!Array.isArray(childNodes)) throw new Error('child_nodes must be an array');
372
+ const lifecycleEvents = (record.lifecycle_events ?? []) as unknown;
373
+ if (!Array.isArray(lifecycleEvents)) throw new Error('lifecycle_events must be an array');
374
+ const stationEdges = (record.station_edges ?? []) as unknown;
375
+ if (!Array.isArray(stationEdges)) throw new Error('station_edges must be an array');
376
+ return {
377
+ schema: RIG_CONDUCTOR_SCHEMA,
378
+ rig_id: rigId,
379
+ workflow_id: asString(record, 'workflow_id'),
380
+ lane,
381
+ execution_strategy: asEnum(EXECUTION_STRATEGIES, record.execution_strategy, 'execution_strategy') as RigExecutionStrategy,
382
+ parent_node: asString(record, 'parent_node'),
383
+ return_to: asString(record, 'return_to'),
384
+ synthesis_owner: asString(record, 'synthesis_owner'),
385
+ allowed_writes: allowedWrites,
386
+ allowed_tools: asStringArray(record, 'allowed_tools'),
387
+ forbidden_actions: asStringArray(record, 'forbidden_actions'),
388
+ child_spawn_budget: asNonNegativeInteger(record, 'child_spawn_budget'),
389
+ max_depth: asNonNegativeInteger(record, 'max_depth'),
390
+ total_descendant_budget: asNonNegativeInteger(record, 'total_descendant_budget'),
391
+ ephemeral_children: asTrue(record, 'ephemeral_children'),
392
+ stations: parseStations(record.stations),
393
+ station_edges: stationEdges.map(parseStationEdge),
394
+ child_nodes: childNodes.map(parseChildNode),
395
+ lifecycle_events: lifecycleEvents.map(parseLifecycleEvent),
396
+ synthesis: parseSynthesis(record.synthesis),
397
+ authority_boundary: parseAuthorityBoundary(record.authority_boundary),
398
+ };
399
+ }
400
+
401
+ function isSubset(child: string[], parent: string[]): boolean {
402
+ return child.every((item) => parent.includes(item));
403
+ }
404
+
405
+ function hasCycle(edges: RigConductorStationEdge[]): boolean {
406
+ const graph = new Map<keyof RigConductorStations, Array<keyof RigConductorStations>>();
407
+ for (const edge of edges) {
408
+ graph.set(edge.from, [...(graph.get(edge.from) ?? []), edge.to]);
409
+ }
410
+ const visiting = new Set<keyof RigConductorStations>();
411
+ const visited = new Set<keyof RigConductorStations>();
412
+ const visit = (node: keyof RigConductorStations): boolean => {
413
+ if (visiting.has(node)) return true;
414
+ if (visited.has(node)) return false;
415
+ visiting.add(node);
416
+ for (const next of graph.get(node) ?? []) {
417
+ if (visit(next)) return true;
418
+ }
419
+ visiting.delete(node);
420
+ visited.add(node);
421
+ return false;
422
+ };
423
+ return (['orchestration_lead', 'rig_lead', 'synthesis_owner', 'reviewer'] as Array<keyof RigConductorStations>).some(visit);
424
+ }
425
+
426
+ function childWritesCanonicalSurface(child: RigConductorChildNode): boolean {
427
+ return child.allowed_writes.some((ref) => CANONICAL_WRITE_PREFIXES.some((prefix) => ref === prefix || ref.startsWith(`${prefix}/`)))
428
+ || child.allowed_tools.some((tool) => (CANONICAL_TOOLS as readonly string[]).includes(tool));
429
+ }
430
+
431
+ function childScratchIsolated(envelope: RigConductorEnvelope): boolean {
432
+ const seen = new Set<string>();
433
+ for (const child of envelope.child_nodes) {
434
+ if (child.allowed_writes.length === 0) continue;
435
+ if (child.scratch_ref === undefined) return false;
436
+ if (seen.has(child.scratch_ref)) return false;
437
+ seen.add(child.scratch_ref);
438
+ }
439
+ return true;
440
+ }
441
+
442
+ function containsAuthorityWriteClaim(value: unknown): boolean {
443
+ if (!isRecord(value)) return false;
444
+ for (const [key, child] of Object.entries(value)) {
445
+ const normalized = key.toLowerCase();
446
+ const isProhibition = normalized.includes('cannot')
447
+ || normalized.includes('requires')
448
+ || normalized.includes('read_only')
449
+ || normalized.includes('false');
450
+ if (!isProhibition && /(authority|authorize|grant|merge|close|promote|push|sign|mint)/i.test(key) && child === true) {
451
+ return true;
452
+ }
453
+ if (containsAuthorityWriteClaim(child)) return true;
454
+ }
455
+ return false;
456
+ }
457
+
458
+ function computeDepths(envelope: RigConductorEnvelope): Map<string, number> {
459
+ const childrenByParent = new Map<string, RigConductorChildNode[]>();
460
+ for (const child of envelope.child_nodes) {
461
+ childrenByParent.set(child.parent_node, [...(childrenByParent.get(child.parent_node) ?? []), child]);
462
+ }
463
+ const depths = new Map<string, number>();
464
+ const visit = (parent: string, depth: number, path: Set<string>): void => {
465
+ for (const child of childrenByParent.get(parent) ?? []) {
466
+ if (path.has(child.node_id)) {
467
+ depths.set(child.node_id, Number.POSITIVE_INFINITY);
468
+ continue;
469
+ }
470
+ depths.set(child.node_id, depth);
471
+ visit(child.node_id, depth + 1, new Set([...path, child.node_id]));
472
+ }
473
+ };
474
+ visit(envelope.parent_node, 1, new Set([envelope.parent_node]));
475
+ return depths;
476
+ }
477
+
478
+ function lifecycleEventsValid(envelope: RigConductorEnvelope): boolean {
479
+ const childIds = new Set(envelope.child_nodes.map((child) => child.node_id));
480
+ return envelope.lifecycle_events.every((event) => {
481
+ if (event.type === 'rig_opened' || event.type === 'rig_closed') return event.node_id === undefined || event.node_id === envelope.rig_id;
482
+ return typeof event.node_id === 'string' && childIds.has(event.node_id);
483
+ });
484
+ }
485
+
486
+ function closedRig(envelope: RigConductorEnvelope): boolean {
487
+ return envelope.lifecycle_events.some((event) => event.type === 'rig_closed');
488
+ }
489
+
490
+ function returnEventsByChild(envelope: RigConductorEnvelope): Map<string, RigConductorLifecycleEvent[]> {
491
+ const byChild = new Map<string, RigConductorLifecycleEvent[]>();
492
+ for (const event of envelope.lifecycle_events) {
493
+ if (event.type !== 'child_returned' || event.node_id === undefined) continue;
494
+ byChild.set(event.node_id, [...(byChild.get(event.node_id) ?? []), event]);
495
+ }
496
+ return byChild;
497
+ }
498
+
499
+ function childReturnChecksPresent(envelope: RigConductorEnvelope): boolean {
500
+ if (!closedRig(envelope)) return true;
501
+ const returnsByChild = returnEventsByChild(envelope);
502
+ return envelope.child_nodes.every((child) => (
503
+ (returnsByChild.get(child.node_id) ?? []).some((event) => (event.checks ?? []).length > 0)
504
+ ));
505
+ }
506
+
507
+ function childReturnChecksNonblocking(envelope: RigConductorEnvelope): boolean {
508
+ if (!closedRig(envelope)) return true;
509
+ const returnsByChild = returnEventsByChild(envelope);
510
+ return envelope.child_nodes.every((child) => {
511
+ const checks = (returnsByChild.get(child.node_id) ?? []).flatMap((event) => event.checks ?? []);
512
+ return checks.length > 0 && checks.every((check) => check.blocking === false);
513
+ });
514
+ }
515
+
516
+ function pushReason(reasons: string[], ok: boolean, reason: string): void {
517
+ if (!ok) reasons.push(reason);
518
+ }
519
+
520
+ export function evaluateRigConductorEnvelope(raw: unknown): RigConductorValidation {
521
+ const envelope = parseRigConductorEnvelope(raw);
522
+ const reasons: string[] = [];
523
+ const childReturnRefs = new Set(envelope.lifecycle_events
524
+ .filter((event) => event.type === 'child_returned' && typeof event.ref === 'string')
525
+ .map((event) => event.ref as string));
526
+ const childIds = new Set(envelope.child_nodes.map((child) => child.node_id));
527
+ const depths = computeDepths(envelope);
528
+ const observedDepths = [...depths.values()];
529
+ const maxObservedDepth = observedDepths.length === 0 ? 0 : Math.max(...observedDepths);
530
+ const descendantBudgetRequested = envelope.child_nodes.length
531
+ + envelope.child_nodes.reduce((sum, child) => sum + child.child_spawn_budget, 0);
532
+ const strategySupported = !(RESERVED_STRATEGIES as readonly string[]).includes(envelope.execution_strategy);
533
+ const stationGraphAcyclic = !hasCycle(envelope.station_edges);
534
+ const reviewerNotImplementer = envelope.child_nodes.every((child) => (
535
+ child.assigned_to !== envelope.stations.reviewer && child.node_id !== envelope.stations.reviewer
536
+ ));
537
+ const childEnvelopesSubsetParent = envelope.child_nodes.every((child) => (
538
+ child.lane === envelope.lane
539
+ && isSubset(child.allowed_writes, envelope.allowed_writes)
540
+ && isSubset(child.allowed_tools, envelope.allowed_tools)
541
+ && isSubset(envelope.forbidden_actions, child.forbidden_actions)
542
+ ));
543
+ const childTaskSpecsSubsetEnvelopes = envelope.child_nodes.every((child) => (
544
+ child.task_spec.lane === child.lane
545
+ && child.task_spec.assigned_to === child.assigned_to
546
+ && isSubset(child.task_spec.allowed_writes, child.allowed_writes)
547
+ && child.task_spec.return_contract.return_to === child.return_to
548
+ && child.task_spec.return_contract.disposal_confirmed_required === true
549
+ && child.task_spec.return_contract.required_refs.length > 0
550
+ && child.task_spec.validation.length > 0
551
+ ));
552
+ const scratchIsolated = childScratchIsolated(envelope);
553
+ const childrenNotCanonicalWriters = envelope.child_nodes.every((child) => !childWritesCanonicalSurface(child));
554
+ const childBudgetWithinParent = envelope.child_nodes.length <= envelope.child_spawn_budget
555
+ && descendantBudgetRequested <= envelope.total_descendant_budget
556
+ && envelope.child_nodes.every((child) => {
557
+ const parentBudget = child.parent_node === envelope.parent_node
558
+ ? envelope.child_spawn_budget
559
+ : envelope.child_nodes.find((candidate) => candidate.node_id === child.parent_node)?.child_spawn_budget ?? -1;
560
+ return parentBudget > 0 && child.child_spawn_budget < parentBudget;
561
+ });
562
+ const depthWithinMax = maxObservedDepth <= envelope.max_depth && observedDepths.every(Number.isFinite) && depths.size === childIds.size;
563
+ const closed = closedRig(envelope);
564
+ const synthesisHasChildRefs = !closed || (
565
+ envelope.synthesis !== undefined
566
+ && envelope.synthesis.owner === envelope.synthesis_owner
567
+ && envelope.synthesis.return_to === envelope.return_to
568
+ && envelope.child_nodes.every((child) => (
569
+ envelope.synthesis?.child_return_refs.includes(child.node_id)
570
+ || envelope.synthesis?.child_return_refs.some((ref) => childReturnRefs.has(ref) && child.task_spec.return_contract.required_refs.includes(ref))
571
+ ))
572
+ );
573
+ const disposalConfirmed = !closed || (
574
+ envelope.synthesis?.disposal_confirmed === true
575
+ && envelope.lifecycle_events
576
+ .filter((event) => event.type === 'child_returned')
577
+ .every((event) => event.disposal_confirmed === true)
578
+ );
579
+ const returnChecksPresent = childReturnChecksPresent(envelope);
580
+ const returnChecksNonblocking = childReturnChecksNonblocking(envelope);
581
+ const eventsValid = lifecycleEventsValid(envelope);
582
+ const authorityBoundaryFalse = !containsAuthorityWriteClaim(raw);
583
+ const checks = {
584
+ strategy_supported: strategySupported,
585
+ station_graph_acyclic: stationGraphAcyclic,
586
+ reviewer_not_implementer: reviewerNotImplementer,
587
+ child_envelopes_subset_parent: childEnvelopesSubsetParent,
588
+ child_task_specs_subset_envelopes: childTaskSpecsSubsetEnvelopes,
589
+ child_scratch_isolated: scratchIsolated,
590
+ children_not_canonical_writers: childrenNotCanonicalWriters,
591
+ child_budget_within_parent: childBudgetWithinParent,
592
+ depth_within_max: depthWithinMax,
593
+ synthesis_has_child_refs: synthesisHasChildRefs,
594
+ child_return_checks_present: returnChecksPresent,
595
+ child_return_checks_nonblocking: returnChecksNonblocking,
596
+ disposal_confirmed: disposalConfirmed,
597
+ lifecycle_events_valid: eventsValid,
598
+ authority_boundary_false: authorityBoundaryFalse,
599
+ };
600
+ pushReason(reasons, checks.strategy_supported, 'strategy_reserved');
601
+ pushReason(reasons, checks.station_graph_acyclic, 'station_cycle');
602
+ pushReason(reasons, checks.reviewer_not_implementer, 'reviewer_is_implementer');
603
+ pushReason(reasons, checks.child_envelopes_subset_parent, 'child_envelope_widens_parent');
604
+ pushReason(reasons, checks.child_task_specs_subset_envelopes, 'task_spec_not_subset_or_missing_return_contract');
605
+ pushReason(reasons, checks.child_scratch_isolated, 'child_scratch_not_isolated');
606
+ pushReason(reasons, checks.children_not_canonical_writers, 'child_canonical_write_surface');
607
+ pushReason(reasons, checks.child_budget_within_parent, 'child_budget_exhausted');
608
+ pushReason(reasons, checks.depth_within_max, 'depth_exceeds_max');
609
+ pushReason(reasons, checks.synthesis_has_child_refs, 'synthesis_missing_child_refs');
610
+ pushReason(reasons, checks.child_return_checks_present, 'child_return_checks_missing');
611
+ pushReason(reasons, checks.child_return_checks_nonblocking, 'child_return_blocking');
612
+ pushReason(reasons, checks.disposal_confirmed, 'disposal_unconfirmed');
613
+ pushReason(reasons, checks.lifecycle_events_valid, 'lifecycle_event_invalid');
614
+ pushReason(reasons, checks.authority_boundary_false, 'authority_field_write_attempt');
615
+ return {
616
+ schema: RIG_CONDUCTOR_VALIDATION_SCHEMA,
617
+ rig_ref: envelope.rig_id,
618
+ verdict: reasons.length === 0 ? 'legitimate' : 'blocked',
619
+ reasons,
620
+ checks,
621
+ derived: {
622
+ max_observed_depth: maxObservedDepth,
623
+ child_count: envelope.child_nodes.length,
624
+ descendant_budget_requested: descendantBudgetRequested,
625
+ },
626
+ authority_boundary: {
627
+ ...envelope.authority_boundary,
628
+ validation_grants_authority: false,
629
+ topology_is_routing_only: true,
630
+ },
631
+ };
632
+ }