release-skill 0.3.0 → 0.4.1

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 (81) hide show
  1. package/.claude-plugin/marketplace.json +6 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codebuddy-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +4 -4
  5. package/.kimi-plugin/plugin.json +3 -3
  6. package/CHANGELOG.md +36 -3
  7. package/INSTALL.md +34 -110
  8. package/INSTALL.zh-CN.md +17 -83
  9. package/LICENSE +183 -18
  10. package/NOTICE +4 -2
  11. package/README.md +61 -75
  12. package/README.zh-CN.md +48 -63
  13. package/adapters/claude/.claude-plugin/marketplace.json +6 -2
  14. package/adapters/claude/.claude-plugin/plugin.json +2 -2
  15. package/adapters/claude/bin/release-skill.bundle.mjs +157 -231
  16. package/adapters/claude/schemas/release-plan.schema.json +7 -0
  17. package/adapters/claude/schemas/release-run.schema.json +43 -0
  18. package/adapters/claude/skills/release-help/SKILL.md +5 -5
  19. package/adapters/claude/skills/release-prepare/SKILL.md +6 -15
  20. package/adapters/claude/skills/release-publish/SKILL.md +5 -6
  21. package/adapters/claude/skills/release-reconcile/SKILL.md +2 -2
  22. package/adapters/claude/skills/release-setup/SKILL.md +6 -0
  23. package/adapters/claude/skills/release-verify/SKILL.md +4 -6
  24. package/adapters/codex/.codex-plugin/plugin.json +4 -4
  25. package/adapters/codex/bin/release-skill.bundle.mjs +157 -231
  26. package/adapters/codex/schemas/release-plan.schema.json +7 -0
  27. package/adapters/codex/schemas/release-run.schema.json +43 -0
  28. package/adapters/codex/skills/release-help/SKILL.md +5 -5
  29. package/adapters/codex/skills/release-prepare/SKILL.md +6 -15
  30. package/adapters/codex/skills/release-publish/SKILL.md +5 -6
  31. package/adapters/codex/skills/release-reconcile/SKILL.md +2 -2
  32. package/adapters/codex/skills/release-setup/SKILL.md +6 -0
  33. package/adapters/codex/skills/release-verify/SKILL.md +4 -6
  34. package/adapters/kimi/.kimi-plugin/plugin.json +3 -3
  35. package/adapters/kimi/bin/release-skill.bundle.mjs +157 -231
  36. package/adapters/kimi/schemas/release-plan.schema.json +7 -0
  37. package/adapters/kimi/schemas/release-run.schema.json +43 -0
  38. package/adapters/kimi/skills/release-help/SKILL.md +5 -5
  39. package/adapters/kimi/skills/release-prepare/SKILL.md +6 -15
  40. package/adapters/kimi/skills/release-publish/SKILL.md +5 -6
  41. package/adapters/kimi/skills/release-reconcile/SKILL.md +2 -2
  42. package/adapters/kimi/skills/release-setup/SKILL.md +6 -0
  43. package/adapters/kimi/skills/release-verify/SKILL.md +4 -6
  44. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +2 -2
  45. package/adapters/workbuddy/bin/release-skill.bundle.mjs +157 -231
  46. package/adapters/workbuddy/schemas/release-plan.schema.json +7 -0
  47. package/adapters/workbuddy/schemas/release-run.schema.json +43 -0
  48. package/adapters/workbuddy/skills/release-help/SKILL.md +5 -5
  49. package/adapters/workbuddy/skills/release-prepare/SKILL.md +6 -15
  50. package/adapters/workbuddy/skills/release-publish/SKILL.md +5 -6
  51. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +2 -2
  52. package/adapters/workbuddy/skills/release-setup/SKILL.md +6 -0
  53. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -6
  54. package/bin/release-skill-cli.mjs +45 -48
  55. package/bin/release-skill.bundle.mjs +157 -231
  56. package/package.json +2 -2
  57. package/references/01-state-machine.md +1 -1
  58. package/references/06-adapter-contract.md +6 -5
  59. package/schemas/release-plan.schema.json +7 -0
  60. package/schemas/release-run.schema.json +43 -0
  61. package/skills/release-help/SKILL.md +5 -5
  62. package/skills/release-prepare/SKILL.md +6 -15
  63. package/skills/release-publish/SKILL.md +5 -6
  64. package/skills/release-reconcile/SKILL.md +2 -2
  65. package/skills/release-setup/SKILL.md +6 -0
  66. package/skills/release-verify/SKILL.md +4 -6
  67. package/skills-src/release-help/SKILL.md +5 -5
  68. package/skills-src/release-prepare/SKILL.md +6 -15
  69. package/skills-src/release-publish/SKILL.md +5 -6
  70. package/skills-src/release-reconcile/SKILL.md +2 -2
  71. package/skills-src/release-setup/SKILL.md +6 -0
  72. package/skills-src/release-verify/SKILL.md +4 -6
  73. package/src/adapters/npm.mjs +31 -0
  74. package/src/commands/approve.mjs +8 -6
  75. package/src/commands/hooks.mjs +4 -8
  76. package/src/commands/prepare.mjs +13 -50
  77. package/src/commands/publish.mjs +0 -8
  78. package/src/commands/reconcile.mjs +3 -37
  79. package/src/commands/ship.mjs +55 -77
  80. package/src/commands/verify.mjs +56 -19
  81. package/src/core/skill-resource-closure.mjs +7 -1
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
4
4
  import { loadProjectConfig } from '../core/config.mjs';
