@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,842 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { access, readFile } from 'node:fs/promises';
3
+ import { join, posix } from 'node:path';
4
+
5
+ type JsonRecord = Record<string, unknown>;
6
+
7
+ export type ReleaseReadinessVerdict = 'ready' | 'blocked';
8
+ export type ReleaseReadinessStatus = 'pass' | 'warn' | 'block';
9
+
10
+ export interface ReleaseReadinessCheck {
11
+ id: string;
12
+ status: ReleaseReadinessStatus;
13
+ summary: string;
14
+ evidence_ref: string;
15
+ next_action?: string;
16
+ }
17
+
18
+ export interface ReleaseDistributionChannel {
19
+ id: string;
20
+ label: string;
21
+ kind: 'primary' | 'wrapper' | 'plugin' | 'runtime-image';
22
+ private_repo_compatible: boolean;
23
+ artifact: string;
24
+ ga_note: string;
25
+ }
26
+
27
+ export interface ReleaseReadinessReport {
28
+ schema: 'release-readiness.v0';
29
+ verdict: ReleaseReadinessVerdict;
30
+ package: {
31
+ name: string | null;
32
+ version: string | null;
33
+ private: boolean | null;
34
+ bin: Record<string, string>;
35
+ files: string[];
36
+ };
37
+ client_compatibility: {
38
+ codex_recommended_minimum: string;
39
+ codex_required_for: string[];
40
+ core_release_requires_codex: false;
41
+ };
42
+ checks: ReleaseReadinessCheck[];
43
+ blockers: string[];
44
+ warnings: string[];
45
+ authority_boundary: {
46
+ command_can_publish: false;
47
+ command_can_flip_private: false;
48
+ command_can_create_distribution_channel: false;
49
+ };
50
+ }
51
+
52
+ export interface ReleaseGaReadinessReport {
53
+ schema: 'release-ga-readiness.v0';
54
+ verdict: ReleaseReadinessVerdict;
55
+ channel: string | null;
56
+ channels: ReleaseDistributionChannel[];
57
+ tag: string | null;
58
+ package_readiness: ReleaseReadinessVerdict;
59
+ checks: ReleaseReadinessCheck[];
60
+ blockers: string[];
61
+ warnings: string[];
62
+ authority_boundary: {
63
+ command_can_publish: false;
64
+ command_can_sign: false;
65
+ command_can_tag: false;
66
+ command_can_create_distribution_channel: false;
67
+ };
68
+ }
69
+
70
+ export interface ReleaseGaReadinessOptions {
71
+ channel?: string | null;
72
+ tagFreeConfirmed?: boolean;
73
+ verifyTagRemote?: string | null;
74
+ }
75
+
76
+ const CODEX_RECOMMENDED_MINIMUM = '0.141.0';
77
+ const CODEX_COMPATIBILITY_DOC = 'docs/CODEX_CLIENT_COMPATIBILITY.md';
78
+ const RELEASE_LINEAGE_REF = 'release/lineage.v0.json';
79
+ const ETQ_PACKAGE_NAME = '@etiquekit/etq';
80
+ export const DEFAULT_RELEASE_BASE_URL = 'https://etiquette.example.com/releases';
81
+ const PRODUCTION_AGENT_PLANE_SOURCE_REF = 'packages/control/src/agent-plane/index.ts';
82
+ const PRODUCTION_AGENT_PLANE_CONTRACT_ROOT = 'internal/docs/contracts/production-agent-plane';
83
+ const PRODUCTION_AGENT_PLANE_EXAMPLES = [
84
+ 'examples/local-solo-code-change.profiles.yaml',
85
+ 'examples/eks-choreography-two-worker.profiles.yaml',
86
+ 'examples/human-gated-policy-change.profiles.yaml',
87
+ ] as const;
88
+ const CODEX_REQUIRED_FOR = [
89
+ 'remote-runtime app-server/exec-server canaries',
90
+ 'plugin-scoped MCP adapter canaries',
91
+ 'hook/action-auth resume canaries',
92
+ ];
93
+
94
+ const RELEASE_DISTRIBUTION_CHANNELS: ReleaseDistributionChannel[] = [
95
+ {
96
+ id: 'shell-installer',
97
+ label: 'signed shell installer',
98
+ kind: 'primary',
99
+ private_repo_compatible: true,
100
+ artifact: `install.sh from https://etiquette.example.com over signed artifacts at ${DEFAULT_RELEASE_BASE_URL}`,
101
+ ga_note: 'Use as the primary first-run path; install user-local and keep agent integrations explicit.',
102
+ },
103
+ {
104
+ id: 'npm',
105
+ label: 'npm-compatible package registry',
106
+ kind: 'primary',
107
+ private_repo_compatible: true,
108
+ artifact: 'signed npm tarball or private npm registry package',
109
+ ga_note: 'Use for Node-heavy teams, CI, and npx-style checks while the source repo remains private.',
110
+ },
111
+ {
112
+ id: 'homebrew-tap',
113
+ label: 'private Homebrew tap',
114
+ kind: 'wrapper',
115
+ private_repo_compatible: true,
116
+ artifact: 'formula wrapper over the same signed CLI artifact',
117
+ ga_note: 'Use for macOS convenience after the npm/global CLI artifact is proven.',
118
+ },
119
+ {
120
+ id: 'github-release',
121
+ label: 'GitHub release artifact',
122
+ kind: 'primary',
123
+ private_repo_compatible: true,
124
+ artifact: 'signed tarball plus SHA256SUMS',
125
+ ga_note: 'Use as a universal artifact channel; the repository may stay private.',
126
+ },
127
+ {
128
+ id: 'codex-plugin-private',
129
+ label: 'private Codex plugin channel',
130
+ kind: 'plugin',
131
+ private_repo_compatible: true,
132
+ artifact: 'Codex plugin/skills/MCP bundle',
133
+ ga_note: 'Use for Codex seat ergonomics; it must not add authority semantics.',
134
+ },
135
+ {
136
+ id: 'claude-code-plugin-private',
137
+ label: 'private Claude Code plugin channel',
138
+ kind: 'plugin',
139
+ private_repo_compatible: true,
140
+ artifact: 'Claude Code plugin/skills/hooks/MCP bundle',
141
+ ga_note: 'Use for Claude Code seat ergonomics; receipts remain workflow proof.',
142
+ },
143
+ {
144
+ id: 'chocolatey-internal',
145
+ label: 'internal Chocolatey feed',
146
+ kind: 'wrapper',
147
+ private_repo_compatible: true,
148
+ artifact: 'Windows wrapper over the same signed CLI artifact',
149
+ ga_note: 'Use after the primary CLI artifact is proven.',
150
+ },
151
+ {
152
+ id: 'winget-private',
153
+ label: 'private WinGet source',
154
+ kind: 'wrapper',
155
+ private_repo_compatible: true,
156
+ artifact: 'Windows manifest over the same signed CLI artifact',
157
+ ga_note: 'Use after the primary CLI artifact is proven.',
158
+ },
159
+ {
160
+ id: 'runtime-oci',
161
+ label: 'runtime cell OCI image',
162
+ kind: 'runtime-image',
163
+ private_repo_compatible: true,
164
+ artifact: 'container image for remote verifier/runtime cells',
165
+ ga_note: 'Use for managed runtime cells, separate from Core CLI GA.',
166
+ },
167
+ ];
168
+
169
+ async function exists(path: string): Promise<boolean> {
170
+ try {
171
+ await access(path);
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ function asRecord(value: unknown): JsonRecord | null {
179
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as JsonRecord : null;
180
+ }
181
+
182
+ function asString(value: unknown): string | null {
183
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
184
+ }
185
+
186
+ function asStringArray(value: unknown): string[] {
187
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
188
+ }
189
+
190
+ function asStringRecord(value: unknown): Record<string, string> {
191
+ const record = asRecord(value);
192
+ if (!record) return {};
193
+ return Object.fromEntries(Object.entries(record).filter((entry): entry is [string, string] => (
194
+ typeof entry[1] === 'string' && entry[1].trim().length > 0
195
+ )));
196
+ }
197
+
198
+ function repositoryUrl(value: unknown): string | null {
199
+ if (typeof value === 'string') return asString(value);
200
+ return asString(asRecord(value)?.url);
201
+ }
202
+
203
+ function semverTuple(value: string | null): [number, number, number] | null {
204
+ const match = value?.match(/^(\d+)\.(\d+)\.(\d+)$/);
205
+ if (!match) return null;
206
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
207
+ }
208
+
209
+ function compareSemver(left: string | null, right: string | null): number | null {
210
+ const a = semverTuple(left);
211
+ const b = semverTuple(right);
212
+ if (!a || !b) return null;
213
+ for (let index = 0; index < a.length; index += 1) {
214
+ if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1;
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ function check(status: ReleaseReadinessStatus, id: string, summary: string, evidenceRef: string, nextAction?: string): ReleaseReadinessCheck {
220
+ return {
221
+ id,
222
+ status,
223
+ summary,
224
+ evidence_ref: evidenceRef,
225
+ ...(nextAction ? { next_action: nextAction } : {}),
226
+ };
227
+ }
228
+
229
+ function scriptExists(scripts: Record<string, string>, name: string): boolean {
230
+ return typeof scripts[name] === 'string' && scripts[name].trim().length > 0;
231
+ }
232
+
233
+ async function scriptDeclarationOk(
234
+ scripts: Record<string, string>,
235
+ name: string,
236
+ projectRoot: string,
237
+ ): Promise<{ declared: boolean; present: boolean; ref: string }> {
238
+ const value = scripts[name]?.trim();
239
+ if (!value) return { declared: false, present: false, ref: 'package.json' };
240
+ const scriptRef = value.match(/\b(scripts\/[A-Za-z0-9._/-]+\.sh)\b/)?.[1] ?? null;
241
+ if (!scriptRef) return { declared: true, present: true, ref: 'package.json' };
242
+ return {
243
+ declared: true,
244
+ present: await exists(join(projectRoot, scriptRef)),
245
+ ref: scriptRef,
246
+ };
247
+ }
248
+
249
+ function safeBinPath(raw: string): string | null {
250
+ const normalized = posix.normalize(raw.replaceAll('\\', '/'));
251
+ if (
252
+ normalized.startsWith('../')
253
+ || normalized === '..'
254
+ || normalized.startsWith('/')
255
+ || /^[A-Za-z]:\//.test(normalized)
256
+ ) {
257
+ return null;
258
+ }
259
+ return normalized;
260
+ }
261
+
262
+ function packageTarballName(packageName: string | null, version: string | null): string | null {
263
+ if (!packageName || !version) return null;
264
+ return `${packageName.replace(/^@/, '').replaceAll('/', '-')}-${version}.tgz`;
265
+ }
266
+
267
+ function validGaVersion(version: string | null): boolean {
268
+ return typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version);
269
+ }
270
+
271
+ function safeChannelName(value: string | null): string | null {
272
+ if (!value) return null;
273
+ return /^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$/.test(value) ? value : null;
274
+ }
275
+
276
+ function splitChannelNames(value: string | null): string[] {
277
+ if (!value) return [];
278
+ return value.split(',').map((item) => item.trim()).filter(Boolean);
279
+ }
280
+
281
+ function verifyTagIsFree(projectRoot: string, remote: string, tag: string | null): ReleaseReadinessCheck {
282
+ if (!tag) {
283
+ return check(
284
+ 'block',
285
+ 'tag_uniqueness',
286
+ 'cannot derive release tag for remote uniqueness check',
287
+ 'package.json',
288
+ 'set a GA package version before checking remote tags',
289
+ );
290
+ }
291
+
292
+ const trimmedRemote = remote.trim();
293
+ if (!trimmedRemote || trimmedRemote.startsWith('-')) {
294
+ return check(
295
+ 'block',
296
+ 'tag_uniqueness',
297
+ 'remote tag uniqueness check requires a safe remote name or URL',
298
+ 'operator:--verify-tag-remote',
299
+ 'rerun with --verify-tag-remote origin or a trusted remote URL',
300
+ );
301
+ }
302
+
303
+ const tagRef = `refs/tags/${tag}`;
304
+ const result = spawnSync('git', ['ls-remote', '--tags', trimmedRemote, tagRef, `${tagRef}^{}`], {
305
+ cwd: projectRoot,
306
+ encoding: 'utf-8',
307
+ env: {
308
+ ...process.env,
309
+ GIT_TERMINAL_PROMPT: '0',
310
+ },
311
+ timeout: 15_000,
312
+ });
313
+ const evidenceRef = `git ls-remote --tags ${trimmedRemote} ${tagRef}`;
314
+
315
+ if (result.error) {
316
+ return check(
317
+ 'block',
318
+ 'tag_uniqueness',
319
+ `remote tag uniqueness check failed: ${result.error.message}`,
320
+ evidenceRef,
321
+ 'verify credentials/network access, then rerun the remote tag check',
322
+ );
323
+ }
324
+ if (result.status !== 0) {
325
+ const message = (result.stderr || result.stdout || 'git ls-remote returned non-zero').trim().split('\n')[0];
326
+ return check(
327
+ 'block',
328
+ 'tag_uniqueness',
329
+ `remote tag uniqueness check failed: ${message}`,
330
+ evidenceRef,
331
+ 'verify the release remote is reachable before GA',
332
+ );
333
+ }
334
+
335
+ if (result.stdout.trim().length > 0) {
336
+ return check(
337
+ 'block',
338
+ 'tag_uniqueness',
339
+ `release tag already exists on target remote: ${tag}`,
340
+ evidenceRef,
341
+ 'choose a new version or reconcile the existing tag before GA',
342
+ );
343
+ }
344
+
345
+ return check(
346
+ 'pass',
347
+ 'tag_uniqueness',
348
+ `remote tag uniqueness verified for ${tag} on ${trimmedRemote}`,
349
+ evidenceRef,
350
+ );
351
+ }
352
+
353
+ function lookupChannels(value: string | null): { selected: ReleaseDistributionChannel[]; unknown: string[] } {
354
+ const names = splitChannelNames(value);
355
+ const catalog = new Map(RELEASE_DISTRIBUTION_CHANNELS.map((item) => [item.id, item]));
356
+ const selected: ReleaseDistributionChannel[] = [];
357
+ const unknown: string[] = [];
358
+ const seen = new Set<string>();
359
+ for (const rawName of names) {
360
+ const name = safeChannelName(rawName);
361
+ if (!name) {
362
+ unknown.push(rawName);
363
+ continue;
364
+ }
365
+ if (seen.has(name)) continue;
366
+ seen.add(name);
367
+ const item = catalog.get(name);
368
+ if (item) {
369
+ selected.push(item);
370
+ } else {
371
+ unknown.push(name);
372
+ }
373
+ }
374
+ return { selected, unknown };
375
+ }
376
+
377
+ export function listReleaseDistributionChannels(): ReleaseDistributionChannel[] {
378
+ return RELEASE_DISTRIBUTION_CHANNELS.map((item) => ({ ...item }));
379
+ }
380
+
381
+ async function readTextIfExists(path: string): Promise<string | null> {
382
+ try {
383
+ return await readFile(path, 'utf-8');
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ function sanitizeError(error: unknown, projectRoot: string): string {
390
+ const raw = error instanceof Error ? error.message : String(error);
391
+ return raw.replaceAll(projectRoot, '<project>').split('\n')[0];
392
+ }
393
+
394
+ async function appendProductionAgentPlaneChecks(
395
+ projectRoot: string,
396
+ checks: ReleaseReadinessCheck[],
397
+ ): Promise<void> {
398
+ const sourcePresent = await exists(join(projectRoot, PRODUCTION_AGENT_PLANE_SOURCE_REF));
399
+ const contractRootPresent = await exists(join(projectRoot, PRODUCTION_AGENT_PLANE_CONTRACT_ROOT, 'README.md'));
400
+ if (!sourcePresent && !contractRootPresent) return;
401
+
402
+ // agent-plane validation is source-workspace-only; in the devkit the presence of
403
+ // agent-plane sources without the validator is itself a release blocker.
404
+ checks.push(check(
405
+ 'block',
406
+ 'production_agent_plane_profiles',
407
+ 'agent-plane sources detected; validate from a source workspace, not the devkit package',
408
+ PRODUCTION_AGENT_PLANE_CONTRACT_ROOT,
409
+ 'run release readiness from a source workspace that carries the agent-plane validator',
410
+ ));
411
+ return;
412
+ }
413
+
414
+ async function appendReleaseLineageChecks(
415
+ projectRoot: string,
416
+ packageJson: JsonRecord,
417
+ packageInfo: {
418
+ name: string | null;
419
+ version: string | null;
420
+ bin: Record<string, string>;
421
+ files: string[];
422
+ scripts: Record<string, string>;
423
+ },
424
+ checks: ReleaseReadinessCheck[],
425
+ ): Promise<void> {
426
+ const lineagePath = join(projectRoot, RELEASE_LINEAGE_REF);
427
+ const lineagePresent = await exists(lineagePath);
428
+ if (!lineagePresent && packageInfo.name !== ETQ_PACKAGE_NAME) return;
429
+
430
+ let lineage: JsonRecord | null = null;
431
+ try {
432
+ lineage = asRecord(JSON.parse(await readFile(lineagePath, 'utf-8')));
433
+ } catch {
434
+ lineage = null;
435
+ }
436
+
437
+ if (!lineage || asString(lineage.schema) !== 'etq-release-lineage.v0') {
438
+ checks.push(check(
439
+ 'block',
440
+ 'release_lineage_manifest',
441
+ 'canonical Etq release lineage manifest is missing or malformed',
442
+ RELEASE_LINEAGE_REF,
443
+ 'restore a valid etq-release-lineage.v0 manifest before release',
444
+ ));
445
+ return;
446
+ }
447
+
448
+ checks.push(check('pass', 'release_lineage_manifest', 'release lineage manifest is present and parseable', RELEASE_LINEAGE_REF));
449
+
450
+ const expectedName = asString(lineage.package_name);
451
+ const candidateVersion = asString(lineage.candidate_version);
452
+ const registryFloor = asString(lineage.registry_floor);
453
+ const distributionMode = asString(lineage.distribution_mode);
454
+ const sourceRepository = asString(lineage.source_repository);
455
+ const distributionRepository = asString(lineage.distribution_repository);
456
+ const sourceBaseCommit = asString(lineage.source_base_commit);
457
+ const requiredBins = asStringArray(lineage.required_bins);
458
+ const packageRepository = repositoryUrl(packageJson.repository);
459
+
460
+ const identityMatches = expectedName === ETQ_PACKAGE_NAME
461
+ && packageInfo.name === expectedName
462
+ && packageInfo.version === candidateVersion;
463
+ checks.push(identityMatches
464
+ ? check('pass', 'release_package_identity', `package identity matches lineage: ${expectedName}@${candidateVersion}`, RELEASE_LINEAGE_REF)
465
+ : check(
466
+ 'block',
467
+ 'release_package_identity',
468
+ `package identity does not match lineage: package=${packageInfo.name ?? 'unknown'}@${packageInfo.version ?? 'unknown'} lineage=${expectedName ?? 'unknown'}@${candidateVersion ?? 'unknown'}`,
469
+ RELEASE_LINEAGE_REF,
470
+ 'align package.json and the reviewed release lineage manifest',
471
+ ));
472
+
473
+ const versionComparison = compareSemver(candidateVersion, registryFloor);
474
+ checks.push(versionComparison !== null && versionComparison > 0
475
+ ? check('pass', 'release_version_floor', `candidate ${candidateVersion} is newer than registry floor ${registryFloor}`, RELEASE_LINEAGE_REF)
476
+ : check(
477
+ 'block',
478
+ 'release_version_floor',
479
+ `candidate version must be a GA semver newer than registry floor: candidate=${candidateVersion ?? 'unknown'} floor=${registryFloor ?? 'unknown'}`,
480
+ RELEASE_LINEAGE_REF,
481
+ 'choose a candidate version strictly above the observed registry version',
482
+ ));
483
+
484
+ const missingBins = requiredBins.filter((name) => !packageInfo.bin[name]);
485
+ checks.push(requiredBins.length > 0 && missingBins.length === 0
486
+ ? check('pass', 'release_required_bins', `required package bins declared: ${requiredBins.join(', ')}`, RELEASE_LINEAGE_REF)
487
+ : check(
488
+ 'block',
489
+ 'release_required_bins',
490
+ `required package bins are missing: ${missingBins.join(', ') || 'manifest declares none'}`,
491
+ RELEASE_LINEAGE_REF,
492
+ 'restore all reviewed CLI aliases before building the candidate',
493
+ ));
494
+
495
+ checks.push(sourceRepository
496
+ ? check('pass', 'release_source_repository', `source repository is declared: ${sourceRepository}`, RELEASE_LINEAGE_REF)
497
+ : check(
498
+ 'block',
499
+ 'release_source_repository',
500
+ 'source repository is missing from release lineage',
501
+ RELEASE_LINEAGE_REF,
502
+ 'declare the reviewed source-of-record repository',
503
+ ));
504
+
505
+ checks.push(distributionRepository && packageRepository === distributionRepository
506
+ ? check('pass', 'release_distribution_repository', `package repository matches public distribution home: ${distributionRepository}`, RELEASE_LINEAGE_REF)
507
+ : check(
508
+ 'block',
509
+ 'release_distribution_repository',
510
+ `package repository does not match public distribution home: package=${packageRepository ?? 'unknown'} lineage=${distributionRepository ?? 'unknown'}`,
511
+ RELEASE_LINEAGE_REF,
512
+ 'point package metadata at the reviewed public distribution repository',
513
+ ));
514
+
515
+ const shipsSource = packageInfo.files.includes('packages/control/src/')
516
+ && packageInfo.files.includes('packages/protocol/src/')
517
+ && packageInfo.files.includes(RELEASE_LINEAGE_REF)
518
+ && !packageInfo.files.some((entry) => entry === 'lib' || entry.startsWith('lib/'))
519
+ && !scriptExists(packageInfo.scripts, 'prepack')
520
+ && !scriptExists(packageInfo.scripts, 'release:build');
521
+ checks.push(distributionMode === 'bun-typescript-source' && shipsSource
522
+ ? check('pass', 'release_distribution_mode', 'package surface matches bun-typescript-source lineage', RELEASE_LINEAGE_REF)
523
+ : check(
524
+ 'block',
525
+ 'release_distribution_mode',
526
+ `package surface does not match declared distribution mode: ${distributionMode ?? 'unknown'}`,
527
+ RELEASE_LINEAGE_REF,
528
+ 'ship reviewed Bun/TypeScript source and remove implicit compiled prepack output',
529
+ ));
530
+
531
+ const candidateScript = await scriptDeclarationOk(packageInfo.scripts, 'release:candidate', projectRoot);
532
+ checks.push(candidateScript.declared && candidateScript.present
533
+ ? check('pass', 'release_candidate_verifier', 'isolated-install candidate verifier is declared and present', candidateScript.ref)
534
+ : check(
535
+ 'block',
536
+ 'release_candidate_verifier',
537
+ `release candidate verifier is ${candidateScript.declared ? 'missing' : 'not declared'}`,
538
+ candidateScript.ref,
539
+ 'wire release:candidate to the isolated-install verifier',
540
+ ));
541
+
542
+ if (!sourceBaseCommit || !/^[0-9a-f]{40}$/.test(sourceBaseCommit)) {
543
+ checks.push(check('block', 'release_source_ancestry', 'source base commit is missing or invalid', RELEASE_LINEAGE_REF, 'record the reviewed 40-character source base commit'));
544
+ return;
545
+ }
546
+
547
+ const ancestry = spawnSync('git', ['merge-base', '--is-ancestor', sourceBaseCommit, 'HEAD'], {
548
+ cwd: projectRoot,
549
+ encoding: 'utf-8',
550
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
551
+ timeout: 10_000,
552
+ });
553
+ checks.push(ancestry.status === 0
554
+ ? check('pass', 'release_source_ancestry', `HEAD descends from reviewed source base ${sourceBaseCommit}`, RELEASE_LINEAGE_REF)
555
+ : check(
556
+ 'block',
557
+ 'release_source_ancestry',
558
+ `HEAD does not prove ancestry from reviewed source base ${sourceBaseCommit}`,
559
+ RELEASE_LINEAGE_REF,
560
+ 'build the candidate from the reviewed source branch in a Git worktree',
561
+ ));
562
+ }
563
+
564
+ export async function buildReleaseReadinessReport(projectRoot: string): Promise<ReleaseReadinessReport> {
565
+ const packageRef = 'package.json';
566
+ const packagePath = join(projectRoot, packageRef);
567
+ const checks: ReleaseReadinessCheck[] = [];
568
+ let packageJson: JsonRecord | null = null;
569
+
570
+ try {
571
+ packageJson = asRecord(JSON.parse(await readFile(packagePath, 'utf-8')));
572
+ } catch {
573
+ packageJson = null;
574
+ }
575
+
576
+ if (!packageJson) {
577
+ checks.push(check(
578
+ 'block',
579
+ 'package_json',
580
+ 'package.json is missing or malformed',
581
+ packageRef,
582
+ 'restore a valid root package.json before evaluating release readiness',
583
+ ));
584
+ return finalize({
585
+ name: null,
586
+ version: null,
587
+ private: null,
588
+ bin: {},
589
+ files: [],
590
+ }, checks);
591
+ }
592
+
593
+ const name = asString(packageJson.name);
594
+ const version = asString(packageJson.version);
595
+ const privateFlag = typeof packageJson.private === 'boolean' ? packageJson.private : null;
596
+ const bin = asStringRecord(packageJson.bin);
597
+ const files = asStringArray(packageJson.files);
598
+ const scripts = asStringRecord(packageJson.scripts);
599
+
600
+ checks.push(name && version
601
+ ? check('pass', 'package_metadata', `package metadata present: ${name}@${version}`, packageRef)
602
+ : check('block', 'package_metadata', 'package name and version are required', packageRef, 'declare stable package name and version'));
603
+
604
+ await appendReleaseLineageChecks(projectRoot, packageJson, {
605
+ name,
606
+ version,
607
+ bin,
608
+ files,
609
+ scripts,
610
+ }, checks);
611
+
612
+ checks.push(privateFlag === false
613
+ ? check('pass', 'package_private_gate', 'package publishing is not blocked by private:true', packageRef)
614
+ : check('block', 'package_private_gate', 'package remains private; publish/global distro is intentionally blocked', packageRef, 'flip private only in an explicit release/distribution decision'));
615
+
616
+ const binEntries = Object.entries(bin);
617
+ if (binEntries.length === 0) {
618
+ checks.push(check('block', 'bin_entrypoint', 'no package bin entrypoint is declared', packageRef, 'declare the etiquette CLI bin entrypoint'));
619
+ } else {
620
+ const missingBins: string[] = [];
621
+ const unsafeBins: string[] = [];
622
+ for (const [, rawPath] of binEntries) {
623
+ const safePath = safeBinPath(rawPath);
624
+ if (!safePath) {
625
+ unsafeBins.push(rawPath);
626
+ continue;
627
+ }
628
+ if (!(await exists(join(projectRoot, safePath)))) missingBins.push(safePath);
629
+ }
630
+ if (unsafeBins.length > 0) {
631
+ checks.push(check('block', 'bin_entrypoint', `bin entrypoint paths must be repo-relative: ${unsafeBins.join(', ')}`, packageRef, 'replace absolute or parent-relative bin paths'));
632
+ } else if (missingBins.length > 0) {
633
+ checks.push(check('block', 'bin_entrypoint', `declared bin files are missing: ${missingBins.join(', ')}`, packageRef, 'restore declared bin files'));
634
+ } else {
635
+ checks.push(check('pass', 'bin_entrypoint', `declared bin entrypoint(s) exist: ${binEntries.map(([key, value]) => `${key}->${value}`).join(', ')}`, packageRef));
636
+ }
637
+ }
638
+
639
+ checks.push(files.length > 0
640
+ ? check('pass', 'files_allowlist', `package files allowlist declared with ${files.length} entr${files.length === 1 ? 'y' : 'ies'}`, packageRef)
641
+ : check('block', 'files_allowlist', 'package files allowlist is missing; pack output may include internal surfaces', packageRef, 'declare package.json files before enabling distribution'));
642
+
643
+ checks.push(scriptExists(scripts, 'test') && scriptExists(scripts, 'typecheck')
644
+ ? check('pass', 'validation_scripts', 'test and typecheck scripts are declared', packageRef)
645
+ : check('block', 'validation_scripts', 'test and typecheck scripts are required for release readiness', packageRef, 'declare test and typecheck scripts'));
646
+
647
+ const releaseScanScript = scriptExists(scripts, 'release:scan') && await exists(join(projectRoot, 'scripts', 'release-scan.sh'));
648
+ checks.push(releaseScanScript
649
+ ? check('pass', 'release_scan', 'release scan script is declared and present', 'scripts/release-scan.sh')
650
+ : check('block', 'release_scan', 'release scan script must be declared and present', packageRef, 'wire npm run release:scan to scripts/release-scan.sh'));
651
+
652
+ const packScript = await scriptDeclarationOk(scripts, 'release:pack', projectRoot);
653
+ if (!packScript.declared) {
654
+ checks.push(check('block', 'pack_canary', 'no release pack canary script is declared', packageRef, 'add a script that proves tarball contents from a fresh package output'));
655
+ } else if (!packScript.present) {
656
+ checks.push(check('block', 'pack_canary', `release pack script is declared but missing: ${packScript.ref}`, packScript.ref, 'restore the referenced pack canary script'));
657
+ } else {
658
+ checks.push(check('pass', 'pack_canary', 'release pack canary script is declared and present', packScript.ref));
659
+ }
660
+
661
+ const checksumScript = await scriptDeclarationOk(scripts, 'release:checksum', projectRoot);
662
+ const signScript = await scriptDeclarationOk(scripts, 'release:sign', projectRoot);
663
+ if (!checksumScript.declared || !signScript.declared) {
664
+ checks.push(check('block', 'checksum_signature', 'checksum/signature posture is not declared', packageRef, 'add checksum and signing scripts or record an explicit internal-channel waiver'));
665
+ } else if (!checksumScript.present || !signScript.present) {
666
+ const missing = [checksumScript, signScript].filter((item) => !item.present).map((item) => item.ref).join(', ');
667
+ checks.push(check('block', 'checksum_signature', `checksum/signature script is declared but missing: ${missing}`, packageRef, 'restore the referenced checksum/signing scripts'));
668
+ } else {
669
+ checks.push(check('pass', 'checksum_signature', 'checksum and signature scripts are declared and present', `${checksumScript.ref}, ${signScript.ref}`));
670
+ }
671
+
672
+ const shellInstaller = await exists(join(projectRoot, 'scripts', 'install.sh'));
673
+ checks.push(shellInstaller
674
+ ? check('pass', 'shell_installer', 'signed shell installer is present for user-local bootstrap', 'scripts/install.sh')
675
+ : check('warn', 'shell_installer', 'signed shell installer is not present; package-manager channels remain the only bootstrap path', 'scripts/install.sh', 'add a user-local installer before org-wide onboarding'));
676
+
677
+ if (shellInstaller) {
678
+ const installerText = await readTextIfExists(join(projectRoot, 'scripts', 'install.sh'));
679
+ const supportsPinnedKeyUrl = installerText?.includes('--public-key-url')
680
+ && installerText.includes('--public-key-sha256')
681
+ && installerText.includes('public key SHA-256 mismatch');
682
+ checks.push(supportsPinnedKeyUrl
683
+ ? check('pass', 'installer_trust_bootstrap', 'installer supports hosted public key URL with SHA-256 pin', 'scripts/install.sh')
684
+ : check('warn', 'installer_trust_bootstrap', 'installer does not expose a pinned hosted public-key bootstrap path', 'scripts/install.sh', 'require public-key URL plus SHA-256 pin before org-wide curl bootstrap'));
685
+ }
686
+
687
+ checks.push(check(
688
+ 'warn',
689
+ 'package_manager_wrappers',
690
+ 'Homebrew, Chocolatey, WinGet, and registry wrappers are not proven by this command',
691
+ 'site/guide/distribution.md',
692
+ 'treat wrappers as follow-up channels after the signed installer and core tarball are green',
693
+ ));
694
+
695
+ const codexCompatibilityDocExists = await exists(join(projectRoot, CODEX_COMPATIBILITY_DOC));
696
+ checks.push(codexCompatibilityDocExists
697
+ ? check(
698
+ 'pass',
699
+ 'codex_client_compatibility',
700
+ `Codex client policy documented; ${CODEX_RECOMMENDED_MINIMUM}+ recommended and required for remote/plugin-MCP canaries`,
701
+ CODEX_COMPATIBILITY_DOC,
702
+ )
703
+ : check(
704
+ 'warn',
705
+ 'codex_client_compatibility',
706
+ `Codex client policy is not documented; ${CODEX_RECOMMENDED_MINIMUM}+ should be treated as the remote/plugin-MCP canary floor`,
707
+ CODEX_COMPATIBILITY_DOC,
708
+ 'add client compatibility guidance before routing remote-runtime or plugin-MCP pilots',
709
+ ));
710
+
711
+ await appendProductionAgentPlaneChecks(projectRoot, checks);
712
+
713
+ return finalize({
714
+ name,
715
+ version,
716
+ private: privateFlag,
717
+ bin,
718
+ files,
719
+ }, checks);
720
+ }
721
+
722
+ export async function buildReleaseGaReadinessReport(
723
+ projectRoot: string,
724
+ options: ReleaseGaReadinessOptions = {},
725
+ ): Promise<ReleaseGaReadinessReport> {
726
+ const packageReport = await buildReleaseReadinessReport(projectRoot);
727
+ const checks: ReleaseReadinessCheck[] = [];
728
+ const tag = packageReport.package.version ? `v${packageReport.package.version}` : null;
729
+ const { selected: channels, unknown: unknownChannels } = lookupChannels(options.channel ?? null);
730
+ const channel = channels.length > 0 ? channels.map((item) => item.id).join(',') : null;
731
+ const tarballName = packageTarballName(packageReport.package.name, packageReport.package.version);
732
+
733
+ checks.push(packageReport.verdict === 'ready'
734
+ ? check('pass', 'package_readiness', 'base release readiness is ready', 'package.json')
735
+ : check('block', 'package_readiness', `base release readiness is blocked: ${packageReport.blockers.join(', ') || 'unknown'}`, 'package.json', 'resolve base readiness blockers first'));
736
+
737
+ checks.push(validGaVersion(packageReport.package.version)
738
+ ? check('pass', 'ga_version', `package version is GA-shaped: ${packageReport.package.version}`, 'package.json')
739
+ : check('block', 'ga_version', `package version is not GA-shaped: ${packageReport.package.version ?? 'unknown'}`, 'package.json', 'set a non-prerelease semver version before GA'));
740
+
741
+ if (channels.length > 0 && unknownChannels.length === 0) {
742
+ checks.push(check('pass', 'distribution_channel', `distribution channel(s) declared: ${channel}`, 'operator:--channel'));
743
+ } else if (unknownChannels.length > 0) {
744
+ checks.push(check('block', 'distribution_channel', `unknown distribution channel(s): ${unknownChannels.join(', ')}`, 'operator:--channel', `choose one or more known channels: ${RELEASE_DISTRIBUTION_CHANNELS.map((item) => item.id).join(', ')}`));
745
+ } else {
746
+ checks.push(check('block', 'distribution_channel', 'GA requires at least one named distribution channel', 'operator:--channel', 'rerun with --channel shell-installer,npm or another known channel'));
747
+ }
748
+
749
+ if (options.verifyTagRemote) {
750
+ checks.push(verifyTagIsFree(projectRoot, options.verifyTagRemote, tag));
751
+ } else if (options.tagFreeConfirmed === true) {
752
+ checks.push(check(
753
+ 'block',
754
+ 'tag_uniqueness',
755
+ `manual confirmation is insufficient for GA tag ${tag ?? 'unknown'}; verify the remote directly`,
756
+ 'operator:--tag-free-confirmed',
757
+ 'rerun with --verify-tag-remote origin after credentials are available',
758
+ ));
759
+ } else {
760
+ checks.push(check(
761
+ 'block',
762
+ 'tag_uniqueness',
763
+ `GA requires remote verification that tag ${tag ?? 'unknown'} is unused`,
764
+ 'operator:--verify-tag-remote',
765
+ 'rerun with --verify-tag-remote origin or another trusted release remote',
766
+ ));
767
+ }
768
+
769
+ if (!tarballName) {
770
+ checks.push(check('block', 'release_artifact', 'cannot derive package tarball name', 'package.json', 'restore package name and version'));
771
+ } else {
772
+ const tarballPath = join(projectRoot, 'dist', tarballName);
773
+ const checksumsPath = join(projectRoot, 'dist', 'SHA256SUMS');
774
+ const checksums = await readTextIfExists(checksumsPath);
775
+ if (!(await exists(tarballPath))) {
776
+ checks.push(check('block', 'release_artifact', `missing release tarball: dist/${tarballName}`, 'dist/', 'run npm run release:checksum after setting the GA version'));
777
+ } else if (!checksums?.includes(tarballName)) {
778
+ checks.push(check('block', 'release_artifact', `dist/SHA256SUMS does not reference ${tarballName}`, 'dist/SHA256SUMS', 'regenerate checksums from the final GA tarball'));
779
+ } else {
780
+ checks.push(check('pass', 'release_artifact', `tarball and checksum manifest present for ${tarballName}`, 'dist/SHA256SUMS'));
781
+ }
782
+ }
783
+
784
+ const signatureManifest = await readTextIfExists(join(projectRoot, 'dist', 'SIGNATURE_MANIFEST.txt'));
785
+ const hasTrustedSignature = signatureManifest?.includes('method=openssl-rsa-sha256')
786
+ && await exists(join(projectRoot, 'dist', 'SHA256SUMS.sig'));
787
+ const hasCanaryHmac = signatureManifest?.includes('method=hmac-sha256')
788
+ || await exists(join(projectRoot, 'dist', 'SHA256SUMS.hmac'));
789
+ if (hasTrustedSignature) {
790
+ checks.push(check('pass', 'trusted_signature', 'trusted RSA signature exists for dist/SHA256SUMS', 'dist/SIGNATURE_MANIFEST.txt'));
791
+ } else if (hasCanaryHmac) {
792
+ checks.push(check('block', 'trusted_signature', 'HMAC signature is canary-only and cannot satisfy GA', 'dist/SIGNATURE_MANIFEST.txt', 'sign dist/SHA256SUMS with ETIQUETTE_RELEASE_SIGNING_KEY'));
793
+ } else {
794
+ checks.push(check('block', 'trusted_signature', 'trusted release signature is missing', 'dist/SIGNATURE_REQUIRED.txt', 'set ETIQUETTE_RELEASE_SIGNING_KEY and run npm run release:sign'));
795
+ }
796
+
797
+ const blockers = checks.filter((item) => item.status === 'block').map((item) => item.id);
798
+ const warnings = checks.filter((item) => item.status === 'warn').map((item) => item.id);
799
+ return {
800
+ schema: 'release-ga-readiness.v0',
801
+ verdict: blockers.length === 0 ? 'ready' : 'blocked',
802
+ channel,
803
+ channels,
804
+ tag,
805
+ package_readiness: packageReport.verdict,
806
+ checks,
807
+ blockers,
808
+ warnings,
809
+ authority_boundary: {
810
+ command_can_publish: false,
811
+ command_can_sign: false,
812
+ command_can_tag: false,
813
+ command_can_create_distribution_channel: false,
814
+ },
815
+ };
816
+ }
817
+
818
+ function finalize(
819
+ packageInfo: ReleaseReadinessReport['package'],
820
+ checks: ReleaseReadinessCheck[],
821
+ ): ReleaseReadinessReport {
822
+ const blockers = checks.filter((item) => item.status === 'block').map((item) => item.id);
823
+ const warnings = checks.filter((item) => item.status === 'warn').map((item) => item.id);
824
+ return {
825
+ schema: 'release-readiness.v0',
826
+ verdict: blockers.length === 0 ? 'ready' : 'blocked',
827
+ package: packageInfo,
828
+ client_compatibility: {
829
+ codex_recommended_minimum: CODEX_RECOMMENDED_MINIMUM,
830
+ codex_required_for: CODEX_REQUIRED_FOR,
831
+ core_release_requires_codex: false,
832
+ },
833
+ checks,
834
+ blockers,
835
+ warnings,
836
+ authority_boundary: {
837
+ command_can_publish: false,
838
+ command_can_flip_private: false,
839
+ command_can_create_distribution_channel: false,
840
+ },
841
+ };
842
+ }