@etiquekit/etq 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -56
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +7 -6
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +11 -11
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4686 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +831 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +14 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,708 @@
1
+ import type { LocalLedgerView, LocalLedgerViewOptions, LocalLedgerViewRow } from './local-ledger-view';
2
+ import { buildLocalLedgerView } from './local-ledger-view';
3
+ import { existsSync } from 'node:fs';
4
+ import { readFile, readdir } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+
7
+ export type TimelineScope = 'project' | 'seat' | 'lane' | 'principal';
8
+ export type TimelineFormat = 'text' | 'json' | 'markdown';
9
+ export type TimelineItemType = 'task_card' | 'return' | 'receipt' | 'decision' | 'chatter';
10
+
11
+ type JsonRecord = Record<string, unknown>;
12
+
13
+ export interface TimelineCardSection {
14
+ label: 'Routing' | 'Evidence' | 'Failure modes' | 'Confidence calibration';
15
+ body: string;
16
+ }
17
+
18
+ export interface TimelineViewOptions extends LocalLedgerViewOptions {
19
+ scope?: TimelineScope;
20
+ }
21
+
22
+ export interface TimelineFeedItem {
23
+ item_id: string;
24
+ item_type: TimelineItemType;
25
+ scope: TimelineScope;
26
+ lane: string | null;
27
+ seat_id: string | null;
28
+ operator_principal: string | null;
29
+ actor_seat_id: string | null;
30
+ created_at: string | null;
31
+ next_owner: string | null;
32
+ role: string | null;
33
+ verdict: string | null;
34
+ status_effect: string | null;
35
+ topic_hints: string[];
36
+ requires_ack: boolean | null;
37
+ display: {
38
+ role_chip: string | null;
39
+ lane_chip: string | null;
40
+ seat_badge: string | null;
41
+ verdict_pill: string | null;
42
+ status_subtitle: string | null;
43
+ topic_chips: string[];
44
+ sections: TimelineCardSection[];
45
+ };
46
+ thread: {
47
+ thread_id: string;
48
+ parent_ref: string | null;
49
+ depth: number;
50
+ is_thread_root: boolean;
51
+ thread_item_count: number;
52
+ };
53
+ title: string;
54
+ body: string;
55
+ source: {
56
+ kind: 'event' | 'packet';
57
+ seq: number | null;
58
+ event_ref: string | null;
59
+ packet_ref: string | null;
60
+ commit_sha: string;
61
+ event_log_hash: string;
62
+ };
63
+ authority_boundary: {
64
+ projection_item_only: true;
65
+ can_execute: false;
66
+ can_trigger_mention: false;
67
+ can_write_back: false;
68
+ can_grant: false;
69
+ can_close: false;
70
+ can_merge: false;
71
+ can_sign: false;
72
+ };
73
+ }
74
+
75
+ export interface TimelineThread {
76
+ thread_id: string;
77
+ lane: string | null;
78
+ title: string;
79
+ item_ids: string[];
80
+ latest_seq: number | null;
81
+ item_count: number;
82
+ }
83
+
84
+ export interface TimelineCuratorPortal {
85
+ schema: 'timeline-curator-portal.v0';
86
+ health_board: {
87
+ registry_ref: '.etiquette/roster/enrollments/*.json';
88
+ seats: {
89
+ seat_id: string;
90
+ lifecycle_state: string;
91
+ ready: boolean;
92
+ operator_principal: string | null;
93
+ execution_seat: string | null;
94
+ source: 'roster' | 'ledger-observed';
95
+ sentinel_state_ref: string;
96
+ cursor_ref: string;
97
+ }[];
98
+ };
99
+ digest_view: {
100
+ window: 'current_projection';
101
+ items_by_type: Record<TimelineItemType, number>;
102
+ threads: number;
103
+ requires_ack: number;
104
+ };
105
+ promotion_queue: {
106
+ display_only: true;
107
+ items: {
108
+ item_id: string;
109
+ source_ref: string;
110
+ lane: string | null;
111
+ next_owner: string | null;
112
+ reason: string;
113
+ }[];
114
+ };
115
+ authority_boundary: {
116
+ display_only: true;
117
+ can_execute: false;
118
+ can_write_back: false;
119
+ can_promote: false;
120
+ can_grant: false;
121
+ can_close: false;
122
+ can_merge: false;
123
+ };
124
+ }
125
+
126
+ export interface TimelineView {
127
+ schema: 'timeline-projection.v0';
128
+ project: string;
129
+ scope: TimelineScope;
130
+ watermark: LocalLedgerView['watermark'];
131
+ source: {
132
+ mode: 'projection-only';
133
+ projection_schema: 'local-ledger-view.v0';
134
+ event_log_ref: '.etiquette/events/local.ndjson';
135
+ packet_glob_ref: 'docs/work/packets/*.json';
136
+ roster_ref: '.etiquette/roster/enrollments/*.json';
137
+ };
138
+ identity_resolution: {
139
+ source: 'one-roster';
140
+ seat_filter: string | null;
141
+ lane_filter: string | null;
142
+ chat_nicknames_are_identity: false;
143
+ parallel_identity_store: false;
144
+ };
145
+ feed: {
146
+ items: TimelineFeedItem[];
147
+ items_matched: number;
148
+ display_modes: ['stream', 'thread'];
149
+ deep_link: {
150
+ query_param: 'seq';
151
+ item_id: string | null;
152
+ event_ref: string | null;
153
+ };
154
+ threads: TimelineThread[];
155
+ };
156
+ curator_portal: TimelineCuratorPortal;
157
+ boundary: string;
158
+ authority_boundary: {
159
+ read_only_projection: true;
160
+ rebuildable_projection: true;
161
+ can_execute: false;
162
+ can_trigger_mention: false;
163
+ can_write_back: false;
164
+ can_create_canonical_chatter: false;
165
+ can_grant: false;
166
+ can_mint_or_widen_lease: false;
167
+ can_close: false;
168
+ can_merge: false;
169
+ can_sign: false;
170
+ can_use_hosted_backend: false;
171
+ };
172
+ next_step: string;
173
+ }
174
+
175
+ const DECISION_WORDS = [
176
+ 'decision',
177
+ 'grant',
178
+ 'approval',
179
+ 'approve',
180
+ 'escalate',
181
+ 'policy',
182
+ 'closeout',
183
+ ];
184
+
185
+ const TIMELINE_ITEM_TYPES: TimelineItemType[] = ['task_card', 'return', 'receipt', 'decision', 'chatter'];
186
+
187
+ function stringField(record: JsonRecord, field: string): string | null {
188
+ const value = record[field];
189
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
190
+ }
191
+
192
+ function boolField(record: JsonRecord, field: string): boolean {
193
+ return record[field] === true;
194
+ }
195
+
196
+ function safeName(value: string): string {
197
+ return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'seat';
198
+ }
199
+
200
+ async function readJsonIfExists(path: string): Promise<JsonRecord | null> {
201
+ try {
202
+ return JSON.parse(await readFile(path, 'utf-8')) as JsonRecord;
203
+ } catch (err) {
204
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
205
+ throw err;
206
+ }
207
+ }
208
+
209
+ async function listRosterEnrollments(root: string): Promise<JsonRecord[]> {
210
+ const dir = join(root, '.etiquette', 'roster', 'enrollments');
211
+ if (!existsSync(dir)) return [];
212
+ const entries = await readdir(dir, { withFileTypes: true });
213
+ const rows: JsonRecord[] = [];
214
+ for (const entry of entries) {
215
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
216
+ const row = await readJsonIfExists(join(dir, entry.name));
217
+ if (row) rows.push(row);
218
+ }
219
+ return rows.sort((left, right) => String(left.seat_id ?? '').localeCompare(String(right.seat_id ?? '')));
220
+ }
221
+
222
+ function classifyTimelineItem(row: LocalLedgerViewRow): TimelineItemType {
223
+ const role = row.role?.toLowerCase() ?? '';
224
+ const type = row.type?.toLowerCase() ?? '';
225
+ const text = row.text.toLowerCase();
226
+ if (role.includes('task') || type.includes('task')) return 'task_card';
227
+ if (role.includes('return') || type.includes('return')) return 'return';
228
+ if (role.includes('receipt') || type.includes('receipt')) return 'receipt';
229
+ if (DECISION_WORDS.some((word) => role.includes(word) || type.includes(word) || text.includes(word))) {
230
+ return 'decision';
231
+ }
232
+ return 'chatter';
233
+ }
234
+
235
+ function isPrincipalRow(row: LocalLedgerViewRow): boolean {
236
+ const haystack = [
237
+ row.role,
238
+ row.type,
239
+ row.from,
240
+ row.to,
241
+ row.next_owner,
242
+ row.text,
243
+ ].filter((value): value is string => Boolean(value)).join(' ').toLowerCase();
244
+ return row.requires_ack === true
245
+ || Boolean(row.next_owner)
246
+ || DECISION_WORDS.some((word) => haystack.includes(word));
247
+ }
248
+
249
+ function rowInTimelineScope(row: LocalLedgerViewRow, scope: TimelineScope, options: TimelineViewOptions): boolean {
250
+ if (scope === 'project') return true;
251
+ if (scope === 'principal') return isPrincipalRow(row);
252
+ if (scope === 'lane') return Boolean(options.lane) && row.lane === options.lane;
253
+ if (scope === 'seat') {
254
+ const seat = options.seat;
255
+ return Boolean(seat) && (
256
+ row.from === seat
257
+ || row.to === seat
258
+ || row.next_owner === seat
259
+ || row.text.includes(String(seat))
260
+ );
261
+ }
262
+ return false;
263
+ }
264
+
265
+ function itemTitle(row: LocalLedgerViewRow, itemType: TimelineItemType): string {
266
+ const label = itemType.replace('_', ' ');
267
+ const lane = row.lane ? ` for ${row.lane}` : '';
268
+ const role = row.role ? ` (${row.role})` : '';
269
+ return `${label}${lane}${role}`;
270
+ }
271
+
272
+ function itemBody(row: LocalLedgerViewRow): string {
273
+ const parts = [
274
+ row.from && row.to ? `${row.from} -> ${row.to}` : row.from ?? row.to ?? null,
275
+ row.next_owner ? `next_owner=${row.next_owner}` : null,
276
+ row.requires_ack === true ? 'requires_ack=true' : null,
277
+ row.verdict ? `verdict=${row.verdict}` : null,
278
+ row.status_effect && row.status_effect !== 'none' ? `status_effect=${row.status_effect}` : null,
279
+ row.packet_ref ? `packet=${row.packet_ref}` : null,
280
+ row.event_ref ? `event=${row.event_ref}` : null,
281
+ ].filter((value): value is string => Boolean(value));
282
+ return parts.join(' | ') || row.text;
283
+ }
284
+
285
+ function sourceIdForRow(row: LocalLedgerViewRow): string {
286
+ return row.event_ref ?? row.packet_ref ?? `${row.kind}:${row.seq ?? 'unsequenced'}`;
287
+ }
288
+
289
+ function sourceRefForRow(row: LocalLedgerViewRow): string {
290
+ return row.event_ref ?? row.packet_ref ?? 'unsequenced';
291
+ }
292
+
293
+ function threadIdForRow(row: LocalLedgerViewRow): string {
294
+ if (row.lane) return `lane:${row.lane}`;
295
+ if (row.in_reply_to) return `reply:${row.in_reply_to}`;
296
+ if (row.parent_event_ref) return `reply:${row.parent_event_ref}`;
297
+ if (row.parent_event_seq !== null) return `reply:local://event/${row.parent_event_seq}`;
298
+ return `item:${sourceIdForRow(row)}`;
299
+ }
300
+
301
+ function parentRefForRow(row: LocalLedgerViewRow): string | null {
302
+ if (row.in_reply_to) return row.in_reply_to;
303
+ if (row.parent_event_ref) return row.parent_event_ref;
304
+ if (row.parent_event_seq !== null) return `local://event/${row.parent_event_seq}`;
305
+ return null;
306
+ }
307
+
308
+ function verdictPill(row: LocalLedgerViewRow): string | null {
309
+ if (row.verdict) return row.verdict;
310
+ const match = row.text.match(/\b(CONFIRM_WITH_AMENDMENTS|CONFIRM|COUNTER|PASS|FAIL|HOLD|GO|NO_GO)\b/i);
311
+ return match ? match[1].toUpperCase() : null;
312
+ }
313
+
314
+ function cardSections(row: LocalLedgerViewRow): TimelineCardSection[] {
315
+ const route = [
316
+ row.from ? `from=${row.from}` : null,
317
+ row.to ? `to=${row.to}` : null,
318
+ row.next_owner ? `next_owner=${row.next_owner}` : null,
319
+ row.mode ? `mode=${row.mode}` : null,
320
+ ].filter((part): part is string => Boolean(part)).join(' | ') || 'routing unresolved in projection';
321
+ const evidence = [
322
+ row.event_ref ? `event=${row.event_ref}` : null,
323
+ row.packet_ref ? `packet=${row.packet_ref}` : null,
324
+ row.packet_tier ? `packet_tier=${row.packet_tier}` : null,
325
+ ].filter((part): part is string => Boolean(part)).join(' | ') || sourceRefForRow(row);
326
+ const confidence = row.seq !== null
327
+ ? `sequenced local event seq=${row.seq}; rebuild from local event log and packet refs`
328
+ : 'legacy packet fallback; unsequenced packet row remains projection-only';
329
+ return [
330
+ { label: 'Routing', body: route },
331
+ { label: 'Evidence', body: evidence },
332
+ { label: 'Failure modes', body: 'display-only projection; no comments, writes, mentions, grants, closes, merges, or execution' },
333
+ { label: 'Confidence calibration', body: confidence },
334
+ ];
335
+ }
336
+
337
+ function buildThreads(items: TimelineFeedItem[]): TimelineThread[] {
338
+ const byThread = new Map<string, TimelineFeedItem[]>();
339
+ for (const item of items) {
340
+ const existing = byThread.get(item.thread.thread_id) ?? [];
341
+ existing.push(item);
342
+ byThread.set(item.thread.thread_id, existing);
343
+ }
344
+ return Array.from(byThread.entries()).map(([threadId, threadItems]) => {
345
+ const sorted = [...threadItems].sort((left, right) => (left.source.seq ?? 0) - (right.source.seq ?? 0));
346
+ const latestSeq = sorted.reduce<number | null>((latest, item) => (
347
+ item.source.seq === null ? latest : Math.max(latest ?? 0, item.source.seq)
348
+ ), null);
349
+ const lane = sorted.find((item) => item.lane)?.lane ?? null;
350
+ return {
351
+ thread_id: threadId,
352
+ lane,
353
+ title: lane ? `Thread ${lane}` : `Thread ${threadId}`,
354
+ item_ids: sorted.map((item) => item.item_id),
355
+ latest_seq: latestSeq,
356
+ item_count: sorted.length,
357
+ };
358
+ }).sort((left, right) => (right.latest_seq ?? 0) - (left.latest_seq ?? 0));
359
+ }
360
+
361
+ function attachThreadCounts(items: TimelineFeedItem[], threads: TimelineThread[]): TimelineFeedItem[] {
362
+ const countByThread = new Map(threads.map((thread) => [thread.thread_id, thread.item_count] as const));
363
+ return items.map((item) => ({
364
+ ...item,
365
+ thread: {
366
+ ...item.thread,
367
+ thread_item_count: countByThread.get(item.thread.thread_id) ?? 1,
368
+ },
369
+ }));
370
+ }
371
+
372
+ function titleCase(value: string): string {
373
+ return value.charAt(0).toUpperCase() + value.slice(1);
374
+ }
375
+
376
+ function itemBadge(itemType: TimelineItemType): string {
377
+ switch (itemType) {
378
+ case 'task_card':
379
+ return 'TASK';
380
+ case 'return':
381
+ return 'RETURN';
382
+ case 'receipt':
383
+ return 'RECEIPT';
384
+ case 'decision':
385
+ return 'DECISION';
386
+ case 'chatter':
387
+ return 'CHATTER';
388
+ }
389
+ }
390
+
391
+ function itemSourceRef(item: TimelineFeedItem): string {
392
+ return item.source.event_ref ?? item.source.packet_ref ?? 'unsequenced';
393
+ }
394
+
395
+ function itemAudienceLine(item: TimelineFeedItem): string {
396
+ const participants = [
397
+ item.actor_seat_id ? `from=${item.actor_seat_id}` : null,
398
+ item.seat_id ? `seat=${item.seat_id}` : null,
399
+ item.operator_principal ? `operator=${item.operator_principal}` : null,
400
+ ].filter((value): value is string => Boolean(value));
401
+ return participants.length > 0 ? participants.join(' ') : 'seat=unresolved';
402
+ }
403
+
404
+ function timelineItemFromRow(
405
+ row: LocalLedgerViewRow,
406
+ scope: TimelineScope,
407
+ watermark: LocalLedgerView['watermark'],
408
+ ): TimelineFeedItem {
409
+ const itemType = classifyTimelineItem(row);
410
+ const sourceId = sourceIdForRow(row);
411
+ const threadId = threadIdForRow(row);
412
+ const parentRef = parentRefForRow(row);
413
+ const itemVerdict = verdictPill(row);
414
+ return {
415
+ item_id: `timeline:${sourceId}`,
416
+ item_type: itemType,
417
+ scope,
418
+ lane: row.lane,
419
+ seat_id: row.to ?? row.next_owner ?? row.from,
420
+ operator_principal: null,
421
+ actor_seat_id: row.from,
422
+ created_at: row.created_at,
423
+ next_owner: row.next_owner,
424
+ role: row.role,
425
+ verdict: itemVerdict,
426
+ status_effect: row.status_effect,
427
+ topic_hints: row.topic_hints,
428
+ requires_ack: row.requires_ack,
429
+ display: {
430
+ role_chip: row.role,
431
+ lane_chip: row.lane,
432
+ seat_badge: row.to ?? row.next_owner ?? row.from,
433
+ verdict_pill: itemVerdict,
434
+ status_subtitle: row.status_effect && row.status_effect !== 'none' ? row.status_effect : null,
435
+ topic_chips: row.topic_hints,
436
+ sections: cardSections(row),
437
+ },
438
+ thread: {
439
+ thread_id: threadId,
440
+ parent_ref: parentRef,
441
+ depth: parentRef ? 1 : 0,
442
+ is_thread_root: parentRef === null,
443
+ thread_item_count: 1,
444
+ },
445
+ title: itemTitle(row, itemType),
446
+ body: itemBody(row),
447
+ source: {
448
+ kind: row.kind,
449
+ seq: row.seq,
450
+ event_ref: row.event_ref,
451
+ packet_ref: row.packet_ref,
452
+ commit_sha: watermark.commit_sha,
453
+ event_log_hash: watermark.event_log_hash,
454
+ },
455
+ authority_boundary: {
456
+ projection_item_only: true,
457
+ can_execute: false,
458
+ can_trigger_mention: false,
459
+ can_write_back: false,
460
+ can_grant: false,
461
+ can_close: false,
462
+ can_merge: false,
463
+ can_sign: false,
464
+ },
465
+ };
466
+ }
467
+
468
+ function collectObservedSeats(rows: LocalLedgerViewRow[]): Set<string> {
469
+ const seats = new Set<string>();
470
+ for (const row of rows) {
471
+ for (const seat of [row.from, row.to, row.next_owner]) {
472
+ if (seat) seats.add(seat);
473
+ }
474
+ }
475
+ return seats;
476
+ }
477
+
478
+ async function buildCuratorPortal(root: string, rows: LocalLedgerViewRow[], items: TimelineFeedItem[], threads: TimelineThread[]): Promise<TimelineCuratorPortal> {
479
+ const enrollments = await listRosterEnrollments(root);
480
+ const enrollmentBySeat = new Map<string, JsonRecord>();
481
+ for (const enrollment of enrollments) {
482
+ const seat = stringField(enrollment, 'seat_id');
483
+ if (seat) enrollmentBySeat.set(seat, enrollment);
484
+ }
485
+ const seats = new Set([...collectObservedSeats(rows), ...enrollmentBySeat.keys()]);
486
+ const itemsByType = TIMELINE_ITEM_TYPES.reduce<Record<TimelineItemType, number>>((acc, type) => {
487
+ acc[type] = 0;
488
+ return acc;
489
+ }, {
490
+ task_card: 0,
491
+ return: 0,
492
+ receipt: 0,
493
+ decision: 0,
494
+ chatter: 0,
495
+ });
496
+ for (const item of items) itemsByType[item.item_type] += 1;
497
+ const queueItems = items
498
+ .filter((item) => item.requires_ack === true || item.item_type === 'decision' || Boolean(item.status_effect && item.status_effect !== 'none'))
499
+ .map((item) => ({
500
+ item_id: item.item_id,
501
+ source_ref: itemSourceRef(item),
502
+ lane: item.lane,
503
+ next_owner: item.next_owner,
504
+ reason: item.requires_ack === true ? 'requires_ack' : item.status_effect && item.status_effect !== 'none' ? `status_effect:${item.status_effect}` : 'decision',
505
+ }));
506
+ return {
507
+ schema: 'timeline-curator-portal.v0',
508
+ health_board: {
509
+ registry_ref: '.etiquette/roster/enrollments/*.json',
510
+ seats: Array.from(seats).sort().map((seat) => {
511
+ const enrollment = enrollmentBySeat.get(seat);
512
+ const key = safeName(seat);
513
+ return {
514
+ seat_id: seat,
515
+ lifecycle_state: stringField(enrollment ?? {}, 'lifecycle_state') ?? (enrollment ? 'enrolled' : 'observed'),
516
+ ready: enrollment ? boolField(enrollment, 'ready') : false,
517
+ operator_principal: enrollment ? stringField(enrollment, 'operator_principal') : null,
518
+ execution_seat: enrollment ? stringField(enrollment, 'execution_seat') : null,
519
+ source: enrollment ? 'roster' : 'ledger-observed',
520
+ sentinel_state_ref: `.etiquette/state/sentinel/${key}.json`,
521
+ cursor_ref: `.etiquette/state/sentinel/${key}.cursor.json`,
522
+ };
523
+ }),
524
+ },
525
+ digest_view: {
526
+ window: 'current_projection',
527
+ items_by_type: itemsByType,
528
+ threads: threads.length,
529
+ requires_ack: items.filter((item) => item.requires_ack === true).length,
530
+ },
531
+ promotion_queue: {
532
+ display_only: true,
533
+ items: queueItems,
534
+ },
535
+ authority_boundary: {
536
+ display_only: true,
537
+ can_execute: false,
538
+ can_write_back: false,
539
+ can_promote: false,
540
+ can_grant: false,
541
+ can_close: false,
542
+ can_merge: false,
543
+ },
544
+ };
545
+ }
546
+
547
+ export async function buildTimelineView(root: string, options: TimelineViewOptions = {}): Promise<TimelineView> {
548
+ const scope = options.scope ?? 'project';
549
+ if (scope === 'seat' && !options.seat) throw new Error('--seat is required when --scope seat');
550
+ if (scope === 'lane' && !options.lane) throw new Error('--lane is required when --scope lane');
551
+ const ledger = await buildLocalLedgerView(root, {
552
+ ...options,
553
+ kind: 'all',
554
+ });
555
+ const rows = [...ledger.events, ...ledger.packets]
556
+ .filter((row) => rowInTimelineScope(row, scope, options))
557
+ .sort((a, b) => (a.seq ?? Number.MAX_SAFE_INTEGER) - (b.seq ?? Number.MAX_SAFE_INTEGER));
558
+ const rawItems = rows.map((row) => timelineItemFromRow(row, scope, ledger.watermark));
559
+ const threads = buildThreads(rawItems);
560
+ const items = attachThreadCounts(rawItems, threads);
561
+ return {
562
+ schema: 'timeline-projection.v0',
563
+ project: root,
564
+ scope,
565
+ watermark: ledger.watermark,
566
+ source: {
567
+ mode: 'projection-only',
568
+ projection_schema: 'local-ledger-view.v0',
569
+ event_log_ref: '.etiquette/events/local.ndjson',
570
+ packet_glob_ref: 'docs/work/packets/*.json',
571
+ roster_ref: '.etiquette/roster/enrollments/*.json',
572
+ },
573
+ identity_resolution: {
574
+ source: 'one-roster',
575
+ seat_filter: options.seat ?? null,
576
+ lane_filter: options.lane ?? null,
577
+ chat_nicknames_are_identity: false,
578
+ parallel_identity_store: false,
579
+ },
580
+ feed: {
581
+ items,
582
+ items_matched: rows.length,
583
+ display_modes: ['stream', 'thread'],
584
+ deep_link: {
585
+ query_param: 'seq',
586
+ item_id: null,
587
+ event_ref: null,
588
+ },
589
+ threads,
590
+ },
591
+ curator_portal: await buildCuratorPortal(root, rows, items, threads),
592
+ boundary: 'read-only rebuildable timeline projection; chatter is projection, not canonical; mentions cannot execute in v0',
593
+ authority_boundary: {
594
+ read_only_projection: true,
595
+ rebuildable_projection: true,
596
+ can_execute: false,
597
+ can_trigger_mention: false,
598
+ can_write_back: false,
599
+ can_create_canonical_chatter: false,
600
+ can_grant: false,
601
+ can_mint_or_widen_lease: false,
602
+ can_close: false,
603
+ can_merge: false,
604
+ can_sign: false,
605
+ can_use_hosted_backend: false,
606
+ },
607
+ next_step: 'use normal ledger workflow for any canonical post, grant, closeout, execution, or mention-trigger experiment',
608
+ };
609
+ }
610
+
611
+ export function applyTimelineDeepLink(view: TimelineView, seq: number | null): TimelineView {
612
+ if (seq === null || !Number.isFinite(seq)) return view;
613
+ const item = view.feed.items.find((candidate) => candidate.source.seq === seq) ?? null;
614
+ return {
615
+ ...view,
616
+ feed: {
617
+ ...view.feed,
618
+ deep_link: {
619
+ query_param: 'seq',
620
+ item_id: item?.item_id ?? null,
621
+ event_ref: item?.source.event_ref ?? null,
622
+ },
623
+ },
624
+ };
625
+ }
626
+
627
+ export function renderTimelineViewText(view: TimelineView): string {
628
+ return [
629
+ `timeline_feed: ${titleCase(view.scope)}`,
630
+ `schema: ${view.schema}`,
631
+ `scope: ${view.scope}`,
632
+ `source: ${view.source.projection_schema}`,
633
+ `watermark: ${view.watermark.latest_event_ref} commit_sha=${view.watermark.commit_sha}`,
634
+ `event_log_hash: ${view.watermark.event_log_hash}`,
635
+ `identity: ${view.identity_resolution.source} (chat nicknames are not identity)`,
636
+ `boundary: read-only projection; no execution, no write-back, no grants`,
637
+ `items_matched: ${view.feed.items_matched}`,
638
+ `display_modes: ${view.feed.display_modes.join(',')}`,
639
+ `threads: ${view.feed.threads.length}`,
640
+ `health_board_seats: ${view.curator_portal.health_board.seats.map((seat) => seat.seat_id).join(',') || 'none'}`,
641
+ `promotion_queue: ${view.curator_portal.promotion_queue.items.length} display_only=true`,
642
+ ...view.feed.items.map((item) => [
643
+ `[${itemBadge(item.item_type)}] ${item.title}`,
644
+ ` ${itemAudienceLine(item)}`,
645
+ ` source: ${itemSourceRef(item)} seq=${String(item.source.seq ?? 'none')}`,
646
+ ` thread: ${item.thread.thread_id} items=${item.thread.thread_item_count}`,
647
+ item.lane ? ` lane: ${item.lane}` : null,
648
+ item.role ? ` role: ${item.role}` : null,
649
+ item.verdict ? ` verdict: ${item.verdict}` : null,
650
+ item.status_effect && item.status_effect !== 'none' ? ` status_effect: ${item.status_effect}` : null,
651
+ item.topic_hints.length > 0 ? ` topics: ${item.topic_hints.join(',')}` : null,
652
+ ` body: ${item.body}`,
653
+ ' authority: projection-only; cannot execute, write back, grant, close, merge, or sign',
654
+ ].filter((line): line is string => line !== null).join('\n')),
655
+ 'can_execute: false',
656
+ 'can_write_back: false',
657
+ 'can_grant: false',
658
+ ].join('\n');
659
+ }
660
+
661
+ export function renderTimelineViewMarkdown(view: TimelineView): string {
662
+ return [
663
+ `# Timeline Feed: ${titleCase(view.scope)}`,
664
+ '',
665
+ '| Field | Value |',
666
+ '| --- | --- |',
667
+ `| Schema | \`${view.schema}\` |`,
668
+ `| Source | \`${view.source.projection_schema}\` |`,
669
+ `| Watermark | \`${view.watermark.latest_event_ref}\` |`,
670
+ `| Commit | \`${view.watermark.commit_sha}\` |`,
671
+ `| Event log hash | \`${view.watermark.event_log_hash}\` |`,
672
+ `| Identity | ${view.identity_resolution.source}; chat nicknames are not identity |`,
673
+ '| Boundary | read-only projection; no execution, no write-back, no grants |',
674
+ `| Display modes | ${view.feed.display_modes.map((mode) => `\`${mode}\``).join(', ')} |`,
675
+ `| Threads | ${view.feed.threads.length} |`,
676
+ `| Health board seats | ${view.curator_portal.health_board.seats.map((seat) => `\`${seat.seat_id}\``).join(', ') || '_none_'} |`,
677
+ '',
678
+ '## Feed',
679
+ '',
680
+ ...(view.feed.items.length === 0
681
+ ? ['_No timeline items._']
682
+ : view.feed.items.map((item) => [
683
+ `### [${itemBadge(item.item_type)}] ${item.title}`,
684
+ '',
685
+ '| Field | Value |',
686
+ '| --- | --- |',
687
+ `| Type | \`${item.item_type}\` |`,
688
+ `| Source | \`${itemSourceRef(item)}\` |`,
689
+ `| Commit | \`${item.source.commit_sha}\` |`,
690
+ `| Thread | \`${item.thread.thread_id}\` (${item.thread.thread_item_count} item${item.thread.thread_item_count === 1 ? '' : 's'}) |`,
691
+ item.lane ? `| Lane | \`${item.lane}\` |` : null,
692
+ item.role ? `| Role | \`${item.role}\` |` : null,
693
+ item.verdict ? `| Verdict | \`${item.verdict}\` |` : null,
694
+ item.topic_hints.length > 0 ? `| Topics | ${item.topic_hints.map((topic) => `\`${topic}\``).join(', ')} |` : null,
695
+ `| Audience | ${itemAudienceLine(item)} |`,
696
+ `| Body | ${item.body} |`,
697
+ '| Authority | projection-only; cannot execute, write back, grant, close, merge, or sign |',
698
+ ].filter((line): line is string => line !== null).join('\n'))),
699
+ '',
700
+ '## Curator Portal',
701
+ '',
702
+ `- Health board seats: ${view.curator_portal.health_board.seats.map((seat) => `\`${seat.seat_id}\``).join(', ') || '_none_'}`,
703
+ `- Digest threads: ${view.curator_portal.digest_view.threads}`,
704
+ `- Promotion queue: ${view.curator_portal.promotion_queue.items.length} display-only item${view.curator_portal.promotion_queue.items.length === 1 ? '' : 's'}`,
705
+ '- Authority: display-only; cannot execute, write back, promote, grant, close, or merge.',
706
+ '',
707
+ ].join('\n');
708
+ }