@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,388 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { extname, join, relative, resolve } from 'node:path';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { evaluateSeatReadiness, readSeatReadiness, type SeatReadinessStatus } from './readiness';
6
+
7
+ export const AGENT_OWNER_CARD_SOURCE_SCHEMA = 'agent-owner-card-source.v0';
8
+ export const AGENT_OWNER_CARD_SCHEMA = 'agent-owner-card.v0';
9
+ export const AGENT_OWNER_CARD_PROJECTION_SCHEMA = 'agent-owner-card-projection.v0';
10
+
11
+ type JsonRecord = Record<string, unknown>;
12
+
13
+ export interface AgentOwnerFailureMode {
14
+ id: string;
15
+ description: string;
16
+ evidence_refs: string[];
17
+ }
18
+
19
+ export interface AgentOwnerCardSource {
20
+ schema: typeof AGENT_OWNER_CARD_SOURCE_SCHEMA;
21
+ seat_id: string;
22
+ job: string;
23
+ owner_principal: string;
24
+ maintainer: string;
25
+ diet_refs: string[];
26
+ authority_contract_ref: string;
27
+ allowed_work: string[];
28
+ forbidden_work: string[];
29
+ review: {
30
+ cadence: string;
31
+ last_reviewed_at: string;
32
+ stale_after: string;
33
+ event_triggers: string[];
34
+ };
35
+ failure_modes: AgentOwnerFailureMode[];
36
+ }
37
+
38
+ export interface AgentOwnerCard {
39
+ schema: typeof AGENT_OWNER_CARD_SCHEMA;
40
+ generated_at: string;
41
+ source_ref: string;
42
+ seat_id: string;
43
+ job: string;
44
+ accountability: {
45
+ owner_principal: string;
46
+ maintainer: string;
47
+ operator_principal_scope: 'per_run_receipts_only';
48
+ owner_is_not_operator: true;
49
+ };
50
+ diet_refs: string[];
51
+ current_occupant: null | {
52
+ source: 'seat-readiness.v0.1';
53
+ readiness_ref: string;
54
+ seat: string;
55
+ execution_surface: string;
56
+ model_tier: string;
57
+ effective_model_tier: string;
58
+ model_id: string | null;
59
+ operational_status: string;
60
+ capability_trusted: boolean;
61
+ is_stale: boolean;
62
+ last_heartbeat_at: string;
63
+ stale_after: string;
64
+ };
65
+ authority: {
66
+ source_ref: string;
67
+ allowed_work: string[];
68
+ forbidden_work: string[];
69
+ derived_from_contract: true;
70
+ card_may_edit_authority: false;
71
+ };
72
+ review: {
73
+ cadence: string;
74
+ last_reviewed_at: string;
75
+ stale_after: string;
76
+ is_stale: boolean;
77
+ event_driven_triggers: string[];
78
+ };
79
+ failure_modes: AgentOwnerFailureMode[];
80
+ warnings: string[];
81
+ authority_boundary: {
82
+ read_model_only: true;
83
+ can_authorize: false;
84
+ can_attach_grant: false;
85
+ can_change_owner: false;
86
+ can_edit_authority: false;
87
+ can_mutate_readiness: false;
88
+ can_merge: false;
89
+ can_close: false;
90
+ };
91
+ }
92
+
93
+ export interface AgentOwnerCardProjection {
94
+ schema: typeof AGENT_OWNER_CARD_PROJECTION_SCHEMA;
95
+ generated_at: string;
96
+ cards: AgentOwnerCard[];
97
+ validation_errors: Array<{ ref: string; reason: string }>;
98
+ authority_boundary: AgentOwnerCard['authority_boundary'];
99
+ }
100
+
101
+ const DEFAULT_EVENT_TRIGGERS = [
102
+ 'model_switch',
103
+ 'new_failure_mode',
104
+ 'authority_change',
105
+ 'seat_readiness_stale',
106
+ ] as const;
107
+
108
+ function isoNow(at?: string): string {
109
+ const value = at ?? new Date().toISOString();
110
+ if (!Number.isFinite(Date.parse(value))) throw new Error('--at must be an ISO-8601 timestamp');
111
+ return new Date(value).toISOString();
112
+ }
113
+
114
+ function isRecord(value: unknown): value is JsonRecord {
115
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
116
+ }
117
+
118
+ function requireRecord(value: unknown, field: string): JsonRecord {
119
+ if (!isRecord(value)) throw new Error(`${field} must be an object`);
120
+ return value;
121
+ }
122
+
123
+ function requireString(record: JsonRecord, key: string): string {
124
+ const value = record[key];
125
+ if (typeof value !== 'string' || value.trim().length === 0) {
126
+ throw new Error(`${key} is required`);
127
+ }
128
+ assertSafeText(value, key);
129
+ return value.trim();
130
+ }
131
+
132
+ function optionalStringArray(record: JsonRecord, key: string): string[] {
133
+ const value = record[key];
134
+ if (value === undefined) return [];
135
+ if (!Array.isArray(value)) throw new Error(`${key} must be a string array`);
136
+ return value.map((item, index) => {
137
+ if (typeof item !== 'string' || item.trim().length === 0) {
138
+ throw new Error(`${key}[${index}] must be a non-empty string`);
139
+ }
140
+ assertSafeText(item, `${key}[${index}]`);
141
+ return item.trim();
142
+ });
143
+ }
144
+
145
+ function requireStringArray(record: JsonRecord, key: string): string[] {
146
+ const values = optionalStringArray(record, key);
147
+ if (values.length === 0) throw new Error(`${key} must contain at least one item`);
148
+ return values;
149
+ }
150
+
151
+ function assertOpaqueId(value: string, field: string): void {
152
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value)) {
153
+ throw new Error(`${field} must be an opaque id using letters, numbers, hyphen, or underscore`);
154
+ }
155
+ }
156
+
157
+ function assertSafeText(value: string, field: string): void {
158
+ if (value.length > 512) throw new Error(`${field} must be 512 characters or fewer`);
159
+ if (/[\u0000-\u001f]/.test(value)) throw new Error(`${field} must not contain control characters`);
160
+ }
161
+
162
+ function assertIso(value: string, field: string): void {
163
+ if (!Number.isFinite(Date.parse(value))) throw new Error(`${field} must be an ISO-8601 timestamp`);
164
+ }
165
+
166
+ function parseFailureModes(value: unknown): AgentOwnerFailureMode[] {
167
+ if (!Array.isArray(value) || value.length === 0) {
168
+ throw new Error('failure_modes must contain at least one item');
169
+ }
170
+ return value.map((item, index) => {
171
+ const record = requireRecord(item, `failure_modes[${index}]`);
172
+ const id = requireString(record, 'id');
173
+ assertOpaqueId(id, `failure_modes[${index}].id`);
174
+ const description = requireString(record, 'description');
175
+ const evidenceRefs = requireStringArray(record, 'evidence_refs');
176
+ return { id, description, evidence_refs: evidenceRefs };
177
+ });
178
+ }
179
+
180
+ function parseSource(raw: unknown): AgentOwnerCardSource {
181
+ const record = requireRecord(raw, 'agent owner card source');
182
+ if (record.schema !== AGENT_OWNER_CARD_SOURCE_SCHEMA) {
183
+ throw new Error(`schema must be ${AGENT_OWNER_CARD_SOURCE_SCHEMA}`);
184
+ }
185
+ if ('current_occupant' in record || 'occupant' in record || 'model_id' in record || 'model_tier' in record) {
186
+ throw new Error('current occupant and model posture are derived from seat-readiness, not authored on the owner card source');
187
+ }
188
+ const seatId = requireString(record, 'seat_id');
189
+ assertOpaqueId(seatId, 'seat_id');
190
+ const ownerPrincipal = requireString(record, 'owner_principal');
191
+ assertOpaqueId(ownerPrincipal, 'owner_principal');
192
+ const maintainer = requireString(record, 'maintainer');
193
+ assertOpaqueId(maintainer, 'maintainer');
194
+ const review = requireRecord(record.review, 'review');
195
+ const lastReviewedAt = requireString(review, 'last_reviewed_at');
196
+ const staleAfter = requireString(review, 'stale_after');
197
+ assertIso(lastReviewedAt, 'review.last_reviewed_at');
198
+ assertIso(staleAfter, 'review.stale_after');
199
+ return {
200
+ schema: AGENT_OWNER_CARD_SOURCE_SCHEMA,
201
+ seat_id: seatId,
202
+ job: requireString(record, 'job'),
203
+ owner_principal: ownerPrincipal,
204
+ maintainer,
205
+ diet_refs: requireStringArray(record, 'diet_refs'),
206
+ authority_contract_ref: requireString(record, 'authority_contract_ref'),
207
+ allowed_work: requireStringArray(record, 'allowed_work'),
208
+ forbidden_work: requireStringArray(record, 'forbidden_work'),
209
+ review: {
210
+ cadence: requireString(review, 'cadence'),
211
+ last_reviewed_at: new Date(lastReviewedAt).toISOString(),
212
+ stale_after: new Date(staleAfter).toISOString(),
213
+ event_triggers: optionalStringArray(review, 'event_triggers'),
214
+ },
215
+ failure_modes: parseFailureModes(record.failure_modes),
216
+ };
217
+ }
218
+
219
+ async function readStructured(path: string): Promise<unknown> {
220
+ const raw = await readFile(path, 'utf-8');
221
+ if (extname(path).toLowerCase() === '.json') return JSON.parse(raw) as unknown;
222
+ return parseYaml(raw) as unknown;
223
+ }
224
+
225
+ function projectRelative(projectRoot: string, path: string): string {
226
+ const rel = relative(resolve(projectRoot), resolve(path));
227
+ if (rel.length === 0 || rel.startsWith('..') || rel.includes('\\')) {
228
+ throw new Error(`path must stay inside project root: ${path}`);
229
+ }
230
+ return rel;
231
+ }
232
+
233
+ function unique(values: string[]): string[] {
234
+ return [...new Set(values)];
235
+ }
236
+
237
+ function currentOccupant(
238
+ status: SeatReadinessStatus | null,
239
+ readinessRef: string,
240
+ ): AgentOwnerCard['current_occupant'] {
241
+ if (!status) return null;
242
+ return {
243
+ source: 'seat-readiness.v0.1',
244
+ readiness_ref: readinessRef,
245
+ seat: status.seat,
246
+ execution_surface: status.execution_surface,
247
+ model_tier: status.model_tier,
248
+ effective_model_tier: status.effective_model_tier,
249
+ model_id: status.model_id ?? null,
250
+ operational_status: status.operational_status,
251
+ capability_trusted: status.capability_trusted,
252
+ is_stale: status.is_stale,
253
+ last_heartbeat_at: status.last_heartbeat_at,
254
+ stale_after: status.stale_after,
255
+ };
256
+ }
257
+
258
+ function boundary(): AgentOwnerCard['authority_boundary'] {
259
+ return {
260
+ read_model_only: true,
261
+ can_authorize: false,
262
+ can_attach_grant: false,
263
+ can_change_owner: false,
264
+ can_edit_authority: false,
265
+ can_mutate_readiness: false,
266
+ can_merge: false,
267
+ can_close: false,
268
+ };
269
+ }
270
+
271
+ export async function buildAgentOwnerCard(options: {
272
+ projectRoot: string;
273
+ manifestPath: string;
274
+ statePath?: string;
275
+ at?: string;
276
+ }): Promise<AgentOwnerCard> {
277
+ const root = resolve(options.projectRoot);
278
+ const generatedAt = isoNow(options.at);
279
+ const manifestPath = resolve(options.manifestPath);
280
+ const source = parseSource(await readStructured(manifestPath));
281
+ const sourceRef = projectRelative(root, manifestPath);
282
+ const readinessRef = options.statePath
283
+ ? projectRelative(root, resolve(options.statePath))
284
+ : '.etiquette/state/seat-readiness.json';
285
+
286
+ let readiness: SeatReadinessStatus | null = null;
287
+ const warnings: string[] = [];
288
+ try {
289
+ const record = await readSeatReadiness(root, options.statePath);
290
+ readiness = evaluateSeatReadiness(record, generatedAt);
291
+ if (readiness.seat !== source.seat_id) warnings.push('seat_readiness_mismatch');
292
+ if (readiness.is_stale) warnings.push('seat_readiness_stale');
293
+ } catch (err) {
294
+ warnings.push(`seat_readiness_unavailable:${err instanceof Error ? err.message : String(err)}`);
295
+ }
296
+
297
+ const reviewIsStale = Date.parse(generatedAt) > Date.parse(source.review.stale_after);
298
+ if (reviewIsStale) warnings.push('owner_card_review_stale');
299
+
300
+ return {
301
+ schema: AGENT_OWNER_CARD_SCHEMA,
302
+ generated_at: generatedAt,
303
+ source_ref: sourceRef,
304
+ seat_id: source.seat_id,
305
+ job: source.job,
306
+ accountability: {
307
+ owner_principal: source.owner_principal,
308
+ maintainer: source.maintainer,
309
+ operator_principal_scope: 'per_run_receipts_only',
310
+ owner_is_not_operator: true,
311
+ },
312
+ diet_refs: source.diet_refs,
313
+ current_occupant: currentOccupant(readiness, readinessRef),
314
+ authority: {
315
+ source_ref: source.authority_contract_ref,
316
+ allowed_work: source.allowed_work,
317
+ forbidden_work: source.forbidden_work,
318
+ derived_from_contract: true,
319
+ card_may_edit_authority: false,
320
+ },
321
+ review: {
322
+ cadence: source.review.cadence,
323
+ last_reviewed_at: source.review.last_reviewed_at,
324
+ stale_after: source.review.stale_after,
325
+ is_stale: reviewIsStale,
326
+ event_driven_triggers: unique([...DEFAULT_EVENT_TRIGGERS, ...source.review.event_triggers]),
327
+ },
328
+ failure_modes: source.failure_modes,
329
+ warnings,
330
+ authority_boundary: boundary(),
331
+ };
332
+ }
333
+
334
+ async function collectFiles(dir: string): Promise<string[]> {
335
+ if (!existsSync(dir)) return [];
336
+ const entries = await readdir(dir, { withFileTypes: true });
337
+ const nested = await Promise.all(entries.map(async (entry) => {
338
+ const path = join(dir, entry.name);
339
+ if (entry.isDirectory()) return collectFiles(path);
340
+ return entry.isFile() ? [path] : [];
341
+ }));
342
+ return nested.flat().sort();
343
+ }
344
+
345
+ export async function buildAgentOwnerCardProjection(options: {
346
+ projectRoot: string;
347
+ statePath?: string;
348
+ at?: string;
349
+ }): Promise<AgentOwnerCardProjection> {
350
+ const root = resolve(options.projectRoot);
351
+ const generatedAt = isoNow(options.at);
352
+ const candidates = [
353
+ join(root, '.etiquette', 'seats', 'owner-card.yaml'),
354
+ join(root, '.etiquette', 'seats', 'owner-card.yml'),
355
+ join(root, '.etiquette', 'seats', 'owner-card.json'),
356
+ ...(await collectFiles(join(root, '.etiquette', 'seats', 'owner-cards'))),
357
+ ...(await collectFiles(join(root, 'docs', 'work', 'seats', 'owner-cards'))),
358
+ ].filter((path, index, all) => {
359
+ const ext = extname(path).toLowerCase();
360
+ return existsSync(path) && ['.yaml', '.yml', '.json'].includes(ext) && all.indexOf(path) === index;
361
+ });
362
+
363
+ const cards: AgentOwnerCard[] = [];
364
+ const validationErrors: AgentOwnerCardProjection['validation_errors'] = [];
365
+ for (const manifestPath of candidates) {
366
+ try {
367
+ cards.push(await buildAgentOwnerCard({
368
+ projectRoot: root,
369
+ manifestPath,
370
+ statePath: options.statePath,
371
+ at: generatedAt,
372
+ }));
373
+ } catch (err) {
374
+ validationErrors.push({
375
+ ref: projectRelative(root, manifestPath),
376
+ reason: err instanceof Error ? err.message : String(err),
377
+ });
378
+ }
379
+ }
380
+
381
+ return {
382
+ schema: AGENT_OWNER_CARD_PROJECTION_SCHEMA,
383
+ generated_at: generatedAt,
384
+ cards,
385
+ validation_errors: validationErrors,
386
+ authority_boundary: boundary(),
387
+ };
388
+ }