release-skill 0.2.2 → 0.2.4

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 (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +41 -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 +265 -916
  11. package/README.zh-CN.md +222 -537
  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 +18710 -16694
  15. package/adapters/claude/schemas/release-plan.schema.json +137 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +70 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +18710 -16694
  20. package/adapters/codex/schemas/release-plan.schema.json +137 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +70 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +18710 -16694
  25. package/adapters/kimi/schemas/release-plan.schema.json +137 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +70 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +18710 -16694
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
  33. package/bin/release-skill.bundle.mjs +18710 -16694
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +137 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +70 -2
  38. package/src/adapters/plugin-marketplace.mjs +1602 -605
  39. package/src/commands/prepare.mjs +441 -32
  40. package/src/commands/publish.mjs +107 -75
  41. package/src/commands/reconcile.mjs +92 -327
  42. package/src/commands/setup.mjs +148 -20
  43. package/src/commands/verify.mjs +315 -25
  44. package/src/core/baseline.mjs +21 -1
  45. package/src/core/checkpoints.mjs +50 -7
  46. package/src/core/config.mjs +15 -0
  47. package/src/core/errors.mjs +2 -0
  48. package/src/core/installation-contract.mjs +341 -0
  49. package/src/core/plan.mjs +307 -6
  50. package/src/platforms/codebuddy.mjs +191 -238
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +180 -4
  54. package/src/producers/build-adapters.mjs +9 -2
@@ -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,29 @@ import {
50
51
  resolveNpmRegistryAuthToken,
51
52
  } from '../adapters/npm.mjs';
52
53
  import { runConsumerVerificationGates } from '../core/verification-gates.mjs';
54
+ import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
55
+ import {
56
+ shouldSkipVerification,
57
+ INSTALLATION_CONTRACT_ALGORITHM_VERSION,
58
+ } from '../core/installation-contract.mjs';
53
59
 
54
60
  // ---------------------------------------------------------------------------
55
61
  // Constants
56
62
  // ---------------------------------------------------------------------------
57
63
 
64
+ /**
65
+ * 验证已解决结果类型。
66
+ *
67
+ * - PASSED_AUTOMATIC: 自动验证通过(adapter.verify 返回 VERIFIED)
68
+ * - PASSED_MANUAL: 人工验证通过(用户手动确认)
69
+ * - NOT_REQUIRED_UNCHANGED: 无需验证(远端状态未变化,跳过验证)
70
+ */
71
+ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
72
+ PASSED_AUTOMATIC: 'PASSED_AUTOMATIC',
73
+ PASSED_MANUAL: 'PASSED_MANUAL',
74
+ NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
75
+ });
76
+
58
77
  /**
59
78
  * Map plan action type to adapter ActionType.
60
79
  * Must match publish.mjs and reconcile.mjs.
@@ -80,6 +99,16 @@ function defaultClock() {
80
99
  return new Date().toISOString();
81
100
  }
82
101
 
102
+ /**
103
+ * 验证摘要格式是否为合法的 64 位十六进制字符串。
104
+ *
105
+ * @param {string} digest - 摘要
106
+ * @returns {boolean} 是否合法
107
+ */
108
+ function isValidDigest(digest) {
109
+ return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
110
+ }
111
+
83
112
  // ---------------------------------------------------------------------------
84
113
  // Smoke test
85
114
  // ---------------------------------------------------------------------------
