@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,384 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import {
5
+ aggregateSliceStatus,
6
+ type ControlDeps,
7
+ type ProjectManifest,
8
+ type SecretarySummary,
9
+ type Slice,
10
+ type SliceTask,
11
+ type Surface,
12
+ } from '../../protocol/src/index';
13
+ import { writeJson, now, sanitize } from './fs';
14
+ import { buildSlice, loadSlice, resolveWorktreePath, sliceStatePath } from './project';
15
+
16
+ export * from './authority/lease';
17
+ export * from './authority/node-delegation';
18
+ export * from './authority/rig-conductor';
19
+ export {
20
+ CODEX_PERMISSIONS_ADAPTER_SCHEMA,
21
+ CODEX_PERMISSION_ACCESS,
22
+ SEAT_PREFLIGHT_RESULT_SCHEMA,
23
+ SEAT_PREFLIGHT_SCHEMA,
24
+ SEAT_PREFLIGHT_REQUIRED_CHECKS,
25
+ SEAT_PROFILE_ROLES,
26
+ SEAT_PROFILE_SCHEMA,
27
+ SEAT_MONITOR_MODES,
28
+ codexFilesystemAccessForPath,
29
+ createDefaultSeatProfile,
30
+ evaluateSeatPreflight,
31
+ normalizeCodexPermissionsAdapter,
32
+ normalizeSeatProfile,
33
+ type CodexFilesystemPermissionRule,
34
+ type CodexNetworkPermissionRule,
35
+ type CodexPermissionAccess,
36
+ type CodexPermissionsAdapter,
37
+ type SeatMonitorMode,
38
+ type SeatPreflight,
39
+ type SeatPreflightRequiredCheck,
40
+ type SeatPreflightResult,
41
+ type SeatProfile,
42
+ type SeatProfileRole,
43
+ } from './seat/readiness';
44
+
45
+ async function persistSlice(project: ProjectManifest, slice: Slice): Promise<void> {
46
+ slice.updatedAt = now();
47
+ await writeJson(sliceStatePath(project, slice.sliceId), slice);
48
+ }
49
+
50
+ type CheckResult = {
51
+ command: string;
52
+ exitCode: number;
53
+ stdout: string;
54
+ stderr: string;
55
+ };
56
+
57
+ export async function startSlice(project: ProjectManifest, sliceId: string, deps: ControlDeps, surfaces: Surface[]): Promise<Slice> {
58
+ await deps.handshake.ensureStore(project);
59
+ const existing = await loadSlice(project, sliceId);
60
+ if (existing) {
61
+ await deps.scrutiny.openOrLinkSlice(project, existing);
62
+ return existing;
63
+ }
64
+ const slice = buildSlice(project, sliceId, surfaces);
65
+ await deps.scrutiny.openOrLinkSlice(project, slice);
66
+ await persistSlice(project, slice);
67
+ return slice;
68
+ }
69
+
70
+ export async function splitSlice(project: ProjectManifest, sliceId: string, deps: ControlDeps): Promise<Slice> {
71
+ const slice = await loadSlice(project, sliceId);
72
+ if (!slice) {
73
+ throw new Error(`slice not started: ${sliceId}`);
74
+ }
75
+ for (const task of slice.tasks) {
76
+ task.packetId = `slice-${sanitize(sliceId)}-${task.surface}`;
77
+ await deps.handshake.newPacket({ packetId: task.packetId, sliceId, task, project });
78
+ }
79
+ slice.state = 'split';
80
+ slice.transitions.push({ state: 'split', actorId: 'etiquette', at: now(), note: 'packets and incoming tasks created' });
81
+ await persistSlice(project, slice);
82
+ return slice;
83
+ }
84
+
85
+ export async function assignSlice(project: ProjectManifest, sliceId: string, deps: ControlDeps): Promise<Slice> {
86
+ const slice = await loadSlice(project, sliceId);
87
+ if (!slice) {
88
+ throw new Error(`slice not started: ${sliceId}`);
89
+ }
90
+ for (const task of slice.tasks) {
91
+ task.worktreePath = resolveWorktreePath(project, sliceId, task);
92
+ task.branchName = `codex/${sanitize(sliceId)}-${task.surface}`;
93
+ await deps.agent.spawnWorker(project, task);
94
+ await deps.agent.bindWorktree(project, task, task.worktreePath, task.owner ?? 'unassigned');
95
+ await deps.handshake.claimTask(project, task.taskId, task.owner ?? 'unassigned');
96
+ task.state = 'claimed';
97
+ task.transitions.push({ state: 'claimed', actorId: task.owner ?? 'unassigned', at: now(), note: `bound ${task.worktreePath}` });
98
+ }
99
+ slice.state = 'assigned';
100
+ slice.transitions.push({ state: 'assigned', actorId: 'etiquette', at: now(), note: 'workers claimed bounded tasks' });
101
+ await persistSlice(project, slice);
102
+ return slice;
103
+ }
104
+
105
+ export async function statusSlice(project: ProjectManifest, sliceId: string, deps: ControlDeps) {
106
+ const slice = await loadSlice(project, sliceId);
107
+ if (!slice) {
108
+ throw new Error(`slice not started: ${sliceId}`);
109
+ }
110
+ const scrutiny = await deps.scrutiny.readSliceState(project, slice.scrutinyId);
111
+ const packetStates = await deps.handshake.readPacketStates(project);
112
+ const ledgerTaskStates = await deps.handshake.readTaskStates(project);
113
+ for (const task of slice.tasks) {
114
+ const ledgerState = ledgerTaskStates[task.taskId];
115
+ if (ledgerState) {
116
+ task.state = ledgerState;
117
+ }
118
+ }
119
+ const snapshot = aggregateSliceStatus(slice, scrutiny.state, packetStates);
120
+ return { slice, scrutiny, snapshot };
121
+ }
122
+
123
+ function resolveTaskBySurface(slice: Slice, surface: Surface): SliceTask {
124
+ const task = slice.tasks.find((candidate) => candidate.surface === surface);
125
+ if (!task) {
126
+ throw new Error(`slice ${slice.sliceId} missing task for surface ${surface}`);
127
+ }
128
+ return task;
129
+ }
130
+
131
+ function ensureTaskNotResolved(task: SliceTask): void {
132
+ if (task.state === 'resolved') {
133
+ throw new Error(`task ${task.taskId} is already resolved`);
134
+ }
135
+ }
136
+
137
+ function runCheck(project: ProjectManifest, command: string): CheckResult {
138
+ const result = spawnSync('zsh', ['-lc', command], {
139
+ cwd: project.projectRoot,
140
+ encoding: 'utf-8',
141
+ });
142
+ return {
143
+ command,
144
+ exitCode: result.status ?? 1,
145
+ stdout: result.stdout ?? '',
146
+ stderr: result.stderr ?? '',
147
+ };
148
+ }
149
+
150
+ function renderEvidenceReport(task: SliceTask, actor: string, note: string, results: CheckResult[]): string {
151
+ const lines = [
152
+ `# Discipline Evidence: ${task.taskId}`,
153
+ '',
154
+ `- Surface: \`${task.surface}\``,
155
+ `- Actor: \`${actor}\``,
156
+ `- Summary: ${note}`,
157
+ `- Captured At: ${now()}`,
158
+ '',
159
+ '## Checks',
160
+ '',
161
+ ];
162
+ for (const result of results) {
163
+ lines.push(`### \`${result.command}\``);
164
+ lines.push('');
165
+ lines.push(`- Exit code: \`${result.exitCode}\``);
166
+ lines.push('');
167
+ if (result.stdout.trim()) {
168
+ lines.push('#### stdout');
169
+ lines.push('');
170
+ lines.push('```text');
171
+ lines.push(result.stdout.trimEnd());
172
+ lines.push('```');
173
+ lines.push('');
174
+ }
175
+ if (result.stderr.trim()) {
176
+ lines.push('#### stderr');
177
+ lines.push('');
178
+ lines.push('```text');
179
+ lines.push(result.stderr.trimEnd());
180
+ lines.push('```');
181
+ lines.push('');
182
+ }
183
+ }
184
+ return `${lines.join('\n')}\n`;
185
+ }
186
+
187
+ async function writeEvidenceFiles(project: ProjectManifest, task: SliceTask, actor: string, note: string, results: CheckResult[]): Promise<string[]> {
188
+ const content = renderEvidenceReport(task, actor, note, results);
189
+ const written: string[] = [];
190
+ for (const relative of task.bounds.evidenceFiles) {
191
+ const target = resolve(project.projectRoot, relative);
192
+ await mkdir(dirname(target), { recursive: true });
193
+ await writeFile(target, content, 'utf-8');
194
+ written.push(relative);
195
+ }
196
+ return written;
197
+ }
198
+
199
+ export async function completeTask(
200
+ project: ProjectManifest,
201
+ sliceId: string,
202
+ surface: Surface,
203
+ deps: ControlDeps,
204
+ actor: string,
205
+ note: string,
206
+ evidencePaths: string[] = [],
207
+ ): Promise<SliceTask> {
208
+ const slice = await loadSlice(project, sliceId);
209
+ if (!slice) {
210
+ throw new Error(`slice not started: ${sliceId}`);
211
+ }
212
+ const task = resolveTaskBySurface(slice, surface);
213
+ ensureTaskNotResolved(task);
214
+ if (!task.packetId) {
215
+ throw new Error(`task ${task.taskId} has no packet`);
216
+ }
217
+ const files = evidencePaths.length > 0 ? evidencePaths : task.bounds.evidenceFiles;
218
+ await deps.agent.captureReceipt(project, task.taskId, note, files);
219
+ await deps.agent.reportTaskState(project, task.taskId, 'evidence_ready', note);
220
+ await deps.handshake.resolveTask(project, task.taskId, actor, note);
221
+ await deps.handshake.respondPacket(project, task.packetId, 'ACCEPTED');
222
+ await deps.handshake.lockPacket(project, task.packetId);
223
+ await deps.handshake.provisionPacket(project, task.packetId);
224
+ await deps.agent.reportTaskState(project, task.taskId, 'resolved', note);
225
+ task.state = 'resolved';
226
+ task.transitions.push({
227
+ state: 'resolved',
228
+ actorId: actor,
229
+ at: now(),
230
+ note,
231
+ });
232
+ if (slice.tasks.every((candidate) => candidate.state === 'resolved')) {
233
+ slice.state = 'validated';
234
+ slice.transitions.push({
235
+ state: 'validated',
236
+ actorId: actor,
237
+ at: now(),
238
+ note: 'all bounded tasks resolved through discipline complete',
239
+ });
240
+ }
241
+ await persistSlice(project, slice);
242
+ return task;
243
+ }
244
+
245
+ export async function runTask(
246
+ project: ProjectManifest,
247
+ sliceId: string,
248
+ surface: Surface,
249
+ deps: ControlDeps,
250
+ actor: string,
251
+ note: string,
252
+ ): Promise<SliceTask> {
253
+ const slice = await loadSlice(project, sliceId);
254
+ if (!slice) {
255
+ throw new Error(`slice not started: ${sliceId}`);
256
+ }
257
+ const task = resolveTaskBySurface(slice, surface);
258
+ ensureTaskNotResolved(task);
259
+ await deps.agent.reportTaskState(project, task.taskId, 'in_progress', `running checks for ${task.taskId}`);
260
+ const results: CheckResult[] = [];
261
+ for (const command of task.bounds.checks) {
262
+ const result = runCheck(project, command);
263
+ results.push(result);
264
+ if (result.exitCode !== 0) {
265
+ const failureNote = `${note} (failed check: ${command})`;
266
+ const evidence = await writeEvidenceFiles(project, task, actor, failureNote, results);
267
+ await deps.agent.captureReceipt(project, task.taskId, failureNote, evidence);
268
+ await deps.agent.reportTaskState(project, task.taskId, 'blocked', failureNote);
269
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${command} failed`);
270
+ }
271
+ }
272
+ const evidence = await writeEvidenceFiles(project, task, actor, note, results);
273
+ return completeTask(project, sliceId, surface, deps, actor, note, evidence);
274
+ }
275
+
276
+ export async function secretarySummary(project: ProjectManifest, sliceId: string, deps: ControlDeps): Promise<SecretarySummary> {
277
+ const { slice, snapshot } = await statusSlice(project, sliceId, deps);
278
+ const actions: SecretarySummary['actions'] = [];
279
+ for (const task of slice.tasks) {
280
+ if (task.state === 'blocked' || task.state === 'countered') {
281
+ actions.push({
282
+ level: 'blocked',
283
+ taskId: task.taskId,
284
+ surface: task.surface,
285
+ summary: `${task.taskId} needs escalation before work can continue`,
286
+ });
287
+ continue;
288
+ }
289
+ if (task.state === 'incoming') {
290
+ actions.push({
291
+ level: 'action',
292
+ taskId: task.taskId,
293
+ surface: task.surface,
294
+ summary: `${task.taskId} has not been claimed`,
295
+ });
296
+ continue;
297
+ }
298
+ if (task.state === 'claimed') {
299
+ actions.push({
300
+ level: 'action',
301
+ taskId: task.taskId,
302
+ surface: task.surface,
303
+ summary: `${task.taskId} is claimed and waiting for worker evidence`,
304
+ });
305
+ continue;
306
+ }
307
+ if (task.state === 'resolved') {
308
+ actions.push({
309
+ level: 'info',
310
+ taskId: task.taskId,
311
+ surface: task.surface,
312
+ summary: `${task.taskId} is resolved and packet lifecycle is complete`,
313
+ });
314
+ }
315
+ }
316
+ if (snapshot.readyToClose) {
317
+ actions.push({
318
+ level: 'action',
319
+ summary: `slice ${slice.sliceId} is ready to close`,
320
+ });
321
+ } else if (actions.length === 0) {
322
+ actions.push({
323
+ level: 'info',
324
+ summary: `slice ${slice.sliceId} has no immediate secretary actions`,
325
+ });
326
+ }
327
+ return { sliceId: slice.sliceId, actions };
328
+ }
329
+
330
+ export async function closeSlice(project: ProjectManifest, sliceId: string, deps: ControlDeps, renderArtifacts = false): Promise<Slice> {
331
+ const { slice, snapshot } = await statusSlice(project, sliceId, deps);
332
+ if (!snapshot.readyToClose) {
333
+ throw new Error(`slice ${sliceId} is not ready to close: ${snapshot.blockers.join(', ') || snapshot.unresolvedTasks.join(', ')}`);
334
+ }
335
+ const contextId = await deps.logix.createDecisionContext(project, slice);
336
+ await deps.logix.requestVerdict(project, slice, contextId);
337
+ if (renderArtifacts) {
338
+ await deps.scrutiny.renderArtifacts(project);
339
+ }
340
+ slice.state = 'closed';
341
+ slice.transitions.push({ state: 'closed', actorId: 'etiquette', at: now(), note: `context=${contextId}` });
342
+ await persistSlice(project, slice);
343
+ return slice;
344
+ }
345
+
346
+ export function formatStatus(result: Awaited<ReturnType<typeof statusSlice>>): string {
347
+ const { slice, scrutiny, snapshot } = result;
348
+ const lines = [
349
+ `slice=${slice.sliceId}`,
350
+ `title=${slice.title}`,
351
+ `slice_state=${slice.state}`,
352
+ `scrutiny=${scrutiny.scrutinyId}:${scrutiny.state}`,
353
+ `tasks=${slice.tasks.length}`,
354
+ `ready_to_close=${snapshot.readyToClose}`,
355
+ ];
356
+ for (const [state, count] of Object.entries(snapshot.taskStates)) {
357
+ if (count > 0) lines.push(`task_${state}=${count}`);
358
+ }
359
+ for (const [state, count] of Object.entries(snapshot.packetStates)) {
360
+ if (count > 0) lines.push(`packet_${state}=${count}`);
361
+ }
362
+ if (scrutiny.kind) lines.push(`scrutiny_kind=${scrutiny.kind}`);
363
+ if (scrutiny.blockedBy && scrutiny.blockedBy.length > 0) lines.push(`scrutiny_blocked_by=${scrutiny.blockedBy.join(',')}`);
364
+ if (scrutiny.dependsOn && scrutiny.dependsOn.length > 0) lines.push(`scrutiny_depends_on=${scrutiny.dependsOn.join(',')}`);
365
+ if (scrutiny.readinessQuestion) lines.push(`scrutiny_readiness_question=${scrutiny.readinessQuestion}`);
366
+ if (scrutiny.readyForRevisit) lines.push(`scrutiny_ready_for_revisit=true`);
367
+ if (snapshot.blockers.length > 0) {
368
+ lines.push(`blockers=${snapshot.blockers.join(',')}`);
369
+ }
370
+ return lines.join('\n');
371
+ }
372
+
373
+ export function formatSecretarySummary(summary: SecretarySummary): string {
374
+ const lines = [`slice=${summary.sliceId}`];
375
+ for (const action of summary.actions) {
376
+ const prefix = action.level.toUpperCase();
377
+ if (action.taskId && action.surface) {
378
+ lines.push(`${prefix} ${action.taskId} [${action.surface}] ${action.summary}`);
379
+ } else {
380
+ lines.push(`${prefix} ${action.summary}`);
381
+ }
382
+ }
383
+ return lines.join('\n');
384
+ }
@@ -0,0 +1,303 @@
1
+ export const LEDGER_ENTRY_TYPES = [
2
+ 'decision',
3
+ 'review',
4
+ 'receipt',
5
+ 'audit',
6
+ 'event',
7
+ 'heartbeat',
8
+ 'capability',
9
+ 'incident',
10
+ 'doctrine',
11
+ 'task',
12
+ 'closeout',
13
+ 'retroactive_import',
14
+ ] as const;
15
+
16
+ export const LEDGER_AUTHORITY_CLASSES = [
17
+ 'advisory',
18
+ 'grant',
19
+ 'receipt',
20
+ 'audit',
21
+ 'status',
22
+ ] as const;
23
+
24
+ export const LEDGER_EXPORT_CLASSES = [
25
+ 'local_private',
26
+ 'workspace_shared',
27
+ 'org_projected',
28
+ 'public_safe',
29
+ ] as const;
30
+
31
+ export const LEDGER_STATUS_EFFECTS = [
32
+ 'none',
33
+ 'observed',
34
+ 'changed',
35
+ 'gap_found',
36
+ 'reserved',
37
+ 'closed',
38
+ 'proved',
39
+ ] as const;
40
+
41
+ export type LedgerEntryType = typeof LEDGER_ENTRY_TYPES[number];
42
+ export type LedgerAuthorityClass = typeof LEDGER_AUTHORITY_CLASSES[number];
43
+ export type LedgerExportClass = typeof LEDGER_EXPORT_CLASSES[number];
44
+ export type LedgerStatusEffect = typeof LEDGER_STATUS_EFFECTS[number];
45
+
46
+ export interface LedgerEntryRefs {
47
+ lanes?: string[];
48
+ commits?: string[];
49
+ prs?: string[];
50
+ bus?: string[];
51
+ artifacts?: string[];
52
+ receipts?: string[];
53
+ }
54
+
55
+ export interface LedgerEntryV0 {
56
+ schema: 'ledger-entry.v0';
57
+ entry_id: string;
58
+ created_at: string;
59
+ entry_type: LedgerEntryType;
60
+ spine_eligible: boolean;
61
+ authority: {
62
+ class: LedgerAuthorityClass;
63
+ grants_execution: boolean;
64
+ };
65
+ refs: LedgerEntryRefs;
66
+ export_class: LedgerExportClass;
67
+ summary: string;
68
+ legacy?: {
69
+ pre_v0?: boolean;
70
+ low_authority?: boolean;
71
+ };
72
+ }
73
+
74
+ export interface LedgerEntryV01 extends Omit<LedgerEntryV0, 'schema'> {
75
+ schema: 'ledger-entry.v0.1';
76
+ topic_hints?: string[];
77
+ status_effect?: LedgerStatusEffect;
78
+ }
79
+
80
+ export interface LedgerEntryV02 extends Omit<LedgerEntryV01, 'schema'> {
81
+ schema: 'ledger-entry.v0.2';
82
+ valid_time?: string;
83
+ valid_time_end?: string;
84
+ }
85
+
86
+ export type LedgerEntry = LedgerEntryV0 | LedgerEntryV01 | LedgerEntryV02;
87
+
88
+ export type LedgerLineClassification =
89
+ | { kind: 'ignored' }
90
+ | { kind: 'legacy'; pre_v0: true; spine_eligible: false; low_authority: true }
91
+ | { kind: 'v0'; pre_v0: false; spine_eligible: boolean };
92
+
93
+ function assertIsoTimestamp(value: string): void {
94
+ if (!Number.isFinite(Date.parse(value))) {
95
+ throw new Error('ledger entry created_at must be an ISO-8601 timestamp');
96
+ }
97
+ }
98
+
99
+ function assertOpaqueRef(value: string, field: string): void {
100
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._/@-]{0,255}$/.test(value)) {
101
+ throw new Error(`${field} must be a compact opaque or repo-relative ref`);
102
+ }
103
+ }
104
+
105
+ function assertSummary(value: string): void {
106
+ if (value.length === 0 || value.length > 240 || /\s{2,}/.test(value)) {
107
+ throw new Error('ledger entry summary must be compact and single-spaced');
108
+ }
109
+ }
110
+
111
+ export function assertLedgerTopicHints(value: string[] | undefined): void {
112
+ if (value === undefined) return;
113
+ if (value.length > 8) {
114
+ throw new Error('ledger entry topic_hints may contain at most 8 items');
115
+ }
116
+ const seen = new Set<string>();
117
+ for (const topic of value) {
118
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(topic)) {
119
+ throw new Error('ledger entry topic_hints must be kebab-case slugs');
120
+ }
121
+ if (seen.has(topic)) {
122
+ throw new Error('ledger entry topic_hints must not contain duplicates');
123
+ }
124
+ seen.add(topic);
125
+ }
126
+ }
127
+
128
+ function refsCount(refs: LedgerEntryRefs): number {
129
+ return Object.values(refs).reduce((total, values) => total + (values?.length ?? 0), 0);
130
+ }
131
+
132
+ function compactRefs(refs: LedgerEntryRefs): string {
133
+ const parts: string[] = [];
134
+ for (const key of ['lanes', 'commits', 'prs', 'bus', 'artifacts', 'receipts'] as const) {
135
+ for (const value of refs[key] ?? []) {
136
+ assertOpaqueRef(value, `refs.${key}`);
137
+ parts.push(`${key}:${value}`);
138
+ }
139
+ }
140
+ return parts.join(',') || 'none';
141
+ }
142
+
143
+ function assertLedgerEntryBase(entry: LedgerEntry): void {
144
+ assertIsoTimestamp(entry.created_at);
145
+ assertOpaqueRef(entry.entry_id, 'entry_id');
146
+ assertSummary(entry.summary);
147
+
148
+ if (entry.authority.grants_execution && entry.authority.class !== 'grant') {
149
+ throw new Error('only grant authority entries may set grants_execution=true');
150
+ }
151
+
152
+ if (entry.authority.class !== 'advisory' && refsCount(entry.refs) === 0) {
153
+ throw new Error('non-advisory ledger entries require at least one durable ref');
154
+ }
155
+ }
156
+
157
+ export function assertLedgerEntryV0(entry: LedgerEntryV0): void {
158
+ assertLedgerEntryBase(entry);
159
+ if (entry.entry_type === 'retroactive_import') {
160
+ throw new Error('retroactive_import entries require schema ledger-entry.v0.2');
161
+ }
162
+ }
163
+
164
+ export function assertLedgerEntryV01(entry: LedgerEntryV01): void {
165
+ assertLedgerEntryBase(entry);
166
+ assertLedgerTopicHints(entry.topic_hints);
167
+ if (entry.status_effect !== undefined && !LEDGER_STATUS_EFFECTS.includes(entry.status_effect)) {
168
+ throw new Error('ledger entry status_effect is not supported');
169
+ }
170
+ if (entry.entry_type === 'retroactive_import') {
171
+ throw new Error('retroactive_import entries require schema ledger-entry.v0.2');
172
+ }
173
+ }
174
+
175
+ export function assertLedgerEntryV02(entry: LedgerEntryV02): void {
176
+ assertLedgerEntryBase(entry);
177
+ assertLedgerTopicHints(entry.topic_hints);
178
+ if (entry.status_effect !== undefined && !LEDGER_STATUS_EFFECTS.includes(entry.status_effect)) {
179
+ throw new Error('ledger entry status_effect is not supported');
180
+ }
181
+
182
+ // Bitemporal honesty: created_at is the recording (transaction) time and is
183
+ // never backdated; valid_time (+ optional end) is when the covered work
184
+ // actually happened.
185
+ if (entry.valid_time !== undefined) {
186
+ if (!Number.isFinite(Date.parse(entry.valid_time))) {
187
+ throw new Error('ledger entry valid_time must be an ISO-8601 timestamp');
188
+ }
189
+ if (Date.parse(entry.valid_time) > Date.parse(entry.created_at)) {
190
+ throw new Error('ledger entry valid_time must not be after created_at (no future coverage, no backdated recording)');
191
+ }
192
+ }
193
+ if (entry.valid_time_end !== undefined) {
194
+ if (entry.valid_time === undefined) {
195
+ throw new Error('ledger entry valid_time_end requires valid_time');
196
+ }
197
+ if (!Number.isFinite(Date.parse(entry.valid_time_end))) {
198
+ throw new Error('ledger entry valid_time_end must be an ISO-8601 timestamp');
199
+ }
200
+ if (Date.parse(entry.valid_time_end) < Date.parse(entry.valid_time)) {
201
+ throw new Error('ledger entry valid_time_end must not precede valid_time');
202
+ }
203
+ if (Date.parse(entry.valid_time_end) > Date.parse(entry.created_at)) {
204
+ throw new Error('ledger entry valid_time_end must not be after created_at');
205
+ }
206
+ }
207
+
208
+ if (entry.entry_type === 'retroactive_import') {
209
+ if (entry.valid_time === undefined) {
210
+ throw new Error('retroactive_import entries must declare valid_time');
211
+ }
212
+ const evidence =
213
+ (entry.refs.commits?.length ?? 0) +
214
+ (entry.refs.receipts?.length ?? 0) +
215
+ (entry.refs.artifacts?.length ?? 0);
216
+ if (evidence === 0) {
217
+ throw new Error('retroactive_import entries require contemporaneous evidence refs (commits, receipts, or artifacts)');
218
+ }
219
+ if (entry.authority.class !== 'advisory' && entry.authority.class !== 'audit') {
220
+ throw new Error('retroactive_import entries must carry advisory or audit authority; late entries never mint grants');
221
+ }
222
+ }
223
+ }
224
+
225
+ export function assertLedgerEntry(entry: LedgerEntry): void {
226
+ if (entry.schema === 'ledger-entry.v0.2') {
227
+ assertLedgerEntryV02(entry);
228
+ return;
229
+ }
230
+ if (entry.schema === 'ledger-entry.v0.1') {
231
+ assertLedgerEntryV01(entry);
232
+ return;
233
+ }
234
+ assertLedgerEntryV0(entry);
235
+ }
236
+
237
+ export function normalizeLedgerEntryV01(entry: LedgerEntryV01): LedgerEntryV01 {
238
+ assertLedgerEntryV01(entry);
239
+ return {
240
+ ...entry,
241
+ topic_hints: entry.topic_hints ?? [],
242
+ status_effect: entry.status_effect ?? 'none',
243
+ };
244
+ }
245
+
246
+ export function formatLedgerEntryLine(entry: LedgerEntry, extra: Record<string, string> = {}): string {
247
+ assertLedgerEntry(entry);
248
+ const parts = [
249
+ `- ${entry.created_at}`,
250
+ `schema=${entry.schema}`,
251
+ `entry_id=${entry.entry_id}`,
252
+ `entry_type=${entry.entry_type}`,
253
+ `spine_eligible=${entry.spine_eligible}`,
254
+ `authority_class=${entry.authority.class}`,
255
+ `grants_execution=${entry.authority.grants_execution}`,
256
+ `export_class=${entry.export_class}`,
257
+ `refs=${compactRefs(entry.refs)}`,
258
+ ];
259
+
260
+ if (entry.schema === 'ledger-entry.v0.1' || entry.schema === 'ledger-entry.v0.2') {
261
+ const topicHints = entry.topic_hints ?? [];
262
+ const statusEffect = entry.status_effect ?? 'none';
263
+ if (topicHints.length > 0) {
264
+ parts.push(`topic_hints=${topicHints.join(',')}`);
265
+ }
266
+ if (statusEffect !== 'none') {
267
+ parts.push(`status_effect=${statusEffect}`);
268
+ }
269
+ }
270
+
271
+ if (entry.schema === 'ledger-entry.v0.2') {
272
+ if (entry.valid_time !== undefined) {
273
+ parts.push(`valid_time=${entry.valid_time}`);
274
+ }
275
+ if (entry.valid_time_end !== undefined) {
276
+ parts.push(`valid_time_end=${entry.valid_time_end}`);
277
+ }
278
+ }
279
+
280
+ for (const [key, value] of Object.entries(extra)) {
281
+ parts.push(`${key}=${value}`);
282
+ }
283
+
284
+ return parts.join(' ');
285
+ }
286
+
287
+ export function classifyLedgerLine(line: string): LedgerLineClassification {
288
+ const trimmed = line.trim();
289
+ if (trimmed.length === 0 || trimmed.startsWith('#')) {
290
+ return { kind: 'ignored' };
291
+ }
292
+ if (!trimmed.startsWith('- ')) {
293
+ return { kind: 'ignored' };
294
+ }
295
+ if (!/\bschema=ledger-entry\.v0(?:\.[12])?\b/.test(trimmed)) {
296
+ return { kind: 'legacy', pre_v0: true, spine_eligible: false, low_authority: true };
297
+ }
298
+ return {
299
+ kind: 'v0',
300
+ pre_v0: false,
301
+ spine_eligible: /\bspine_eligible=true\b/.test(trimmed),
302
+ };
303
+ }