release-skill 0.2.3 → 0.2.5

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 (55) 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 +46 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +266 -913
  11. package/README.zh-CN.md +224 -535
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +19805 -17556
  15. package/adapters/claude/schemas/release-plan.schema.json +228 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +155 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
  20. package/adapters/codex/schemas/release-plan.schema.json +228 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +155 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
  25. package/adapters/kimi/schemas/release-plan.schema.json +228 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +155 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
  33. package/bin/release-skill.bundle.mjs +19805 -17556
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +228 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +155 -2
  38. package/scripts/sync-public-files.mjs +20 -9
  39. package/src/adapters/plugin-marketplace.mjs +1237 -403
  40. package/src/commands/prepare.mjs +454 -40
  41. package/src/commands/publish.mjs +169 -75
  42. package/src/commands/reconcile.mjs +92 -327
  43. package/src/commands/setup.mjs +148 -20
  44. package/src/commands/verify.mjs +454 -20
  45. package/src/core/baseline.mjs +8 -1
  46. package/src/core/checkpoints.mjs +50 -7
  47. package/src/core/config.mjs +15 -0
  48. package/src/core/errors.mjs +2 -0
  49. package/src/core/installation-contract.mjs +341 -0
  50. package/src/core/plan.mjs +158 -6
  51. package/src/core/skill-resource-closure.mjs +425 -0
  52. package/src/platforms/codebuddy.mjs +206 -280
  53. package/src/platforms/codex.mjs +369 -0
  54. package/src/platforms/kimi.mjs +191 -119
  55. package/src/platforms/registry.mjs +24 -6
@@ -12,8 +12,9 @@
12
12
  * @module commands/verify
13
13
  */
14
14
 
15
- import { readFile, writeFile, mkdtemp, rm, mkdir, lstat, realpath } from 'node:fs/promises';
16
- import { dirname, join, relative, isAbsolute, resolve } from 'node:path';
15
+ import { readFile, writeFile, mkdtemp, rm, mkdir, lstat, realpath, readdir } from 'node:fs/promises';
16
+ import { realpathSync } from 'node:fs';
17
+ import { dirname, join, relative, isAbsolute, resolve, basename } from 'node:path';
17
18
  import { tmpdir } from 'node:os';
18
19
  import { execFile as execFileCb } from 'node:child_process';
19
20
  import { promisify } from 'node:util';
@@ -50,11 +51,34 @@ import {
50
51
  resolveNpmRegistryAuthToken,
51
52
  } from '../adapters/npm.mjs';
52
53
  import { runConsumerVerificationGates } from '../core/verification-gates.mjs';
54
+ import {
55
+ checkSkillResourceClosure,
56
+ createSkillResourceClosureReceipt,
57
+ evaluateConsumerSkillResourceClosureReceipts,
58
+ } from '../core/skill-resource-closure.mjs';
59
+ import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
60
+ import {
61
+ shouldSkipVerification,
62
+ INSTALLATION_CONTRACT_ALGORITHM_VERSION,
63
+ } from '../core/installation-contract.mjs';
53
64
 
54
65
  // ---------------------------------------------------------------------------
55
66
  // Constants
56
67
  // ---------------------------------------------------------------------------
57
68
 
69
+ /**
70
+ * 验证已解决结果类型。
71
+ *
72
+ * - PASSED_AUTOMATIC: 自动验证通过(adapter.verify 返回 VERIFIED)
73
+ * - PASSED_MANUAL: 人工验证通过(用户手动确认)
74
+ * - NOT_REQUIRED_UNCHANGED: 无需验证(远端状态未变化,跳过验证)
75
+ */
76
+ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
77
+ PASSED_AUTOMATIC: 'PASSED_AUTOMATIC',
78
+ PASSED_MANUAL: 'PASSED_MANUAL',
79
+ NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
80
+ });
81
+
58
82
  /**
59
83
  * Map plan action type to adapter ActionType.
60
84
  * Must match publish.mjs and reconcile.mjs.
@@ -80,6 +104,16 @@ function defaultClock() {
80
104
  return new Date().toISOString();
81
105
  }
82
106
 
107
+ /**
108
+ * 验证摘要格式是否为合法的 64 位十六进制字符串。
109
+ *
110
+ * @param {string} digest - 摘要
111
+ * @returns {boolean} 是否合法
112
+ */
113
+ function isValidDigest(digest) {
114
+ return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
115
+ }
116
+
83
117
  // ---------------------------------------------------------------------------
