@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,279 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join, relative, resolve } from 'node:path';
3
+ import { optionalString, requireString } from '../argv';
4
+ import { evaluateOperationalDispatch } from '../../dispatch/decision';
5
+ import {
6
+ loadOperationalDispatchRecord,
7
+ projectDispatchRecordToHostedReceipt,
8
+ } from '../../dispatch/projection';
9
+ import { recordOperationalDispatch } from '../../dispatch/record';
10
+ import { loadSandboxReceipt } from '../../rubric/loader';
11
+ import { evaluateSeatReadiness, readSeatReadiness } from '../../seat/readiness';
12
+ import { evaluateWorkflowCheck } from '../../workflow/evaluator';
13
+ import { loadWorkflowEnvelope, loadWorkflowMode } from '../../workflow/loader';
14
+
15
+ const DISPATCH_VERBS = new Set(['check', 'record', 'projection', 'scaffold']);
16
+
17
+ export function isDispatchCommand(command: string): boolean {
18
+ return command === 'dispatch';
19
+ }
20
+
21
+ function renderDecision(result: ReturnType<typeof evaluateOperationalDispatch>): string {
22
+ return [
23
+ 'operational_dispatch_decision:',
24
+ ` task_ref: ${result.task_ref}`,
25
+ ` seat: ${result.seat}`,
26
+ ` decision: ${result.decision}`,
27
+ ` reasons: ${result.reasons.join(', ') || 'none'}`,
28
+ ` warnings: ${result.warnings.join(', ') || 'none'}`,
29
+ ` profile_hint: ${result.profile_hint ?? 'none'}`,
30
+ ` seat_routing: ${result.inputs.seat_readiness.routing_recommendation}`,
31
+ ` workflow_recommendation: ${result.inputs.workflow_check.recommendation}`,
32
+ ` rubric_receipt_accepted: ${String(result.inputs.rubric_receipt.accepted)}`,
33
+ ` can_dispatch: ${String(result.authority_boundary.can_dispatch)}`,
34
+ ` can_authorize: ${String(result.authority_boundary.can_authorize)}`,
35
+ ` consumer_is_engine: ${String(result.authority_boundary.consumer_is_engine)}`,
36
+ ].join('\n');
37
+ }
38
+
39
+ function renderRecord(result: Awaited<ReturnType<typeof recordOperationalDispatch>>): string {
40
+ return [
41
+ 'operational_dispatch_record:',
42
+ ` record_id: ${result.record.record_id}`,
43
+ ` task_ref: ${result.record.task_ref}`,
44
+ ` seat: ${result.record.seat}`,
45
+ ` decision: ${result.record.decision}`,
46
+ ` reasons: ${result.record.reasons.join(', ') || 'none'}`,
47
+ ` warnings: ${result.record.warnings.join(', ') || 'none'}`,
48
+ ` receipt_ref: ${result.record.receipt_ref}`,
49
+ ` ledger_ref: ${result.record.ledger_ref}`,
50
+ ` can_execute: ${String(result.record.authority_boundary.can_execute)}`,
51
+ ].join('\n');
52
+ }
53
+
54
+ function renderProjection(result: ReturnType<typeof projectDispatchRecordToHostedReceipt>): string {
55
+ const payload = result.hosted_payload;
56
+ return [
57
+ 'hosted_receipt_metadata_projection:',
58
+ ` projection_id: ${result.projection_id}`,
59
+ ` source_record_id: ${result.source_record_id}`,
60
+ ` task_id: ${payload.task_id}`,
61
+ ` seat_id: ${payload.seat_id}`,
62
+ ` status: ${payload.status}`,
63
+ ` artifact_ref: ${payload.artifact_ref ?? 'none'}`,
64
+ ` receipt_ref: ${payload.receipt_ref}`,
65
+ ` validation_summary: ${payload.validation_summary}`,
66
+ ' boundary: metadata-only; hosted plane cannot execute, authorize, attach grants, or mutate the local ledger',
67
+ ].join('\n');
68
+ }
69
+
70
+ function addHours(at: string, hours: number): string {
71
+ const date = new Date(at);
72
+ if (Number.isNaN(date.getTime())) throw new Error('--at must be an ISO-8601 timestamp');
73
+ date.setUTCHours(date.getUTCHours() + hours);
74
+ return date.toISOString();
75
+ }
76
+
77
+ function rel(root: string, path: string): string {
78
+ const value = relative(root, path);
79
+ return value && !value.startsWith('..') ? value : path;
80
+ }
81
+
82
+ function renderScaffold(result: Awaited<ReturnType<typeof scaffoldDispatch>>): string {
83
+ return [
84
+ 'dispatch_scaffold:',
85
+ ` out: ${result.out_ref}`,
86
+ ` mode: ${result.files.mode_ref}`,
87
+ ` task: ${result.files.task_ref}`,
88
+ ` sandbox_receipt: ${result.files.sandbox_receipt_ref}`,
89
+ ` state: ${result.files.state_ref}`,
90
+ ` check_command: ${result.check_command}`,
91
+ ' boundary: scaffold only; does not record dispatch, authorize, grant, merge, close, or execute',
92
+ ].join('\n');
93
+ }
94
+
95
+ async function scaffoldDispatch(flags: Record<string, string | boolean>) {
96
+ const projectRoot = resolve(optionalString(flags, 'project') ?? process.cwd());
97
+ const taskId = optionalString(flags, 'task-id') ?? optionalString(flags, 'task') ?? 'APP-001';
98
+ const seat = optionalString(flags, 'seat') ?? 'codex-seat';
99
+ const modeId = optionalString(flags, 'mode-id') ?? 'dispatch-mixed-mode-v0';
100
+ const strategy = optionalString(flags, 'strategy') ?? 'local_adversarial_tango';
101
+ const at = optionalString(flags, 'at') ?? new Date().toISOString();
102
+ const outDir = resolve(optionalString(flags, 'out') ?? join(projectRoot, 'docs', 'work', 'dispatch', taskId));
103
+ const modePath = join(outDir, 'mode.yaml');
104
+ const taskPath = join(outDir, 'task.yaml');
105
+ const receiptPath = join(outDir, 'receipt.yaml');
106
+ const statePath = join(outDir, 'seat-readiness.json');
107
+ const receiptId = `sandbox-run:${taskId}`;
108
+
109
+ const modeYaml = `mode_id: ${modeId}
110
+ mode_version: 0.1.0
111
+ mode_type: mixed
112
+ drafting:
113
+ surface: coordination-ledger
114
+ requires_review: true
115
+ min_reviewers: 1
116
+ amendment_rounds_allowed: true
117
+ adversarial_review_required: false
118
+ compact_reason_required: false
119
+ grant:
120
+ required: true
121
+ preauthorization_allowed: false
122
+ granted_by: operator_principal
123
+ advisory_consensus_can_authorize: false
124
+ execution:
125
+ surface: local-etiquette
126
+ dispatch: immediate_after_grant
127
+ strategy_policy: executor_selected
128
+ rubric_required: true
129
+ proof:
130
+ receipt_required: true
131
+ validation_required: true
132
+ closeout_requires_receipt: true
133
+ audit:
134
+ event_log_required: true
135
+ dissent_capture_required: true
136
+ authority_status: policy_preset_only
137
+ `;
138
+ const taskYaml = `task_ref: task:${taskId}
139
+ workflow_mode_ref: workflow-mode:${modeId}
140
+ requested_mode_type: mixed
141
+ mutates_state: true
142
+ grant_ref: grant:${taskId}
143
+ consensus_ref: consensus:${taskId}
144
+ receipt_ref: receipt:${taskId}
145
+ validation_refs:
146
+ - ${receiptId}
147
+ bounded: true
148
+ preauthorized: false
149
+ advisory_consensus_claims_authority: false
150
+ risk: medium
151
+ `;
152
+ const receiptYaml = `schema: sandbox-run-receipt.v0
153
+ receipt_id: ${receiptId}
154
+ rubric_id: ${taskId}
155
+ rubric_version: 0.1.0
156
+ fixture_set_ref: fixture-set:${taskId}
157
+ results:
158
+ - fixture_id: ready
159
+ expected: pass
160
+ actual: pass
161
+ reason: scaffolded receipt example
162
+ summary:
163
+ total_fixtures: 1
164
+ known_bad_rejected: 0
165
+ known_good_accepted: 1
166
+ unexpected_outcomes: 0
167
+ run_at: ${at}
168
+ run_by: ${seat}
169
+ run_environment: local
170
+ `;
171
+ const seatState = {
172
+ schema_version: 'seat-readiness.v0.1',
173
+ seat,
174
+ seat_type: 'control_seat',
175
+ execution_surface: 'local-etiquette',
176
+ declared_by: 'config',
177
+ capability_source: 'local_validation',
178
+ capability_trusted: true,
179
+ operational_status: 'operational',
180
+ model_tier: 'local_unknown',
181
+ model_id: 'local-session',
182
+ active_workers: 0,
183
+ max_recommended_workers: 1,
184
+ supports_parallel: false,
185
+ supported_strategies: [strategy],
186
+ unsupported_strategies: [],
187
+ constraints: ['scaffold-example'],
188
+ last_checked_at: at,
189
+ last_heartbeat_at: at,
190
+ ttl_seconds: 86400,
191
+ stale_after: addHours(at, 24),
192
+ };
193
+
194
+ await mkdir(outDir, { recursive: true });
195
+ await writeFile(modePath, modeYaml);
196
+ await writeFile(taskPath, taskYaml);
197
+ await writeFile(receiptPath, receiptYaml);
198
+ await writeFile(statePath, `${JSON.stringify(seatState, null, 2)}\n`);
199
+
200
+ const files = {
201
+ mode_ref: rel(projectRoot, modePath),
202
+ task_ref: rel(projectRoot, taskPath),
203
+ sandbox_receipt_ref: rel(projectRoot, receiptPath),
204
+ state_ref: rel(projectRoot, statePath),
205
+ };
206
+ return {
207
+ schema: 'dispatch-scaffold.v0',
208
+ out_ref: rel(projectRoot, outDir),
209
+ task_id: taskId,
210
+ seat,
211
+ files,
212
+ check_command: `etiquette dispatch check --project . --mode ${files.mode_ref} --task ${files.task_ref} --sandbox-receipt ${files.sandbox_receipt_ref} --state ${files.state_ref} --strategy ${strategy}`,
213
+ authority_boundary: {
214
+ scaffold_only: true,
215
+ can_record_dispatch: false,
216
+ can_authorize: false,
217
+ can_attach_grant: false,
218
+ can_execute: false,
219
+ can_merge: false,
220
+ can_close: false,
221
+ },
222
+ };
223
+ }
224
+
225
+ export async function dispatchDispatch(
226
+ positionals: string[],
227
+ flags: Record<string, string | boolean>,
228
+ ): Promise<void> {
229
+ const verb = positionals[1];
230
+ if (!verb || !DISPATCH_VERBS.has(verb)) {
231
+ throw new Error(`dispatch verb must be one of: ${[...DISPATCH_VERBS].join(', ')}`);
232
+ }
233
+ const json = Boolean(flags.json);
234
+ if (verb === 'scaffold') {
235
+ const scaffold = await scaffoldDispatch(flags);
236
+ console.log(json ? JSON.stringify(scaffold, null, 2) : renderScaffold(scaffold));
237
+ return;
238
+ }
239
+ if (verb === 'projection') {
240
+ const record = await loadOperationalDispatchRecord(resolve(requireString(flags, 'record')));
241
+ const projected = projectDispatchRecordToHostedReceipt({
242
+ record,
243
+ artifactRef: optionalString(flags, 'artifact-ref'),
244
+ receiptRef: optionalString(flags, 'receipt-ref'),
245
+ status: optionalString(flags, 'status'),
246
+ validationSummary: optionalString(flags, 'validation-summary'),
247
+ });
248
+ console.log(json ? JSON.stringify(projected, null, 2) : renderProjection(projected));
249
+ return;
250
+ }
251
+ const projectRoot = resolve(optionalString(flags, 'project') ?? process.cwd());
252
+ const mode = await loadWorkflowMode(resolve(requireString(flags, 'mode')));
253
+ const envelope = await loadWorkflowEnvelope(resolve(requireString(flags, 'task')));
254
+ const readiness = evaluateSeatReadiness(
255
+ await readSeatReadiness(projectRoot, optionalString(flags, 'state')),
256
+ optionalString(flags, 'at'),
257
+ );
258
+ const receiptPath = optionalString(flags, 'sandbox-receipt');
259
+ const sandboxReceipt = receiptPath ? await loadSandboxReceipt(resolve(receiptPath)) : null;
260
+ const result = evaluateOperationalDispatch({
261
+ seat: readiness,
262
+ mode,
263
+ envelope,
264
+ workflow: evaluateWorkflowCheck(mode, envelope),
265
+ sandboxReceipt,
266
+ requestedStrategy: optionalString(flags, 'strategy'),
267
+ });
268
+ if (verb === 'record') {
269
+ const recorded = await recordOperationalDispatch({
270
+ projectRoot,
271
+ decision: result,
272
+ recordedBy: optionalString(flags, 'recorded-by'),
273
+ at: optionalString(flags, 'at'),
274
+ });
275
+ console.log(json ? JSON.stringify(recorded.record, null, 2) : renderRecord(recorded));
276
+ return;
277
+ }
278
+ console.log(json ? JSON.stringify(result, null, 2) : renderDecision(result));
279
+ }
@@ -0,0 +1,89 @@
1
+ import { resolve } from 'node:path';
2
+ import { optionalString, requireString } from '../argv';
3
+ import {
4
+ buildHarnessInventoryReportFile,
5
+ loadHarnessPruneReceipt,
6
+ evaluateHarnessCanary,
7
+ validateHarnessPruneReceiptFile,
8
+ type HarnessCanaryResult,
9
+ type HarnessInventoryReport,
10
+ type HarnessReceiptValidation,
11
+ } from '../../harness/pruning';
12
+
13
+ const HARNESS_VERBS = new Set(['receipt', 'inventory', 'canary']);
14
+
15
+ export function isHarnessCommand(command: string): boolean {
16
+ return command === 'harness';
17
+ }
18
+
19
+ function renderReceipt(result: HarnessReceiptValidation): string {
20
+ return [
21
+ 'harness_prune_receipt:',
22
+ ` receipt_id: ${result.receipt_id}`,
23
+ ` object_id: ${result.object_id}`,
24
+ ` verdict: ${result.verdict}`,
25
+ ` proposed_disposition: ${result.proposed_disposition}`,
26
+ ` canary_verdict: ${result.canary.verdict}`,
27
+ ` can_authorize_pruning: ${String(result.authority_boundary.receipt_can_authorize_pruning)}`,
28
+ ` human_disposition_required: ${String(result.authority_boundary.human_disposition_required)}`,
29
+ ].join('\n');
30
+ }
31
+
32
+ function renderCanary(result: HarnessCanaryResult): string {
33
+ return [
34
+ 'harness_canary:',
35
+ ` object_id: ${result.object_id}`,
36
+ ` verdict: ${result.verdict}`,
37
+ ` required: ${result.required_canaries.join(', ')}`,
38
+ ` observed: ${result.observed_canaries.join(', ')}`,
39
+ ` missing: ${result.missing_canaries.join(', ') || 'none'}`,
40
+ ].join('\n');
41
+ }
42
+
43
+ function renderInventory(result: HarnessInventoryReport): string {
44
+ return [
45
+ 'harness_inventory:',
46
+ ` item_count: ${result.item_count}`,
47
+ ` keep: ${result.counts.keep}`,
48
+ ` consolidate: ${result.counts.consolidate}`,
49
+ ` quarantine: ${result.counts.quarantine}`,
50
+ ` delete_ready: ${result.counts['delete-ready']}`,
51
+ ` blocked: ${result.counts.blocked}`,
52
+ ` can_delete: ${String(result.authority_boundary.inventory_can_delete)}`,
53
+ ` pruning_receipt_required: ${String(result.authority_boundary.pruning_receipt_required)}`,
54
+ ...result.items.map((item) => (
55
+ ` - ${item.object_id}: ${item.suggested_disposition} (${item.reason})`
56
+ )),
57
+ ].join('\n');
58
+ }
59
+
60
+ export async function dispatchHarness(
61
+ positionals: string[],
62
+ flags: Record<string, string | boolean>,
63
+ ): Promise<void> {
64
+ const verb = positionals[1];
65
+ if (!verb || !HARNESS_VERBS.has(verb)) {
66
+ throw new Error(`harness verb must be one of: ${[...HARNESS_VERBS].join(', ')}`);
67
+ }
68
+ const json = Boolean(flags.json);
69
+
70
+ if (verb === 'receipt') {
71
+ const result = await validateHarnessPruneReceiptFile(resolve(requireString(flags, 'receipt')));
72
+ console.log(json ? JSON.stringify(result, null, 2) : renderReceipt(result));
73
+ return;
74
+ }
75
+
76
+ if (verb === 'canary') {
77
+ const receipt = await loadHarnessPruneReceipt(resolve(requireString(flags, 'receipt')));
78
+ const result = evaluateHarnessCanary(receipt);
79
+ console.log(json ? JSON.stringify(result, null, 2) : renderCanary(result));
80
+ return;
81
+ }
82
+
83
+ const result = await buildHarnessInventoryReportFile(resolve(requireString(flags, 'manifest')));
84
+ if (optionalString(flags, 'project')) {
85
+ // Parsed intentionally as a harmless compatibility flag for callers that
86
+ // consistently pass --project to read-only Etiquette commands.
87
+ }
88
+ console.log(json ? JSON.stringify(result, null, 2) : renderInventory(result));
89
+ }
@@ -0,0 +1,50 @@
1
+ import { dispatchHook } from '../../hooks/dispatcher';
2
+ import { HOOK_STAGES, type HookStage } from '../../hooks/types';
3
+ import { optionalString } from '../argv';
4
+
5
+ export function isHookCommand(command: string): boolean {
6
+ return command === 'hook';
7
+ }
8
+
9
+ export async function dispatchHookCli(
10
+ positionals: string[],
11
+ flags: Record<string, string | boolean>,
12
+ ): Promise<void> {
13
+ const stage = positionals[1];
14
+ if (!stage) {
15
+ console.error(`hook: missing stage (one of: ${HOOK_STAGES.join(', ')})`);
16
+ process.exit(2);
17
+ }
18
+ if (!(HOOK_STAGES as readonly string[]).includes(stage)) {
19
+ console.error(`hook: unknown stage "${stage}"; expected one of: ${HOOK_STAGES.join(', ')}`);
20
+ process.exit(2);
21
+ }
22
+
23
+ const workspace_id = optionalString(flags, 'workspace');
24
+ const payloadRaw = optionalString(flags, 'payload');
25
+ let payload: Record<string, unknown> = {};
26
+ if (payloadRaw) {
27
+ try {
28
+ const parsed = JSON.parse(payloadRaw);
29
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
30
+ throw new Error('payload must be a JSON object');
31
+ }
32
+ payload = parsed as Record<string, unknown>;
33
+ } catch (err) {
34
+ console.error(`hook: --payload must be valid JSON object: ${(err as Error).message}`);
35
+ process.exit(2);
36
+ }
37
+ }
38
+
39
+ const result = dispatchHook({
40
+ stage: stage as HookStage,
41
+ workspace_id: workspace_id ?? undefined,
42
+ payload,
43
+ });
44
+
45
+ // Always exit 0 — operational results (disabled / written / skipped) are
46
+ // non-error states per the V0.1 envelope. Only argv-parse failures above
47
+ // exit non-zero. Hooks must NEVER block the operator's runtime.
48
+ console.log(JSON.stringify(result));
49
+ process.exit(0);
50
+ }
@@ -0,0 +1,91 @@
1
+ import { resolve } from 'node:path';
2
+ import { optionalString, parseInt32 } from '../argv';
3
+ import {
4
+ defaultLedgerIndexDbPath,
5
+ ledgerIndexNeedsRebuild,
6
+ rebuildLedgerIndex,
7
+ searchLedgerIndex,
8
+ showLedgerIndexEntry,
9
+ type CompactLedgerSearchResult,
10
+ } from '../../ledger/indexer';
11
+
12
+ const LEDGER_VERBS = new Set(['index', 'search', 'show']);
13
+
14
+ export function isLedgerCommand(command: string): boolean {
15
+ return command === 'ledger';
16
+ }
17
+
18
+ function asLimit(raw: string | boolean | undefined, fallback: number): number {
19
+ if (raw === undefined) return fallback;
20
+ const parsed = parseInt32(raw, 'limit');
21
+ if (parsed < 1 || parsed > 50) {
22
+ throw new Error('--limit must be between 1 and 50');
23
+ }
24
+ return parsed;
25
+ }
26
+
27
+ function renderCompact(items: CompactLedgerSearchResult[]): string {
28
+ if (items.length === 0) return 'ledger: no matching entries\n';
29
+ return `${items.map((item) => [
30
+ `entry: ${item.entry_id}`,
31
+ `type: ${item.type}`,
32
+ `topic_hints: ${item.topic_hints.join(', ') || 'none'}`,
33
+ `status_effect: ${item.status_effect}`,
34
+ `refs: ${item.refs.join(', ') || 'none'}`,
35
+ `confidence: ${item.confidence}`,
36
+ `is_canonical: ${item.is_canonical}`,
37
+ `regenerable: ${item.regenerable}`,
38
+ `can_authorize: ${item.can_authorize}`,
39
+ ].join('\n')).join('\n---\n')}\n`;
40
+ }
41
+
42
+ async function ensureFresh(projectRoot: string, dbPath: string): Promise<void> {
43
+ if (await ledgerIndexNeedsRebuild({ projectRoot, dbPath })) {
44
+ throw new Error('ledger index is missing or stale; run `etiquette ledger index` first');
45
+ }
46
+ }
47
+
48
+ export async function dispatchLedger(
49
+ positionals: string[],
50
+ flags: Record<string, string | boolean>,
51
+ ): Promise<void> {
52
+ const verb = positionals[1];
53
+ if (!verb || !LEDGER_VERBS.has(verb)) {
54
+ throw new Error(`ledger verb must be one of: ${[...LEDGER_VERBS].join(', ')}`);
55
+ }
56
+ const projectRoot = resolve(optionalString(flags, 'project') ?? process.cwd());
57
+ const dbPath = optionalString(flags, 'db') ?? defaultLedgerIndexDbPath(projectRoot);
58
+ const json = Boolean(flags.json);
59
+
60
+ if (verb === 'index') {
61
+ const result = await rebuildLedgerIndex({ projectRoot, dbPath });
62
+ console.log(json ? JSON.stringify(result, null, 2) : [
63
+ 'ledger_index:',
64
+ ` db: ${result.dbPath}`,
65
+ ` ledger_entries: ${result.ledger_entries}`,
66
+ ` refs: ${result.refs}`,
67
+ ` topics: ${result.topics}`,
68
+ ` documents: ${result.documents}`,
69
+ ].join('\n'));
70
+ return;
71
+ }
72
+
73
+ if (verb === 'search') {
74
+ const topic = optionalString(flags, 'topic');
75
+ const query = topic ?? positionals.slice(2).join(' ').trim();
76
+ if (!query) throw new Error('<query> or --topic is required');
77
+ await ensureFresh(projectRoot, dbPath);
78
+ const items = searchLedgerIndex({ dbPath, query, limit: asLimit(flags.limit, 10) });
79
+ console.log(json ? JSON.stringify(items, null, 2) : renderCompact(items));
80
+ return;
81
+ }
82
+
83
+ if (verb === 'show') {
84
+ const id = positionals[2];
85
+ if (!id) throw new Error('<entry_or_ref_id> is required');
86
+ await ensureFresh(projectRoot, dbPath);
87
+ const entry = showLedgerIndexEntry({ dbPath, entryIdOrRef: id });
88
+ if (!entry) throw new Error(`unknown ledger entry or ref: ${id}`);
89
+ console.log(JSON.stringify(entry, null, 2));
90
+ }
91
+ }