@etiquekit/etq 1.0.11 → 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,834 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { writeJson } from '../fs';
4
+
5
+ export const SEAT_READINESS_SCHEMA = 'seat-readiness.v0.1';
6
+ export const SEAT_READINESS_EVENT_SCHEMA = 'seat-readiness-event.v0.1';
7
+ export const SEAT_PROFILE_SCHEMA = 'seat_profile.v0';
8
+ export const SEAT_PREFLIGHT_SCHEMA = 'seat_preflight.v0';
9
+ export const SEAT_PREFLIGHT_RESULT_SCHEMA = 'seat_preflight_result.v0';
10
+ export const CODEX_PERMISSIONS_ADAPTER_SCHEMA = 'codex_permissions_adapter.v0';
11
+
12
+ export const SEAT_TYPES = [
13
+ 'control_seat',
14
+ 'execution_seat',
15
+ 'review_seat',
16
+ 'seat_maintainer',
17
+ ] as const;
18
+ export const OPERATIONAL_STATUSES = ['operational', 'degraded', 'blocked', 'offline'] as const;
19
+ export const CAPABILITY_SOURCES = [
20
+ 'config',
21
+ 'local_validation',
22
+ 'seat_doctor',
23
+ 'agent_self_declared',
24
+ ] as const;
25
+ export const MODEL_TIERS = [
26
+ 'lite_fast',
27
+ 'balanced',
28
+ 'frontier_high_reasoning',
29
+ 'local_unknown',
30
+ ] as const;
31
+ export const SEAT_PROFILE_ROLES = [
32
+ 'implementation',
33
+ 'review',
34
+ 'orchestration',
35
+ 'satellite',
36
+ 'advisory',
37
+ ] as const;
38
+ export const SEAT_MONITOR_MODES = ['pull', 'push'] as const;
39
+ export const CODEX_PERMISSION_ACCESS = ['read', 'write', 'deny'] as const;
40
+ export const SEAT_PREFLIGHT_REQUIRED_CHECKS = [
41
+ 'identity',
42
+ 'workflow_binding',
43
+ 'routing_surface',
44
+ 'evidence_surface',
45
+ 'monitor_skill',
46
+ 'authority_boundary',
47
+ ] as const;
48
+
49
+ export type SeatType = typeof SEAT_TYPES[number];
50
+ export type OperationalStatus = typeof OPERATIONAL_STATUSES[number];
51
+ export type CapabilitySource = typeof CAPABILITY_SOURCES[number];
52
+ export type ModelTier = typeof MODEL_TIERS[number];
53
+ export type SeatProfileRole = typeof SEAT_PROFILE_ROLES[number];
54
+ export type SeatMonitorMode = typeof SEAT_MONITOR_MODES[number];
55
+ export type CodexPermissionAccess = typeof CODEX_PERMISSION_ACCESS[number];
56
+ export type SeatPreflightRequiredCheck = typeof SEAT_PREFLIGHT_REQUIRED_CHECKS[number];
57
+
58
+ type JsonRecord = Record<string, unknown>;
59
+
60
+ export interface CodexFilesystemPermissionRule {
61
+ path: string;
62
+ access: CodexPermissionAccess;
63
+ }
64
+
65
+ export interface CodexNetworkPermissionRule {
66
+ host: string;
67
+ access: 'allow' | 'deny';
68
+ }
69
+
70
+ export interface CodexPermissionsAdapter {
71
+ schema: typeof CODEX_PERMISSIONS_ADAPTER_SCHEMA;
72
+ profile_ref: string;
73
+ filesystem: CodexFilesystemPermissionRule[];
74
+ network: {
75
+ enabled: boolean;
76
+ domains: CodexNetworkPermissionRule[];
77
+ allow_local_binding: boolean;
78
+ };
79
+ authority_boundary: {
80
+ permissions_grant_authority: false;
81
+ mutates_codex_config: false;
82
+ installs_profile: false;
83
+ };
84
+ }
85
+
86
+ export interface SeatProfile {
87
+ schema: typeof SEAT_PROFILE_SCHEMA;
88
+ profile_id: string;
89
+ permission_profile_ref?: string;
90
+ seat_role: SeatProfileRole;
91
+ monitor_mode: SeatMonitorMode;
92
+ preloaded_skills: string[];
93
+ manuals: string[];
94
+ required_preflight: SeatPreflightRequiredCheck[];
95
+ forbidden_defaults: string[];
96
+ authority_defaults: {
97
+ grants_authority: false;
98
+ can_merge: false;
99
+ can_close: false;
100
+ can_push: false;
101
+ can_mutate_global_agent_config: false;
102
+ can_auto_start_monitor: false;
103
+ };
104
+ }
105
+
106
+ export interface SeatPreflight {
107
+ schema: typeof SEAT_PREFLIGHT_SCHEMA;
108
+ seat_id?: string;
109
+ operator_principal?: string;
110
+ profile_id?: string;
111
+ project_id?: string;
112
+ workflow_id?: string;
113
+ task_ref?: string;
114
+ routing_surface?: string;
115
+ evidence_surface?: string;
116
+ monitor_mode?: SeatMonitorMode;
117
+ allowed_writes?: string[];
118
+ network_required?: boolean;
119
+ network_domains?: string[];
120
+ codex_permissions?: CodexPermissionsAdapter;
121
+ checks?: Partial<Record<SeatPreflightRequiredCheck, boolean>>;
122
+ authority_boundary?: {
123
+ defaults_grant_authority?: boolean;
124
+ task_packet_required?: boolean;
125
+ can_merge?: boolean;
126
+ can_close?: boolean;
127
+ can_push?: boolean;
128
+ can_mutate_global_agent_config?: boolean;
129
+ can_auto_start_monitor?: boolean;
130
+ };
131
+ }
132
+
133
+ export interface SeatPreflightResult {
134
+ schema: typeof SEAT_PREFLIGHT_RESULT_SCHEMA;
135
+ seat_id: string | null;
136
+ profile_id: string;
137
+ ready: boolean;
138
+ monitor_mode: SeatMonitorMode;
139
+ missing: string[];
140
+ failed: string[];
141
+ warnings: string[];
142
+ permission_profile_ref: string | null;
143
+ authority_boundary: {
144
+ defaults_grant_authority: false;
145
+ profile_grants_authority: false;
146
+ permissions_grant_authority: false;
147
+ monitor_autostart: false;
148
+ mutates_global_agent_config: false;
149
+ mutates_codex_config: false;
150
+ installs_permission_profile: false;
151
+ can_merge: false;
152
+ can_close: false;
153
+ can_push: false;
154
+ };
155
+ }
156
+
157
+ export interface SeatReadinessRecord {
158
+ schema_version: typeof SEAT_READINESS_SCHEMA;
159
+ seat: string;
160
+ seat_type: SeatType;
161
+ execution_surface: string;
162
+ declared_by: 'operator_principal' | 'config';
163
+ capability_source: CapabilitySource;
164
+ capability_trusted: boolean;
165
+ operational_status: OperationalStatus;
166
+ model_tier: ModelTier;
167
+ model_id?: string;
168
+ active_workers: number;
169
+ max_recommended_workers: number;
170
+ supports_parallel: boolean;
171
+ supported_strategies: string[];
172
+ unsupported_strategies: string[];
173
+ constraints: string[];
174
+ last_checked_at: string;
175
+ last_heartbeat_at: string;
176
+ ttl_seconds: number;
177
+ stale_after: string;
178
+ }
179
+
180
+ export interface SeatReadinessStatus extends SeatReadinessRecord {
181
+ checked_at: string;
182
+ is_stale: boolean;
183
+ effective_model_tier: ModelTier;
184
+ routing_recommendation:
185
+ | 'route_if_policy_allows'
186
+ | 'seek_validation'
187
+ | 'reject_or_refresh'
188
+ | 'defer_with_reason';
189
+ }
190
+
191
+ export interface ReadinessInput {
192
+ projectRoot: string;
193
+ statePath?: string;
194
+ seat: string;
195
+ seatType: SeatType;
196
+ executionSurface: string;
197
+ declaredBy: 'operator_principal' | 'config';
198
+ capabilitySource: CapabilitySource;
199
+ operationalStatus: OperationalStatus;
200
+ modelTier: ModelTier;
201
+ modelId?: string;
202
+ activeWorkers: number;
203
+ maxRecommendedWorkers: number;
204
+ supportsParallel: boolean;
205
+ supportedStrategies: string[];
206
+ unsupportedStrategies: string[];
207
+ constraints: string[];
208
+ at?: string;
209
+ }
210
+
211
+ export interface HeartbeatInput {
212
+ projectRoot: string;
213
+ statePath?: string;
214
+ seat?: string;
215
+ operationalStatus?: OperationalStatus;
216
+ activeWorkers?: number;
217
+ constraints?: string[];
218
+ at?: string;
219
+ }
220
+
221
+ function isRecord(value: unknown): value is JsonRecord {
222
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
223
+ }
224
+
225
+ function requireRecord(value: unknown, name: string): JsonRecord {
226
+ if (!isRecord(value)) throw new Error(`${name} must be an object`);
227
+ return value;
228
+ }
229
+
230
+ function requireString(record: JsonRecord, key: string): string {
231
+ const value = record[key];
232
+ if (typeof value !== 'string' || value.trim().length === 0) {
233
+ throw new Error(`${key} is required`);
234
+ }
235
+ return value.trim();
236
+ }
237
+
238
+ function optionalString(record: JsonRecord, key: string): string | undefined {
239
+ const value = record[key];
240
+ if (value === undefined) return undefined;
241
+ if (typeof value !== 'string' || value.trim().length === 0) {
242
+ throw new Error(`${key} must be a non-empty string`);
243
+ }
244
+ return value.trim();
245
+ }
246
+
247
+ function optionalStringArray(record: JsonRecord, key: string): string[] {
248
+ const value = record[key];
249
+ if (value === undefined) return [];
250
+ if (!Array.isArray(value)) throw new Error(`${key} must be a string array`);
251
+ return value.map((item, index) => {
252
+ if (typeof item !== 'string' || item.trim().length === 0) {
253
+ throw new Error(`${key}[${index}] must be a non-empty string`);
254
+ }
255
+ return item.trim();
256
+ });
257
+ }
258
+
259
+ function assertSafeRef(value: string, name: string): void {
260
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.:@-]{0,127}$/.test(value)) {
261
+ throw new Error(`${name} must be an opaque ref using letters, numbers, hyphen, underscore, dot, colon, or at-sign`);
262
+ }
263
+ }
264
+
265
+ function assertRelativeTaskPath(value: string, name: string): void {
266
+ if (value.startsWith('/') || value.startsWith('~') || value.includes('\\')) {
267
+ throw new Error(`${name} must be a workspace-relative path`);
268
+ }
269
+ const parts = value.split('/').filter(Boolean);
270
+ if (parts.length === 0 || parts.includes('..')) throw new Error(`${name} must be a workspace-relative path`);
271
+ if (/[\u0000-\u001f]/.test(value)) throw new Error(`${name} must not contain control characters`);
272
+ }
273
+
274
+ function optionalBoolean(record: JsonRecord, key: string, fallback: boolean): boolean {
275
+ const value = record[key];
276
+ if (value === undefined) return fallback;
277
+ if (typeof value !== 'boolean') throw new Error(`${key} must be a boolean`);
278
+ return value;
279
+ }
280
+
281
+ function assertFalseValue(value: unknown, name: string): void {
282
+ if (value !== false) throw new Error(`${name} must be false`);
283
+ }
284
+
285
+ function optionalFalseValue(record: JsonRecord, key: string): false {
286
+ if (record[key] === undefined) return false;
287
+ assertFalseValue(record[key], key);
288
+ return false;
289
+ }
290
+
291
+ function requireBooleanTrue(value: unknown, name: string): boolean {
292
+ if (value !== true) throw new Error(`${name} must be true`);
293
+ return true;
294
+ }
295
+
296
+ function parseFilesystemRule(raw: unknown, index: number): CodexFilesystemPermissionRule {
297
+ const record = requireRecord(raw, `filesystem[${index}]`);
298
+ const path = requireString(record, 'path');
299
+ if (!['.', ':workspace_roots'].includes(path) && !path.startsWith(':') && !path.includes('*')) {
300
+ assertRelativeTaskPath(path, `filesystem[${index}].path`);
301
+ }
302
+ const access = requireString(record, 'access');
303
+ assertKnown(CODEX_PERMISSION_ACCESS, access, `filesystem[${index}].access`);
304
+ return { path, access };
305
+ }
306
+
307
+ function parseDomainRule(raw: unknown, index: number): CodexNetworkPermissionRule {
308
+ const record = requireRecord(raw, `network.domains[${index}]`);
309
+ const host = requireString(record, 'host');
310
+ if (host !== '*' && !/^(?:\*\.|\*\*\.)?[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/.test(host)) {
311
+ throw new Error(`network.domains[${index}].host must be a host, scoped wildcard, or *`);
312
+ }
313
+ const access = requireString(record, 'access');
314
+ assertKnown(['allow', 'deny'] as const, access, `network.domains[${index}].access`);
315
+ return { host, access };
316
+ }
317
+
318
+ export function normalizeCodexPermissionsAdapter(raw: unknown): CodexPermissionsAdapter {
319
+ const record = requireRecord(raw, 'codex permissions adapter');
320
+ if (record.schema !== CODEX_PERMISSIONS_ADAPTER_SCHEMA) {
321
+ throw new Error(`schema must be ${CODEX_PERMISSIONS_ADAPTER_SCHEMA}`);
322
+ }
323
+ const profileRef = requireString(record, 'profile_ref');
324
+ assertSafeRef(profileRef, 'profile_ref');
325
+ const filesystemRaw = record.filesystem;
326
+ if (!Array.isArray(filesystemRaw) || filesystemRaw.length === 0) {
327
+ throw new Error('filesystem must contain at least one permission rule');
328
+ }
329
+ const network = requireRecord(record.network, 'network');
330
+ const domainsRaw = network.domains;
331
+ const domains = domainsRaw === undefined ? [] : (() => {
332
+ if (!Array.isArray(domainsRaw)) throw new Error('network.domains must be an array');
333
+ return domainsRaw.map(parseDomainRule);
334
+ })();
335
+ const authority = requireRecord(record.authority_boundary, 'authority_boundary');
336
+ return {
337
+ schema: CODEX_PERMISSIONS_ADAPTER_SCHEMA,
338
+ profile_ref: profileRef,
339
+ filesystem: filesystemRaw.map(parseFilesystemRule),
340
+ network: {
341
+ enabled: optionalBoolean(network, 'enabled', false),
342
+ domains,
343
+ allow_local_binding: optionalBoolean(network, 'allow_local_binding', false),
344
+ },
345
+ authority_boundary: {
346
+ permissions_grant_authority: optionalFalseValue(authority, 'permissions_grant_authority'),
347
+ mutates_codex_config: optionalFalseValue(authority, 'mutates_codex_config'),
348
+ installs_profile: optionalFalseValue(authority, 'installs_profile'),
349
+ },
350
+ };
351
+ }
352
+
353
+ function permissionAccessRank(access: CodexPermissionAccess): number {
354
+ if (access === 'deny') return 3;
355
+ if (access === 'write') return 2;
356
+ return 1;
357
+ }
358
+
359
+ function globPatternToRegex(pattern: string): RegExp {
360
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
361
+ .replace(/\*\*/g, '\u0000')
362
+ .replace(/\*/g, '[^/]*')
363
+ .replace(/\u0000/g, '.*');
364
+ return new RegExp(`^${escaped}$`);
365
+ }
366
+
367
+ function filesystemRuleMatches(rulePath: string, taskPath: string): boolean {
368
+ if (rulePath === '.' || rulePath === ':workspace_roots') return true;
369
+ if (rulePath.endsWith('/**')) return taskPath === rulePath.slice(0, -3) || taskPath.startsWith(rulePath.slice(0, -2));
370
+ if (rulePath.includes('*')) return globPatternToRegex(rulePath).test(taskPath);
371
+ return taskPath === rulePath || taskPath.startsWith(`${rulePath}/`);
372
+ }
373
+
374
+ function filesystemRuleSpecificity(rulePath: string): number {
375
+ if (rulePath === '.' || rulePath === ':workspace_roots') return 0;
376
+ return rulePath.replace(/\*/g, '').length;
377
+ }
378
+
379
+ export function codexFilesystemAccessForPath(adapterRaw: unknown, taskPath: string): CodexPermissionAccess | 'none' {
380
+ assertRelativeTaskPath(taskPath, 'taskPath');
381
+ const adapter = normalizeCodexPermissionsAdapter(adapterRaw);
382
+ const matches = adapter.filesystem.filter((rule) => filesystemRuleMatches(rule.path, taskPath));
383
+ if (matches.length === 0) return 'none';
384
+ matches.sort((a, b) => {
385
+ const specificity = filesystemRuleSpecificity(b.path) - filesystemRuleSpecificity(a.path);
386
+ if (specificity !== 0) return specificity;
387
+ return permissionAccessRank(b.access) - permissionAccessRank(a.access);
388
+ });
389
+ return matches[0].access;
390
+ }
391
+
392
+ function domainRuleMatches(ruleHost: string, host: string): boolean {
393
+ if (ruleHost === '*') return true;
394
+ if (ruleHost.startsWith('**.')) {
395
+ const suffix = ruleHost.slice(3);
396
+ return host === suffix || host.endsWith(`.${suffix}`);
397
+ }
398
+ if (ruleHost.startsWith('*.')) {
399
+ const suffix = ruleHost.slice(2);
400
+ return host.endsWith(`.${suffix}`) && host !== suffix;
401
+ }
402
+ return host === ruleHost;
403
+ }
404
+
405
+ function hostAllowed(adapter: CodexPermissionsAdapter, host: string): boolean {
406
+ const matches = adapter.network.domains.filter((rule) => domainRuleMatches(rule.host, host));
407
+ if (matches.some((rule) => rule.access === 'deny')) return false;
408
+ return matches.some((rule) => rule.access === 'allow');
409
+ }
410
+
411
+ function networkIsDomainScoped(adapter: CodexPermissionsAdapter): boolean {
412
+ return adapter.network.domains.some((rule) => rule.access === 'allow' && rule.host !== '*');
413
+ }
414
+
415
+ export function createDefaultSeatProfile(role: SeatProfileRole = 'review'): SeatProfile {
416
+ assertKnown(SEAT_PROFILE_ROLES, role, 'seat_role');
417
+ const profileId = `${role}-seat`;
418
+ return normalizeSeatProfile({
419
+ schema: SEAT_PROFILE_SCHEMA,
420
+ profile_id: profileId,
421
+ seat_role: role,
422
+ preloaded_skills: [
423
+ 'etiquette-seat-pickup',
424
+ ],
425
+ manuals: [
426
+ 'AGENTS.md',
427
+ 'RUNBOOK.md',
428
+ ],
429
+ required_preflight: [...SEAT_PREFLIGHT_REQUIRED_CHECKS],
430
+ forbidden_defaults: [
431
+ 'grant_authority',
432
+ 'auto_start_monitor',
433
+ 'mutate_global_agent_config',
434
+ 'merge',
435
+ 'close',
436
+ 'push',
437
+ ],
438
+ authority_defaults: {
439
+ grants_authority: false,
440
+ can_merge: false,
441
+ can_close: false,
442
+ can_push: false,
443
+ can_mutate_global_agent_config: false,
444
+ can_auto_start_monitor: false,
445
+ },
446
+ });
447
+ }
448
+
449
+ export function normalizeSeatProfile(raw: unknown): SeatProfile {
450
+ const record = requireRecord(raw, 'seat profile');
451
+ if (record.schema !== SEAT_PROFILE_SCHEMA) throw new Error(`schema must be ${SEAT_PROFILE_SCHEMA}`);
452
+ const profileId = requireString(record, 'profile_id');
453
+ assertOpaqueId(profileId, 'profile_id');
454
+ const permissionProfileRef = optionalString(record, 'permission_profile_ref');
455
+ if (permissionProfileRef) assertSafeRef(permissionProfileRef, 'permission_profile_ref');
456
+ const seatRole = requireString(record, 'seat_role');
457
+ assertKnown(SEAT_PROFILE_ROLES, seatRole, 'seat_role');
458
+ const rawMonitorMode = optionalString(record, 'monitor_mode') ?? 'pull';
459
+ assertKnown(SEAT_MONITOR_MODES, rawMonitorMode, 'monitor_mode');
460
+
461
+ const authorityDefaults = requireRecord(record.authority_defaults, 'authority_defaults');
462
+ const normalizedAuthority = {
463
+ grants_authority: optionalFalseValue(authorityDefaults, 'grants_authority'),
464
+ can_merge: optionalFalseValue(authorityDefaults, 'can_merge'),
465
+ can_close: optionalFalseValue(authorityDefaults, 'can_close'),
466
+ can_push: optionalFalseValue(authorityDefaults, 'can_push'),
467
+ can_mutate_global_agent_config: optionalFalseValue(authorityDefaults, 'can_mutate_global_agent_config'),
468
+ can_auto_start_monitor: optionalFalseValue(authorityDefaults, 'can_auto_start_monitor'),
469
+ };
470
+
471
+ const preloadedSkills = optionalStringArray(record, 'preloaded_skills');
472
+ const manuals = optionalStringArray(record, 'manuals');
473
+ const requiredPreflight = optionalStringArray(record, 'required_preflight');
474
+ const forbiddenDefaults = optionalStringArray(record, 'forbidden_defaults');
475
+ if (requiredPreflight.length === 0) throw new Error('required_preflight must contain at least one check');
476
+ for (const [index, skill] of preloadedSkills.entries()) assertSafeRef(skill, `preloaded_skills[${index}]`);
477
+ for (const [index, manual] of manuals.entries()) assertSafeRef(manual, `manuals[${index}]`);
478
+ for (const [index, item] of forbiddenDefaults.entries()) assertSafeRef(item, `forbidden_defaults[${index}]`);
479
+ const checks = requiredPreflight.map((item, index) => {
480
+ assertKnown(SEAT_PREFLIGHT_REQUIRED_CHECKS, item, `required_preflight[${index}]`);
481
+ return item;
482
+ });
483
+ const missingRequired = SEAT_PREFLIGHT_REQUIRED_CHECKS.filter((item) => !checks.includes(item));
484
+ if (missingRequired.length > 0) {
485
+ throw new Error(`required_preflight missing required checks: ${missingRequired.join(', ')}`);
486
+ }
487
+ for (const required of ['grant_authority', 'auto_start_monitor', 'mutate_global_agent_config']) {
488
+ if (!forbiddenDefaults.includes(required)) {
489
+ throw new Error(`forbidden_defaults must include ${required}`);
490
+ }
491
+ }
492
+
493
+ return {
494
+ schema: SEAT_PROFILE_SCHEMA,
495
+ profile_id: profileId,
496
+ ...(permissionProfileRef ? { permission_profile_ref: permissionProfileRef } : {}),
497
+ seat_role: seatRole,
498
+ monitor_mode: rawMonitorMode,
499
+ preloaded_skills: preloadedSkills,
500
+ manuals,
501
+ required_preflight: checks,
502
+ forbidden_defaults: forbiddenDefaults,
503
+ authority_defaults: normalizedAuthority,
504
+ };
505
+ }
506
+
507
+ export function evaluateSeatPreflight(profileRaw: unknown, preflightRaw: unknown): SeatPreflightResult {
508
+ const profile = normalizeSeatProfile(profileRaw);
509
+ const record = requireRecord(preflightRaw, 'seat preflight');
510
+ if (record.schema !== SEAT_PREFLIGHT_SCHEMA) throw new Error(`schema must be ${SEAT_PREFLIGHT_SCHEMA}`);
511
+ const seatId = optionalString(record, 'seat_id');
512
+ if (seatId) assertOpaqueId(seatId, 'seat_id');
513
+ const profileId = optionalString(record, 'profile_id');
514
+ if (profileId && profileId !== profile.profile_id) {
515
+ throw new Error(`profile_id mismatch: expected ${profile.profile_id}, got ${profileId}`);
516
+ }
517
+ const monitorMode = optionalString(record, 'monitor_mode') ?? profile.monitor_mode;
518
+ assertKnown(SEAT_MONITOR_MODES, monitorMode, 'monitor_mode');
519
+ const allowedWrites = optionalStringArray(record, 'allowed_writes');
520
+ for (const [index, path] of allowedWrites.entries()) assertRelativeTaskPath(path, `allowed_writes[${index}]`);
521
+ const networkRequired = optionalBoolean(record, 'network_required', false);
522
+ const networkDomains = optionalStringArray(record, 'network_domains');
523
+ for (const [index, host] of networkDomains.entries()) {
524
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,252}$/.test(host)) {
525
+ throw new Error(`network_domains[${index}] must be a host`);
526
+ }
527
+ }
528
+
529
+ const missing: string[] = [];
530
+ for (const key of ['seat_id', 'operator_principal', 'project_id', 'workflow_id', 'task_ref', 'routing_surface', 'evidence_surface'] as const) {
531
+ const value = optionalString(record, key);
532
+ if (!value) missing.push(key);
533
+ else if (key === 'seat_id' || key === 'operator_principal' || key === 'project_id' || key === 'workflow_id') {
534
+ assertOpaqueId(value, key);
535
+ } else {
536
+ assertSafeRef(value, key);
537
+ }
538
+ }
539
+
540
+ const checks = isRecord(record.checks) ? record.checks : {};
541
+ const failed: string[] = [];
542
+ for (const check of profile.required_preflight) {
543
+ if (checks[check] !== true) failed.push(check);
544
+ }
545
+ const boundary = isRecord(record.authority_boundary) ? record.authority_boundary : {};
546
+ if (boundary.defaults_grant_authority !== false) failed.push('authority_boundary.defaults_grant_authority');
547
+ if (boundary.can_auto_start_monitor !== false) failed.push('authority_boundary.can_auto_start_monitor');
548
+ if (boundary.can_mutate_global_agent_config !== false) failed.push('authority_boundary.can_mutate_global_agent_config');
549
+ if (boundary.can_merge !== false) failed.push('authority_boundary.can_merge');
550
+ if (boundary.can_close !== false) failed.push('authority_boundary.can_close');
551
+ if (boundary.can_push !== false) failed.push('authority_boundary.can_push');
552
+ try {
553
+ requireBooleanTrue(boundary.task_packet_required, 'authority_boundary.task_packet_required');
554
+ } catch {
555
+ failed.push('authority_boundary.task_packet_required');
556
+ }
557
+
558
+ const warnings: string[] = [];
559
+ const codexPermissions = record.codex_permissions === undefined ? null : normalizeCodexPermissionsAdapter(record.codex_permissions);
560
+ if (profile.permission_profile_ref) {
561
+ if (!codexPermissions) {
562
+ failed.push('codex_permissions.missing');
563
+ } else if (codexPermissions.profile_ref !== profile.permission_profile_ref) {
564
+ failed.push('codex_permissions.profile_ref_mismatch');
565
+ }
566
+ } else if (codexPermissions) {
567
+ warnings.push('codex_permissions_present_without_profile_ref');
568
+ }
569
+ if (codexPermissions) {
570
+ for (const path of allowedWrites) {
571
+ const access = codexFilesystemAccessForPath(codexPermissions, path);
572
+ if (access !== 'write') failed.push(`codex_permissions.allowed_writes:${path}`);
573
+ }
574
+ if (networkRequired) {
575
+ if (!codexPermissions.network.enabled) failed.push('codex_permissions.network_disabled');
576
+ if (networkDomains.length === 0) failed.push('codex_permissions.network_domains_missing');
577
+ for (const host of networkDomains) {
578
+ if (!hostAllowed(codexPermissions, host)) failed.push(`codex_permissions.network_domain:${host}`);
579
+ }
580
+ } else if (codexPermissions.network.enabled && !networkIsDomainScoped(codexPermissions)) {
581
+ failed.push('codex_permissions.network_unscoped');
582
+ }
583
+ if (codexPermissions.authority_boundary.permissions_grant_authority !== false) {
584
+ failed.push('codex_permissions.permissions_grant_authority');
585
+ }
586
+ if (codexPermissions.authority_boundary.mutates_codex_config !== false) {
587
+ failed.push('codex_permissions.mutates_codex_config');
588
+ }
589
+ if (codexPermissions.authority_boundary.installs_profile !== false) {
590
+ failed.push('codex_permissions.installs_profile');
591
+ }
592
+ }
593
+ if (monitorMode === 'push') warnings.push('push_monitor_requires_explicit_wait_window');
594
+ return {
595
+ schema: SEAT_PREFLIGHT_RESULT_SCHEMA,
596
+ seat_id: seatId ?? null,
597
+ profile_id: profile.profile_id,
598
+ ready: missing.length === 0 && failed.length === 0,
599
+ monitor_mode: monitorMode,
600
+ missing,
601
+ failed,
602
+ warnings,
603
+ permission_profile_ref: profile.permission_profile_ref ?? null,
604
+ authority_boundary: {
605
+ defaults_grant_authority: false,
606
+ profile_grants_authority: false,
607
+ permissions_grant_authority: false,
608
+ monitor_autostart: false,
609
+ mutates_global_agent_config: false,
610
+ mutates_codex_config: false,
611
+ installs_permission_profile: false,
612
+ can_merge: false,
613
+ can_close: false,
614
+ can_push: false,
615
+ },
616
+ };
617
+ }
618
+
619
+ export function defaultSeatReadinessPath(projectRoot: string): string {
620
+ return join(resolve(projectRoot), '.etiquette', 'state', 'seat-readiness.json');
621
+ }
622
+
623
+ export function defaultSeatReadinessEventsPath(projectRoot: string): string {
624
+ return join(resolve(projectRoot), '.etiquette', 'state', 'seat-readiness-events.ndjson');
625
+ }
626
+
627
+ function resolveStatePath(projectRoot: string, statePath?: string): string {
628
+ return statePath ? resolve(statePath) : defaultSeatReadinessPath(projectRoot);
629
+ }
630
+
631
+ function eventsPathForState(projectRoot: string, statePath?: string): string {
632
+ if (!statePath) return defaultSeatReadinessEventsPath(projectRoot);
633
+ return join(dirname(resolve(statePath)), 'seat-readiness-events.ndjson');
634
+ }
635
+
636
+ function assertKnown<T extends readonly string[]>(values: T, value: string, name: string): asserts value is T[number] {
637
+ if (!(values as readonly string[]).includes(value)) {
638
+ throw new Error(`${name} must be one of: ${values.join(', ')}`);
639
+ }
640
+ }
641
+
642
+ function assertNonNegativeInteger(value: number, name: string): void {
643
+ if (!Number.isInteger(value) || value < 0) {
644
+ throw new Error(`${name} must be a non-negative integer`);
645
+ }
646
+ }
647
+
648
+ function assertPositiveInteger(value: number, name: string): void {
649
+ if (!Number.isInteger(value) || value < 1) {
650
+ throw new Error(`${name} must be a positive integer`);
651
+ }
652
+ }
653
+
654
+ function assertOpaqueId(value: string, name: string): void {
655
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value)) {
656
+ throw new Error(`${name} must be an opaque id using letters, numbers, hyphen, or underscore`);
657
+ }
658
+ }
659
+
660
+ function assertStrategy(value: string, name: string): void {
661
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value)) {
662
+ throw new Error(`${name} must contain only letters, numbers, hyphen, or underscore`);
663
+ }
664
+ }
665
+
666
+ function assertModelId(value: string, name: string): void {
667
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
668
+ throw new Error(`${name} must be an opaque model id without whitespace, paths, or hostnames`);
669
+ }
670
+ }
671
+
672
+ function assertIsoTimestamp(value: string, name: string): void {
673
+ if (!Number.isFinite(Date.parse(value))) {
674
+ throw new Error(`${name} must be an ISO-8601 timestamp`);
675
+ }
676
+ }
677
+
678
+ function nowIso(at?: string): string {
679
+ if (!at) return new Date().toISOString();
680
+ assertIsoTimestamp(at, '--at');
681
+ return new Date(at).toISOString();
682
+ }
683
+
684
+ export function ttlSecondsForModelTier(tier: ModelTier): number {
685
+ void tier;
686
+ // Heartbeat freshness tracks local seat posture, not theoretical model class stability.
687
+ // Model switches are common enough that every tier must refresh daily in V0.1.
688
+ return 24 * 60 * 60;
689
+ }
690
+
691
+ function staleAfter(at: string, ttlSeconds: number): string {
692
+ return new Date(Date.parse(at) + ttlSeconds * 1000).toISOString();
693
+ }
694
+
695
+ function capabilityTrusted(source: CapabilitySource): boolean {
696
+ return source !== 'agent_self_declared';
697
+ }
698
+
699
+ function validateRecord(record: SeatReadinessRecord): void {
700
+ assertKnown(SEAT_TYPES, record.seat_type, 'seat_type');
701
+ assertKnown(OPERATIONAL_STATUSES, record.operational_status, 'operational_status');
702
+ assertKnown(CAPABILITY_SOURCES, record.capability_source, 'capability_source');
703
+ assertKnown(MODEL_TIERS, record.model_tier, 'model_tier');
704
+ assertKnown(['operator_principal', 'config'] as const, record.declared_by, 'declared_by');
705
+ assertOpaqueId(record.seat, 'seat');
706
+ assertOpaqueId(record.execution_surface, 'execution_surface');
707
+ if (record.model_id) assertModelId(record.model_id, 'model_id');
708
+ assertNonNegativeInteger(record.active_workers, 'active_workers');
709
+ assertPositiveInteger(record.max_recommended_workers, 'max_recommended_workers');
710
+ for (const strategy of record.supported_strategies) assertStrategy(strategy, 'supported_strategies[]');
711
+ for (const strategy of record.unsupported_strategies) assertStrategy(strategy, 'unsupported_strategies[]');
712
+ for (const constraint of record.constraints) assertStrategy(constraint, 'constraints[]');
713
+ assertIsoTimestamp(record.last_checked_at, 'last_checked_at');
714
+ assertIsoTimestamp(record.last_heartbeat_at, 'last_heartbeat_at');
715
+ assertIsoTimestamp(record.stale_after, 'stale_after');
716
+ }
717
+
718
+ async function appendReadinessEvent(
719
+ projectRoot: string,
720
+ statePath: string | undefined,
721
+ event: Record<string, unknown>,
722
+ ): Promise<void> {
723
+ const path = eventsPathForState(projectRoot, statePath);
724
+ await mkdir(dirname(path), { recursive: true });
725
+ await appendFile(path, `${JSON.stringify({
726
+ schema_version: SEAT_READINESS_EVENT_SCHEMA,
727
+ ...event,
728
+ })}\n`);
729
+ }
730
+
731
+ async function writeRecord(projectRoot: string, statePath: string | undefined, record: SeatReadinessRecord): Promise<void> {
732
+ validateRecord(record);
733
+ await writeJson(resolveStatePath(projectRoot, statePath), record);
734
+ }
735
+
736
+ export async function readSeatReadiness(projectRoot: string, statePath?: string): Promise<SeatReadinessRecord> {
737
+ const path = resolveStatePath(projectRoot, statePath);
738
+ let parsed: SeatReadinessRecord;
739
+ try {
740
+ parsed = JSON.parse(await readFile(path, 'utf-8')) as SeatReadinessRecord;
741
+ } catch (err) {
742
+ throw new Error(`seat readiness state could not be read at ${path}: ${(err as NodeJS.ErrnoException).code ?? String(err)}`);
743
+ }
744
+ validateRecord(parsed);
745
+ return parsed;
746
+ }
747
+
748
+ export async function reportSeatReadiness(input: ReadinessInput): Promise<SeatReadinessRecord> {
749
+ const at = nowIso(input.at);
750
+ const ttl = ttlSecondsForModelTier(input.modelTier);
751
+ const record: SeatReadinessRecord = {
752
+ schema_version: SEAT_READINESS_SCHEMA,
753
+ seat: input.seat,
754
+ seat_type: input.seatType,
755
+ execution_surface: input.executionSurface,
756
+ declared_by: input.declaredBy,
757
+ capability_source: input.capabilitySource,
758
+ capability_trusted: capabilityTrusted(input.capabilitySource),
759
+ operational_status: input.operationalStatus,
760
+ model_tier: input.modelTier,
761
+ ...(input.modelId ? { model_id: input.modelId } : {}),
762
+ active_workers: input.activeWorkers,
763
+ max_recommended_workers: input.maxRecommendedWorkers,
764
+ supports_parallel: input.supportsParallel,
765
+ supported_strategies: input.supportedStrategies,
766
+ unsupported_strategies: input.unsupportedStrategies,
767
+ constraints: input.constraints,
768
+ last_checked_at: at,
769
+ last_heartbeat_at: at,
770
+ ttl_seconds: ttl,
771
+ stale_after: staleAfter(at, ttl),
772
+ };
773
+ await writeRecord(input.projectRoot, input.statePath, record);
774
+ await appendReadinessEvent(input.projectRoot, input.statePath, {
775
+ event_type: 'seat_readiness_reported',
776
+ at,
777
+ seat: record.seat,
778
+ operational_status: record.operational_status,
779
+ model_tier: record.model_tier,
780
+ capability_source: record.capability_source,
781
+ capability_trusted: record.capability_trusted,
782
+ active_workers: record.active_workers,
783
+ max_recommended_workers: record.max_recommended_workers,
784
+ });
785
+ return record;
786
+ }
787
+
788
+ export async function heartbeatSeat(input: HeartbeatInput): Promise<SeatReadinessRecord> {
789
+ const previous = await readSeatReadiness(input.projectRoot, input.statePath);
790
+ if (input.seat && input.seat !== previous.seat) {
791
+ throw new Error(`heartbeat seat mismatch: expected ${previous.seat}, got ${input.seat}`);
792
+ }
793
+ const at = nowIso(input.at);
794
+ const activeWorkers = input.activeWorkers ?? previous.active_workers;
795
+ assertNonNegativeInteger(activeWorkers, 'active_workers');
796
+ const record: SeatReadinessRecord = {
797
+ ...previous,
798
+ operational_status: input.operationalStatus ?? previous.operational_status,
799
+ active_workers: activeWorkers,
800
+ constraints: input.constraints ?? previous.constraints,
801
+ last_heartbeat_at: at,
802
+ last_checked_at: at,
803
+ stale_after: staleAfter(at, previous.ttl_seconds),
804
+ };
805
+ await writeRecord(input.projectRoot, input.statePath, record);
806
+ await appendReadinessEvent(input.projectRoot, input.statePath, {
807
+ event_type: 'seat_heartbeat',
808
+ at,
809
+ seat: record.seat,
810
+ operational_status: record.operational_status,
811
+ active_workers: record.active_workers,
812
+ max_recommended_workers: record.max_recommended_workers,
813
+ });
814
+ return record;
815
+ }
816
+
817
+ export function evaluateSeatReadiness(record: SeatReadinessRecord, at?: string): SeatReadinessStatus {
818
+ const checkedAt = nowIso(at);
819
+ const isStale = Date.parse(checkedAt) > Date.parse(record.stale_after);
820
+ let routing: SeatReadinessStatus['routing_recommendation'] = 'route_if_policy_allows';
821
+ if (isStale) routing = 'reject_or_refresh';
822
+ else if (record.operational_status === 'blocked' || record.operational_status === 'offline') {
823
+ routing = 'defer_with_reason';
824
+ } else if (!record.capability_trusted) {
825
+ routing = 'seek_validation';
826
+ }
827
+ return {
828
+ ...record,
829
+ checked_at: checkedAt,
830
+ is_stale: isStale,
831
+ effective_model_tier: isStale ? 'local_unknown' : record.model_tier,
832
+ routing_recommendation: routing,
833
+ };
834
+ }