@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,375 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { assertSharedMemoryScopeId, detectMemoryRiskFlags } from '../memory/sanitize';
5
+
6
+ export interface SyncJoinPlanOptions {
7
+ invitePath: string;
8
+ joinedBy: string;
9
+ claimRef: string;
10
+ at?: string;
11
+ }
12
+
13
+ export interface SyncJoinPlan {
14
+ schema: 'sync-join-plan.v0';
15
+ plan_id: string;
16
+ created_at: string;
17
+ joined_by: string;
18
+ identity_claim_ref: string;
19
+ invite: {
20
+ invite_id: string;
21
+ org_id: string;
22
+ workspace_id: string;
23
+ expires_at: string;
24
+ credential_ref: string;
25
+ credential: {
26
+ single_use: true;
27
+ scoped: true;
28
+ revocable: true;
29
+ };
30
+ };
31
+ installer: {
32
+ artifact_ref: string;
33
+ checksum_ref: string;
34
+ signature_ref: string;
35
+ curl_pipe_allowed: false;
36
+ reviewable_download_required: true;
37
+ };
38
+ git_sync: {
39
+ substrate: 'git';
40
+ remote_ref: string;
41
+ default_branch: string;
42
+ protected_branch: string;
43
+ invite_maps_to: 'git_clone';
44
+ downstream_sync_maps_to: 'git_fetch';
45
+ upstream_maps_to: 'protected_branch_pr';
46
+ drain_maps_to: 'single_writer_drain';
47
+ auto_fetch: true;
48
+ auto_merge: false;
49
+ merge_control: 'developer_controlled';
50
+ pull_request_required: true;
51
+ drain_required: true;
52
+ event_log_merge_driver_required: true;
53
+ webhook_is_nudge_only: true;
54
+ bespoke_sync_engine: false;
55
+ };
56
+ receipt_policy: {
57
+ receipt_reverify_required: true;
58
+ local_receipt_is_claim_only: true;
59
+ ci_rubrics_required: true;
60
+ protected_branch_required: true;
61
+ };
62
+ authorization_policy: {
63
+ joiner_verified: true;
64
+ workspace_admission_required: true;
65
+ action_authorization_required: true;
66
+ grant_required_for_mutation: true;
67
+ };
68
+ content_boundary: {
69
+ raw_invite_secret_included: false;
70
+ raw_provider_token_included: false;
71
+ raw_git_remote_url_included: false;
72
+ local_paths_included: false;
73
+ install_pipe_included: false;
74
+ };
75
+ authority_boundary: {
76
+ can_authorize_work: false;
77
+ can_execute_work: false;
78
+ can_grant: false;
79
+ can_close_work: false;
80
+ can_merge: false;
81
+ hosted_plane_is_source_of_truth: false;
82
+ sync_plan_is_source_of_truth: false;
83
+ };
84
+ next_steps: string[];
85
+ }
86
+
87
+ type UnknownObject = Record<string, unknown>;
88
+
89
+ const MAX_INVITE_TTL_MS = 24 * 60 * 60 * 1000;
90
+ const URL_RE = /\b[a-z][a-z0-9+.-]*:\/\//i;
91
+ const HOSTLIKE_RE = /\b[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+\.[a-z]{2,}\b/i;
92
+ const LOCAL_PATH_RE = /(?:^|[\s:])(?:\/Users\/|\/home\/|\/private\/|\/tmp\/|~\/|\$HOME|[A-Za-z]:[\\/])/;
93
+ const RAW_SECRET_RE = /(?:sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16}|(?:^|[\s:=])(?:token|secret|password|api[_-]?key)\s*[:=])/i;
94
+
95
+ function isoNow(at?: string): string {
96
+ const value = at ?? new Date().toISOString();
97
+ if (!Number.isFinite(Date.parse(value))) {
98
+ throw new Error('--at must be an ISO-8601 timestamp');
99
+ }
100
+ return new Date(value).toISOString();
101
+ }
102
+
103
+ function stableHash(value: unknown): string {
104
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
105
+ }
106
+
107
+ function parseObject(raw: string, path: string): UnknownObject {
108
+ const parsed = parseYaml(raw);
109
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
110
+ throw new Error(`${path} must parse as an object`);
111
+ }
112
+ return parsed as UnknownObject;
113
+ }
114
+
115
+ function objectField(parent: UnknownObject, key: string): UnknownObject {
116
+ const value = parent[key];
117
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
118
+ throw new Error(`${key} must be an object`);
119
+ }
120
+ return value as UnknownObject;
121
+ }
122
+
123
+ function stringField(parent: UnknownObject, key: string, field = key): string {
124
+ const value = parent[key];
125
+ if (typeof value !== 'string' || value.trim().length === 0) {
126
+ throw new Error(`${field} must be a non-empty string`);
127
+ }
128
+ return value.trim();
129
+ }
130
+
131
+ function requireBoolean(parent: UnknownObject, key: string, expected: boolean): void {
132
+ if (parent[key] !== expected) {
133
+ throw new Error(`${key} must be ${String(expected)}`);
134
+ }
135
+ }
136
+
137
+ function assertSafeRef(value: string, field: string, options: { allowHost?: boolean } = {}): string {
138
+ const trimmed = value.trim();
139
+ if (!trimmed || trimmed.length > 240) {
140
+ throw new Error(`${field} must be a non-empty ref <= 240 chars`);
141
+ }
142
+ if (URL_RE.test(trimmed) || (!options.allowHost && HOSTLIKE_RE.test(trimmed))) {
143
+ throw new Error(`${field} must be an opaque ref, not a URL or host`);
144
+ }
145
+ if (LOCAL_PATH_RE.test(trimmed) || trimmed.startsWith('/') || trimmed.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(trimmed)) {
146
+ throw new Error(`${field} must not include a local path`);
147
+ }
148
+ if (trimmed.includes('..') || trimmed.includes('\\')) {
149
+ throw new Error(`${field} must not include path traversal`);
150
+ }
151
+ if (RAW_SECRET_RE.test(trimmed)) {
152
+ throw new Error(`${field} must not include a raw secret or token`);
153
+ }
154
+ const flags = detectMemoryRiskFlags(trimmed).filter((flag) => flag !== 'contains_hostname');
155
+ if (flags.length > 0) {
156
+ throw new Error(`${field} contains restricted shared-sync content: ${flags.join(', ')}`);
157
+ }
158
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:@/+,-]{0,239}$/.test(trimmed)) {
159
+ throw new Error(`${field} must be an opaque ref`);
160
+ }
161
+ return trimmed;
162
+ }
163
+
164
+ function assertOpaqueId(value: string, field: string): string {
165
+ assertSharedMemoryScopeId(value, field);
166
+ return value;
167
+ }
168
+
169
+ function requireIso(value: string, field: string): string {
170
+ if (!Number.isFinite(Date.parse(value))) {
171
+ throw new Error(`${field} must be an ISO-8601 timestamp`);
172
+ }
173
+ return new Date(value).toISOString();
174
+ }
175
+
176
+ function assertInviteWindow(createdAt: string, expiresAt: string, now: string): void {
177
+ const created = Date.parse(createdAt);
178
+ const expires = Date.parse(expiresAt);
179
+ const current = Date.parse(now);
180
+ if (expires <= created) {
181
+ throw new Error('expires_at must be after created_at');
182
+ }
183
+ if (expires - created > MAX_INVITE_TTL_MS) {
184
+ throw new Error('invite expiry must be short-lived (<= 24h)');
185
+ }
186
+ if (current < created) {
187
+ throw new Error('invite is not valid yet');
188
+ }
189
+ if (current >= expires) {
190
+ throw new Error('invite is expired');
191
+ }
192
+ }
193
+
194
+ function requireOneOf<T extends string>(value: string, allowed: readonly T[], field: string): T {
195
+ if (!(allowed as readonly string[]).includes(value)) {
196
+ throw new Error(`${field} must be one of: ${allowed.join(', ')}`);
197
+ }
198
+ return value as T;
199
+ }
200
+
201
+ export async function buildSyncJoinPlan(options: SyncJoinPlanOptions): Promise<SyncJoinPlan> {
202
+ const createdAt = isoNow(options.at);
203
+ const raw = await readFile(options.invitePath, 'utf-8');
204
+ const invite = parseObject(raw, options.invitePath);
205
+ if (stringField(invite, 'schema') !== 'sync-invite.v0') {
206
+ throw new Error('invite schema must be sync-invite.v0');
207
+ }
208
+
209
+ const inviteId = assertSafeRef(stringField(invite, 'invite_id'), 'invite_id');
210
+ const orgId = assertOpaqueId(stringField(invite, 'org_id'), 'org_id');
211
+ const workspaceId = assertOpaqueId(stringField(invite, 'workspace_id'), 'workspace_id');
212
+ const inviteCreatedAt = requireIso(stringField(invite, 'created_at'), 'created_at');
213
+ const expiresAt = requireIso(stringField(invite, 'expires_at'), 'expires_at');
214
+ assertInviteWindow(inviteCreatedAt, expiresAt, createdAt);
215
+
216
+ const credential = objectField(invite, 'credential');
217
+ const credentialRef = assertSafeRef(stringField(credential, 'ref', 'credential.ref'), 'credential.ref');
218
+ requireBoolean(credential, 'single_use', true);
219
+ requireBoolean(credential, 'scoped', true);
220
+ requireBoolean(credential, 'revocable', true);
221
+
222
+ const joiner = objectField(invite, 'joiner');
223
+ requireOneOf(
224
+ stringField(joiner, 'required_assurance', 'joiner.required_assurance'),
225
+ ['verified_identity'] as const,
226
+ 'joiner.required_assurance',
227
+ );
228
+
229
+ const installer = objectField(invite, 'installer');
230
+ const artifactRef = assertSafeRef(stringField(installer, 'artifact_ref', 'installer.artifact_ref'), 'installer.artifact_ref');
231
+ const checksumRef = assertSafeRef(stringField(installer, 'checksum_ref', 'installer.checksum_ref'), 'installer.checksum_ref');
232
+ const signatureRef = assertSafeRef(stringField(installer, 'signature_ref', 'installer.signature_ref'), 'installer.signature_ref');
233
+ requireBoolean(installer, 'curl_pipe_allowed', false);
234
+ requireBoolean(installer, 'reviewable_download_required', true);
235
+
236
+ const git = objectField(invite, 'git');
237
+ const remoteRef = assertSafeRef(stringField(git, 'remote_ref', 'git.remote_ref'), 'git.remote_ref');
238
+ const defaultBranch = assertSafeRef(stringField(git, 'default_branch', 'git.default_branch'), 'git.default_branch', { allowHost: true });
239
+ const protectedBranch = assertSafeRef(stringField(git, 'protected_branch', 'git.protected_branch'), 'git.protected_branch', { allowHost: true });
240
+ requireBoolean(git, 'pull_request_required', true);
241
+ requireBoolean(git, 'drain_required', true);
242
+
243
+ const syncPolicy = objectField(invite, 'sync_policy');
244
+ requireBoolean(syncPolicy, 'auto_fetch', true);
245
+ requireBoolean(syncPolicy, 'auto_merge', false);
246
+ const mergeControl = requireOneOf(
247
+ stringField(syncPolicy, 'merge_control'),
248
+ ['developer_controlled', 'dev_controlled'] as const,
249
+ 'sync_policy.merge_control',
250
+ );
251
+ requireBoolean(syncPolicy, 'receipt_reverify_required', true);
252
+ requireBoolean(syncPolicy, 'ci_rubrics_required', true);
253
+ if (syncPolicy.hosted_plane_source_of_truth !== undefined) {
254
+ requireBoolean(syncPolicy, 'hosted_plane_source_of_truth', false);
255
+ }
256
+
257
+ const webhook = invite.webhook === undefined ? undefined : objectField(invite, 'webhook');
258
+ if (webhook) requireBoolean(webhook, 'nudge_only', true);
259
+
260
+ const joinedBy = assertOpaqueId(options.joinedBy, '--joined-by');
261
+ const claimRef = assertSafeRef(options.claimRef, '--claim-ref');
262
+ const planBasis = {
263
+ inviteId,
264
+ orgId,
265
+ workspaceId,
266
+ joinedBy,
267
+ claimRef,
268
+ createdAt,
269
+ };
270
+
271
+ return {
272
+ schema: 'sync-join-plan.v0',
273
+ plan_id: `sync-join:${stableHash(planBasis)}`,
274
+ created_at: createdAt,
275
+ joined_by: joinedBy,
276
+ identity_claim_ref: claimRef,
277
+ invite: {
278
+ invite_id: inviteId,
279
+ org_id: orgId,
280
+ workspace_id: workspaceId,
281
+ expires_at: expiresAt,
282
+ credential_ref: credentialRef,
283
+ credential: {
284
+ single_use: true,
285
+ scoped: true,
286
+ revocable: true,
287
+ },
288
+ },
289
+ installer: {
290
+ artifact_ref: artifactRef,
291
+ checksum_ref: checksumRef,
292
+ signature_ref: signatureRef,
293
+ curl_pipe_allowed: false,
294
+ reviewable_download_required: true,
295
+ },
296
+ git_sync: {
297
+ substrate: 'git',
298
+ remote_ref: remoteRef,
299
+ default_branch: defaultBranch,
300
+ protected_branch: protectedBranch,
301
+ invite_maps_to: 'git_clone',
302
+ downstream_sync_maps_to: 'git_fetch',
303
+ upstream_maps_to: 'protected_branch_pr',
304
+ drain_maps_to: 'single_writer_drain',
305
+ auto_fetch: true,
306
+ auto_merge: false,
307
+ merge_control: mergeControl === 'dev_controlled' ? 'developer_controlled' : mergeControl,
308
+ pull_request_required: true,
309
+ drain_required: true,
310
+ event_log_merge_driver_required: true,
311
+ webhook_is_nudge_only: true,
312
+ bespoke_sync_engine: false,
313
+ },
314
+ receipt_policy: {
315
+ receipt_reverify_required: true,
316
+ local_receipt_is_claim_only: true,
317
+ ci_rubrics_required: true,
318
+ protected_branch_required: true,
319
+ },
320
+ authorization_policy: {
321
+ joiner_verified: true,
322
+ workspace_admission_required: true,
323
+ action_authorization_required: true,
324
+ grant_required_for_mutation: true,
325
+ },
326
+ content_boundary: {
327
+ raw_invite_secret_included: false,
328
+ raw_provider_token_included: false,
329
+ raw_git_remote_url_included: false,
330
+ local_paths_included: false,
331
+ install_pipe_included: false,
332
+ },
333
+ authority_boundary: {
334
+ can_authorize_work: false,
335
+ can_execute_work: false,
336
+ can_grant: false,
337
+ can_close_work: false,
338
+ can_merge: false,
339
+ hosted_plane_is_source_of_truth: false,
340
+ sync_plan_is_source_of_truth: false,
341
+ },
342
+ next_steps: [
343
+ 'verify installer signature and checksum before installing etiquette',
344
+ 'resolve remote_ref through the organization credential broker, then git clone',
345
+ 'run etiquette sync install-git-merge-driver once inside the clone',
346
+ 'run workspace admission with the joined_by principal and identity_claim_ref',
347
+ 'use git fetch for downstream updates; merges remain developer controlled',
348
+ 'submit work through a protected branch and PR; drain serializes canonical ledger writes',
349
+ 're-run rubrics in CI before accepting any receipt projection',
350
+ ],
351
+ };
352
+ }
353
+
354
+ export function renderSyncJoinPlan(plan: SyncJoinPlan): string {
355
+ return [
356
+ 'sync_join_plan:',
357
+ ` plan_id: ${plan.plan_id}`,
358
+ ` joined_by: ${plan.joined_by}`,
359
+ ` workspace: ${plan.invite.org_id}/${plan.invite.workspace_id}`,
360
+ ` invite_expires_at: ${plan.invite.expires_at}`,
361
+ ` remote_ref: ${plan.git_sync.remote_ref}`,
362
+ ` installer_signature_required: ${String(Boolean(plan.installer.signature_ref))}`,
363
+ ` installer_checksum_required: ${String(Boolean(plan.installer.checksum_ref))}`,
364
+ ` curl_pipe_allowed: ${String(plan.installer.curl_pipe_allowed)}`,
365
+ ` substrate: ${plan.git_sync.substrate}`,
366
+ ` auto_fetch: ${String(plan.git_sync.auto_fetch)}`,
367
+ ` auto_merge: ${String(plan.git_sync.auto_merge)}`,
368
+ ` pull_request_required: ${String(plan.git_sync.pull_request_required)}`,
369
+ ` drain_required: ${String(plan.git_sync.drain_required)}`,
370
+ ` event_log_merge_driver_required: ${String(plan.git_sync.event_log_merge_driver_required)}`,
371
+ ` receipt_reverify_required: ${String(plan.receipt_policy.receipt_reverify_required)}`,
372
+ ` bespoke_sync_engine: ${String(plan.git_sync.bespoke_sync_engine)}`,
373
+ ` hosted_plane_is_source_of_truth: ${String(plan.authority_boundary.hosted_plane_is_source_of_truth)}`,
374
+ ].join('\n');
375
+ }