@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,499 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { assertSharedMemoryScopeId, assertSharedMemoryText, detectMemoryRiskFlags } from '../memory/sanitize';
5
+
6
+ type JsonRecord = Record<string, unknown>;
7
+
8
+ export const WORKSPACE_AUTHORITY_SCHEMAS = ['workspace-authority.v0'] as const;
9
+ export const WORKSPACE_AUTHORITY_MODES = ['personal_local', 'org_remote', 'runtime_cell'] as const;
10
+ export const WORKSPACE_CANONICAL_LOCATIONS = ['local_workspace', 'org_workspace', 'none'] as const;
11
+ export const WORKSPACE_MEMORY_SCOPES = ['org', 'workspace', 'lane', 'seat', 'session'] as const;
12
+ export const WORKSPACE_MEMORY_ACCESS = ['read_only', 'read_write'] as const;
13
+ export const WORKSPACE_MEMORY_MOUNT_MODES = ['filesystem_like', 'context_pack', 'mcp_resource'] as const;
14
+ export const WORKSPACE_MEMORY_WRITE_POLICIES = ['none', 'candidate_only', 'curator_promoted'] as const;
15
+ export const WORKSPACE_CONSOLIDATION_MODES = ['disabled', 'scheduled_rollup', 'async_rollup'] as const;
16
+ export const WORKSPACE_REPLACEMENT_POLICIES = [
17
+ 'creator_only',
18
+ 'explicit_grant',
19
+ 'org_admin_quorum',
20
+ 'board_policy',
21
+ ] as const;
22
+ export const WORKSPACE_INITIATION_LEVELS = ['workspace', 'plane'] as const;
23
+
24
+ export type WorkspaceAuthorityMode = (typeof WORKSPACE_AUTHORITY_MODES)[number];
25
+ export type WorkspaceCanonicalLocation = (typeof WORKSPACE_CANONICAL_LOCATIONS)[number];
26
+ export type WorkspaceMemoryScope = (typeof WORKSPACE_MEMORY_SCOPES)[number];
27
+ export type WorkspaceMemoryAccess = (typeof WORKSPACE_MEMORY_ACCESS)[number];
28
+ export type WorkspaceMemoryMountMode = (typeof WORKSPACE_MEMORY_MOUNT_MODES)[number];
29
+ export type WorkspaceMemoryWritePolicy = (typeof WORKSPACE_MEMORY_WRITE_POLICIES)[number];
30
+ export type WorkspaceConsolidationMode = (typeof WORKSPACE_CONSOLIDATION_MODES)[number];
31
+ export type WorkspaceReplacementPolicy = (typeof WORKSPACE_REPLACEMENT_POLICIES)[number];
32
+ export type WorkspaceInitiationLevel = (typeof WORKSPACE_INITIATION_LEVELS)[number];
33
+
34
+ export interface WorkspaceCanonicalRefs {
35
+ ledger_ref?: string;
36
+ memory_ref?: string;
37
+ policy_ref?: string;
38
+ roster_ref?: string;
39
+ signing_ref?: string;
40
+ }
41
+
42
+ export interface WorkspaceOwnerRole {
43
+ role_id: string;
44
+ occupant_principal: string;
45
+ grant_ref: string;
46
+ replacement_policy: WorkspaceReplacementPolicy;
47
+ }
48
+
49
+ export interface WorkspaceTenancyPolicy {
50
+ initiation_level: WorkspaceInitiationLevel;
51
+ initiator_principal: string;
52
+ verify_before_initiate: boolean;
53
+ initiator_becomes_owner_role: boolean;
54
+ workspace_owner_may_delegate: boolean;
55
+ plane_authority_granted: boolean;
56
+ host_can_forge_workflow_truth: boolean;
57
+ }
58
+
59
+ export interface WorkspaceMemoryStore {
60
+ store_id: string;
61
+ scope: WorkspaceMemoryScope;
62
+ access: WorkspaceMemoryAccess;
63
+ mount_mode: WorkspaceMemoryMountMode;
64
+ write_policy: WorkspaceMemoryWritePolicy;
65
+ evidence_required: boolean;
66
+ }
67
+
68
+ export interface WorkspaceConsolidationPolicy {
69
+ mode: WorkspaceConsolidationMode;
70
+ input_stores: string[];
71
+ output_store: string;
72
+ mutates_input_store: boolean;
73
+ output_policy: 'candidate_only' | 'curator_promoted';
74
+ }
75
+
76
+ export interface WorkspaceAuthorityBoundary {
77
+ local_workspace_is_canonical: boolean;
78
+ org_workspace_is_canonical: boolean;
79
+ runtime_cell_can_mutate_canonical_truth: boolean;
80
+ creator_is_root_authority: boolean;
81
+ portal_rbac_is_workflow_authority: boolean;
82
+ memory_can_authorize_work: boolean;
83
+ }
84
+
85
+ export interface WorkspaceAuthorityProfile {
86
+ schema: 'workspace-authority.v0';
87
+ workspace_id: string;
88
+ org_id?: string;
89
+ authority_mode: WorkspaceAuthorityMode;
90
+ canonical_location: WorkspaceCanonicalLocation;
91
+ creator_principal: string;
92
+ owner_role: WorkspaceOwnerRole;
93
+ tenancy?: WorkspaceTenancyPolicy;
94
+ canonical_refs: WorkspaceCanonicalRefs;
95
+ memory_stores: WorkspaceMemoryStore[];
96
+ consolidation?: WorkspaceConsolidationPolicy;
97
+ authority_boundary: WorkspaceAuthorityBoundary;
98
+ }
99
+
100
+ export interface WorkspaceAuthorityValidation {
101
+ schema: 'workspace-authority-validation.v0';
102
+ workspace_id: string;
103
+ org_id?: string;
104
+ verdict: 'valid';
105
+ authority_mode: WorkspaceAuthorityMode;
106
+ canonical_location: WorkspaceCanonicalLocation;
107
+ creator_is_root_authority: boolean;
108
+ local_workspace_is_canonical: boolean;
109
+ org_workspace_is_canonical: boolean;
110
+ runtime_cell_can_mutate_canonical_truth: boolean;
111
+ owner_role: {
112
+ role_id: string;
113
+ occupant_principal: string;
114
+ replacement_policy: WorkspaceReplacementPolicy;
115
+ };
116
+ tenancy_summary: {
117
+ initiation_level: WorkspaceInitiationLevel | 'none';
118
+ initiator_principal: string | null;
119
+ verify_before_initiate: boolean;
120
+ plane_authority_granted: boolean;
121
+ host_can_forge_workflow_truth: boolean;
122
+ workspace_owner_may_delegate: boolean;
123
+ };
124
+ memory_summary: {
125
+ stores: number;
126
+ filesystem_like_stores: number;
127
+ shared_read_write_stores: number;
128
+ consolidation_mode: WorkspaceConsolidationMode;
129
+ runner_memory_boundary: 'external_runner_recall_not_etiquette_authority';
130
+ consolidation_mutates_input_store: boolean;
131
+ };
132
+ authority_boundary: WorkspaceAuthorityBoundary;
133
+ }
134
+
135
+ function isRecord(value: unknown): value is JsonRecord {
136
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
137
+ }
138
+
139
+ function asRecord(value: unknown, name: string): JsonRecord {
140
+ if (!isRecord(value)) throw new Error(`${name} must be an object`);
141
+ return value;
142
+ }
143
+
144
+ function asString(record: JsonRecord, key: string): string {
145
+ const value = record[key];
146
+ if (typeof value !== 'string' || value.trim().length === 0) {
147
+ throw new Error(`${key} must be a non-empty string`);
148
+ }
149
+ return value.trim();
150
+ }
151
+
152
+ function optionalString(record: JsonRecord, key: string): string | undefined {
153
+ const value = record[key];
154
+ if (value === undefined) return undefined;
155
+ if (typeof value !== 'string' || value.trim().length === 0) {
156
+ throw new Error(`${key} must be a non-empty string when present`);
157
+ }
158
+ return value.trim();
159
+ }
160
+
161
+ function asBoolean(record: JsonRecord, key: string): boolean {
162
+ const value = record[key];
163
+ if (typeof value !== 'boolean') throw new Error(`${key} must be a boolean`);
164
+ return value;
165
+ }
166
+
167
+ function asEnum<T extends readonly string[]>(values: T, raw: unknown, key: string): T[number] {
168
+ if (typeof raw !== 'string' || !(values as readonly string[]).includes(raw)) {
169
+ throw new Error(`${key} must be one of: ${values.join(', ')}`);
170
+ }
171
+ return raw as T[number];
172
+ }
173
+
174
+ function asStringArray(record: JsonRecord, key: string): string[] {
175
+ const value = record[key];
176
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.trim().length === 0)) {
177
+ throw new Error(`${key} must be a string array`);
178
+ }
179
+ return value.map((item) => item.trim());
180
+ }
181
+
182
+ function assertOpaqueRef(value: string, field: string): void {
183
+ if (value.length > 160) throw new Error(`${field} must be <= 160 chars`);
184
+ if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
185
+ throw new Error(`${field} must be opaque or repo-relative, not a local absolute path`);
186
+ }
187
+ if (/[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
188
+ throw new Error(`${field} must be an opaque ref, not a URL`);
189
+ }
190
+ const flags = detectMemoryRiskFlags(value);
191
+ if (flags.length > 0) {
192
+ throw new Error(`${field} contains restricted shared content: ${flags.join(', ')}`);
193
+ }
194
+ }
195
+
196
+ function assertIdentity(value: string, field: string): void {
197
+ assertSharedMemoryScopeId(value, field);
198
+ }
199
+
200
+ function assertTextRef(value: string, field: string): void {
201
+ assertSharedMemoryText(value, field);
202
+ assertOpaqueRef(value, field);
203
+ }
204
+
205
+ async function readStructured(path: string): Promise<unknown> {
206
+ const raw = await readFile(path, 'utf-8');
207
+ if (extname(path).toLowerCase() === '.json') return JSON.parse(raw) as unknown;
208
+ return parseYaml(raw) as unknown;
209
+ }
210
+
211
+ function parseCanonicalRefs(raw: unknown): WorkspaceCanonicalRefs {
212
+ const record = asRecord(raw, 'canonical_refs');
213
+ const refs: WorkspaceCanonicalRefs = {
214
+ ledger_ref: optionalString(record, 'ledger_ref'),
215
+ memory_ref: optionalString(record, 'memory_ref'),
216
+ policy_ref: optionalString(record, 'policy_ref'),
217
+ roster_ref: optionalString(record, 'roster_ref'),
218
+ signing_ref: optionalString(record, 'signing_ref'),
219
+ };
220
+ for (const [key, value] of Object.entries(refs)) {
221
+ if (value) assertTextRef(value, `canonical_refs.${key}`);
222
+ }
223
+ return refs;
224
+ }
225
+
226
+ function parseOwnerRole(raw: unknown): WorkspaceOwnerRole {
227
+ const record = asRecord(raw, 'owner_role');
228
+ const owner: WorkspaceOwnerRole = {
229
+ role_id: asString(record, 'role_id'),
230
+ occupant_principal: asString(record, 'occupant_principal'),
231
+ grant_ref: asString(record, 'grant_ref'),
232
+ replacement_policy: asEnum(WORKSPACE_REPLACEMENT_POLICIES, record.replacement_policy, 'owner_role.replacement_policy'),
233
+ };
234
+ assertIdentity(owner.role_id, 'owner_role.role_id');
235
+ assertIdentity(owner.occupant_principal, 'owner_role.occupant_principal');
236
+ assertTextRef(owner.grant_ref, 'owner_role.grant_ref');
237
+ return owner;
238
+ }
239
+
240
+ function parseTenancy(raw: unknown): WorkspaceTenancyPolicy | undefined {
241
+ if (raw === undefined) return undefined;
242
+ const record = asRecord(raw, 'tenancy');
243
+ const tenancy: WorkspaceTenancyPolicy = {
244
+ initiation_level: asEnum(WORKSPACE_INITIATION_LEVELS, record.initiation_level, 'tenancy.initiation_level') as WorkspaceInitiationLevel,
245
+ initiator_principal: asString(record, 'initiator_principal'),
246
+ verify_before_initiate: asBoolean(record, 'verify_before_initiate'),
247
+ initiator_becomes_owner_role: asBoolean(record, 'initiator_becomes_owner_role'),
248
+ workspace_owner_may_delegate: asBoolean(record, 'workspace_owner_may_delegate'),
249
+ plane_authority_granted: asBoolean(record, 'plane_authority_granted'),
250
+ host_can_forge_workflow_truth: asBoolean(record, 'host_can_forge_workflow_truth'),
251
+ };
252
+ assertIdentity(tenancy.initiator_principal, 'tenancy.initiator_principal');
253
+ return tenancy;
254
+ }
255
+
256
+ function parseMemoryStores(raw: unknown): WorkspaceMemoryStore[] {
257
+ if (!Array.isArray(raw) || raw.length === 0) throw new Error('memory_stores must be a non-empty array');
258
+ return raw.map((item, index) => {
259
+ const record = asRecord(item, `memory_stores[${index}]`);
260
+ const store: WorkspaceMemoryStore = {
261
+ store_id: asString(record, 'store_id'),
262
+ scope: asEnum(WORKSPACE_MEMORY_SCOPES, record.scope, `memory_stores[${index}].scope`) as WorkspaceMemoryScope,
263
+ access: asEnum(WORKSPACE_MEMORY_ACCESS, record.access, `memory_stores[${index}].access`) as WorkspaceMemoryAccess,
264
+ mount_mode: asEnum(WORKSPACE_MEMORY_MOUNT_MODES, record.mount_mode, `memory_stores[${index}].mount_mode`) as WorkspaceMemoryMountMode,
265
+ write_policy: asEnum(WORKSPACE_MEMORY_WRITE_POLICIES, record.write_policy, `memory_stores[${index}].write_policy`) as WorkspaceMemoryWritePolicy,
266
+ evidence_required: asBoolean(record, 'evidence_required'),
267
+ };
268
+ assertIdentity(store.store_id, `memory_stores[${index}].store_id`);
269
+ if (store.access === 'read_only' && store.write_policy !== 'none') {
270
+ throw new Error(`memory_stores[${index}] read_only stores must use write_policy: none`);
271
+ }
272
+ if (store.access === 'read_write' && store.write_policy === 'none') {
273
+ throw new Error(`memory_stores[${index}] read_write stores require candidate_only or curator_promoted write_policy`);
274
+ }
275
+ if (store.scope === 'org' && store.access === 'read_write' && store.write_policy !== 'curator_promoted') {
276
+ throw new Error(`memory_stores[${index}] org shared read_write stores require write_policy: curator_promoted`);
277
+ }
278
+ if (store.scope === 'workspace' && store.access === 'read_write' && store.write_policy === 'candidate_only') {
279
+ throw new Error(`memory_stores[${index}] workspace shared read_write stores require curator_promoted writes`);
280
+ }
281
+ if (store.access === 'read_write' && store.evidence_required !== true) {
282
+ throw new Error(`memory_stores[${index}] read_write stores must require evidence`);
283
+ }
284
+ return store;
285
+ });
286
+ }
287
+
288
+ function parseConsolidation(raw: unknown, memoryStores: WorkspaceMemoryStore[]): WorkspaceConsolidationPolicy | undefined {
289
+ if (raw === undefined) return undefined;
290
+ const record = asRecord(raw, 'consolidation');
291
+ const policy: WorkspaceConsolidationPolicy = {
292
+ mode: asEnum(WORKSPACE_CONSOLIDATION_MODES, record.mode, 'consolidation.mode') as WorkspaceConsolidationMode,
293
+ input_stores: asStringArray(record, 'input_stores'),
294
+ output_store: asString(record, 'output_store'),
295
+ mutates_input_store: asBoolean(record, 'mutates_input_store'),
296
+ output_policy: asEnum(['candidate_only', 'curator_promoted'] as const, record.output_policy, 'consolidation.output_policy'),
297
+ };
298
+ const storeIds = new Set(memoryStores.map((store) => store.store_id));
299
+ for (const storeId of policy.input_stores) {
300
+ assertIdentity(storeId, 'consolidation.input_stores[]');
301
+ if (!storeIds.has(storeId)) throw new Error(`consolidation input store is not declared: ${storeId}`);
302
+ }
303
+ assertIdentity(policy.output_store, 'consolidation.output_store');
304
+ if (!storeIds.has(policy.output_store)) throw new Error(`consolidation output store is not declared: ${policy.output_store}`);
305
+ if (policy.mode !== 'disabled' && policy.input_stores.length === 0) {
306
+ throw new Error('consolidation input_stores must not be empty unless mode is disabled');
307
+ }
308
+ if (policy.mode !== 'disabled' && policy.mutates_input_store !== false) {
309
+ throw new Error('evidence consolidation must not mutate input memory stores');
310
+ }
311
+ const outputStore = memoryStores.find((store) => store.store_id === policy.output_store);
312
+ if (policy.mode !== 'disabled') {
313
+ if (!outputStore || outputStore.access !== 'read_write') {
314
+ throw new Error('consolidation output_store must be read_write');
315
+ }
316
+ if (outputStore.write_policy !== policy.output_policy) {
317
+ throw new Error('consolidation output_policy must match the output store write_policy');
318
+ }
319
+ }
320
+ return policy;
321
+ }
322
+
323
+ function parseBoundary(raw: unknown): WorkspaceAuthorityBoundary {
324
+ const record = asRecord(raw, 'authority_boundary');
325
+ return {
326
+ local_workspace_is_canonical: asBoolean(record, 'local_workspace_is_canonical'),
327
+ org_workspace_is_canonical: asBoolean(record, 'org_workspace_is_canonical'),
328
+ runtime_cell_can_mutate_canonical_truth: asBoolean(record, 'runtime_cell_can_mutate_canonical_truth'),
329
+ creator_is_root_authority: asBoolean(record, 'creator_is_root_authority'),
330
+ portal_rbac_is_workflow_authority: asBoolean(record, 'portal_rbac_is_workflow_authority'),
331
+ memory_can_authorize_work: asBoolean(record, 'memory_can_authorize_work'),
332
+ };
333
+ }
334
+
335
+ export function parseWorkspaceAuthorityProfile(value: unknown): WorkspaceAuthorityProfile {
336
+ const record = asRecord(value, 'workspace_authority');
337
+ const memoryStores = parseMemoryStores(record.memory_stores);
338
+ const profile: WorkspaceAuthorityProfile = {
339
+ schema: asEnum(WORKSPACE_AUTHORITY_SCHEMAS, record.schema, 'schema'),
340
+ workspace_id: asString(record, 'workspace_id'),
341
+ org_id: optionalString(record, 'org_id'),
342
+ authority_mode: asEnum(WORKSPACE_AUTHORITY_MODES, record.authority_mode, 'authority_mode') as WorkspaceAuthorityMode,
343
+ canonical_location: asEnum(WORKSPACE_CANONICAL_LOCATIONS, record.canonical_location, 'canonical_location') as WorkspaceCanonicalLocation,
344
+ creator_principal: asString(record, 'creator_principal'),
345
+ owner_role: parseOwnerRole(record.owner_role),
346
+ tenancy: parseTenancy(record.tenancy),
347
+ canonical_refs: parseCanonicalRefs(record.canonical_refs),
348
+ memory_stores: memoryStores,
349
+ consolidation: parseConsolidation(record.consolidation, memoryStores),
350
+ authority_boundary: parseBoundary(record.authority_boundary),
351
+ };
352
+ validateWorkspaceAuthorityProfile(profile);
353
+ return profile;
354
+ }
355
+
356
+ export async function loadWorkspaceAuthorityProfile(path: string): Promise<WorkspaceAuthorityProfile> {
357
+ return parseWorkspaceAuthorityProfile(await readStructured(path));
358
+ }
359
+
360
+ function assertRequiredRefs(profile: WorkspaceAuthorityProfile, required: Array<keyof WorkspaceCanonicalRefs>): void {
361
+ for (const key of required) {
362
+ if (!profile.canonical_refs[key]) throw new Error(`canonical_refs.${key} is required for ${profile.authority_mode}`);
363
+ }
364
+ }
365
+
366
+ export function validateWorkspaceAuthorityProfile(profile: WorkspaceAuthorityProfile): void {
367
+ assertIdentity(profile.workspace_id, 'workspace_id');
368
+ if (profile.org_id) assertIdentity(profile.org_id, 'org_id');
369
+ assertIdentity(profile.creator_principal, 'creator_principal');
370
+
371
+ if (profile.authority_boundary.portal_rbac_is_workflow_authority !== false) {
372
+ throw new Error('portal RBAC cannot be workflow authority');
373
+ }
374
+ if (profile.authority_boundary.memory_can_authorize_work !== false) {
375
+ throw new Error('memory cannot authorize workflow actions');
376
+ }
377
+ if (profile.tenancy) {
378
+ if (profile.tenancy.initiation_level !== 'workspace') {
379
+ throw new Error('workspace-authority cannot grant plane provisioning authority');
380
+ }
381
+ if (profile.tenancy.initiator_principal !== profile.creator_principal) {
382
+ throw new Error('tenancy initiator_principal must match creator_principal');
383
+ }
384
+ if (profile.tenancy.plane_authority_granted !== false) {
385
+ throw new Error('workspace provisioning must not grant plane authority');
386
+ }
387
+ if (profile.tenancy.host_can_forge_workflow_truth !== false) {
388
+ throw new Error('workspace host must not be able to forge workflow truth');
389
+ }
390
+ }
391
+
392
+ if (profile.authority_mode === 'personal_local') {
393
+ if (profile.canonical_location !== 'local_workspace') {
394
+ throw new Error('personal_local workspaces require canonical_location: local_workspace');
395
+ }
396
+ if (profile.authority_boundary.local_workspace_is_canonical !== true) {
397
+ throw new Error('personal_local workspaces must mark local_workspace_is_canonical true');
398
+ }
399
+ if (profile.authority_boundary.org_workspace_is_canonical !== false) {
400
+ throw new Error('personal_local workspaces must mark org_workspace_is_canonical false');
401
+ }
402
+ if (profile.authority_boundary.runtime_cell_can_mutate_canonical_truth !== false) {
403
+ throw new Error('runtime cells must not mutate canonical truth');
404
+ }
405
+ assertRequiredRefs(profile, ['ledger_ref', 'memory_ref', 'policy_ref', 'roster_ref']);
406
+ }
407
+
408
+ if (profile.authority_mode === 'org_remote') {
409
+ if (!profile.org_id) throw new Error('org_remote workspaces require org_id');
410
+ if (!profile.tenancy) throw new Error('org_remote workspaces require tenancy policy');
411
+ if (profile.tenancy.verify_before_initiate !== true) {
412
+ throw new Error('org_remote workspace initiation requires verified principal');
413
+ }
414
+ if (profile.tenancy.initiator_becomes_owner_role !== true) {
415
+ throw new Error('org_remote workspace initiator must create the owner role');
416
+ }
417
+ if (profile.tenancy.workspace_owner_may_delegate !== true) {
418
+ throw new Error('org_remote workspace owner role must be delegatable');
419
+ }
420
+ if (profile.canonical_location !== 'org_workspace') {
421
+ throw new Error('org_remote workspaces require canonical_location: org_workspace');
422
+ }
423
+ if (profile.authority_boundary.local_workspace_is_canonical !== false) {
424
+ throw new Error('org_remote local working copies must not be canonical truth');
425
+ }
426
+ if (profile.authority_boundary.org_workspace_is_canonical !== true) {
427
+ throw new Error('org_remote workspaces must mark org_workspace_is_canonical true');
428
+ }
429
+ if (profile.authority_boundary.creator_is_root_authority !== false) {
430
+ throw new Error('org_remote creator is provenance only, not root authority');
431
+ }
432
+ if (profile.owner_role.replacement_policy === 'creator_only') {
433
+ throw new Error('org_remote owner role must be replaceable by org policy');
434
+ }
435
+ if (profile.authority_boundary.runtime_cell_can_mutate_canonical_truth !== false) {
436
+ throw new Error('runtime cells must not mutate canonical truth');
437
+ }
438
+ assertRequiredRefs(profile, ['ledger_ref', 'memory_ref', 'policy_ref', 'roster_ref', 'signing_ref']);
439
+ }
440
+
441
+ if (profile.authority_mode === 'runtime_cell') {
442
+ if (profile.tenancy) throw new Error('runtime_cell profiles must not carry tenancy authority');
443
+ if (profile.canonical_location !== 'none') {
444
+ throw new Error('runtime_cell profiles require canonical_location: none');
445
+ }
446
+ if (profile.authority_boundary.local_workspace_is_canonical !== false
447
+ || profile.authority_boundary.org_workspace_is_canonical !== false
448
+ || profile.authority_boundary.runtime_cell_can_mutate_canonical_truth !== false
449
+ || profile.authority_boundary.creator_is_root_authority !== false) {
450
+ throw new Error('runtime_cell profiles must be authority-false');
451
+ }
452
+ }
453
+ }
454
+
455
+ export function validateWorkspaceAuthorityFile(profilePath: string): Promise<WorkspaceAuthorityValidation> {
456
+ return loadWorkspaceAuthorityProfile(profilePath).then(createWorkspaceAuthorityValidation);
457
+ }
458
+
459
+ export function createWorkspaceAuthorityValidation(profile: WorkspaceAuthorityProfile): WorkspaceAuthorityValidation {
460
+ validateWorkspaceAuthorityProfile(profile);
461
+ const consolidationMode = profile.consolidation?.mode ?? 'disabled';
462
+ const tenancy = profile.tenancy;
463
+ return {
464
+ schema: 'workspace-authority-validation.v0',
465
+ workspace_id: profile.workspace_id,
466
+ ...(profile.org_id ? { org_id: profile.org_id } : {}),
467
+ verdict: 'valid',
468
+ authority_mode: profile.authority_mode,
469
+ canonical_location: profile.canonical_location,
470
+ creator_is_root_authority: profile.authority_boundary.creator_is_root_authority,
471
+ local_workspace_is_canonical: profile.authority_boundary.local_workspace_is_canonical,
472
+ org_workspace_is_canonical: profile.authority_boundary.org_workspace_is_canonical,
473
+ runtime_cell_can_mutate_canonical_truth: profile.authority_boundary.runtime_cell_can_mutate_canonical_truth,
474
+ owner_role: {
475
+ role_id: profile.owner_role.role_id,
476
+ occupant_principal: profile.owner_role.occupant_principal,
477
+ replacement_policy: profile.owner_role.replacement_policy,
478
+ },
479
+ tenancy_summary: {
480
+ initiation_level: tenancy?.initiation_level ?? (profile.authority_mode === 'runtime_cell' ? 'none' : 'workspace'),
481
+ initiator_principal: tenancy?.initiator_principal ?? (profile.authority_mode === 'runtime_cell' ? null : profile.creator_principal),
482
+ verify_before_initiate: tenancy?.verify_before_initiate ?? (profile.authority_mode === 'org_remote'),
483
+ plane_authority_granted: tenancy?.plane_authority_granted ?? false,
484
+ host_can_forge_workflow_truth: tenancy?.host_can_forge_workflow_truth ?? false,
485
+ workspace_owner_may_delegate: tenancy?.workspace_owner_may_delegate ?? (profile.owner_role.replacement_policy !== 'creator_only'),
486
+ },
487
+ memory_summary: {
488
+ stores: profile.memory_stores.length,
489
+ filesystem_like_stores: profile.memory_stores.filter((store) => store.mount_mode === 'filesystem_like').length,
490
+ shared_read_write_stores: profile.memory_stores.filter((store) => (
491
+ store.access === 'read_write' && (store.scope === 'org' || store.scope === 'workspace')
492
+ )).length,
493
+ consolidation_mode: consolidationMode,
494
+ runner_memory_boundary: 'external_runner_recall_not_etiquette_authority',
495
+ consolidation_mutates_input_store: profile.consolidation?.mutates_input_store ?? false,
496
+ },
497
+ authority_boundary: profile.authority_boundary,
498
+ };
499
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ ACTOR_KINDS,
3
+ PACKET_STATES,
4
+ SCRUTINY_STATES,
5
+ SLICE_STATES,
6
+ TASK_STATES,
7
+ type ProjectManifest,
8
+ type Slice,
9
+ type SliceState,
10
+ type SliceTask,
11
+ type SliceTaskBlueprint,
12
+ type Surface,
13
+ type TaskState,
14
+ } from './types';
15
+
16
+ function assertMember<T extends string>(name: string, value: string, allowed: readonly T[]): T {
17
+ if (!allowed.includes(value as T)) {
18
+ throw new Error(`invalid ${name}: ${value}`);
19
+ }
20
+ return value as T;
21
+ }
22
+
23
+ export function assertSliceState(value: string): SliceState {
24
+ return assertMember('slice_state', value, SLICE_STATES);
25
+ }
26
+
27
+ export function assertTaskState(value: string): TaskState {
28
+ return assertMember('task_state', value, TASK_STATES);
29
+ }
30
+
31
+ export function assertPacketState(value: string) {
32
+ return assertMember('packet_state', value, PACKET_STATES);
33
+ }
34
+
35
+ export function assertScrutinyState(value: string) {
36
+ return assertMember('scrutiny_state', value, SCRUTINY_STATES);
37
+ }
38
+
39
+ export function assertSurface(value: string): Surface {
40
+ if (!value.trim()) {
41
+ throw new Error('invalid surface: empty');
42
+ }
43
+ return value;
44
+ }
45
+
46
+ export function assertActorKind(value: string) {
47
+ return assertMember('actor_kind', value, ACTOR_KINDS);
48
+ }
49
+
50
+ export function validateTask(task: SliceTask): void {
51
+ assertTaskState(task.state);
52
+ assertSurface(task.surface);
53
+ if (task.bounds.repoRoots.length === 0) {
54
+ throw new Error(`task ${task.taskId} must declare at least one repo root`);
55
+ }
56
+ if (task.bounds.ownedPaths.length === 0) {
57
+ throw new Error(`task ${task.taskId} must declare owned paths`);
58
+ }
59
+ }
60
+
61
+ export function validateSlice(slice: Slice): void {
62
+ assertSliceState(slice.state);
63
+ if (!slice.scrutinyId) {
64
+ throw new Error(`slice ${slice.sliceId} missing scrutinyId`);
65
+ }
66
+ const seen = new Set<string>();
67
+ for (const task of slice.tasks) {
68
+ validateTask(task);
69
+ if (seen.has(task.taskId)) {
70
+ throw new Error(`duplicate taskId: ${task.taskId}`);
71
+ }
72
+ seen.add(task.taskId);
73
+ }
74
+ }
75
+
76
+ function validateSurfaceDefaults(manifest: ProjectManifest): void {
77
+ const entries = Object.entries(manifest.surfaces);
78
+ if (entries.length === 0) {
79
+ throw new Error('project manifest must declare at least one surface');
80
+ }
81
+ for (const [surface, value] of entries) {
82
+ assertSurface(surface);
83
+ if (!value.owner || !value.workerRole || !value.worktreeTemplate) {
84
+ throw new Error(`surface ${surface} missing owner/workerRole/worktreeTemplate`);
85
+ }
86
+ if (!manifest.workerScripts[surface]) {
87
+ throw new Error(`surface ${surface} missing worker script`);
88
+ }
89
+ }
90
+ }
91
+
92
+ function validateSliceBlueprint(task: SliceTaskBlueprint, surface: Surface): void {
93
+ if (!task.title || !task.summary) {
94
+ throw new Error(`slice task ${surface} must define title and summary`);
95
+ }
96
+ }
97
+
98
+ export function validateProjectManifest(manifest: ProjectManifest): void {
99
+ if (manifest.schemaVersion !== 'discipline-project.v1') {
100
+ throw new Error(`unsupported project manifest schema: ${manifest.schemaVersion}`);
101
+ }
102
+ if (!manifest.projectRoot || !manifest.handshakeStorePath) {
103
+ throw new Error('project manifest missing projectRoot or handshakeStorePath');
104
+ }
105
+ validateSurfaceDefaults(manifest);
106
+ for (const [sliceId, slice] of Object.entries(manifest.slices)) {
107
+ if (!slice.scrutinyId || !slice.title || !slice.objective) {
108
+ throw new Error(`slice ${sliceId} missing title/objective/scrutinyId`);
109
+ }
110
+ for (const [surface, task] of Object.entries(slice.tasks)) {
111
+ if (!task) continue;
112
+ const resolvedSurface = assertSurface(surface);
113
+ if (!manifest.surfaces[resolvedSurface]) {
114
+ throw new Error(`slice ${sliceId} references unknown surface ${resolvedSurface}`);
115
+ }
116
+ validateSliceBlueprint(task, resolvedSurface);
117
+ }
118
+ }
119
+ }