84
118
  // Smoke test
85
119
  // ---------------------------------------------------------------------------
@@ -187,11 +221,13 @@ export async function runSmokeTest(plan, root, options = {}) {
187
221
  skipped: true,
188
222
  details: { message: 'No npm distributions in plan; smoke test skipped' },
189
223
  gateResults: [],
224
+ skillResourceClosureReceipts: [],
190
225
  };
191
226
  }
192
227
 
193
228
  const results = [];
194
229
  const gateResults = [];
230
+ const skillResourceClosureReceipts = [];
195
231
 
196
232
  for (const { package: pkgName, registry, targetVersion, unitId, smokeBin, smokeArgs, smokeExpectedJson } of npmDistributions) {
197
233
  const packageAtVersion = `${pkgName}@${targetVersion}`;
@@ -256,6 +292,47 @@ export async function runSmokeTest(plan, root, options = {}) {
256
292
  }
257
293
 
258
294
  const pkgRoot = join(installDir, 'node_modules', pkgName);
295
+ if (plan.skillResourceClosure) {
296
+ const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
297
+ .find((item) => item.unitId === unitId);
298
+ if (!expectedUnitReceipt) {
299
+ return {
300
+ passed: false,
301
+ details: { error: `Skill resource closure receipt missing for npm unit "${unitId}"` },
302
+ };
303
+ }
304
+ const closureResult = await checkSkillResourceClosure({
305
+ snapshotDir: pkgRoot,
306
+ host: 'npm',
307
+ });
308
+ if (closureResult.findings.length > 0) {
309
+ return {
310
+ passed: false,
311
+ details: {
312
+ error: `Skill resource closure failed for ${packageAtVersion}`,
313
+ findings: closureResult.findings,
314
+ },
315
+ };
316
+ }
317
+ if (expectedUnitReceipt.skillCount > 0 && closureResult.skillCount === 0) {
318
+ return {
319
+ passed: false,
320
+ details: {
321
+ error: `Installed npm package ${packageAtVersion} contains no skills`,
322
+ },
323
+ };
324
+ }
325
+ skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
326
+ closureResult,
327
+ {
328
+ unitId,
329
+ distribution: 'npm',
330
+ host: 'npm',
331
+ checkedAt: new Date().toISOString(),
332
+ exitCode: 0,
333
+ },
334
+ ));
335
+ }
259
336
  gateResults.push(...await runConsumerVerificationGates({
260
337
  plan,
261
338
  unitId,
@@ -442,6 +519,7 @@ export async function runSmokeTest(plan, root, options = {}) {
442
519
  count: results.length,
443
520
  },
444
521
  gateResults,
522
+ skillResourceClosureReceipts,
445
523
  };
446
524
  } finally {
447
525
  await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
@@ -519,6 +597,7 @@ const defaultNpmExecutor = {
519
597
  * @param {string} [options.root] - Project root for source access.
520
598
  * @param {string} [options.runDir] - Evidence directory.
521
599
  * @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
600
+ * @param {Object} [options.previousVerifyRun] - 上一次验证成功的 verify run 记录,用于安装契约摘要免验。
522
601
  *
523
602
  * @returns {Promise<{ planPath: string, status: string, adapterChecks: Object[], smokeTest: Object }>}
524
603
  *
@@ -536,6 +615,7 @@ export async function verifyRelease(options) {
536
615
  npmExecutor,
537
616
  verificationGatesAuthorized,
538
617
  gateEnv,
618
+ previousVerifyRun,
539
619
  } = options ?? {};
540
620
 
541
621
  const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
@@ -697,9 +777,13 @@ export async function verifyRelease(options) {
697
777
  // Validate checkpoint mapping
698
778
  validateRunCheckpointMapping(sourceRun, plan.externalActions ?? []);
699
779
 
700
- // All checkpoints must be succeeded or skipped (no failed/pending)
780
+ // All checkpoints must be succeeded or skipped (no failed/pending),
781
+ // except marketplace install checkpoints which are re-verified by verify
782
+ // itself via consumer verification (human attestation or automatic).
783
+ // Deferred marketplace checkpoints from publish are also allowed through.
701
784
  const incompleteCheckpoints = sourceRun.checkpoints.filter(
702
- (cp) => cp.status !== 'succeeded' && cp.status !== 'skipped',
785
+ (cp) => cp.status !== 'succeeded' && cp.status !== 'skipped'
786
+ && !((cp.status === 'failed' || cp.status === 'deferred') && isMarketplaceAction(cp.actionType)),
703
787
  );
704
788
  if (incompleteCheckpoints.length > 0) {
705
789
  throw new ReleaseError(
@@ -731,13 +815,130 @@ export async function verifyRelease(options) {
731
815
 
732
816
  const adapterChecks = [];
733
817
  const consumerGateResults = [];
818
+ const consumerVerificationReceipts = [];
734
819
  const actions = plan.externalActions ?? [];
735
- const MARKETPLACE_TYPES = new Set([
736
- 'claude-marketplace-install',
737
- 'codex-marketplace-install',
738
- 'kimi-marketplace-install',
739
- 'codebuddy-marketplace-install',
740
- ]);
820
+
821
+ // --- 自动发现可信消费端验证收据 ---
822
+ // 收据选择逻辑(per-action 从所有候选中选最新匹配):
823
+ // - 只读取同一权威 .release-skill/runs 的真实直接子目录
824
+ // - runs 根或候选目录不得是符号链接,不得物理越界
825
+ // - 候选必须经 loadRun(..., { requireDigest: true })、状态 VERIFIED、合法 finishedAt
826
+ // - 对当前 action 收集所有可信匹配收据,按 finishedAt 最新选择
827
+ // - 收据必须是 consumerVerificationReceipts,严格匹配 actionId/unitId/platform
828
+ // - 收据 planDigest 等于候选 run 自身 planDigest
829
+ // - 显式注入的 previousVerifyRun 也必须经过同等语义校验
830
+
831
+ /** @type {Array<Object>} 所有可信的验证 run 候选 */
832
+ const trustedVerifyRuns = [];
833
+
834
+ // 计算权威 runs 目录路径(基于 plan 的物理位置)
835
+ const planDir = dirname(planPath);
836
+ const releaseDir = basename(planDir) === 'plans' ? dirname(planDir) : planDir;
837
+ const runsDir = resolve(releaseDir, 'runs');
838
+ let runsDirReal = null;
839
+ let authorityDirReal = null;
840
+
841
+ // 校验并纳入显式注入的 previousVerifyRun
842
+ // 必须经过完整语义校验:status=VERIFIED、合法 runDigest、合法 finishedAt、
843
+ // 合法 planDigest、收据身份绑定。不满足则不注入(不参与复用),但不阻断。
844
+ if (previousVerifyRun) {
845
+ if (
846
+ previousVerifyRun.status === 'VERIFIED'
847
+ && isValidDigest(previousVerifyRun.planDigest)
848
+ && previousVerifyRun.finishedAt
849
+ && typeof previousVerifyRun.finishedAt === 'string'
850
+ && !isNaN(Date.parse(previousVerifyRun.finishedAt))
851
+ ) {
852
+ const computedRunDigest = computeRunDigest(previousVerifyRun);
853
+ if (
854
+ typeof previousVerifyRun.runDigest === 'string'
855
+ && previousVerifyRun.runDigest.length > 0
856
+ && previousVerifyRun.runDigest === computedRunDigest
857
+ ) {
858
+ trustedVerifyRuns.push(previousVerifyRun);
859
+ }
860
+ }
861
+ }
862
+
863
+ // 自动发现:从同一 .release-skill/runs 权威目录中发现
864
+ {
865
+ try {
866
+ const runsDirStat = await lstat(runsDir);
867
+ if (runsDirStat.isSymbolicLink()) {
868
+ // runs 根是符号链接:权威目录身份错误,失败关闭
869
+ throw new ReleaseError(
870
+ GATE_FAILED,
871
+ 'runs directory is a symbolic link; authority identity compromised',
872
+ { runsDir },
873
+ );
874
+ }
875
+ if (!runsDirStat.isDirectory()) {
876
+ throw new ReleaseError(
877
+ GATE_FAILED,
878
+ 'runs path is not a directory',
879
+ { runsDir },
880
+ );
881
+ }
882
+ // 以 plan 的物理权威目录为基准,验证 runs 是其中真实 runs 子目录
883
+ runsDirReal = realpathSync(runsDir);
884
+ authorityDirReal = realpathSync(releaseDir);
885
+ if (!runsDirReal.startsWith(authorityDirReal + '/') && runsDirReal !== authorityDirReal) {
886
+ throw new ReleaseError(
887
+ GATE_FAILED,
888
+ 'runs directory is not a real child of the plan authority directory',
889
+ { runsDir, runsDirReal, authorityDirReal },
890
+ );
891
+ }
892
+ } catch (err) {
893
+ if (err instanceof ReleaseError) throw err;
894
+ // runs 目录不存在等非致命情况:跳过自动发现
895
+ runsDirReal = null;
896
+ }
897
+
898
+ if (runsDirReal) {
899
+ const entries = await readdir(runsDirReal, { withFileTypes: true });
900
+ for (const entry of entries) {
901
+ if (!entry.name.startsWith('verify-')) continue;
902
+ // 非目录或符号链接:失败关闭
903
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
904
+ throw new ReleaseError(
905
+ GATE_FAILED,
906
+ 'verify-* candidate is a symbolic link or not a directory; authority identity compromised',
907
+ { entry: entry.name, runsDir: runsDirReal },
908
+ );
909
+ }
910
+ const candidateDir = resolve(runsDirReal, entry.name);
911
+ const candidateStat = await lstat(candidateDir).catch(() => null);
912
+ if (!candidateStat || candidateStat.isSymbolicLink()) {
913
+ throw new ReleaseError(
914
+ GATE_FAILED,
915
+ 'verify-* candidate is a symbolic link; authority identity compromised',
916
+ { candidateDir, runsDir: runsDirReal },
917
+ );
918
+ }
919
+ // realpath 包含性校验:候选必须在权威 runs 目录内
920
+ const candidateReal = realpathSync(candidateDir);
921
+ if (!candidateReal.startsWith(runsDirReal + '/') && candidateReal !== runsDirReal) {
922
+ throw new ReleaseError(
923
+ GATE_FAILED,
924
+ 'verify-* candidate real path is not contained in authority runs directory',
925
+ { candidateDir, candidateReal, runsDir: runsDirReal },
926
+ );
927
+ }
928
+ const candidatePath = resolve(candidateDir, 'release-run.json');
929
+ try {
930
+ const candidate = await loadRun(candidatePath, { requireDigest: true });
931
+ if (candidate.status !== 'VERIFIED') continue;
932
+ if (!candidate.planDigest) continue;
933
+ if (!candidate.finishedAt || typeof candidate.finishedAt !== 'string') continue;
934
+ trustedVerifyRuns.push(candidate);
935
+ } catch {
936
+ // 真实直接目录里的坏/缺 release-run.json 可忽略
937
+ continue;
938
+ }
939
+ }
940
+ }
941
+ }
741
942
 
742
943
  for (const action of actions) {
743
944
  const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
@@ -765,7 +966,7 @@ export async function verifyRelease(options) {
765
966
  );
766
967
  }
767
968
 
768
- if (MARKETPLACE_TYPES.has(action.type)) {
969
+ if (isMarketplaceAction(action.type)) {
769
970
  // --- Marketplace: fresh consumer verification in verify's own runDir ---
770
971
  // Context: isolatedConsumerWritesAuthorized allows writing to verify's
771
972
  // runDir/consumers/ directory; externalWritesAuthorized stays false.
@@ -783,22 +984,108 @@ export async function verifyRelease(options) {
783
984
  ...action.parameters,
784
985
  };
785
986
 
786
- // Step 3a: Preflight (validate frozen snapshot, parameters)
987
+ // --- 安装契约摘要免验检查 ---
988
+ // 从计划中读取冻结的安装契约摘要。
989
+ // 新计划在 prepare 阶段计算并冻结 installationContractDigest;
990
+ // 旧计划(无此字段)跳过免验检查,强制重新验证。
991
+ const unit = (plan.units ?? []).find((u) => u.id === action.unitId);
992
+ const typeToDist = {
993
+ 'claude-marketplace-install': 'claude-plugin',
994
+ 'codex-marketplace-install': 'codex-plugin',
995
+ 'kimi-marketplace-install': 'kimi-plugin',
996
+ 'codebuddy-marketplace-install': 'codebuddy-plugin',
997
+ };
998
+ const dist = unit?.distributions?.find((d) => d.type === typeToDist[action.type]);
999
+
1000
+ // Step 3a: Preflight(始终先执行 adapter preflight,完成静态身份、版本、
1001
+ // tag/ref/sha、来源和 payload 校验;只有 preflight 通过后才允许免验)
1002
+ // 必须明确要求 PREFLIGHT_PASSED,其他状态全部失败关闭。
787
1003
  const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
788
- if (preflightResult.status === 'PREFLIGHT_FAILED') {
1004
+ if (preflightResult.status !== 'PREFLIGHT_PASSED') {
789
1005
  adapterChecks.push({
790
1006
  actionId: action.id,
791
1007
  actionType: action.type,
792
1008
  status: 'FAILED',
793
- error: `preflight failed: ${preflightResult.error}`,
1009
+ error: `preflight did not pass: status=${preflightResult.status}, error=${preflightResult.error}`,
794
1010
  });
795
1011
  throw new ReleaseError(
796
1012
  POST_PUBLISH_VERIFY_FAILED,
797
- `marketplace preflight failed for action "${action.id}": ${preflightResult.error}`,
1013
+ `marketplace preflight did not pass for action "${action.id}": status=${preflightResult.status}, error=${preflightResult.error}`,
798
1014
  { actionId: action.id },
799
1015
  );
800
1016
  }
801
1017
 
1018
+ // Preflight 通过后,检查是否可以免验
1019
+ if (dist?.installationContractDigest) {
1020
+ const currentDigest = dist.installationContractDigest;
1021
+ const currentPlatform = action.parameters?.consumer ?? action.type.replace('-marketplace-install', '');
1022
+
1023
+ // 紧邻可信公开基线:只检查最近一个 VERIFIED run 的收据。
1024
+ // 不能从任意更老历史中捞出相同摘要来免验。
1025
+ // 若最近 run 无该 action/platform 的收据,则 REQUIRE_VERIFICATION。
1026
+ let previousActionCheck = null;
1027
+ if (trustedVerifyRuns.length > 0) {
1028
+ // 按 finishedAt 降序排序,取最近一个
1029
+ const sorted = [...trustedVerifyRuns].sort((a, b) => {
1030
+ const ta = Date.parse(a.finishedAt) || 0;
1031
+ const tb = Date.parse(b.finishedAt) || 0;
1032
+ return tb - ta;
1033
+ });
1034
+ const latest = sorted[0];
1035
+ const matchingReceipt = (latest.consumerVerificationReceipts ?? []).find(
1036
+ (r) => r.actionId === action.id
1037
+ && r.unitId === action.unitId
1038
+ && r.platform === currentPlatform
1039
+ && r.planDigest === latest.planDigest,
1040
+ );
1041
+ if (matchingReceipt) {
1042
+ previousActionCheck = { ...matchingReceipt, _runFinishedAtTime: Date.parse(latest.finishedAt) };
1043
+ }
1044
+ }
1045
+ const previousDigest = previousActionCheck?.installationContractDigest ?? null;
1046
+
1047
+ const skipDecision = shouldSkipVerification({
1048
+ currentDigest,
1049
+ previousDigest,
1050
+ previousReceipt: previousActionCheck,
1051
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1052
+ });
1053
+
1054
+ // A current-run resource-closure receipt requires a fresh isolated
1055
+ // install. Installation-contract reuse alone cannot prove the
1056
+ // installed Skill resources are reachable.
1057
+ if (skipDecision === 'NOT_REQUIRED_UNCHANGED' && !plan.skillResourceClosure) {
1058
+ adapterChecks.push({
1059
+ actionId: action.id,
1060
+ actionType: action.type,
1061
+ status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1062
+ installationContractDigest: currentDigest,
1063
+ reason: '安装契约摘要未变化,跳过验证',
1064
+ });
1065
+
1066
+ consumerVerificationReceipts.push({
1067
+ actionId: action.id,
1068
+ unitId: action.unitId,
1069
+ platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
1070
+ result: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1071
+ installationContractDigest: currentDigest,
1072
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1073
+ planDigest: plan.digest,
1074
+ verifiedAt: clockFn(),
1075
+ });
1076
+
1077
+ await evidence.append({
1078
+ phase: 'verify-marketplace',
1079
+ actionId: action.id,
1080
+ actionType: action.type,
1081
+ status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1082
+ installationContractDigest: currentDigest,
1083
+ });
1084
+
1085
+ continue;
1086
+ }
1087
+ }
1088
+
802
1089
  // Step 3b: Execute (install to isolated consumer directory)
803
1090
  const executeResult = await adapter.execute(actionInput, marketplaceContext);
804
1091
  if (executeResult.status !== 'EXECUTED') {
@@ -821,15 +1108,39 @@ export async function verifyRelease(options) {
821
1108
  marketplaceContext,
822
1109
  );
823
1110
 
1111
+ // 正确分类:人工确认 -> PASSED_MANUAL,其他自动通路 -> PASSED_AUTOMATIC
1112
+ const isHumanConfirmed = verifyResult.observation?.humanConfirmed === true;
1113
+ const resolvedStatus = verifyResult.status === 'VERIFIED'
1114
+ ? (isHumanConfirmed
1115
+ ? VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL
1116
+ : VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC)
1117
+ : 'FAILED';
1118
+
824
1119
  const check = {
825
1120
  actionId: action.id,
826
1121
  actionType: action.type,
827
- status: verifyResult.status === 'VERIFIED' ? 'PASSED' : 'FAILED',
1122
+ status: resolvedStatus,
828
1123
  observation: verifyResult.observation,
829
1124
  error: verifyResult.error,
1125
+ ...(dist?.installationContractDigest ? { installationContractDigest: dist.installationContractDigest } : {}),
830
1126
  };
831
1127
  adapterChecks.push(check);
832
1128
 
1129
+ // 持久化消费端验证收据
1130
+ // 旧计划无摘要时可不生成"安装契约复用收据",但不得写非法空字符串
1131
+ if (resolvedStatus !== 'FAILED' && dist?.installationContractDigest && /^[a-f0-9]{64}$/.test(dist.installationContractDigest)) {
1132
+ consumerVerificationReceipts.push({
1133
+ actionId: action.id,
1134
+ unitId: action.unitId,
1135
+ platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
1136
+ result: resolvedStatus,
1137
+ installationContractDigest: dist.installationContractDigest,
1138
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1139
+ planDigest: plan.digest,
1140
+ verifiedAt: clockFn(),
1141
+ });
1142
+ }
1143
+
833
1144
  await evidence.append({
834
1145
  phase: 'verify-marketplace',
835
1146
  actionId: action.id,
@@ -841,7 +1152,13 @@ export async function verifyRelease(options) {
841
1152
  throw new ReleaseError(
842
1153
  POST_PUBLISH_VERIFY_FAILED,
843
1154
  `marketplace verification failed for action "${action.id}": ${verifyResult.error}`,
844
- { actionId: action.id, observation: verifyResult.observation },
1155
+ {
1156
+ actionId: action.id,
1157
+ actionType: action.type,
1158
+ verificationResult: check.status,
1159
+ observation: verifyResult.observation,
1160
+ expected: action.expected,
1161
+ },
845
1162
  );
846
1163
  }
847
1164
 
@@ -901,7 +1218,7 @@ export async function verifyRelease(options) {
901
1218
  const check = {
902
1219
  actionId: action.id,
903
1220
  actionType: action.type,
904
- status: verifyResult.status === 'VERIFIED' ? 'PASSED' : 'FAILED',
1221
+ status: verifyResult.status === 'VERIFIED' ? VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC : 'FAILED',
905
1222
  observation: verifyResult.observation,
906
1223
  error: verifyResult.error,
907
1224
  };
@@ -919,7 +1236,13 @@ export async function verifyRelease(options) {
919
1236
  throw new ReleaseError(
920
1237
  POST_PUBLISH_VERIFY_FAILED,
921
1238
  `adapter verification failed for action "${action.id}": ${verifyResult.error}`,
922
- { actionId: action.id, observation: verifyResult.observation },
1239
+ {
1240
+ actionId: action.id,
1241
+ actionType: action.type,
1242
+ verificationResult: check.status,
1243
+ observation: verifyResult.observation,
1244
+ expected: action.expected,
1245
+ },
923
1246
  );
924
1247
  }
925
1248
  }
@@ -970,6 +1293,103 @@ export async function verifyRelease(options) {
970
1293
  );
971
1294
  }
972
1295
 
1296
+ // =======================================================================
1297
+ // Step 4b: Skill resource closure check on consumer install surfaces
1298
+ // Re-run the closure check on each declared consumer install surface.
1299
+ // If the plan declares skillResourceClosure, every declared host surface
1300
+ // must be checked and pass; undeclared or failed surfaces block VERIFIED.
1301
+ // =======================================================================
1302
+ const skillResourceClosureReceipts = [
1303
+ ...(smokeTest.skillResourceClosureReceipts ?? []),
1304
+ ];
1305
+ if (plan.skillResourceClosure) {
1306
+ await evidence.append({ phase: 'verify', step: 'skill-resource-closure', status: 'started' });
1307
+
1308
+ // Collect install paths from marketplace adapter checks
1309
+ const installSurfaces = [];
1310
+ const actionDistribution = {
1311
+ 'claude-marketplace-install': 'claude-plugin',
1312
+ 'codex-marketplace-install': 'codex-plugin',
1313
+ 'kimi-marketplace-install': 'kimi-plugin',
1314
+ 'codebuddy-marketplace-install': 'codebuddy-plugin',
1315
+ };
1316
+ for (const check of adapterChecks) {
1317
+ const distribution = actionDistribution[check.actionType];
1318
+ if (distribution && check.observation?.installPath) {
1319
+ installSurfaces.push({
1320
+ host: distribution.replace('-plugin', ''),
1321
+ distribution,
1322
+ installPath: check.observation.installPath,
1323
+ actionId: check.actionId,
1324
+ unitId: actions.find((a) => a.id === check.actionId)?.unitId,
1325
+ });
1326
+ }
1327
+ }
1328
+
1329
+ for (const surface of installSurfaces) {
1330
+ const closureResult = await checkSkillResourceClosure({
1331
+ snapshotDir: surface.installPath,
1332
+ host: surface.host,
1333
+ });
1334
+
1335
+ if (closureResult.findings.length > 0) {
1336
+ await evidence.append({
1337
+ phase: 'verify',
1338
+ step: 'skill-resource-closure',
1339
+ status: 'failed',
1340
+ host: surface.host,
1341
+ findingCount: closureResult.findings.length,
1342
+ findings: closureResult.findings,
1343
+ });
1344
+ throw new ReleaseError(
1345
+ POST_PUBLISH_VERIFY_FAILED,
1346
+ `skill resource closure check failed for ${surface.host} consumer install: ${closureResult.findings.length} finding(s)`,
1347
+ { host: surface.host, findings: closureResult.findings },
1348
+ );
1349
+ }
1350
+ const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
1351
+ .find((item) => item.unitId === surface.unitId);
1352
+ if (!expectedUnitReceipt || closureResult.skillCount === 0) {
1353
+ throw new ReleaseError(
1354
+ POST_PUBLISH_VERIFY_FAILED,
1355
+ `skill resource closure installed surface is empty or unbound for ${surface.distribution}`,
1356
+ { unitId: surface.unitId, distribution: surface.distribution },
1357
+ );
1358
+ }
1359
+ skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
1360
+ closureResult,
1361
+ {
1362
+ host: surface.host,
1363
+ distribution: surface.distribution,
1364
+ actionId: surface.actionId,
1365
+ unitId: surface.unitId,
1366
+ checkedAt: clockFn(),
1367
+ exitCode: 0,
1368
+ },
1369
+ ));
1370
+ }
1371
+
1372
+ const receiptCoverage = evaluateConsumerSkillResourceClosureReceipts(
1373
+ plan,
1374
+ skillResourceClosureReceipts,
1375
+ );
1376
+ if (!receiptCoverage.passed) {
1377
+ throw new ReleaseError(
1378
+ POST_PUBLISH_VERIFY_FAILED,
1379
+ 'skill resource closure receipt set does not match declared consumer distributions',
1380
+ receiptCoverage,
1381
+ );
1382
+ }
1383
+
1384
+ await evidence.append({
1385
+ phase: 'verify',
1386
+ step: 'skill-resource-closure',
1387
+ status: 'completed',
1388
+ surfaceCount: installSurfaces.length,
1389
+ receipts: skillResourceClosureReceipts,
1390
+ });
1391
+ }
1392
+
973
1393
  // =======================================================================
974
1394
  // All verifications passed — write verify run
975
1395
  // =======================================================================
@@ -994,13 +1414,27 @@ export async function verifyRelease(options) {
994
1414
  status: VERIFIED,
995
1415
  checkpoints: actions.map((a) => {
996
1416
  const check = adapterChecks.find((c) => c.actionId === a.id);
1417
+ let status;
1418
+ if (check?.status === 'SKIPPED') {
1419
+ status = 'skipped';
1420
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC) {
1421
+ status = 'succeeded';
1422
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL) {
1423
+ status = 'succeeded';
1424
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED) {
1425
+ status = 'skipped';
1426
+ } else {
1427
+ status = 'succeeded';
1428
+ }
997
1429
  return {
998
1430
  actionId: a.id,
999
1431
  actionType: a.type,
1000
- status: check?.status === 'SKIPPED' ? 'skipped' : 'succeeded',
1432
+ status,
1001
1433
  };
1002
1434
  }),
1003
1435
  gateResults: consumerGateResults,
1436
+ consumerVerificationReceipts,
1437
+ skillResourceClosureReceipts,
1004
1438
  startedAt: clockFn(),
1005
1439
  finishedAt: clockFn(),
1006
1440
  };
@@ -172,12 +172,19 @@ async function computeWorkspaceDigest(root) {
172
172
 
173
173
  // Ask Git for unambiguous NUL-delimited names, then request the patch for
174
174
  // each exact argv path. This avoids parsing C-quoted `diff --git` headers.
175
+ //
176
+ // Per-file `git diff --binary` can exceed Node.js default ~1 MiB maxBuffer
177
+ // when generated bundles (e.g. 3.8 MiB) are modified. We use an explicit
178
+ // 64 MiB upper bound: large enough for any realistic single-file diff, small
179
+ // enough to fail closed before exhausting memory.
180
+ const DIFF_MAX_BUFFER = 64 * 1024 * 1024; // 64 MiB
181
+ const diffOpts = { ...opts, maxBuffer: DIFF_MAX_BUFFER };
175
182
  const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
176
183
  for (const changedPath of changedPaths) {
177
184
  const { stdout: patch } = await execFile(
178
185
  'git',
179
186
  ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', '--', changedPath],
180
- opts,
187
+ diffOpts,
181
188
  );
182
189
  parts.push(`UNSTAGED:${changedPath}\0${patch}`);
183
190
  }