@@ -519,6 +548,7 @@ const defaultNpmExecutor = {
519
548
  * @param {string} [options.root] - Project root for source access.
520
549
  * @param {string} [options.runDir] - Evidence directory.
521
550
  * @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
551
+ * @param {Object} [options.previousVerifyRun] - 上一次验证成功的 verify run 记录,用于安装契约摘要免验。
522
552
  *
523
553
  * @returns {Promise<{ planPath: string, status: string, adapterChecks: Object[], smokeTest: Object }>}
524
554
  *
@@ -536,6 +566,7 @@ export async function verifyRelease(options) {
536
566
  npmExecutor,
537
567
  verificationGatesAuthorized,
538
568
  gateEnv,
569
+ previousVerifyRun,
539
570
  } = options ?? {};
540
571
 
541
572
  const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
@@ -697,9 +728,13 @@ export async function verifyRelease(options) {
697
728
  // Validate checkpoint mapping
698
729
  validateRunCheckpointMapping(sourceRun, plan.externalActions ?? []);
699
730
 
700
- // All checkpoints must be succeeded or skipped (no failed/pending)
731
+ // All checkpoints must be succeeded or skipped (no failed/pending),
732
+ // except marketplace install checkpoints which are re-verified by verify
733
+ // itself via consumer verification (human attestation or automatic).
734
+ // Deferred marketplace checkpoints from publish are also allowed through.
701
735
  const incompleteCheckpoints = sourceRun.checkpoints.filter(
702
- (cp) => cp.status !== 'succeeded' && cp.status !== 'skipped',
736
+ (cp) => cp.status !== 'succeeded' && cp.status !== 'skipped'
737
+ && !((cp.status === 'failed' || cp.status === 'deferred') && isMarketplaceAction(cp.actionType)),
703
738
  );
704
739
  if (incompleteCheckpoints.length > 0) {
705
740
  throw new ReleaseError(
@@ -731,13 +766,130 @@ export async function verifyRelease(options) {
731
766
 
732
767
  const adapterChecks = [];
733
768
  const consumerGateResults = [];
769
+ const consumerVerificationReceipts = [];
734
770
  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
- ]);
771
+
772
+ // --- 自动发现可信消费端验证收据 ---
773
+ // 收据选择逻辑(per-action 从所有候选中选最新匹配):
774
+ // - 只读取同一权威 .release-skill/runs 的真实直接子目录
775
+ // - runs 根或候选目录不得是符号链接,不得物理越界
776
+ // - 候选必须经 loadRun(..., { requireDigest: true })、状态 VERIFIED、合法 finishedAt
777
+ // - 对当前 action 收集所有可信匹配收据,按 finishedAt 最新选择
778
+ // - 收据必须是 consumerVerificationReceipts,严格匹配 actionId/unitId/platform
779
+ // - 收据 planDigest 等于候选 run 自身 planDigest
780
+ // - 显式注入的 previousVerifyRun 也必须经过同等语义校验
781
+
782
+ /** @type {Array<Object>} 所有可信的验证 run 候选 */
783
+ const trustedVerifyRuns = [];
784
+
785
+ // 计算权威 runs 目录路径(基于 plan 的物理位置)
786
+ const planDir = dirname(planPath);
787
+ const releaseDir = basename(planDir) === 'plans' ? dirname(planDir) : planDir;
788
+ const runsDir = resolve(releaseDir, 'runs');
789
+ let runsDirReal = null;
790
+ let authorityDirReal = null;
791
+
792
+ // 校验并纳入显式注入的 previousVerifyRun
793
+ // 必须经过完整语义校验:status=VERIFIED、合法 runDigest、合法 finishedAt、
794
+ // 合法 planDigest、收据身份绑定。不满足则不注入(不参与复用),但不阻断。
795
+ if (previousVerifyRun) {
796
+ if (
797
+ previousVerifyRun.status === 'VERIFIED'
798
+ && isValidDigest(previousVerifyRun.planDigest)
799
+ && previousVerifyRun.finishedAt
800
+ && typeof previousVerifyRun.finishedAt === 'string'
801
+ && !isNaN(Date.parse(previousVerifyRun.finishedAt))
802
+ ) {
803
+ const computedRunDigest = computeRunDigest(previousVerifyRun);
804
+ if (
805
+ typeof previousVerifyRun.runDigest === 'string'
806
+ && previousVerifyRun.runDigest.length > 0
807
+ && previousVerifyRun.runDigest === computedRunDigest
808
+ ) {
809
+ trustedVerifyRuns.push(previousVerifyRun);
810
+ }
811
+ }
812
+ }
813
+
814
+ // 自动发现:从同一 .release-skill/runs 权威目录中发现
815
+ {
816
+ try {
817
+ const runsDirStat = await lstat(runsDir);
818
+ if (runsDirStat.isSymbolicLink()) {
819
+ // runs 根是符号链接:权威目录身份错误,失败关闭
820
+ throw new ReleaseError(
821
+ GATE_FAILED,
822
+ 'runs directory is a symbolic link; authority identity compromised',
823
+ { runsDir },
824
+ );
825
+ }
826
+ if (!runsDirStat.isDirectory()) {
827
+ throw new ReleaseError(
828
+ GATE_FAILED,
829
+ 'runs path is not a directory',
830
+ { runsDir },
831
+ );
832
+ }
833
+ // 以 plan 的物理权威目录为基准,验证 runs 是其中真实 runs 子目录
834
+ runsDirReal = realpathSync(runsDir);
835
+ authorityDirReal = realpathSync(releaseDir);
836
+ if (!runsDirReal.startsWith(authorityDirReal + '/') && runsDirReal !== authorityDirReal) {
837
+ throw new ReleaseError(
838
+ GATE_FAILED,
839
+ 'runs directory is not a real child of the plan authority directory',
840
+ { runsDir, runsDirReal, authorityDirReal },
841
+ );
842
+ }
843
+ } catch (err) {
844
+ if (err instanceof ReleaseError) throw err;
845
+ // runs 目录不存在等非致命情况:跳过自动发现
846
+ runsDirReal = null;
847
+ }
848
+
849
+ if (runsDirReal) {
850
+ const entries = await readdir(runsDirReal, { withFileTypes: true });
851
+ for (const entry of entries) {
852
+ if (!entry.name.startsWith('verify-')) continue;
853
+ // 非目录或符号链接:失败关闭
854
+ if (!entry.isDirectory() || entry.isSymbolicLink()) {
855
+ throw new ReleaseError(
856
+ GATE_FAILED,
857
+ 'verify-* candidate is a symbolic link or not a directory; authority identity compromised',
858
+ { entry: entry.name, runsDir: runsDirReal },
859
+ );
860
+ }
861
+ const candidateDir = resolve(runsDirReal, entry.name);
862
+ const candidateStat = await lstat(candidateDir).catch(() => null);
863
+ if (!candidateStat || candidateStat.isSymbolicLink()) {
864
+ throw new ReleaseError(
865
+ GATE_FAILED,
866
+ 'verify-* candidate is a symbolic link; authority identity compromised',
867
+ { candidateDir, runsDir: runsDirReal },
868
+ );
869
+ }
870
+ // realpath 包含性校验:候选必须在权威 runs 目录内
871
+ const candidateReal = realpathSync(candidateDir);
872
+ if (!candidateReal.startsWith(runsDirReal + '/') && candidateReal !== runsDirReal) {
873
+ throw new ReleaseError(
874
+ GATE_FAILED,
875
+ 'verify-* candidate real path is not contained in authority runs directory',
876
+ { candidateDir, candidateReal, runsDir: runsDirReal },
877
+ );
878
+ }
879
+ const candidatePath = resolve(candidateDir, 'release-run.json');
880
+ try {
881
+ const candidate = await loadRun(candidatePath, { requireDigest: true });
882
+ if (candidate.status !== 'VERIFIED') continue;
883
+ if (!candidate.planDigest) continue;
884
+ if (!candidate.finishedAt || typeof candidate.finishedAt !== 'string') continue;
885
+ trustedVerifyRuns.push(candidate);
886
+ } catch {
887
+ // 真实直接目录里的坏/缺 release-run.json 可忽略
888
+ continue;
889
+ }
890
+ }
891
+ }
892
+ }
741
893
 
742
894
  for (const action of actions) {
743
895
  const adapterActionType = ADAPTER_ACTION_TYPE_MAP[action.type];
@@ -765,7 +917,7 @@ export async function verifyRelease(options) {
765
917
  );
766
918
  }
767
919
 
768
- if (MARKETPLACE_TYPES.has(action.type)) {
920
+ if (isMarketplaceAction(action.type)) {
769
921
  // --- Marketplace: fresh consumer verification in verify's own runDir ---
770
922
  // Context: isolatedConsumerWritesAuthorized allows writing to verify's
771
923
  // runDir/consumers/ directory; externalWritesAuthorized stays false.
@@ -783,22 +935,105 @@ export async function verifyRelease(options) {
783
935
  ...action.parameters,
784
936
  };
785
937
 
786
- // Step 3a: Preflight (validate frozen snapshot, parameters)
938
+ // --- 安装契约摘要免验检查 ---
939
+ // 从计划中读取冻结的安装契约摘要。
940
+ // 新计划在 prepare 阶段计算并冻结 installationContractDigest;
941
+ // 旧计划(无此字段)跳过免验检查,强制重新验证。
942
+ const unit = (plan.units ?? []).find((u) => u.id === action.unitId);
943
+ const typeToDist = {
944
+ 'claude-marketplace-install': 'claude-plugin',
945
+ 'codex-marketplace-install': 'codex-plugin',
946
+ 'kimi-marketplace-install': 'kimi-plugin',
947
+ 'codebuddy-marketplace-install': 'codebuddy-plugin',
948
+ };
949
+ const dist = unit?.distributions?.find((d) => d.type === typeToDist[action.type]);
950
+
951
+ // Step 3a: Preflight(始终先执行 adapter preflight,完成静态身份、版本、
952
+ // tag/ref/sha、来源和 payload 校验;只有 preflight 通过后才允许免验)
953
+ // 必须明确要求 PREFLIGHT_PASSED,其他状态全部失败关闭。
787
954
  const preflightResult = await adapter.preflight(actionInput, marketplaceContext);
788
- if (preflightResult.status === 'PREFLIGHT_FAILED') {
955
+ if (preflightResult.status !== 'PREFLIGHT_PASSED') {
789
956
  adapterChecks.push({
790
957
  actionId: action.id,
791
958
  actionType: action.type,
792
959
  status: 'FAILED',
793
- error: `preflight failed: ${preflightResult.error}`,
960
+ error: `preflight did not pass: status=${preflightResult.status}, error=${preflightResult.error}`,
794
961
  });
795
962
  throw new ReleaseError(
796
963
  POST_PUBLISH_VERIFY_FAILED,
797
- `marketplace preflight failed for action "${action.id}": ${preflightResult.error}`,
964
+ `marketplace preflight did not pass for action "${action.id}": status=${preflightResult.status}, error=${preflightResult.error}`,
798
965
  { actionId: action.id },
799
966
  );
800
967
  }
801
968
 
969
+ // Preflight 通过后,检查是否可以免验
970
+ if (dist?.installationContractDigest) {
971
+ const currentDigest = dist.installationContractDigest;
972
+ const currentPlatform = action.parameters?.consumer ?? action.type.replace('-marketplace-install', '');
973
+
974
+ // 紧邻可信公开基线:只检查最近一个 VERIFIED run 的收据。
975
+ // 不能从任意更老历史中捞出相同摘要来免验。
976
+ // 若最近 run 无该 action/platform 的收据,则 REQUIRE_VERIFICATION。
977
+ let previousActionCheck = null;
978
+ if (trustedVerifyRuns.length > 0) {
979
+ // 按 finishedAt 降序排序,取最近一个
980
+ const sorted = [...trustedVerifyRuns].sort((a, b) => {
981
+ const ta = Date.parse(a.finishedAt) || 0;
982
+ const tb = Date.parse(b.finishedAt) || 0;
983
+ return tb - ta;
984
+ });
985
+ const latest = sorted[0];
986
+ const matchingReceipt = (latest.consumerVerificationReceipts ?? []).find(
987
+ (r) => r.actionId === action.id
988
+ && r.unitId === action.unitId
989
+ && r.platform === currentPlatform
990
+ && r.planDigest === latest.planDigest,
991
+ );
992
+ if (matchingReceipt) {
993
+ previousActionCheck = { ...matchingReceipt, _runFinishedAtTime: Date.parse(latest.finishedAt) };
994
+ }
995
+ }
996
+ const previousDigest = previousActionCheck?.installationContractDigest ?? null;
997
+
998
+ const skipDecision = shouldSkipVerification({
999
+ currentDigest,
1000
+ previousDigest,
1001
+ previousReceipt: previousActionCheck,
1002
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1003
+ });
1004
+
1005
+ if (skipDecision === 'NOT_REQUIRED_UNCHANGED') {
1006
+ adapterChecks.push({
1007
+ actionId: action.id,
1008
+ actionType: action.type,
1009
+ status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1010
+ installationContractDigest: currentDigest,
1011
+ reason: '安装契约摘要未变化,跳过验证',
1012
+ });
1013
+
1014
+ consumerVerificationReceipts.push({
1015
+ actionId: action.id,
1016
+ unitId: action.unitId,
1017
+ platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
1018
+ result: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1019
+ installationContractDigest: currentDigest,
1020
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1021
+ planDigest: plan.digest,
1022
+ verifiedAt: clockFn(),
1023
+ });
1024
+
1025
+ await evidence.append({
1026
+ phase: 'verify-marketplace',
1027
+ actionId: action.id,
1028
+ actionType: action.type,
1029
+ status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1030
+ installationContractDigest: currentDigest,
1031
+ });
1032
+
1033
+ continue;
1034
+ }
1035
+ }
1036
+
802
1037
  // Step 3b: Execute (install to isolated consumer directory)
803
1038
  const executeResult = await adapter.execute(actionInput, marketplaceContext);
804
1039
  if (executeResult.status !== 'EXECUTED') {
@@ -821,15 +1056,39 @@ export async function verifyRelease(options) {
821
1056
  marketplaceContext,
822
1057
  );
823
1058
 
1059
+ // 正确分类:人工确认 -> PASSED_MANUAL,其他自动通路 -> PASSED_AUTOMATIC
1060
+ const isHumanConfirmed = verifyResult.observation?.humanConfirmed === true;
1061
+ const resolvedStatus = verifyResult.status === 'VERIFIED'
1062
+ ? (isHumanConfirmed
1063
+ ? VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL
1064
+ : VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC)
1065
+ : 'FAILED';
1066
+
824
1067
  const check = {
825
1068
  actionId: action.id,
826
1069
  actionType: action.type,
827
- status: verifyResult.status === 'VERIFIED' ? 'PASSED' : 'FAILED',
1070
+ status: resolvedStatus,
828
1071
  observation: verifyResult.observation,
829
1072
  error: verifyResult.error,
1073
+ ...(dist?.installationContractDigest ? { installationContractDigest: dist.installationContractDigest } : {}),
830
1074
  };
831
1075
  adapterChecks.push(check);
832
1076
 
1077
+ // 持久化消费端验证收据
1078
+ // 旧计划无摘要时可不生成"安装契约复用收据",但不得写非法空字符串
1079
+ if (resolvedStatus !== 'FAILED' && dist?.installationContractDigest && /^[a-f0-9]{64}$/.test(dist.installationContractDigest)) {
1080
+ consumerVerificationReceipts.push({
1081
+ actionId: action.id,
1082
+ unitId: action.unitId,
1083
+ platform: action.parameters?.consumer ?? action.type.replace('-marketplace-install', ''),
1084
+ result: resolvedStatus,
1085
+ installationContractDigest: dist.installationContractDigest,
1086
+ algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
1087
+ planDigest: plan.digest,
1088
+ verifiedAt: clockFn(),
1089
+ });
1090
+ }
1091
+
833
1092
  await evidence.append({
834
1093
  phase: 'verify-marketplace',
835
1094
  actionId: action.id,
@@ -841,7 +1100,13 @@ export async function verifyRelease(options) {
841
1100
  throw new ReleaseError(
842
1101
  POST_PUBLISH_VERIFY_FAILED,
843
1102
  `marketplace verification failed for action "${action.id}": ${verifyResult.error}`,
844
- { actionId: action.id, observation: verifyResult.observation },
1103
+ {
1104
+ actionId: action.id,
1105
+ actionType: action.type,
1106
+ verificationResult: check.status,
1107
+ observation: verifyResult.observation,
1108
+ expected: action.expected,
1109
+ },
845
1110
  );
846
1111
  }
847
1112
 
@@ -849,7 +1114,9 @@ export async function verifyRelease(options) {
849
1114
  ? 'claude-plugin'
850
1115
  : action.type === 'codex-marketplace-install'
851
1116
  ? 'codex-plugin'
852
- : 'kimi-plugin';
1117
+ : action.type === 'codebuddy-marketplace-install'
1118
+ ? 'codebuddy-plugin'
1119
+ : 'kimi-plugin';
853
1120
  const installPath = verifyResult.observation?.installPath;
854
1121
  consumerGateResults.push(...await runConsumerVerificationGates({
855
1122
  plan,
@@ -868,10 +1135,14 @@ export async function verifyRelease(options) {
868
1135
  HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
869
1136
  CODEX_HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
870
1137
  }
871
- : {
872
- HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
873
- KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
874
- },
1138
+ : action.type === 'codebuddy-marketplace-install'
1139
+ ? {
1140
+ HOME: resolve(runDir, 'consumers', `codebuddy-${action.parameters.plugin}`),
1141
+ }
1142
+ : {
1143
+ HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1144
+ KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1145
+ },
875
1146
  }));
876
1147
  } else {
877
1148
  // --- Non-marketplace: read-only adapter.verify() ---
@@ -895,7 +1166,7 @@ export async function verifyRelease(options) {
895
1166
  const check = {
896
1167
  actionId: action.id,
897
1168
  actionType: action.type,
898
- status: verifyResult.status === 'VERIFIED' ? 'PASSED' : 'FAILED',
1169
+ status: verifyResult.status === 'VERIFIED' ? VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC : 'FAILED',
899
1170
  observation: verifyResult.observation,
900
1171
  error: verifyResult.error,
901
1172
  };
@@ -913,7 +1184,13 @@ export async function verifyRelease(options) {
913
1184
  throw new ReleaseError(
914
1185
  POST_PUBLISH_VERIFY_FAILED,
915
1186
  `adapter verification failed for action "${action.id}": ${verifyResult.error}`,
916
- { actionId: action.id, observation: verifyResult.observation },
1187
+ {
1188
+ actionId: action.id,
1189
+ actionType: action.type,
1190
+ verificationResult: check.status,
1191
+ observation: verifyResult.observation,
1192
+ expected: action.expected,
1193
+ },
917
1194
  );
918
1195
  }
919
1196
  }
@@ -988,13 +1265,26 @@ export async function verifyRelease(options) {
988
1265
  status: VERIFIED,
989
1266
  checkpoints: actions.map((a) => {
990
1267
  const check = adapterChecks.find((c) => c.actionId === a.id);
1268
+ let status;
1269
+ if (check?.status === 'SKIPPED') {
1270
+ status = 'skipped';
1271
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_AUTOMATIC) {
1272
+ status = 'succeeded';
1273
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.PASSED_MANUAL) {
1274
+ status = 'succeeded';
1275
+ } else if (check?.status === VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED) {
1276
+ status = 'skipped';
1277
+ } else {
1278
+ status = 'succeeded';
1279
+ }
991
1280
  return {
992
1281
  actionId: a.id,
993
1282
  actionType: a.type,
994
- status: check?.status === 'SKIPPED' ? 'skipped' : 'succeeded',
1283
+ status,
995
1284
  };
996
1285
  }),
997
1286
  gateResults: consumerGateResults,
1287
+ consumerVerificationReceipts,
998
1288
  startedAt: clockFn(),
999
1289
  finishedAt: clockFn(),
1000
1290
  };
@@ -38,11 +38,24 @@ const CONTROL_PLANE_PREFIXES = [
38
38
  '.release-skill/runs',
39
39
  '.release-skill/transactions',
40
40
  '.release-skill/kimi-attestations',
41
+ // codebuddy-attestations: same rationale as kimi-attestations above.
42
+ // Holds CodeBuddy's closure-protocol lifecycle artifacts: manual install
43
+ // requirements and human attestations bound to planDigest, payloadDigest,
44
+ // version, install path, responsible person, and expiry. Neither is
45
+ // publishable source or project configuration — they never enter the frozen
46
+ // snapshot — so excluding them keeps reconcile's own requirement output and
47
+ // the flow-required attestation from invalidating the baseline.
48
+ '.release-skill/codebuddy-attestations',
41
49
  // T3.2 incremental hook cache: a pure local optimisation written by prepare.
42
50
  // Excluding it keeps cache records from destabilising workspaceDigest on
43
51
  // every prepare (and hook-cache.mjs also skips this prefix when fingerprinting
44
52
  // inputs, so records never hash themselves).
45
53
  '.release-skill/cache',
54
+ // waivers: exception records with reason, responsible person, and expiry.
55
+ // Like attestations, they are runtime closure artifacts — not publishable
56
+ // source or project configuration — so excluding them keeps waiver writes
57
+ // from invalidating the workspace baseline.
58
+ '.release-skill/waivers',
46
59
  ];
47
60
  const RESERVED_CONTROL_PREFIXES = [
48
61
  ...CONTROL_PLANE_PREFIXES,
@@ -159,12 +172,19 @@ async function computeWorkspaceDigest(root) {
159
172
 
160
173
  // Ask Git for unambiguous NUL-delimited names, then request the patch for
161
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 };
162
182
  const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
163
183
  for (const changedPath of changedPaths) {
164
184
  const { stdout: patch } = await execFile(
165
185
  'git',
166
186
  ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', '--', changedPath],
167
- opts,
187
+ diffOpts,
168
188
  );
169
189
  parts.push(`UNSTAGED:${changedPath}\0${patch}`);
170
190
  }
@@ -66,13 +66,14 @@ export const ADAPTER_ACTION_TYPE_MAP = {
66
66
  * `push-snapshot` (the frozen commit must exist on the remote before a tag
67
67
  * or branch tip can point at it). `npm-publish` has no git dependency and is
68
68
  * placed in Tier 1 only for conservative scheduling.
69
- * - Tier 2 `github-release` and the claude/codex marketplace installs depend
70
- * on Tier 1 `create-tag` (release `--verify-tag`; install ref is the tag).
71
- * - Tier 3 `kimi-marketplace-install` depends on Tier 2 `github-release`
72
- * (its install URL points at the Release page). `codebuddy-marketplace-install`
73
- * is also a non-automatable human-attestation closure and runs in Tier 3 after
74
- * the automated writes (its install is from a unified marketplace, proven by a
75
- * human attestation rather than an automated install checkpoint).
69
+ * - Tier 2 `github-release` depends on Tier 1 `create-tag` (release `--verify-tag`).
70
+ *
71
+ * Marketplace actions (claude/codex/kimi/codebuddy-marketplace-install) are
72
+ * included in the tier table for ADAPTER_ACTION_TYPE_MAP lookup but are
73
+ * filtered out before tier grouping in both publish and reconcile. They are
74
+ * recorded as DEFERRED with CONSUMER_VERIFICATION_DEFERRED reason and never
75
+ * participate in tier execution. Their verification is handled exclusively
76
+ * by the verify command.
76
77
  *
77
78
  * Action types not listed in any tier are unknown to the scheduler and fail
78
79
  * closed (see groupActionsByTier); they are never silently scheduled.
@@ -146,3 +147,45 @@ export function groupActionsByTier(orderedActions) {
146
147
  }
147
148
  return { tiers, unknown };
148
149
  }
150
+
151
+ /**
152
+ * 远端写入动作类型集合。
153
+ * 这些动作的结果决定 PUBLISHED 状态:全部一致后即可进入 PUBLISHED。
154
+ */
155
+ export const REMOTE_WRITE_ACTION_TYPES = new Set([
156
+ 'push-commit',
157
+ 'push-snapshot',
158
+ 'set-default-branch',
159
+ 'create-tag',
160
+ 'npm-publish',
161
+ 'github-release',
162
+ ]);
163
+
164
+ /**
165
+ * 市场安装动作类型集合。
166
+ * 这些动作的结果记录在 run 中,但不阻止 PUBLISHED 状态。
167
+ */
168
+ export const MARKETPLACE_ACTION_TYPES = new Set([
169
+ 'claude-marketplace-install',
170
+ 'codex-marketplace-install',
171
+ 'kimi-marketplace-install',
172
+ 'codebuddy-marketplace-install',
173
+ ]);
174
+
175
+ /**
176
+ * 判断动作类型是否为远端写入动作。
177
+ * @param {string} actionType - 计划中的动作类型
178
+ * @returns {boolean}
179
+ */
180
+ export function isRemoteWriteAction(actionType) {
181
+ return REMOTE_WRITE_ACTION_TYPES.has(actionType);
182
+ }
183
+
184
+ /**
185
+ * 判断动作类型是否为市场安装动作。
186
+ * @param {string} actionType - 计划中的动作类型
187
+ * @returns {boolean}
188
+ */
189
+ export function isMarketplaceAction(actionType) {
190
+ return MARKETPLACE_ACTION_TYPES.has(actionType);
191
+ }
@@ -346,6 +346,21 @@ export async function loadProjectConfig({ root, configPath } = {}) {
346
346
  }
347
347
  } // end of contextual prevalidation else block
348
348
 
349
+ // --- Normalize marketplaceSourceType for old configs ---
350
+ // Old configs may lack marketplaceSourceType; determine by marketplaceRepo existence.
351
+ // This runs before schema validation so the required rule passes.
352
+ if (Array.isArray(config.releaseUnits)) {
353
+ for (const unit of config.releaseUnits) {
354
+ if (!unit?.distributions) continue;
355
+ for (const dist of unit.distributions) {
356
+ if (dist.type === 'npm') continue;
357
+ if (dist.marketplaceSourceType === undefined || dist.marketplaceSourceType === null) {
358
+ dist.marketplaceSourceType = dist.marketplaceRepo ? 'standalone-index' : 'bundled-family';
359
+ }
360
+ }
361
+ }
362
+ }
363
+
349
364
  // --- Schema validation (using formal JSON schema) ---
350
365
  const valid = validateConfig(config);
351
366
  if (!valid) {
@@ -87,6 +87,7 @@ const EXIT_CODE_MAP = Object.freeze({
87
87
  RELEASE_DOCS_CONFLICT: 44,
88
88
  RELEASE_DOCS_REFRESH_STALE: 45,
89
89
  RELEASE_DOCS_STALE: 46,
90
+ CONSUMER_VERIFICATION_DEFERRED: 47,
90
91
  });
91
92
 
92
93
  // ---- Error code constants ----
@@ -128,6 +129,7 @@ export const RELEASE_DOCS_TRANSLATION_MISSING = 'RELEASE_DOCS_TRANSLATION_MISSIN
128
129
  export const RELEASE_DOCS_CONFLICT = 'RELEASE_DOCS_CONFLICT';
129
130
  export const RELEASE_DOCS_REFRESH_STALE = 'RELEASE_DOCS_REFRESH_STALE';
130
131
  export const RELEASE_DOCS_STALE = 'RELEASE_DOCS_STALE';
132
+ export const CONSUMER_VERIFICATION_DEFERRED = 'CONSUMER_VERIFICATION_DEFERRED';
131
133
 
132
134
  /**
133
135
  * Typed error for release-skill operations.