5
5
  import { createEvidenceWriter } from '../core/evidence.mjs';
6
6
  import { runDeclaredHooks } from './prepare.mjs';
7
- import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
7
+ // ReleaseError and GATE_FAILED removed: no authorization gate remains.
8
8
 
9
9
  /**
10
10
  * Run the declared development gates and populate the exact same content-bound
@@ -14,16 +14,12 @@ import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
14
14
  export async function validateDeclaredHooks(options = {}) {
15
15
  const {
16
16
  root = process.cwd(),
17
- hooksAuthorized,
17
+ hooksAuthorized: _hooksAuthorized,
18
18
  hookCache = true,
19
19
  runDir = resolve(root, '.release-skill', 'runs', `hooks-${Date.now()}`),
20
20
  } = options;
21
- if (hooksAuthorized !== true) {
22
- throw new ReleaseError(
23
- GATE_FAILED,
24
- 'hooks validate executes declared project commands; pass --acknowledge-hook-side-effects',
25
- );
26
- }
21
+ // The command invocation itself authorizes execution of configured hooks.
22
+ // Old --acknowledge-hook-side-effects is accepted as a no-effect compatibility input.
27
23
  const { config, configDigest } = await loadProjectConfig({ root });
28
24
  await mkdir(runDir, { recursive: true });
29
25
  const evidence = createEvidenceWriter({
@@ -1809,41 +1809,16 @@ export async function prepareRelease(options) {
1809
1809
  cwd: hook.cwd ?? '.',
1810
1810
  }));
1811
1811
 
1812
+ // Hook and gate authorization: the command invocation itself authorizes
1813
+ // execution of configured commands. Old acknowledgement parameters
1814
+ // (--acknowledge-hook-side-effects, --acknowledge-gate-side-effects) are
1815
+ // accepted as no-effect compatibility inputs but are not required.
1812
1816
  if (declaredHooks.length > 0) {
1813
- await evidence.append({
1814
- phase: 'hook-authorization',
1815
- status: 'started',
1816
- hookCount: declaredHooks.length,
1817
- hooks: declaredHooks.map((h) => `${h.name}: ${h.executable} ${h.args.join(' ')}`),
1818
- });
1819
-
1820
- if (hooksAuthorized !== true) {
1821
- const hookList = declaredHooks
1822
- .map((h) => ` - ${h.name}: executable="${h.executable}", args=[${h.args.join(', ')}], cwd="${h.cwd}"`)
1823
- .join('\n');
1824
-
1825
- await evidence.append({
1826
- phase: 'hook-authorization',
1827
- status: 'denied',
1828
- reason: 'hooks not explicitly authorized',
1829
- });
1830
-
1831
- throw new ReleaseError(
1832
- GATE_FAILED,
1833
- `project declares ${declaredHooks.length} hook(s) that will be executed as arbitrary local processes.\n` +
1834
- `These hooks are NOT sandboxed — they may write to the filesystem outside the project, ` +
1835
- `access local credentials, or make network calls.\n` +
1836
- `The following hooks will run:\n${hookList}\n\n` +
1837
- `To proceed, pass --acknowledge-hook-side-effects (CLI) or hooksAuthorized=true (API). ` +
1838
- `Authorization means you accept hook side-effect risks; it does NOT make hooks safe.`,
1839
- { hookNames: declaredHooks.map((h) => h.name), hookCount: declaredHooks.length },
1840
- );
1841
- }
1842
-
1843
1817
  await evidence.append({
1844
1818
  phase: 'hook-authorization',
1845
1819
  status: 'authorized',
1846
1820
  hookCount: declaredHooks.length,
1821
+ hooks: declaredHooks.map((h) => `${h.name}: ${h.executable} ${h.args.join(' ')}`),
1847
1822
  });
1848
1823
  }
1849
1824
 
@@ -1851,7 +1826,7 @@ export async function prepareRelease(options) {
1851
1826
  if (declaredVerificationGates.length > 0) {
1852
1827
  await evidence.append({
1853
1828
  phase: 'verification-gate-authorization',
1854
- status: 'started',
1829
+ status: 'authorized',
1855
1830
  gateCount: declaredVerificationGates.length,
1856
1831
  gates: declaredVerificationGates.map((gate) => ({
1857
1832
  id: gate.id,
@@ -1863,25 +1838,6 @@ export async function prepareRelease(options) {
1863
1838
  cwd: gate.cwd,
1864
1839
  })),
1865
1840
  });
1866
- if (verificationGatesAuthorized !== true) {
1867
- await evidence.append({
1868
- phase: 'verification-gate-authorization',
1869
- status: 'denied',
1870
- gateCount: declaredVerificationGates.length,
1871
- });
1872
- throw new ReleaseError(
1873
- GATE_FAILED,
1874
- `project declares ${declaredVerificationGates.length} verification gate(s). ` +
1875
- 'They run local project commands without a network sandbox. ' +
1876
- 'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
1877
- { gateIds: declaredVerificationGates.map((gate) => gate.id) },
1878
- );
1879
- }
1880
- await evidence.append({
1881
- phase: 'verification-gate-authorization',
1882
- status: 'authorized',
1883
- gateCount: declaredVerificationGates.length,
1884
- });
1885
1841
  }
1886
1842
 
1887
1843
  // --- Step 3: Run declared hooks ---
@@ -2848,6 +2804,12 @@ export async function prepareRelease(options) {
2848
2804
  // Compute overall snapshot digest
2849
2805
  const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
2850
2806
 
2807
+ // Detect human consumer platforms (Kimi/CodeBuddy) in the plan.
2808
+ // When present, new plans mark them as non-blocking manual follow-up tasks.
2809
+ const hasHumanConsumerActions = externalActions.some(
2810
+ (a) => a.type === 'kimi-marketplace-install' || a.type === 'codebuddy-marketplace-install',
2811
+ );
2812
+
2851
2813
  const plan = {
2852
2814
  planVersion: 2,
2853
2815
  status: 'PREPARED',
@@ -2870,6 +2832,7 @@ export async function prepareRelease(options) {
2870
2832
  },
2871
2833
  verificationGates: config.verificationGates ?? [],
2872
2834
  snapshotDigest: overallSnapshotDigest,
2835
+ ...(hasHumanConsumerActions ? { humanConsumersStrategy: 'manualFollowUps' } : {}),
2873
2836
  ...(production ? {
2874
2837
  production: {
2875
2838
  mode: 'github-npm-v1',
@@ -465,7 +465,6 @@ export async function publishRelease(options) {
465
465
  clock: clockOpt,
466
466
  captureBaselineFn,
467
467
  productionMode = false,
468
- productionConfirmation,
469
468
  observePreviousPublicBaselineFn,
470
469
  observeRetrySleep,
471
470
  } = options ?? {};
@@ -544,13 +543,6 @@ export async function publishRelease(options) {
544
543
  if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
545
544
  throw new ReleaseError(GATE_FAILED, 'production plan requires a dedicated assetRoot');
546
545
  }
547
- if (!productionConfirmation || productionConfirmation !== actualDigest) {
548
- throw new ReleaseError(
549
- GATE_FAILED,
550
- 'production confirmation must exactly match the current plan digest',
551
- { planDigest: actualDigest },
552
- );
553
- }
554
546
  }
555
547
 
556
548
  // =======================================================================
@@ -91,7 +91,6 @@ function defaultClock() {
91
91
  * @param {string} options.planPath - Absolute path to the frozen release plan.
92
92
  * @param {string} options.sourceRunPath - Absolute path to the source run (publish or prior reconcile).
93
93
  * @param {string} [options.approvalPath] - Path to the approval record (required if any action needs retry).
94
- * @param {string} [options.productionConfirmation] - Exact plan digest required before retrying production writes.
95
94
  * @param {Object} options.adapterRegistry - Adapter registry for action execution.
96
95
  * @param {string} [options.runDir] - Evidence directory. Defaults to `<planDir>/runs/reconcile-<ts>`.
97
96
  * @param {string} [options.root] - Project root for baseline capture.
@@ -114,7 +113,6 @@ export async function reconcileRelease(options) {
114
113
  root = process.cwd(),
115
114
  clock: clockOpt,
116
115
  captureBaselineFn,
117
- productionConfirmation,
118
116
  observePreviousPublicBaselineFn,
119
117
  observeRetrySleep,
120
118
  } = options ?? {};
@@ -332,29 +330,6 @@ export async function reconcileRelease(options) {
332
330
  );
333
331
  }
334
332
 
335
- // A production retry authority must be confirmed before any remote
336
- // observation can influence whether a permanent write will be retried.
337
- // Isolated marketplace consumer checks remain outside this requirement.
338
- const planActionsById = new Map(
339
- (plan.externalActions ?? []).map((action) => [action.id, action]),
340
- );
341
- const hasNonMarketplaceRetryCandidate = sourceRun.checkpoints.some((checkpoint) => {
342
- if (checkpoint.status === 'succeeded') return false;
343
- const action = planActionsById.get(checkpoint.actionId);
344
- return action && !isMarketplaceAction(action.type);
345
- });
346
- if (
347
- plan.production?.mode === 'github-npm-v1' &&
348
- hasNonMarketplaceRetryCandidate &&
349
- productionConfirmation !== actualDigest
350
- ) {
351
- throw new ReleaseError(
352
- GATE_FAILED,
353
- 'production reconcile confirmation must exactly match the current plan digest before retry',
354
- { planDigest: actualDigest },
355
- );
356
- }
357
-
358
333
  await evidence.append({
359
334
  phase: 'safety-gate',
360
335
  gate: 'source-run-load',
@@ -918,7 +893,7 @@ export async function reconcileRelease(options) {
918
893
  // --- Phase 2: Validate approval and global preflight before retrying ---
919
894
  //
920
895
  // Marketplace recovery is deferred to phase 4 and does NOT require
921
- // approval or productionConfirmation; marketplace actions are isolated
896
+ // approval; marketplace actions are isolated
922
897
  // consumer checks, not permanent remote writes. Only non-marketplace
923
898
  // retries require approval.
924
899
  // =======================================================================
@@ -926,17 +901,8 @@ export async function reconcileRelease(options) {
926
901
  const nonMarketplaceRetries = actionsToRetry.filter((a) => !isMarketplaceAction(a.type));
927
902
 
928
903
  if (nonMarketplaceRetries.length > 0) {
929
- // Non-marketplace retries require approval and productionConfirmation
930
- if (
931
- plan.production?.mode === 'github-npm-v1' &&
932
- productionConfirmation !== actualDigest
933
- ) {
934
- throw new ReleaseError(
935
- GATE_FAILED,
936
- 'production reconcile confirmation must exactly match the current plan digest before retry',
937
- { planDigest: actualDigest, actionsToRetry: nonMarketplaceRetries.map((action) => action.id) },
938
- );
939
- }
904
+ // Non-marketplace retries require a current immutable approval. The
905
+ // approval already binds the exact plan digest and action set.
940
906
  if (!approval) {
941
907
  throw new ReleaseError(
942
908
  GATE_FAILED,
@@ -90,8 +90,7 @@ function publicState(state) {
90
90
  status: state.status,
91
91
  statePath: state.statePath,
92
92
  targetVersion: state.targetVersion,
93
- ...(state.hookAuthorizationDigest ? {
94
- hookAuthorizationDigest: state.hookAuthorizationDigest,
93
+ ...(state.hooks && state.hooks.length > 0 ? {
95
94
  hooks: state.hooks,
96
95
  } : {}),
97
96
  ...(state.planPath ? {
@@ -100,34 +99,52 @@ function publicState(state) {
100
99
  evidenceDir: state.evidenceDir,
101
100
  warnings: state.warnings ?? [],
102
101
  } : {}),
102
+ ...(state.approvalSummary ? { approvalSummary: state.approvalSummary } : {}),
103
103
  ...(state.approvalPath ? { approvalPath: state.approvalPath } : {}),
104
104
  ...(state.sourceRunPath ? { sourceRunPath: state.sourceRunPath } : {}),
105
105
  ...(state.requirements ? { requirements: state.requirements } : {}),
106
+ ...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
106
107
  ...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
107
- verificationGateAuthorizationIncludedInPlanApproval:
108
- state.status !== 'NEEDS_HOOK_AUTHORIZATION',
109
- postVerifyMetadataUpdateIncludedInPlanApproval:
110
- state.status !== 'NEEDS_HOOK_AUTHORIZATION',
108
+ verificationGateAuthorizationIncludedInPlanApproval: true,
109
+ postVerifyMetadataUpdateIncludedInPlanApproval: true,
111
110
  };
112
111
  }
113
112
 
114
- function hookAuthority(loaded, targetVersion) {
115
- const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
116
- return {
117
- hooks,
118
- digest: sha256Hex(canonicalJson({
119
- kind: 'release-skill-hook-authorization/v1',
120
- configDigest: loaded.configDigest,
121
- hooks: loaded.config.hooks ?? {},
122
- targetVersion: targetVersion ?? null,
123
- })),
124
- };
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
+ }
125
137
  }
126
138
 
127
139
  /**
128
140
  * Advance one durable production release. Re-running is safe: the state file
129
141
  * carries the immutable plan, approval and source-run paths so the command
130
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'.
131
148
  */
