@etiquekit/etq 1.0.12 → 1.0.14

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 -54
  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 +8 -7
  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 +9 -9
  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 +4690 -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 +842 -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 +15 -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,748 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { dirname, extname, join, relative, resolve } from 'node:path';
5
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
+ import {
7
+ buildHarnessInventoryReportFile,
8
+ validateHarnessPruneReceiptFile,
9
+ type HarnessDisposition,
10
+ } from '../harness/pruning';
11
+ import { assertSharedMemoryScopeId, assertSharedMemoryText, detectMemoryRiskFlags } from '../memory/sanitize';
12
+ import { buildAgentOwnerCardProjection, type AgentOwnerCardProjection } from '../seat/owner-card';
13
+
14
+ type JsonRecord = Record<string, unknown>;
15
+
16
+ const RUNTIME_PROFILE_TIERS = ['local', 'remote', 'self_hosted'] as const;
17
+ const PERIMETER_MODES = ['local_process', 'self_hosted_sandbox', 'managed_sandbox'] as const;
18
+
19
+ export type RuntimeProfileTier = typeof RUNTIME_PROFILE_TIERS[number];
20
+ export type RuntimePerimeterMode = typeof PERIMETER_MODES[number];
21
+
22
+ export interface ModelFingerprint {
23
+ seat?: string;
24
+ model_id?: string;
25
+ model_tier?: string;
26
+ adapter_backend?: string;
27
+ execution_surface?: string;
28
+ runtime_tier?: string;
29
+ }
30
+
31
+ export interface ModelDriftReceipt {
32
+ schema: 'model-drift-receipt.v0';
33
+ receipt_id: string;
34
+ created_at: string;
35
+ previous_ref: string;
36
+ current_ref: string;
37
+ previous: ModelFingerprint;
38
+ current: ModelFingerprint;
39
+ drift_detected: boolean;
40
+ invalidates_social_calibration: boolean;
41
+ drift: Array<{ field: keyof ModelFingerprint; previous: string | null; current: string | null }>;
42
+ authority_boundary: {
43
+ can_authorize: false;
44
+ can_attach_grant: false;
45
+ mutates_roster: false;
46
+ };
47
+ }
48
+
49
+ export interface RuntimeProfile {
50
+ schema: 'runtime-profile.v0';
51
+ profile_id: string;
52
+ runtime_tier: RuntimeProfileTier;
53
+ perimeter_mode: RuntimePerimeterMode;
54
+ adapter_backend: string;
55
+ target_ref: string;
56
+ supports_verifier: boolean;
57
+ network_egress?: {
58
+ allowlist: boolean;
59
+ allowed_hosts: string[];
60
+ };
61
+ authority_boundary: {
62
+ can_execute_verifier: boolean;
63
+ can_write: false;
64
+ can_merge: false;
65
+ can_close: false;
66
+ can_grant: false;
67
+ mutates_canonical_truth: false;
68
+ };
69
+ }
70
+
71
+ export interface RuntimeProfileValidation {
72
+ schema: 'runtime-profile-validation.v0';
73
+ profile_id: string;
74
+ validated_at: string;
75
+ verdict: 'accepted';
76
+ runtime_tier: RuntimeProfileTier;
77
+ perimeter_mode: RuntimePerimeterMode;
78
+ adapter_backend: string;
79
+ supported_modes: ['verifier'];
80
+ authority_boundary: RuntimeProfile['authority_boundary'];
81
+ }
82
+
83
+ export interface VaultReferenceReceipt {
84
+ schema: 'vault-reference-receipt.v0';
85
+ receipt_id: string;
86
+ created_at: string;
87
+ provider: string;
88
+ vault_ref: string;
89
+ purpose: string;
90
+ requested_by: string;
91
+ raw_secret_stored: false;
92
+ content_boundary: {
93
+ stores_secret_value: false;
94
+ stores_provider_token: false;
95
+ stores_raw_identity_document: false;
96
+ };
97
+ authority_boundary: {
98
+ can_read_secret: false;
99
+ can_inject_secret: false;
100
+ can_grant_secret_access: false;
101
+ };
102
+ }
103
+
104
+ export interface SchedulerWakeProposal {
105
+ schema: 'scheduler-wake-proposal.v0';
106
+ proposal_id: string;
107
+ created_at: string;
108
+ created_by: string;
109
+ task_ref: string;
110
+ wake_at: string;
111
+ reason: string;
112
+ status: 'proposed';
113
+ authority_boundary: {
114
+ scheduler_can_authorize: false;
115
+ scheduler_can_merge: false;
116
+ scheduler_can_grant: false;
117
+ scheduler_can_execute: false;
118
+ };
119
+ }
120
+
121
+ export interface McpWriteProposal {
122
+ schema: 'mcp-write-proposal.v0';
123
+ proposal_id: string;
124
+ created_at: string;
125
+ proposed_by: string;
126
+ task_ref: string;
127
+ summary: string;
128
+ evidence_ref?: string;
129
+ outbox_ref: string;
130
+ content_boundary: {
131
+ raw_prompt_included: false;
132
+ raw_tool_output_included: false;
133
+ source_snippets_included: false;
134
+ local_paths_included: false;
135
+ };
136
+ authority_boundary: {
137
+ can_authorize: false;
138
+ can_merge: false;
139
+ can_close: false;
140
+ can_grant: false;
141
+ mutates_ledger: false;
142
+ };
143
+ }
144
+
145
+ export interface ConsoleProvisioningEvent {
146
+ event_id: string;
147
+ created_at: string;
148
+ event_kind: string;
149
+ plan_ref: string;
150
+ terraform_ref: string;
151
+ target: {
152
+ provider: string;
153
+ substrate: string;
154
+ cluster_name: string;
155
+ region: string;
156
+ namespace: string;
157
+ };
158
+ summary: string;
159
+ }
160
+
161
+ export interface ConsoleHarnessMaintenanceProjection {
162
+ schema: 'harness-maintenance-console-projection.v0';
163
+ counts: Record<HarnessDisposition, number> & {
164
+ inventory_items: number;
165
+ pruning_receipts: number;
166
+ validation_errors: number;
167
+ };
168
+ inventory_reports: Array<{
169
+ manifest_ref: string;
170
+ item_count: number;
171
+ counts: Record<HarnessDisposition, number>;
172
+ }>;
173
+ pruning_receipts: Array<{
174
+ receipt_ref: string;
175
+ object_id: string;
176
+ proposed_disposition: HarnessDisposition;
177
+ canary_verdict: 'pass' | 'blocked';
178
+ human_disposition_required: true;
179
+ }>;
180
+ validation_errors: Array<{
181
+ ref: string;
182
+ reason: string;
183
+ }>;
184
+ authority_boundary: {
185
+ read_only: true;
186
+ can_prune: false;
187
+ can_delete: false;
188
+ can_grant: false;
189
+ can_close_lane: false;
190
+ };
191
+ }
192
+
193
+ export interface ConsoleReadModel {
194
+ schema: 'console-read-model.v0';
195
+ generated_at: string;
196
+ project_ref: '.';
197
+ counts: {
198
+ ledger_entries: number;
199
+ receipts: number;
200
+ tasks: number;
201
+ outbox_items: number;
202
+ provisioning_events: number;
203
+ owner_cards: number;
204
+ };
205
+ seat_readiness: JsonRecord | null;
206
+ owner_cards: AgentOwnerCardProjection;
207
+ provisioning_events: ConsoleProvisioningEvent[];
208
+ harness_maintenance: ConsoleHarnessMaintenanceProjection;
209
+ recent_ledger: string[];
210
+ authority_boundary: {
211
+ read_only: true;
212
+ can_emit_events: false;
213
+ can_attach_grants: false;
214
+ can_merge: false;
215
+ can_close: false;
216
+ };
217
+ }
218
+
219
+ function isoNow(at?: string): string {
220
+ const value = at ?? new Date().toISOString();
221
+ if (!Number.isFinite(Date.parse(value))) throw new Error('--at must be an ISO-8601 timestamp');
222
+ return new Date(value).toISOString();
223
+ }
224
+
225
+ function stableHash(value: unknown): string {
226
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 12);
227
+ }
228
+
229
+ function isRecord(value: unknown): value is JsonRecord {
230
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
231
+ }
232
+
233
+ function stringValue(record: JsonRecord, key: string): string | undefined {
234
+ const value = record[key];
235
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
236
+ }
237
+
238
+ function nestedString(record: JsonRecord, objectKey: string, key: string): string | undefined {
239
+ const nested = record[objectKey];
240
+ if (!isRecord(nested)) return undefined;
241
+ return stringValue(nested, key);
242
+ }
243
+
244
+ async function readStructured(path: string): Promise<unknown> {
245
+ const raw = await readFile(path, 'utf-8');
246
+ if (extname(path).toLowerCase() === '.json') return JSON.parse(raw) as unknown;
247
+ return parseYaml(raw) as unknown;
248
+ }
249
+
250
+ function safeLabel(path: string, label?: string): string {
251
+ if (label) {
252
+ assertSharedMemoryText(label, 'label');
253
+ return label;
254
+ }
255
+ return `input:${stableHash(path)}`;
256
+ }
257
+
258
+ function assertOpaque(value: string, field: string): void {
259
+ assertSharedMemoryScopeId(value, field);
260
+ }
261
+
262
+ function assertSharedText(value: string, field: string): void {
263
+ if (value.trim().length === 0) throw new Error(`${field} must not be empty`);
264
+ assertSharedMemoryText(value, field);
265
+ }
266
+
267
+ function assertArtifactRef(value: string, field: string): void {
268
+ if (value.length === 0 || value.length > 256) throw new Error(`${field} must be 1-256 chars`);
269
+ if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
270
+ throw new Error(`${field} must not be an absolute path`);
271
+ }
272
+ assertSharedMemoryText(value, field);
273
+ }
274
+
275
+ function assertVaultRef(value: string): void {
276
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,159}$/.test(value)) {
277
+ throw new Error('--vault-ref must be an opaque provider reference');
278
+ }
279
+ if (value.includes('=') || /sk-[A-Za-z0-9_-]{12,}/.test(value)) {
280
+ throw new Error('--vault-ref must not contain a secret value');
281
+ }
282
+ const restricted = detectMemoryRiskFlags(value).filter((flag) => flag !== 'contains_credential');
283
+ if (restricted.length > 0) {
284
+ throw new Error(`--vault-ref contains restricted shared content: ${restricted.join(', ')}`);
285
+ }
286
+ }
287
+
288
+ function assertEnum<T extends readonly string[]>(values: T, value: string, field: string): asserts value is T[number] {
289
+ if (!(values as readonly string[]).includes(value)) {
290
+ throw new Error(`${field} must be one of: ${values.join(', ')}`);
291
+ }
292
+ }
293
+
294
+ function fingerprint(value: unknown): ModelFingerprint {
295
+ if (!isRecord(value)) throw new Error('model posture input must be an object');
296
+ return {
297
+ seat: stringValue(value, 'seat') ?? stringValue(value, 'seat_id'),
298
+ model_id: stringValue(value, 'model_id') ?? stringValue(value, 'model') ?? nestedString(value, 'model_config', 'model'),
299
+ model_tier: stringValue(value, 'model_tier') ?? stringValue(value, 'tier'),
300
+ adapter_backend: stringValue(value, 'adapter_backend') ?? stringValue(value, 'provider'),
301
+ execution_surface: stringValue(value, 'execution_surface') ?? stringValue(value, 'surface'),
302
+ runtime_tier: stringValue(value, 'runtime_tier') ?? stringValue(value, 'hosting_tier'),
303
+ };
304
+ }
305
+
306
+ export async function createModelDriftReceipt(options: {
307
+ previousPath: string;
308
+ currentPath: string;
309
+ previousLabel?: string;
310
+ currentLabel?: string;
311
+ receiptPath?: string;
312
+ at?: string;
313
+ }): Promise<ModelDriftReceipt> {
314
+ const createdAt = isoNow(options.at);
315
+ const previous = fingerprint(await readStructured(options.previousPath));
316
+ const current = fingerprint(await readStructured(options.currentPath));
317
+ const fields: Array<keyof ModelFingerprint> = [
318
+ 'seat',
319
+ 'model_id',
320
+ 'model_tier',
321
+ 'adapter_backend',
322
+ 'execution_surface',
323
+ 'runtime_tier',
324
+ ];
325
+ const drift = fields
326
+ .filter((field) => (previous[field] ?? null) !== (current[field] ?? null))
327
+ .map((field) => ({
328
+ field,
329
+ previous: previous[field] ?? null,
330
+ current: current[field] ?? null,
331
+ }));
332
+ const invalidates = drift.some((entry) => (
333
+ entry.field === 'model_id' ||
334
+ entry.field === 'model_tier' ||
335
+ entry.field === 'adapter_backend' ||
336
+ entry.field === 'runtime_tier'
337
+ ));
338
+ const receipt: ModelDriftReceipt = {
339
+ schema: 'model-drift-receipt.v0',
340
+ receipt_id: `model-drift:${stableHash({ previous, current, createdAt })}`,
341
+ created_at: createdAt,
342
+ previous_ref: safeLabel(options.previousPath, options.previousLabel),
343
+ current_ref: safeLabel(options.currentPath, options.currentLabel),
344
+ previous,
345
+ current,
346
+ drift_detected: drift.length > 0,
347
+ invalidates_social_calibration: invalidates,
348
+ drift,
349
+ authority_boundary: {
350
+ can_authorize: false,
351
+ can_attach_grant: false,
352
+ mutates_roster: false,
353
+ },
354
+ };
355
+ if (options.receiptPath) await writeYaml(options.receiptPath, receipt);
356
+ return receipt;
357
+ }
358
+
359
+ export async function validateRuntimeProfileFile(path: string, at?: string): Promise<RuntimeProfileValidation> {
360
+ const raw = await readStructured(path);
361
+ if (!isRecord(raw)) throw new Error('runtime profile must be an object');
362
+ if (raw.schema !== 'runtime-profile.v0') throw new Error('runtime profile schema must be runtime-profile.v0');
363
+
364
+ const profileId = stringValue(raw, 'profile_id');
365
+ const runtimeTier = stringValue(raw, 'runtime_tier');
366
+ const perimeterMode = stringValue(raw, 'perimeter_mode');
367
+ const adapterBackend = stringValue(raw, 'adapter_backend');
368
+ const targetRef = stringValue(raw, 'target_ref');
369
+ if (!profileId) throw new Error('profile_id is required');
370
+ if (!runtimeTier) throw new Error('runtime_tier is required');
371
+ if (!perimeterMode) throw new Error('perimeter_mode is required');
372
+ if (!adapterBackend) throw new Error('adapter_backend is required');
373
+ if (!targetRef) throw new Error('target_ref is required');
374
+ assertOpaque(profileId, 'profile_id');
375
+ assertEnum(RUNTIME_PROFILE_TIERS, runtimeTier, 'runtime_tier');
376
+ assertEnum(PERIMETER_MODES, perimeterMode, 'perimeter_mode');
377
+ assertOpaque(adapterBackend, 'adapter_backend');
378
+ assertArtifactRef(targetRef, 'target_ref');
379
+ if (raw.supports_verifier !== true) throw new Error('supports_verifier must be true');
380
+
381
+ const boundary = raw.authority_boundary;
382
+ if (!isRecord(boundary)) throw new Error('authority_boundary is required');
383
+ const expectedFalse = ['can_write', 'can_merge', 'can_close', 'can_grant', 'mutates_canonical_truth'];
384
+ for (const key of expectedFalse) {
385
+ if (boundary[key] !== false) throw new Error(`authority_boundary.${key} must be false`);
386
+ }
387
+
388
+ return {
389
+ schema: 'runtime-profile-validation.v0',
390
+ profile_id: profileId,
391
+ validated_at: isoNow(at),
392
+ verdict: 'accepted',
393
+ runtime_tier: runtimeTier,
394
+ perimeter_mode: perimeterMode,
395
+ adapter_backend: adapterBackend,
396
+ supported_modes: ['verifier'],
397
+ authority_boundary: {
398
+ can_execute_verifier: true,
399
+ can_write: false,
400
+ can_merge: false,
401
+ can_close: false,
402
+ can_grant: false,
403
+ mutates_canonical_truth: false,
404
+ },
405
+ };
406
+ }
407
+
408
+ export async function createVaultReferenceReceipt(options: {
409
+ provider: string;
410
+ vaultRef: string;
411
+ purpose: string;
412
+ requestedBy: string;
413
+ receiptPath?: string;
414
+ at?: string;
415
+ }): Promise<VaultReferenceReceipt> {
416
+ assertOpaque(options.provider, '--provider');
417
+ assertVaultRef(options.vaultRef);
418
+ assertSharedText(options.purpose, '--purpose');
419
+ assertOpaque(options.requestedBy, '--requested-by');
420
+ const createdAt = isoNow(options.at);
421
+ const receipt: VaultReferenceReceipt = {
422
+ schema: 'vault-reference-receipt.v0',
423
+ receipt_id: `vault-ref:${stableHash({ provider: options.provider, vaultRef: options.vaultRef, purpose: options.purpose, createdAt })}`,
424
+ created_at: createdAt,
425
+ provider: options.provider,
426
+ vault_ref: options.vaultRef,
427
+ purpose: options.purpose,
428
+ requested_by: options.requestedBy,
429
+ raw_secret_stored: false,
430
+ content_boundary: {
431
+ stores_secret_value: false,
432
+ stores_provider_token: false,
433
+ stores_raw_identity_document: false,
434
+ },
435
+ authority_boundary: {
436
+ can_read_secret: false,
437
+ can_inject_secret: false,
438
+ can_grant_secret_access: false,
439
+ },
440
+ };
441
+ if (options.receiptPath) await writeYaml(options.receiptPath, receipt);
442
+ return receipt;
443
+ }
444
+
445
+ export async function createSchedulerWakeProposal(options: {
446
+ projectRoot: string;
447
+ taskRef: string;
448
+ wakeAt: string;
449
+ reason: string;
450
+ createdBy: string;
451
+ proposalPath?: string;
452
+ at?: string;
453
+ }): Promise<SchedulerWakeProposal> {
454
+ assertArtifactRef(options.taskRef, '--task');
455
+ assertSharedText(options.reason, '--reason');
456
+ assertOpaque(options.createdBy, '--created-by');
457
+ const wakeAt = isoNow(options.wakeAt);
458
+ const createdAt = isoNow(options.at);
459
+ const proposal: SchedulerWakeProposal = {
460
+ schema: 'scheduler-wake-proposal.v0',
461
+ proposal_id: `scheduler-wake:${stableHash({ taskRef: options.taskRef, wakeAt, reason: options.reason })}`,
462
+ created_at: createdAt,
463
+ created_by: options.createdBy,
464
+ task_ref: options.taskRef,
465
+ wake_at: wakeAt,
466
+ reason: options.reason,
467
+ status: 'proposed',
468
+ authority_boundary: {
469
+ scheduler_can_authorize: false,
470
+ scheduler_can_merge: false,
471
+ scheduler_can_grant: false,
472
+ scheduler_can_execute: false,
473
+ },
474
+ };
475
+ const path = options.proposalPath ?? join(resolve(options.projectRoot), '.etiquette', 'outbox', 'scheduler', `${proposal.proposal_id.replace(/[^A-Za-z0-9_-]+/g, '-')}.yaml`);
476
+ await writeYaml(path, proposal);
477
+ return proposal;
478
+ }
479
+
480
+ export async function stageMcpWriteProposal(options: {
481
+ projectRoot: string;
482
+ taskRef: string;
483
+ summary: string;
484
+ proposedBy?: string;
485
+ evidenceRef?: string;
486
+ at?: string;
487
+ }): Promise<McpWriteProposal> {
488
+ assertArtifactRef(options.taskRef, 'task_ref');
489
+ assertSharedText(options.summary, 'summary');
490
+ if (options.evidenceRef) assertArtifactRef(options.evidenceRef, 'evidence_ref');
491
+ const proposedBy = options.proposedBy ?? 'mcp-client';
492
+ assertOpaque(proposedBy, 'proposed_by');
493
+ const createdAt = isoNow(options.at);
494
+ const proposalId = `mcp-proposal:${stableHash({
495
+ taskRef: options.taskRef,
496
+ summary: options.summary,
497
+ evidenceRef: options.evidenceRef,
498
+ proposedBy,
499
+ createdAt,
500
+ })}`;
501
+ const outPath = join(resolve(options.projectRoot), '.etiquette', 'outbox', 'mcp-proposals', `${proposalId.replace(/[^A-Za-z0-9_-]+/g, '-')}.json`);
502
+ const outboxRef = projectRelative(options.projectRoot, outPath);
503
+ const proposal: McpWriteProposal = {
504
+ schema: 'mcp-write-proposal.v0',
505
+ proposal_id: proposalId,
506
+ created_at: createdAt,
507
+ proposed_by: proposedBy,
508
+ task_ref: options.taskRef,
509
+ summary: options.summary,
510
+ ...(options.evidenceRef ? { evidence_ref: options.evidenceRef } : {}),
511
+ outbox_ref: outboxRef,
512
+ content_boundary: {
513
+ raw_prompt_included: false,
514
+ raw_tool_output_included: false,
515
+ source_snippets_included: false,
516
+ local_paths_included: false,
517
+ },
518
+ authority_boundary: {
519
+ can_authorize: false,
520
+ can_merge: false,
521
+ can_close: false,
522
+ can_grant: false,
523
+ mutates_ledger: false,
524
+ },
525
+ };
526
+ await mkdir(dirname(outPath), { recursive: true });
527
+ await writeFile(outPath, `${JSON.stringify(proposal, null, 2)}\n`);
528
+ return proposal;
529
+ }
530
+
531
+ export async function buildConsoleReadModel(projectRoot: string, at?: string): Promise<ConsoleReadModel> {
532
+ const root = resolve(projectRoot);
533
+ const ledgerPath = join(root, 'docs', 'work', 'ledger', 'LEDGER.md');
534
+ const ledgerRaw = await readTextIfExists(ledgerPath);
535
+ const ledgerLines = ledgerRaw
536
+ ? ledgerRaw.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '))
537
+ : [];
538
+ const receipts = await countFiles(join(root, 'docs', 'work', 'receipts'));
539
+ const tasks = await countFiles(join(root, 'docs', 'work', 'tasks'));
540
+ const outbox = await countFiles(join(root, '.etiquette', 'outbox'));
541
+ const provisioningEvents = await readProvisioningEvents(join(root, '.etiquette', 'provisioning', 'events'));
542
+ const harnessMaintenance = await readHarnessMaintenanceProjection(root);
543
+ const ownerCards = await buildAgentOwnerCardProjection({ projectRoot: root, at });
544
+ const readinessRaw = await readTextIfExists(join(root, '.etiquette', 'state', 'seat-readiness.json'));
545
+ const readiness = readinessRaw ? JSON.parse(readinessRaw) as JsonRecord : null;
546
+
547
+ return {
548
+ schema: 'console-read-model.v0',
549
+ generated_at: isoNow(at),
550
+ project_ref: '.',
551
+ counts: {
552
+ ledger_entries: ledgerLines.length,
553
+ receipts,
554
+ tasks,
555
+ outbox_items: outbox,
556
+ provisioning_events: provisioningEvents.length,
557
+ owner_cards: ownerCards.cards.length,
558
+ },
559
+ seat_readiness: readiness,
560
+ owner_cards: ownerCards,
561
+ provisioning_events: provisioningEvents.slice(-8),
562
+ harness_maintenance: harnessMaintenance,
563
+ recent_ledger: ledgerLines.slice(-8),
564
+ authority_boundary: {
565
+ read_only: true,
566
+ can_emit_events: false,
567
+ can_attach_grants: false,
568
+ can_merge: false,
569
+ can_close: false,
570
+ },
571
+ };
572
+ }
573
+
574
+ async function readTextIfExists(path: string): Promise<string | null> {
575
+ try {
576
+ return await readFile(path, 'utf-8');
577
+ } catch (err) {
578
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
579
+ throw err;
580
+ }
581
+ }
582
+
583
+ async function countFiles(dir: string): Promise<number> {
584
+ if (!existsSync(dir)) return 0;
585
+ const entries = await readdir(dir, { withFileTypes: true });
586
+ const counts = await Promise.all(entries.map(async (entry) => {
587
+ const path = join(dir, entry.name);
588
+ if (entry.isDirectory()) return countFiles(path);
589
+ return entry.isFile() ? 1 : 0;
590
+ }));
591
+ return counts.reduce((sum, count) => sum + count, 0);
592
+ }
593
+
594
+ async function collectFiles(dir: string): Promise<string[]> {
595
+ if (!existsSync(dir)) return [];
596
+ const entries = await readdir(dir, { withFileTypes: true });
597
+ const nested = await Promise.all(entries.map(async (entry) => {
598
+ const path = join(dir, entry.name);
599
+ if (entry.isDirectory()) return collectFiles(path);
600
+ return entry.isFile() ? [path] : [];
601
+ }));
602
+ return nested.flat().sort();
603
+ }
604
+
605
+ async function readProvisioningEvents(dir: string): Promise<ConsoleProvisioningEvent[]> {
606
+ const files = (await collectFiles(dir)).filter((file) => ['.yaml', '.yml', '.json'].includes(extname(file).toLowerCase()));
607
+ const events = await Promise.all(files.map(async (file) => {
608
+ const raw = await readStructured(file);
609
+ if (!isRecord(raw) || raw.schema !== 'provisioning-event.v0') return null;
610
+ const target = raw.target;
611
+ if (!isRecord(target)) return null;
612
+ const eventId = stringValue(raw, 'event_id');
613
+ const createdAt = stringValue(raw, 'created_at');
614
+ const eventKind = stringValue(raw, 'event_kind');
615
+ const planRef = stringValue(raw, 'plan_ref');
616
+ const terraformRef = stringValue(raw, 'terraform_ref');
617
+ const summary = stringValue(raw, 'summary');
618
+ const provider = stringValue(target, 'provider');
619
+ const substrate = stringValue(target, 'substrate');
620
+ const clusterName = stringValue(target, 'cluster_name');
621
+ const region = stringValue(target, 'region');
622
+ const namespace = stringValue(target, 'namespace');
623
+ if (!eventId || !createdAt || !eventKind || !planRef || !terraformRef || !summary || !provider || !substrate || !clusterName || !region || !namespace) {
624
+ return null;
625
+ }
626
+ return {
627
+ event_id: eventId,
628
+ created_at: createdAt,
629
+ event_kind: eventKind,
630
+ plan_ref: planRef,
631
+ terraform_ref: terraformRef,
632
+ target: {
633
+ provider,
634
+ substrate,
635
+ cluster_name: clusterName,
636
+ region,
637
+ namespace,
638
+ },
639
+ summary,
640
+ };
641
+ }));
642
+ return events
643
+ .filter((event): event is ConsoleProvisioningEvent => event !== null)
644
+ .sort((a, b) => a.created_at.localeCompare(b.created_at));
645
+ }
646
+
647
+ async function readHarnessMaintenanceProjection(root: string): Promise<ConsoleHarnessMaintenanceProjection> {
648
+ const counts: ConsoleHarnessMaintenanceProjection['counts'] = {
649
+ keep: 0,
650
+ consolidate: 0,
651
+ quarantine: 0,
652
+ 'delete-ready': 0,
653
+ blocked: 0,
654
+ inventory_items: 0,
655
+ pruning_receipts: 0,
656
+ validation_errors: 0,
657
+ };
658
+ const inventoryReports: ConsoleHarnessMaintenanceProjection['inventory_reports'] = [];
659
+ const pruningReceipts: ConsoleHarnessMaintenanceProjection['pruning_receipts'] = [];
660
+ const validationErrors: ConsoleHarnessMaintenanceProjection['validation_errors'] = [];
661
+
662
+ const inventoryCandidates = [
663
+ join(root, '.etiquette', 'harness', 'inventory.yaml'),
664
+ join(root, '.etiquette', 'harness', 'inventory.yml'),
665
+ join(root, 'docs', 'work', 'harness-inventory.yaml'),
666
+ join(root, 'docs', 'work', 'harness-inventory.yml'),
667
+ join(root, 'docs', 'work', 'harness', 'inventory.yaml'),
668
+ join(root, 'docs', 'work', 'harness', 'inventory.yml'),
669
+ ].filter((path, index, all) => existsSync(path) && all.indexOf(path) === index);
670
+
671
+ for (const path of inventoryCandidates) {
672
+ const ref = projectRelative(root, path);
673
+ try {
674
+ const report = await buildHarnessInventoryReportFile(path);
675
+ inventoryReports.push({
676
+ manifest_ref: ref,
677
+ item_count: report.item_count,
678
+ counts: report.counts,
679
+ });
680
+ counts.inventory_items += report.item_count;
681
+ for (const disposition of Object.keys(report.counts) as HarnessDisposition[]) {
682
+ counts[disposition] += report.counts[disposition];
683
+ }
684
+ } catch (err) {
685
+ validationErrors.push({ ref, reason: errorMessage(err) });
686
+ }
687
+ }
688
+
689
+ const receiptDirs = [
690
+ join(root, '.etiquette', 'harness', 'receipts'),
691
+ join(root, 'docs', 'work', 'harness', 'receipts'),
692
+ join(root, 'docs', 'work', 'receipts'),
693
+ ];
694
+ const receiptFiles = [...new Set((await Promise.all(receiptDirs.map(collectFiles))).flat())]
695
+ .filter((file) => ['.yaml', '.yml', '.json'].includes(extname(file).toLowerCase()));
696
+
697
+ for (const path of receiptFiles) {
698
+ const raw = await readTextIfExists(path);
699
+ if (!raw?.includes('harness-prune-receipt.v0')) continue;
700
+ const ref = projectRelative(root, path);
701
+ try {
702
+ const receipt = await validateHarnessPruneReceiptFile(path);
703
+ pruningReceipts.push({
704
+ receipt_ref: ref,
705
+ object_id: receipt.object_id,
706
+ proposed_disposition: receipt.proposed_disposition,
707
+ canary_verdict: receipt.canary.verdict,
708
+ human_disposition_required: true,
709
+ });
710
+ counts.pruning_receipts += 1;
711
+ } catch (err) {
712
+ validationErrors.push({ ref, reason: errorMessage(err) });
713
+ }
714
+ }
715
+
716
+ counts.validation_errors = validationErrors.length;
717
+ return {
718
+ schema: 'harness-maintenance-console-projection.v0',
719
+ counts,
720
+ inventory_reports: inventoryReports,
721
+ pruning_receipts: pruningReceipts,
722
+ validation_errors: validationErrors,
723
+ authority_boundary: {
724
+ read_only: true,
725
+ can_prune: false,
726
+ can_delete: false,
727
+ can_grant: false,
728
+ can_close_lane: false,
729
+ },
730
+ };
731
+ }
732
+
733
+ function errorMessage(err: unknown): string {
734
+ return err instanceof Error ? err.message : String(err);
735
+ }
736
+
737
+ function projectRelative(projectRoot: string, path: string): string {
738
+ const rel = relative(resolve(projectRoot), resolve(path));
739
+ if (rel.length === 0 || rel.startsWith('..') || rel.includes('\\')) {
740
+ throw new Error(`path must stay inside project root: ${path}`);
741
+ }
742
+ return rel;
743
+ }
744
+
745
+ async function writeYaml(path: string, value: unknown): Promise<void> {
746
+ await mkdir(dirname(path), { recursive: true });
747
+ await writeFile(path, stringifyYaml(value));
748
+ }