release-skill 0.2.9 → 0.4.0

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 (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +41 -0
  7. package/INSTALL.md +34 -110
  8. package/INSTALL.zh-CN.md +17 -83
  9. package/README.md +49 -41
  10. package/README.zh-CN.md +43 -34
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +4131 -3180
  14. package/adapters/claude/schemas/release-plan.schema.json +7 -0
  15. package/adapters/claude/schemas/release-run.schema.json +43 -0
  16. package/adapters/claude/skills/release-help/SKILL.md +10 -2
  17. package/adapters/claude/skills/release-prepare/SKILL.md +6 -15
  18. package/adapters/claude/skills/release-publish/SKILL.md +5 -6
  19. package/adapters/claude/skills/release-reconcile/SKILL.md +2 -2
  20. package/adapters/claude/skills/release-verify/SKILL.md +4 -6
  21. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  22. package/adapters/codex/bin/release-skill.bundle.mjs +4131 -3180
  23. package/adapters/codex/schemas/release-plan.schema.json +7 -0
  24. package/adapters/codex/schemas/release-run.schema.json +43 -0
  25. package/adapters/codex/skills/release-help/SKILL.md +10 -2
  26. package/adapters/codex/skills/release-prepare/SKILL.md +6 -15
  27. package/adapters/codex/skills/release-publish/SKILL.md +5 -6
  28. package/adapters/codex/skills/release-reconcile/SKILL.md +2 -2
  29. package/adapters/codex/skills/release-verify/SKILL.md +4 -6
  30. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  31. package/adapters/kimi/bin/release-skill.bundle.mjs +4131 -3180
  32. package/adapters/kimi/schemas/release-plan.schema.json +7 -0
  33. package/adapters/kimi/schemas/release-run.schema.json +43 -0
  34. package/adapters/kimi/skills/release-help/SKILL.md +10 -2
  35. package/adapters/kimi/skills/release-prepare/SKILL.md +6 -15
  36. package/adapters/kimi/skills/release-publish/SKILL.md +5 -6
  37. package/adapters/kimi/skills/release-reconcile/SKILL.md +2 -2
  38. package/adapters/kimi/skills/release-verify/SKILL.md +4 -6
  39. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  40. package/adapters/workbuddy/bin/release-skill.bundle.mjs +4131 -3180
  41. package/adapters/workbuddy/schemas/release-plan.schema.json +7 -0
  42. package/adapters/workbuddy/schemas/release-run.schema.json +43 -0
  43. package/adapters/workbuddy/skills/release-help/SKILL.md +10 -2
  44. package/adapters/workbuddy/skills/release-prepare/SKILL.md +6 -15
  45. package/adapters/workbuddy/skills/release-publish/SKILL.md +5 -6
  46. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +2 -2
  47. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -6
  48. package/bin/release-skill-cli.mjs +212 -43
  49. package/bin/release-skill.bundle.mjs +4131 -3180
  50. package/package.json +1 -1
  51. package/references/01-state-machine.md +1 -1
  52. package/references/06-adapter-contract.md +6 -5
  53. package/schemas/release-plan.schema.json +7 -0
  54. package/schemas/release-run.schema.json +43 -0
  55. package/skills/release-help/SKILL.md +10 -2
  56. package/skills/release-prepare/SKILL.md +6 -15
  57. package/skills/release-publish/SKILL.md +5 -6
  58. package/skills/release-reconcile/SKILL.md +2 -2
  59. package/skills/release-verify/SKILL.md +4 -6
  60. package/skills-src/release-help/SKILL.md +10 -2
  61. package/skills-src/release-prepare/SKILL.md +6 -15
  62. package/skills-src/release-publish/SKILL.md +5 -6
  63. package/skills-src/release-reconcile/SKILL.md +2 -2
  64. package/skills-src/release-verify/SKILL.md +4 -6
  65. package/src/adapters/plugin-marketplace.mjs +1 -1
  66. package/src/adapters/push-snapshot.mjs +3 -1
  67. package/src/commands/approve.mjs +8 -6
  68. package/src/commands/attest.mjs +195 -0
  69. package/src/commands/hooks.mjs +42 -0
  70. package/src/commands/prepare.mjs +13 -50
  71. package/src/commands/publish.mjs +0 -8
  72. package/src/commands/reconcile.mjs +3 -37
  73. package/src/commands/ship.mjs +334 -0
  74. package/src/commands/verify.mjs +195 -15
  75. package/src/core/git-transport.mjs +93 -0
  76. package/src/core/release-metadata.mjs +105 -0
  77. package/src/core/skill-resource-closure.mjs +7 -1
@@ -0,0 +1,334 @@
1
+ import { readFile, lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
5
+ import {
6
+ ReleaseError,
7
+ GATE_FAILED,
8
+ PLAN_DIGEST_MISMATCH,
9
+ CONSUMER_VERIFICATION_DEFERRED,
10
+ } from '../core/errors.mjs';
11
+
12
+ async function writeJsonAtomic(path, value) {
13
+ await mkdir(dirname(path), { recursive: true });
14
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
15
+ const { stateDigest: _oldDigest, ...body } = value;
16
+ const sealed = {
17
+ ...body,
18
+ stateDigest: sha256Hex(canonicalJson(body)),
19
+ };
20
+ try {
21
+ await writeFile(temp, `${JSON.stringify(sealed, null, 2)}\n`, {
22
+ encoding: 'utf8',
23
+ mode: 0o600,
24
+ flag: 'wx',
25
+ });
26
+ await rename(temp, path);
27
+ } catch (error) {
28
+ await rm(temp, { force: true }).catch(() => {});
29
+ throw error;
30
+ }
31
+ }
32
+
33
+ async function readState(path) {
34
+ try {
35
+ const stat = await lstat(path);
36
+ if (!stat.isFile() || stat.isSymbolicLink()) {
37
+ throw new Error('ship state must be a regular non-symlink file');
38
+ }
39
+ const state = JSON.parse(await readFile(path, 'utf8'));
40
+ const { stateDigest, ...body } = state;
41
+ if (
42
+ typeof stateDigest !== 'string'
43
+ || stateDigest !== sha256Hex(canonicalJson(body))
44
+ || body.statePath !== path
45
+ ) {
46
+ throw new Error('ship state digest or authority path does not match');
47
+ }
48
+ return state;
49
+ } catch (error) {
50
+ if (error?.code === 'ENOENT') return null;
51
+ throw new ReleaseError(GATE_FAILED, `cannot read ship state: ${error.message}`, { statePath: path });
52
+ }
53
+ }
54
+
55
+ async function defaultDependencies() {
56
+ const [
57
+ configModule,
58
+ prepareModule,
59
+ approveModule,
60
+ publishModule,
61
+ reconcileModule,
62
+ verifyModule,
63
+ transportModule,
64
+ metadataModule,
65
+ ] = await Promise.all([
66
+ import('../core/config.mjs'),
67
+ import('./prepare.mjs'),
68
+ import('./approve.mjs'),
69
+ import('./publish.mjs'),
70
+ import('./reconcile.mjs'),
71
+ import('./verify.mjs'),
72
+ import('../core/git-transport.mjs'),
73
+ import('../core/release-metadata.mjs'),
74
+ ]);
75
+ return {
76
+ loadProjectConfig: configModule.loadProjectConfig,
77
+ prepareRelease: prepareModule.prepareRelease,
78
+ approvePlan: approveModule.approvePlan,
79
+ publishRelease: publishModule.publishRelease,
80
+ reconcileRelease: reconcileModule.reconcileRelease,
81
+ verifyRelease: verifyModule.verifyRelease,
82
+ preflightGitTransports: transportModule.preflightGitTransports,
83
+ updatePreviousPublicBaselines: metadataModule.updatePreviousPublicBaselines,
84
+ };
85
+ }
86
+
87
+ function publicState(state) {
88
+ return {
89
+ command: 'ship',
90
+ status: state.status,
91
+ statePath: state.statePath,
92
+ targetVersion: state.targetVersion,
93
+ ...(state.hooks && state.hooks.length > 0 ? {
94
+ hooks: state.hooks,
95
+ } : {}),
96
+ ...(state.planPath ? {
97
+ planPath: state.planPath,
98
+ planDigest: state.planDigest,
99
+ evidenceDir: state.evidenceDir,
100
+ warnings: state.warnings ?? [],
101
+ } : {}),
102
+ ...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
103
+ ...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
104
+ ...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
105
+ ...(state.requirements ? { requirements: state.requirements } : {}),
106
+ ...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
107
+ ...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
108
+ verificationGateAuthorizationIncludedInPlanApproval: true,
109
+ postVerifyMetadataUpdateIncludedInPlanApproval: true,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Build a human-readable approval summary from the frozen plan.
115
+ * Lists each unit's version and every external action's id/type/unitId.
116
+ * Returns an empty summary when the plan file is not yet available (e.g., in tests).
117
+ *
118
+ * @param {string} planPath - Path to the frozen release plan.
119
+ * @returns {Promise<object>} The approval summary.
120
+ */
121
+ async function buildApprovalSummary(planPath) {
122
+ try {
123
+ const plan = JSON.parse(await readFile(planPath, 'utf8'));
124
+ const units = (plan.units ?? []).map((unit) => ({
125
+ id: unit.id,
126
+ targetVersion: unit.targetVersion ?? unit.version,
127
+ }));
128
+ const actions = (plan.externalActions ?? []).map((action) => ({
129
+ id: action.id,
130
+ type: action.type,
131
+ unitId: action.unitId,
132
+ }));
133
+ return { units, actions };
134
+ } catch {
135
+ return { units: [], actions: [] };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Advance one durable production release. Re-running is safe: the state file
141
+ * carries the immutable plan, approval and source-run paths so the command
142
+ * resumes instead of reconstructing authority from terminal/chat output.
143
+ *
144
+ * New flow (v0.4+): ship directly runs configured hooks and verification gates
145
+ * without a separate hook authorization step. The only human gate is plan
146
+ * approval. Kimi/CodeBuddy installations are non-blocking manual follow-up
147
+ * tasks when the plan declares humanConsumersStrategy: 'manualFollowUps'.
148
+ */
149
+ export async function advanceShip(options = {}, injected = {}) {
150
+ const root = resolve(options.root ?? process.cwd());
151
+ const statePath = resolve(
152
+ options.statePath ?? resolve(root, '.release-skill', 'ships', 'current.json'),
153
+ );
154
+ const deps = Object.keys(injected).length > 0
155
+ ? injected
156
+ : await defaultDependencies();
157
+ let state = await readState(statePath);
158
+ if (state?.gitTransport) {
159
+ process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
160
+ }
161
+
162
+ if (!state) {
163
+ const loaded = await deps.loadProjectConfig({ root });
164
+ const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
165
+ state = {
166
+ schemaVersion: 2,
167
+ root,
168
+ statePath,
169
+ targetVersion: options.targetVersion ?? null,
170
+ configDigest: loaded.configDigest,
171
+ hooks,
172
+ status: 'NEW',
173
+ createdAt: new Date().toISOString(),
174
+ updatedAt: new Date().toISOString(),
175
+ };
176
+ await writeJsonAtomic(statePath, state);
177
+ }
178
+
179
+ // Backward compatibility: auto-recover from legacy NEEDS_HOOK_AUTHORIZATION
180
+ // state. Old state files may have this status from the previous two-gate
181
+ // flow; the new flow runs hooks directly so we advance to NEW immediately.
182
+ if (state.status === 'NEEDS_HOOK_AUTHORIZATION') {
183
+ state.status = 'NEW';
184
+ state.updatedAt = new Date().toISOString();
185
+ await writeJsonAtomic(statePath, state);
186
+ }
187
+
188
+ if (state.status === 'NEW') {
189
+ const loaded = await deps.loadProjectConfig({ root });
190
+ if (loaded.configDigest !== state.configDigest) {
191
+ const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
192
+ state = {
193
+ ...state,
194
+ configDigest: loaded.configDigest,
195
+ hooks,
196
+ status: 'NEW',
197
+ updatedAt: new Date().toISOString(),
198
+ };
199
+ await writeJsonAtomic(statePath, state);
200
+ }
201
+ const prepared = await deps.prepareRelease({
202
+ root,
203
+ version: state.targetVersion ?? undefined,
204
+ offline: false,
205
+ production: true,
206
+ hooksAuthorized: true,
207
+ verificationGatesAuthorized: true,
208
+ hookCache: true,
209
+ });
210
+ let transportPreflight = null;
211
+ if (deps.preflightGitTransports) {
212
+ const frozenPlan = JSON.parse(await readFile(prepared.planPath, 'utf8'));
213
+ transportPreflight = await deps.preflightGitTransports(frozenPlan);
214
+ process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
215
+ }
216
+ const approvalSummary = await buildApprovalSummary(prepared.planPath);
217
+ state = {
218
+ ...state,
219
+ status: 'NEEDS_PLAN_APPROVAL',
220
+ planPath: prepared.planPath,
221
+ planDigest: prepared.planDigest,
222
+ evidenceDir: prepared.evidenceDir,
223
+ warnings: prepared.warnings,
224
+ approvalSummary,
225
+ ...(transportPreflight ? {
226
+ gitTransport: transportPreflight.transport,
227
+ gitTransportPreflight: transportPreflight.repositories,
228
+ } : {}),
229
+ updatedAt: new Date().toISOString(),
230
+ };
231
+ await writeJsonAtomic(statePath, state);
232
+ }
233
+
234
+ if (state.status === 'NEEDS_PLAN_APPROVAL') {
235
+ // Boolean --approve: auto-use the stored planDigest (no user digest input needed).
236
+ // Legacy --approve-plan <digest>: explicit digest must match.
237
+ const approveRequested = options.approve === true
238
+ || (typeof options.planApprovalDigest === 'string' && options.planApprovalDigest.length > 0);
239
+ if (!approveRequested) return publicState(state);
240
+ if (options.approve === true) {
241
+ // Boolean approve: no digest input; the stored planDigest is authoritative.
242
+ } else if (options.planApprovalDigest !== state.planDigest) {
243
+ throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'ship plan approval digest does not match the frozen plan');
244
+ }
245
+ if (!options.actor) {
246
+ throw new ReleaseError(GATE_FAILED, 'ship plan approval requires --actor <person>');
247
+ }
248
+ const approval = await deps.approvePlan({
249
+ planPath: state.planPath,
250
+ expectedDigest: state.planDigest,
251
+ actor: options.actor,
252
+ });
253
+ state = {
254
+ ...state,
255
+ status: 'APPROVED',
256
+ approvalPath: approval.approvalPath,
257
+ verificationGatesAuthorized: true,
258
+ approvedBy: options.actor,
259
+ updatedAt: new Date().toISOString(),
260
+ };
261
+ await writeJsonAtomic(statePath, state);
262
+ }
263
+
264
+ if (state.status === 'APPROVED' || state.status === 'PUBLISHING') {
265
+ if (!options.adapterRegistry) {
266
+ throw new ReleaseError(GATE_FAILED, 'ship requires an adapter registry to publish');
267
+ }
268
+ state.status = 'PUBLISHING';
269
+ await writeJsonAtomic(statePath, state);
270
+ const published = await deps.publishRelease({
271
+ planPath: state.planPath,
272
+ approvalPath: state.approvalPath,
273
+ adapterRegistry: options.adapterRegistry,
274
+ root,
275
+ productionMode: true,
276
+ });
277
+ state = {
278
+ ...state,
279
+ status: published.status,
280
+ sourceRunPath: published.runPath,
281
+ updatedAt: new Date().toISOString(),
282
+ };
283
+ await writeJsonAtomic(statePath, state);
284
+ }
285
+
286
+ if (state.status === 'PARTIAL') {
287
+ const reconciled = await deps.reconcileRelease({
288
+ planPath: state.planPath,
289
+ sourceRunPath: state.sourceRunPath,
290
+ approvalPath: state.approvalPath,
291
+ adapterRegistry: options.adapterRegistry,
292
+ root,
293
+ });
294
+ state = {
295
+ ...state,
296
+ status: reconciled.status,
297
+ sourceRunPath: reconciled.runPath,
298
+ updatedAt: new Date().toISOString(),
299
+ };
300
+ await writeJsonAtomic(statePath, state);
301
+ }
302
+
303
+ if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
304
+ try {
305
+ const verified = await deps.verifyRelease({
306
+ planPath: state.planPath,
307
+ sourceRunPath: state.sourceRunPath,
308
+ adapterRegistry: options.adapterRegistry,
309
+ root,
310
+ verificationGatesAuthorized: state.verificationGatesAuthorized === true,
311
+ });
312
+ state = {
313
+ ...state,
314
+ status: verified.status,
315
+ verifyRunPath: verified.runPath,
316
+ requirements: undefined,
317
+ manualFollowUps: verified.manualFollowUps ?? undefined,
318
+ updatedAt: new Date().toISOString(),
319
+ };
320
+ await writeJsonAtomic(statePath, state);
321
+ } catch (error) {
322
+ if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
323
+ state = {
324
+ ...state,
325
+ status: 'NEEDS_MANUAL_ATTESTATIONS',
326
+ requirements: error.details?.requirements ?? [],
327
+ updatedAt: new Date().toISOString(),
328
+ };
329
+ await writeJsonAtomic(statePath, state);
330
+ }
331
+ }
332
+
333
+ return publicState(state);
334
+ }
@@ -43,6 +43,7 @@ import {
43
43
  GATE_FAILED,
44
44
  CONFIG_MISSING,
45
45
  POST_PUBLISH_VERIFY_FAILED,
46
+ CONSUMER_VERIFICATION_DEFERRED,
46
47
  } from '../core/errors.mjs';
47
48
  import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
48
49
  import { assertTransition, PUBLISHED, VERIFIED } from '../core/state-machine.mjs';
@@ -67,6 +68,20 @@ import {
67
68
  buildDirectoryFileIndex,
68
69
  checkNpmEntryClosure,
69
70
  } from '../npm/npm-entry-closure.mjs';
71
+ import {
72
+ KIMI_ATTESTATION_FILE,
73
+ KIMI_REQUIREMENT_FILE,
74
+ kimiAuthorityDir,
75
+ resolveBoundPlanDigest,
76
+ validateKimiAttestation,
77
+ } from '../platforms/kimi.mjs';
78
+ import {
79
+ CODEBUDDY_ATTESTATION_FILE,
80
+ CODEBUDDY_REQUIREMENT_FILE,
81
+ codebuddyAuthorityDir,
82
+ resolveCodeBuddyBoundPlanDigest,
83
+ validateCodeBuddyAttestation,
84
+ } from '../platforms/codebuddy.mjs';
70
85
 
71
86
  // ---------------------------------------------------------------------------
72
87
  // Constants
@@ -120,6 +135,103 @@ function isValidDigest(digest) {
120
135
  return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
121
136
  }
122
137
 
138
+ /**
139
+ * Generate every manual consumer requirement before any expensive automatic
140
+ * consumer install starts. This turns a sequence of "install Kimi, rerun,
141
+ * then discover CodeBuddy" failures into one actionable response.
142
+ */
143
+ async function collectMissingManualAttestations({
144
+ actions,
145
+ adapterRegistry,
146
+ plan,
147
+ root,
148
+ runDir,
149
+ clockFn,
150
+ }) {
151
+ const manual = actions.filter((action) => (
152
+ action.type === 'kimi-marketplace-install'
153
+ || action.type === 'codebuddy-marketplace-install'
154
+ ));
155
+ if (manual.length === 0) return [];
156
+
157
+ const missing = [];
158
+ for (const action of manual) {
159
+ const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
160
+ const adapter = adapterRegistry.getAdapter(adapterActionType);
161
+ // Tests and embedders may intentionally register a fully automatic
162
+ // adapter for these action types. Only the real platform adapter owns the
163
+ // interactive manual-install protocol and its stable authority files.
164
+ if (adapter.name !== 'plugin-marketplace') continue;
165
+ const context = {
166
+ externalWritesAuthorized: false,
167
+ isolatedConsumerWritesAuthorized: true,
168
+ plan,
169
+ baseline: plan.baseline,
170
+ root,
171
+ runDir,
172
+ };
173
+ const actionInput = { actionType: adapterActionType, ...action.parameters };
174
+ const preflight = await adapter.preflight(actionInput, context);
175
+ if (preflight.status !== 'PREFLIGHT_PASSED') {
176
+ throw new ReleaseError(
177
+ POST_PUBLISH_VERIFY_FAILED,
178
+ `manual consumer preflight did not pass for action "${action.id}": ${preflight.error}`,
179
+ { actionId: action.id, platform: action.type.split('-')[0] },
180
+ );
181
+ }
182
+ const executed = await adapter.execute(actionInput, context);
183
+ if (executed.status !== 'EXECUTED') {
184
+ throw new ReleaseError(
185
+ POST_PUBLISH_VERIFY_FAILED,
186
+ `cannot generate manual consumer requirement for action "${action.id}": ${executed.error}`,
187
+ { actionId: action.id, platform: action.type.split('-')[0] },
188
+ );
189
+ }
190
+
191
+ const isKimi = action.type === 'kimi-marketplace-install';
192
+ const planDigest = isKimi
193
+ ? resolveBoundPlanDigest(context)
194
+ : await resolveCodeBuddyBoundPlanDigest(context);
195
+ const authorityDir = isKimi
196
+ ? kimiAuthorityDir(context, planDigest, action.parameters.plugin)
197
+ : codebuddyAuthorityDir(context, planDigest, action.parameters.plugin);
198
+ const requirementPath = join(
199
+ authorityDir,
200
+ isKimi ? KIMI_REQUIREMENT_FILE : CODEBUDDY_REQUIREMENT_FILE,
201
+ );
202
+ const attestationPath = join(
203
+ authorityDir,
204
+ isKimi ? KIMI_ATTESTATION_FILE : CODEBUDDY_ATTESTATION_FILE,
205
+ );
206
+
207
+ let valid = false;
208
+ let reason = 'attestation file is missing';
209
+ try {
210
+ const attestation = JSON.parse(await readFile(attestationPath, 'utf8'));
211
+ const validation = isKimi
212
+ ? validateKimiAttestation(attestation, action.parameters, clockFn(), planDigest)
213
+ : validateCodeBuddyAttestation(attestation, action.parameters, clockFn(), planDigest);
214
+ valid = validation.valid;
215
+ reason = validation.error;
216
+ } catch (error) {
217
+ if (error?.code !== 'ENOENT') reason = `attestation cannot be read: ${error.message}`;
218
+ }
219
+ if (!valid) {
220
+ missing.push({
221
+ actionId: action.id,
222
+ platform: isKimi ? 'kimi' : 'codebuddy',
223
+ plugin: action.parameters.plugin,
224
+ version: action.parameters.version,
225
+ planDigest,
226
+ requirementPath,
227
+ attestationPath,
228
+ reason,
229
+ });
230
+ }
231
+ }
232
+ return missing;
233
+ }
234
+
123
235
  // ---------------------------------------------------------------------------
124
236
  // Smoke test
125
237
  // ---------------------------------------------------------------------------
@@ -643,7 +755,7 @@ export async function verifyRelease(options) {
643
755
  runDir: runDirOpt,
644
756
  clock: clockOpt,
645
757
  npmExecutor,
646
- verificationGatesAuthorized,
758
+ verificationGatesAuthorized: _verificationGatesAuthorized,
647
759
  gateEnv,
648
760
  previousVerifyRun,
649
761
  } = options ?? {};
@@ -714,16 +826,9 @@ export async function verifyRelease(options) {
714
826
  .filter((distribution) => distribution.type === 'npm' && distribution.smokeBin)
715
827
  .map((distribution) => ({ unitId: unit.id, smokeBin: distribution.smokeBin }))
716
828
  ));
717
- if ((consumerGates.length > 0 || configuredSmokeBins.length > 0) && verificationGatesAuthorized !== true) {
718
- throw new ReleaseError(
719
- GATE_FAILED,
720
- `plan declares ${consumerGates.length} consumer verification gate(s) and ` +
721
- `${configuredSmokeBins.length} npm CLI smoke process(es). ` +
722
- 'They execute installed project code without an OS or network sandbox. ' +
723
- 'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
724
- { gateIds: consumerGates.map((gate) => gate.id), configuredSmokeBins },
725
- );
726
- }
829
+ // The command invocation itself authorizes execution of configured
830
+ // verification gates and smoke processes. Old --acknowledge-gate-side-effects
831
+ // is accepted as a no-effect compatibility input.
727
832
 
728
833
  await evidence.append({ phase: 'verify', step: 'plan-load', status: 'passed' });
729
834
 
@@ -1000,7 +1105,43 @@ export async function verifyRelease(options) {
1000
1105
  }
1001
1106
  }
1002
1107
 
1003
- for (const action of actions) {
1108
+ // When the plan declares humanConsumersStrategy: 'manualFollowUps',
1109
+ // Kimi/CodeBuddy installations are non-blocking post-release manual tasks.
1110
+ // Skip attestation collection and blocking for these platforms.
1111
+ const isHumanConsumerFollowUp = plan.humanConsumersStrategy === 'manualFollowUps';
1112
+
1113
+ const missingManualAttestations = isHumanConsumerFollowUp
1114
+ ? []
1115
+ : await collectMissingManualAttestations({
1116
+ actions,
1117
+ adapterRegistry,
1118
+ plan,
1119
+ root,
1120
+ runDir,
1121
+ clockFn,
1122
+ });
1123
+ if (missingManualAttestations.length > 0) {
1124
+ await evidence.append({
1125
+ phase: 'verify',
1126
+ step: 'manual-attestations',
1127
+ status: 'needs-input',
1128
+ requirements: missingManualAttestations,
1129
+ });
1130
+ throw new ReleaseError(
1131
+ CONSUMER_VERIFICATION_DEFERRED,
1132
+ `${missingManualAttestations.length} manual consumer attestation(s) are required`,
1133
+ { requirements: missingManualAttestations },
1134
+ );
1135
+ }
1136
+
1137
+ // Collect non-blocking manual follow-up tasks for human consumer platforms
1138
+ const manualFollowUps = [];
1139
+
1140
+ // Every action is identity-bound and adapters receive read-only or
1141
+ // per-action isolated consumer paths. Run independent checks concurrently;
1142
+ // the evidence writer serializes append operations and result arrays are
1143
+ // sorted afterwards for deterministic receipts.
1144
+ const actionResults = await Promise.allSettled(actions.map(async (action) => {
1004
1145
  const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
1005
1146
 
1006
1147
  // Skip meta-checkpoints
@@ -1011,7 +1152,7 @@ export async function verifyRelease(options) {
1011
1152
  status: 'SKIPPED',
1012
1153
  reason: 'meta-checkpoint',
1013
1154
  });
1014
- continue;
1155
+ return;
1015
1156
  }
1016
1157
 
1017
1158
  let adapter;
@@ -1027,6 +1168,38 @@ export async function verifyRelease(options) {
1027
1168
  }
1028
1169
 
1029
1170
  if (isMarketplaceAction(action.type)) {
1171
+ // When humanConsumersStrategy is 'manualFollowUps', Kimi/CodeBuddy
1172
+ // installations are non-blocking manual follow-up tasks. Skip adapter
1173
+ // processing and collect them as manualFollowUps.
1174
+ const isHumanConsumerPlatform = action.type === 'kimi-marketplace-install'
1175
+ || action.type === 'codebuddy-marketplace-install';
1176
+ if (isHumanConsumerFollowUp && isHumanConsumerPlatform) {
1177
+ const platform = action.type === 'kimi-marketplace-install' ? 'kimi' : 'codebuddy';
1178
+ manualFollowUps.push({
1179
+ actionId: action.id,
1180
+ platform,
1181
+ plugin: action.parameters?.plugin,
1182
+ version: action.parameters?.version,
1183
+ unitId: action.unitId,
1184
+ verifiedBySystem: false,
1185
+ reason: 'human consumer platform — installation is a manual post-release task; not verified by system',
1186
+ });
1187
+ adapterChecks.push({
1188
+ actionId: action.id,
1189
+ actionType: action.type,
1190
+ status: 'SKIPPED',
1191
+ reason: 'manual-follow-up',
1192
+ });
1193
+ await evidence.append({
1194
+ phase: 'verify-marketplace',
1195
+ actionId: action.id,
1196
+ actionType: action.type,
1197
+ status: 'SKIPPED',
1198
+ reason: 'manual-follow-up',
1199
+ });
1200
+ return;
1201
+ }
1202
+
1030
1203
  // --- Marketplace: fresh consumer verification in verify's own runDir ---
1031
1204
  // Context: isolatedConsumerWritesAuthorized allows writing to verify's
1032
1205
  // runDir/consumers/ directory; externalWritesAuthorized stays false.
@@ -1142,7 +1315,7 @@ export async function verifyRelease(options) {
1142
1315
  installationContractDigest: currentDigest,
1143
1316
  });
1144
1317
 
1145
- continue;
1318
+ return;
1146
1319
  }
1147
1320
  }
1148
1321
 
@@ -1305,7 +1478,12 @@ export async function verifyRelease(options) {
1305
1478
  );
1306
1479
  }
1307
1480
  }