132
149
  export async function advanceShip(options = {}, injected = {}) {
133
150
  const root = resolve(options.root ?? process.cwd());
@@ -144,69 +161,25 @@ export async function advanceShip(options = {}, injected = {}) {
144
161
 
145
162
  if (!state) {
146
163
  const loaded = await deps.loadProjectConfig({ root });
147
- const authority = hookAuthority(loaded, options.targetVersion);
148
- const hooks = authority.hooks;
149
- const hookAuthorizationDigest = authority.digest;
164
+ const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
150
165
  state = {
151
- schemaVersion: 1,
166
+ schemaVersion: 2,
152
167
  root,
153
168
  statePath,
154
169
  targetVersion: options.targetVersion ?? null,
155
170
  configDigest: loaded.configDigest,
156
171
  hooks,
157
- hookAuthorizationDigest,
158
- status: hooks.length > 0 ? 'NEEDS_HOOK_AUTHORIZATION' : 'NEW',
172
+ status: 'NEW',
159
173
  createdAt: new Date().toISOString(),
160
174
  updatedAt: new Date().toISOString(),
161
175
  };
162
176
  await writeJsonAtomic(statePath, state);
163
- if (hooks.length > 0 && options.hookAuthorizationDigest !== hookAuthorizationDigest) {
164
- return publicState(state);
165
- }
166
177
  }
167
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.
168
182
  if (state.status === 'NEEDS_HOOK_AUTHORIZATION') {
169
- const loaded = await deps.loadProjectConfig({ root });
170
- const current = hookAuthority(loaded, state.targetVersion);
171
- if (
172
- loaded.configDigest !== state.configDigest
173
- || current.digest !== state.hookAuthorizationDigest
174
- ) {
175
- state = {
176
- ...state,
177
- configDigest: loaded.configDigest,
178
- hooks: current.hooks,
179
- hookAuthorizationDigest: current.digest,
180
- updatedAt: new Date().toISOString(),
181
- };
182
- await writeJsonAtomic(statePath, state);
183
- if (verified.status === 'VERIFIED' && deps.updatePreviousPublicBaselines) {
184
- try {
185
- state.metadataUpdate = await deps.updatePreviousPublicBaselines({
186
- root,
187
- planPath: state.planPath,
188
- });
189
- } catch (error) {
190
- state.metadataUpdate = {
191
- status: 'FAILED',
192
- error: error.message,
193
- };
194
- }
195
- state.updatedAt = new Date().toISOString();
196
- await writeJsonAtomic(statePath, state);
197
- }
198
- return publicState(state);
199
- }
200
- if (options.hookAuthorizationDigest !== state.hookAuthorizationDigest) {
201
- if (options.hookAuthorizationDigest) {
202
- throw new ReleaseError(
203
- PLAN_DIGEST_MISMATCH,
204
- 'hook authorization digest does not match the current config, hooks and target version',
205
- );
206
- }
207
- return publicState(state);
208
- }
209
- state.hookAuthorizedBy = options.actor ?? null;
210
183
  state.status = 'NEW';
211
184
  state.updatedAt = new Date().toISOString();
212
185
  await writeJsonAtomic(statePath, state);
@@ -215,17 +188,15 @@ export async function advanceShip(options = {}, injected = {}) {
215
188
  if (state.status === 'NEW') {
216
189
  const loaded = await deps.loadProjectConfig({ root });
217
190
  if (loaded.configDigest !== state.configDigest) {
218
- const current = hookAuthority(loaded, state.targetVersion);
191
+ const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
219
192
  state = {
220
193
  ...state,
221
194
  configDigest: loaded.configDigest,
222
- hooks: current.hooks,
223
- hookAuthorizationDigest: current.digest,
224
- status: current.hooks.length > 0 ? 'NEEDS_HOOK_AUTHORIZATION' : 'NEW',
195
+ hooks,
196
+ status: 'NEW',
225
197
  updatedAt: new Date().toISOString(),
226
198
  };
227
199
  await writeJsonAtomic(statePath, state);
228
- if (state.status === 'NEEDS_HOOK_AUTHORIZATION') return publicState(state);
229
200
  }
230
201
  const prepared = await deps.prepareRelease({
231
202
  root,
@@ -233,7 +204,7 @@ export async function advanceShip(options = {}, injected = {}) {
233
204
  offline: false,
234
205
  production: true,
235
206
  hooksAuthorized: true,
236
- verificationGatesAuthorized: false,
207
+ verificationGatesAuthorized: true,
237
208
  hookCache: true,
238
209
  });
239
210
  let transportPreflight = null;
@@ -242,6 +213,7 @@ export async function advanceShip(options = {}, injected = {}) {
242
213
  transportPreflight = await deps.preflightGitTransports(frozenPlan);
243
214
  process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
244
215
  }
216
+ const approvalSummary = await buildApprovalSummary(prepared.planPath);
245
217
  state = {
246
218
  ...state,
247
219
  status: 'NEEDS_PLAN_APPROVAL',
@@ -249,6 +221,7 @@ export async function advanceShip(options = {}, injected = {}) {
249
221
  planDigest: prepared.planDigest,
250
222
  evidenceDir: prepared.evidenceDir,
251
223
  warnings: prepared.warnings,
224
+ approvalSummary,
252
225
  ...(transportPreflight ? {
253
226
  gitTransport: transportPreflight.transport,
254
227
  gitTransportPreflight: transportPreflight.repositories,
@@ -259,8 +232,14 @@ export async function advanceShip(options = {}, injected = {}) {
259
232
  }
260
233
 
261
234
  if (state.status === 'NEEDS_PLAN_APPROVAL') {
262
- if (!options.planApprovalDigest) return publicState(state);
263
- if (options.planApprovalDigest !== state.planDigest) {
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) {
264
243
  throw new ReleaseError(PLAN_DIGEST_MISMATCH, 'ship plan approval digest does not match the frozen plan');
265
244
  }
266
245
  if (!options.actor) {
@@ -294,7 +273,6 @@ export async function advanceShip(options = {}, injected = {}) {
294
273
  adapterRegistry: options.adapterRegistry,
295
274
  root,
296
275
  productionMode: true,
297
- productionConfirmation: state.planDigest,
298
276
  });
299
277
  state = {
300
278
  ...state,
@@ -312,7 +290,6 @@ export async function advanceShip(options = {}, injected = {}) {
312
290
  approvalPath: state.approvalPath,
313
291
  adapterRegistry: options.adapterRegistry,
314
292
  root,
315
- productionConfirmation: state.planDigest,
316
293
  });
317
294
  state = {
318
295
  ...state,
@@ -337,6 +314,7 @@ export async function advanceShip(options = {}, injected = {}) {
337
314
  status: verified.status,
338
315
  verifyRunPath: verified.runPath,
339
316
  requirements: undefined,
317
+ manualFollowUps: verified.manualFollowUps ?? undefined,
340
318
  updatedAt: new Date().toISOString(),
341
319
  };
342
320
  await writeJsonAtomic(statePath, state);
@@ -755,7 +755,7 @@ export async function verifyRelease(options) {
755
755
  runDir: runDirOpt,
756
756
  clock: clockOpt,
757
757
  npmExecutor,
758
- verificationGatesAuthorized,
758
+ verificationGatesAuthorized: _verificationGatesAuthorized,
759
759
  gateEnv,
760
760
  previousVerifyRun,
761
761
  } = options ?? {};
@@ -826,16 +826,9 @@ export async function verifyRelease(options) {
826
826
  .filter((distribution) => distribution.type === 'npm' && distribution.smokeBin)
827
827
  .map((distribution) => ({ unitId: unit.id, smokeBin: distribution.smokeBin }))
828
828
  ));
829
- if ((consumerGates.length > 0 || configuredSmokeBins.length > 0) && verificationGatesAuthorized !== true) {
830
- throw new ReleaseError(
831
- GATE_FAILED,
832
- `plan declares ${consumerGates.length} consumer verification gate(s) and ` +
833
- `${configuredSmokeBins.length} npm CLI smoke process(es). ` +
834
- 'They execute installed project code without an OS or network sandbox. ' +
835
- 'To proceed, pass --acknowledge-gate-side-effects (CLI) or verificationGatesAuthorized=true (API).',
836
- { gateIds: consumerGates.map((gate) => gate.id), configuredSmokeBins },
837
- );
838
- }
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.
839
832
 
840
833
  await evidence.append({ phase: 'verify', step: 'plan-load', status: 'passed' });
841
834
 
@@ -1112,14 +1105,21 @@ export async function verifyRelease(options) {
1112
1105
  }
1113
1106
  }
1114
1107
 
1115
- const missingManualAttestations = await collectMissingManualAttestations({
1116
- actions,
1117
- adapterRegistry,
1118
- plan,
1119
- root,
1120
- runDir,
1121
- clockFn,
1122
- });
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
1123
  if (missingManualAttestations.length > 0) {
1124
1124
  await evidence.append({
1125
1125
  phase: 'verify',
@@ -1134,6 +1134,9 @@ export async function verifyRelease(options) {
1134
1134
  );
1135
1135
  }
1136
1136
 
1137
+ // Collect non-blocking manual follow-up tasks for human consumer platforms
1138
+ const manualFollowUps = [];
1139
+
1137
1140
  // Every action is identity-bound and adapters receive read-only or
1138
1141
  // per-action isolated consumer paths. Run independent checks concurrently;
1139
1142
  // the evidence writer serializes append operations and result arrays are
@@ -1165,6 +1168,38 @@ export async function verifyRelease(options) {
1165
1168
  }
1166
1169
 
1167
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
+
1168
1203
  // --- Marketplace: fresh consumer verification in verify's own runDir ---
1169
1204
  // Context: isolatedConsumerWritesAuthorized allows writing to verify's
1170
1205
  // runDir/consumers/ directory; externalWritesAuthorized stays false.
@@ -1637,6 +1672,7 @@ export async function verifyRelease(options) {
1637
1672
  gateResults: consumerGateResults,
1638
1673
  consumerVerificationReceipts,
1639
1674
  skillResourceClosureReceipts,
1675
+ ...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
1640
1676
  startedAt: clockFn(),
1641
1677
  finishedAt: clockFn(),
1642
1678
  };
@@ -1660,6 +1696,7 @@ export async function verifyRelease(options) {
1660
1696
  adapterChecks,
1661
1697
  smokeTest,
1662
1698
  gateResults: consumerGateResults,
1699
+ ...(manualFollowUps.length > 0 ? { manualFollowUps } : {}),
1663
1700
  };
1664
1701
  } catch (err) {
1665
1702
  await evidence.append({
@@ -275,10 +275,16 @@ export function evaluateConsumerSkillResourceClosureReceipts(plan, receipts) {
275
275
  'kimi-plugin',
276
276
  'codebuddy-plugin',
277
277
  ]);
278
+ // When plan declares humanConsumersStrategy: 'manualFollowUps', Kimi/CodeBuddy
279
+ // are non-blocking manual follow-up tasks and are not verified by the system.
280
+ // Exclude them from the expected receipt set.
281
+ const humanConsumerTypes = plan.humanConsumersStrategy === 'manualFollowUps'
282
+ ? new Set(['kimi-plugin', 'codebuddy-plugin'])
283
+ : new Set();
278
284
  const expectedKeys = [];
279
285
  for (const unit of plan.units ?? []) {
280
286
  for (const distribution of unit.distributions ?? []) {
281
- if (supported.has(distribution.type)) {
287
+ if (supported.has(distribution.type) && !humanConsumerTypes.has(distribution.type)) {
282
288
  expectedKeys.push(`${unit.id}:${distribution.type}`);
283
289
  }
284
290
  }