1308
- }
1481
+ }));
1482
+ adapterChecks.sort((a, b) => a.actionId.localeCompare(b.actionId));
1483
+ consumerVerificationReceipts.sort((a, b) => a.actionId.localeCompare(b.actionId));
1484
+ consumerGateResults.sort((a, b) => a.id.localeCompare(b.id));
1485
+ const rejectedAction = actionResults.find((result) => result.status === 'rejected');
1486
+ if (rejectedAction) throw rejectedAction.reason;
1309
1487
 
1310
1488
  await evidence.append({ phase: 'verify', step: 'adapter-verify', status: 'completed' });
1311
1489
 
@@ -1494,6 +1672,7 @@ export async function verifyRelease(options) {
1494
1672
  gateResults: consumerGateResults,
1495
1673
  consumerVerificationReceipts,
1496
1674
  skillResourceClosureReceipts,
1675
+ ...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
1497
1676
  startedAt: clockFn(),
1498
1677
  finishedAt: clockFn(),
1499
1678
  };
@@ -1517,6 +1696,7 @@ export async function verifyRelease(options) {
1517
1696
  adapterChecks,
1518
1697
  smokeTest,
1519
1698
  gateResults: consumerGateResults,
1699
+ ...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
1520
1700
  };
1521
1701
  } catch (err) {
1522
1702
  await evidence.append({