release-skill 0.2.1 → 0.2.3

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 +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +40 -0
  7. package/INSTALL.md +183 -21
  8. package/INSTALL.zh-CN.md +160 -16
  9. package/README.md +112 -13
  10. package/README.zh-CN.md +72 -11
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +2240 -587
  14. package/adapters/claude/schemas/release-plan.schema.json +44 -2
  15. package/adapters/claude/schemas/release-project.schema.json +46 -4
  16. package/adapters/claude/schemas/release-run.schema.json +1 -0
  17. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  18. package/adapters/codex/bin/release-skill.bundle.mjs +2240 -587
  19. package/adapters/codex/schemas/release-plan.schema.json +44 -2
  20. package/adapters/codex/schemas/release-project.schema.json +46 -4
  21. package/adapters/codex/schemas/release-run.schema.json +1 -0
  22. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  23. package/adapters/kimi/bin/release-skill.bundle.mjs +2240 -587
  24. package/adapters/kimi/schemas/release-plan.schema.json +44 -2
  25. package/adapters/kimi/schemas/release-project.schema.json +46 -4
  26. package/adapters/kimi/schemas/release-run.schema.json +1 -0
  27. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +4 -2
  28. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2240 -587
  29. package/adapters/workbuddy/schemas/release-plan.schema.json +44 -2
  30. package/adapters/workbuddy/schemas/release-project.schema.json +46 -4
  31. package/adapters/workbuddy/schemas/release-run.schema.json +1 -0
  32. package/bin/release-skill-cli.mjs +46 -4
  33. package/bin/release-skill.bundle.mjs +2240 -587
  34. package/package.json +1 -1
  35. package/references/02-project-config.md +7 -0
  36. package/references/06-adapter-contract.md +21 -2
  37. package/schemas/release-plan.schema.json +44 -2
  38. package/schemas/release-project.schema.json +46 -4
  39. package/schemas/release-run.schema.json +1 -0
  40. package/scripts/sync-public-files.mjs +8 -4
  41. package/src/adapters/contract.mjs +1 -0
  42. package/src/adapters/plugin-marketplace.mjs +796 -41
  43. package/src/commands/assess.mjs +50 -1
  44. package/src/commands/prepare.mjs +342 -9
  45. package/src/commands/publish.mjs +1 -0
  46. package/src/commands/reconcile.mjs +1 -0
  47. package/src/commands/setup.mjs +7 -3
  48. package/src/commands/verify.mjs +13 -5
  49. package/src/core/baseline.mjs +13 -0
  50. package/src/core/checkpoints.mjs +7 -2
  51. package/src/core/plan.mjs +275 -4
  52. package/src/core/verification-gates.mjs +1 -1
  53. package/src/platforms/codebuddy.mjs +658 -0
  54. package/src/platforms/registry.mjs +258 -5
  55. package/src/producers/build-adapters.mjs +30 -15
@@ -129,7 +129,8 @@ function identifyTopology(config) {
129
129
  const hasPlugin =
130
130
  uniqueDistTypes.includes('claude-plugin') ||
131
131
  uniqueDistTypes.includes('codex-plugin') ||
132
- uniqueDistTypes.includes('kimi-plugin');
132
+ uniqueDistTypes.includes('kimi-plugin') ||
133
+ uniqueDistTypes.includes('codebuddy-plugin');
133
134
 
134
135
  if (units.length === 0) {
135
136
  type = 'no-release-units';
@@ -428,6 +429,54 @@ async function checkPluginManifests(root, config) {
428
429
  }
429
430
  }
430
431
  }
432
+
433
+ if (distributionTypes.has('codebuddy-plugin')) {
434
+ const manifestPath = resolve(unitRoot, '.codebuddy-plugin', 'plugin.json');
435
+ const displayPath = unitFile(unit, '.codebuddy-plugin/plugin.json');
436
+ const exists = await fileExists(manifestPath);
437
+ if (!exists) {
438
+ gaps.push(
439
+ createGap({
440
+ scope: GapScope.PROFILE,
441
+ category: GapCategory.MANIFEST,
442
+ severity: Severity.ERROR,
443
+ code: 'CODEBUDDY_MANIFEST_MISSING',
444
+ message: `发布单元 "${unit.id}" 缺少 .codebuddy-plugin/plugin.json 插件清单`,
445
+ file: displayPath,
446
+ }),
447
+ );
448
+ } else {
449
+ try {
450
+ const content = await readFile(manifestPath, 'utf8');
451
+ const manifest = JSON.parse(content);
452
+ const requiredFields = ['name', 'version', 'description'];
453
+ const missingFields = requiredFields.filter((f) => !(f in manifest));
454
+ if (missingFields.length > 0) {
455
+ gaps.push(
456
+ createGap({
457
+ scope: GapScope.PROFILE,
458
+ category: GapCategory.MANIFEST,
459
+ severity: Severity.ERROR,
460
+ code: 'CODEBUDDY_MANIFEST_INCOMPLETE',
461
+ message: `CodeBuddy 插件清单缺少必填字段: ${missingFields.join(', ')}`,
462
+ file: displayPath,
463
+ }),
464
+ );
465
+ }
466
+ } catch {
467
+ gaps.push(
468
+ createGap({
469
+ scope: GapScope.PROFILE,
470
+ category: GapCategory.MANIFEST,
471
+ severity: Severity.ERROR,
472
+ code: 'CODEBUDDY_MANIFEST_INVALID',
473
+ message: '.codebuddy-plugin/plugin.json 解析失败',
474
+ file: displayPath,
475
+ }),
476
+ );
477
+ }
478
+ }
479
+ }
431
480
  }
432
481
 
433
482
  return gaps;
@@ -941,6 +941,231 @@ async function buildProductionAssets(
941
941
  return assets;
942
942
  }
943
943
 
944
+ // ---------------------------------------------------------------------------
945
+ // External independent marketplace freeze (production + online only)
946
+ // ---------------------------------------------------------------------------
947
+
948
+ const EXTERNAL_MARKETPLACE_SHA_RE = /^[0-9a-f]{40}$/;
949
+
950
+ /**
951
+ * Parse `git ls-remote --symref <url> HEAD` output into the resolved HEAD
952
+ * commit sha and the default branch name. Pure: no I/O.
953
+ *
954
+ * Expected lines (tab-separated):
955
+ * ref: refs/heads/<branch>\tHEAD
956
+ * <40-hex sha>\tHEAD
957
+ *
958
+ * @param {string} stdout
959
+ * @returns {{sha:string, defaultBranch:string}|null} null when either is absent.
960
+ */
961
+ export function parseExternalMarketplaceLsRemote(stdout) {
962
+ if (typeof stdout !== 'string') return null;
963
+ const lines = stdout.trim().split('\n').filter((line) => line.length > 0);
964
+ let defaultBranch = null;
965
+ let sha = null;
966
+ for (const line of lines) {
967
+ const tabIndex = line.indexOf('\t');
968
+ if (tabIndex < 0) continue;
969
+ const left = line.slice(0, tabIndex);
970
+ const right = line.slice(tabIndex + 1);
971
+ if (right !== 'HEAD') continue;
972
+ if (left.startsWith('ref: refs/heads/')) {
973
+ defaultBranch = left.slice('ref: refs/heads/'.length);
974
+ } else if (EXTERNAL_MARKETPLACE_SHA_RE.test(left)) {
975
+ sha = left;
976
+ }
977
+ }
978
+ if (!sha || !defaultBranch) return null;
979
+ return { sha, defaultBranch };
980
+ }
981
+
982
+ /**
983
+ * Decode a GitHub contents-API base64 `.content` field and parse it as the
984
+ * marketplace index JSON. Pure: no I/O.
985
+ *
986
+ * @param {string} base64Content
987
+ * @returns {object|null} parsed index, or null on decode/parse failure.
988
+ */
989
+ export function decodeExternalMarketplaceIndex(base64Content) {
990
+ if (typeof base64Content !== 'string') return null;
991
+ try {
992
+ const base64 = base64Content.replace(/\s/g, '');
993
+ const content = Buffer.from(base64, 'base64').toString('utf8');
994
+ const parsed = JSON.parse(content);
995
+ return parsed && typeof parsed === 'object' ? parsed : null;
996
+ } catch {
997
+ return null;
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Resolve an external marketplace repository's current HEAD via
1003
+ * `git ls-remote --symref`, returning both the resolved commit sha and the
1004
+ * default branch name. Read-only: never writes to the remote.
1005
+ *
1006
+ * @param {string} repo - External marketplace repository (owner/name).
1007
+ * @param {object} [opts]
1008
+ * @param {string} [opts.githubHost]
1009
+ * @returns {Promise<{status:string, sha?:string, defaultBranch?:string, error?:string}>}
1010
+ */
1011
+ async function defaultObserveExternalMarketplaceHead(repo, { githubHost = 'github.com' } = {}) {
1012
+ try {
1013
+ const { stdout } = await execFile(
1014
+ 'git',
1015
+ ['ls-remote', '--symref', `https://${githubHost}/${repo}.git`, 'HEAD'],
1016
+ { shell: false, encoding: 'utf8', timeout: 30000 },
1017
+ );
1018
+ const parsed = parseExternalMarketplaceLsRemote(stdout);
1019
+ if (!parsed) {
1020
+ return { status: 'unknown', error: 'could not resolve HEAD commit sha and default branch from ls-remote --symref output' };
1021
+ }
1022
+ return { status: 'observed', sha: parsed.sha, defaultBranch: parsed.defaultBranch };
1023
+ } catch (error) {
1024
+ return { status: 'unknown', error: error.message };
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Fetch and parse an external marketplace index manifest at a frozen ref via
1030
+ * the GitHub contents API. Read-only: never writes to the remote.
1031
+ *
1032
+ * @param {string} repo - External marketplace repository (owner/name).
1033
+ * @param {string} manifestPath - Platform marketplace manifest path.
1034
+ * @param {string} ref - Frozen commit sha to read the index at.
1035
+ * @param {object} [opts]
1036
+ * @param {string} [opts.githubHost]
1037
+ * @returns {Promise<{status:string, index?:object, error?:string}>}
1038
+ */
1039
+ async function defaultFetchExternalMarketplaceIndex(repo, manifestPath, ref, { githubHost = 'github.com' } = {}) {
1040
+ try {
1041
+ const { stdout } = await execFile(
1042
+ 'gh',
1043
+ ['api', `repos/${repo}/contents/${manifestPath}?ref=${ref}`, '--jq', '.content'],
1044
+ {
1045
+ shell: false,
1046
+ encoding: 'utf8',
1047
+ timeout: 30000,
1048
+ env: { ...process.env, GH_HOST: githubHost },
1049
+ },
1050
+ );
1051
+ const index = decodeExternalMarketplaceIndex(stdout);
1052
+ if (!index) {
1053
+ return { status: 'unknown', error: 'could not decode external marketplace index content' };
1054
+ }
1055
+ return { status: 'fetched', index };
1056
+ } catch (error) {
1057
+ return { status: 'unknown', error: error.message };
1058
+ }
1059
+ }
1060
+
1061
+ /**
1062
+ * Freeze the external marketplace HEAD for every claude/codex distribution
1063
+ * that declares `marketplaceRepo` (production + online only). For each such
1064
+ * distribution: resolve the external repository's HEAD commit sha + default
1065
+ * branch name, validate the marketplace index entry at that sha (name match,
1066
+ * exactly one plugin entry, claude-form entry version equals the target
1067
+ * version), then record the add-ref (codex=sha, claude=default branch name)
1068
+ * and the frozen marketplaceCommitSha. Any failure fails closed. The remote is
1069
+ * only ever read (git ls-remote / gh api), never written.
1070
+ *
1071
+ * @returns {Promise<Map<string, {repo:string, ref:string, marketplaceCommitSha:string, marketplace:string}>>}
1072
+ * keyed by `${unitId} ${distributionType}`.
1073
+ */
1074
+ export async function resolveExternalMarketplaceFreezes({
1075
+ unitResults,
1076
+ resolvedVersions,
1077
+ offline,
1078
+ evidence,
1079
+ observeHeadFn,
1080
+ fetchIndexFn,
1081
+ }) {
1082
+ const freezes = new Map();
1083
+ for (let index = 0; index < unitResults.length; index += 1) {
1084
+ const { unit } = unitResults[index];
1085
+ const version = resolvedVersions[index];
1086
+ const githubHost = unit.production?.githubHost ?? 'github.com';
1087
+ for (const dist of unit.distributions ?? []) {
1088
+ if (dist.marketplaceRepo === undefined || dist.marketplaceRepo === null) continue;
1089
+ const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
1090
+ if (!platform || platform.marketplaceRefForm === null) {
1091
+ throw new ReleaseError(
1092
+ GATE_FAILED,
1093
+ `unit "${unit.id}" ${dist.type} distribution declares marketplaceRepo but the platform has no marketplace add capability`,
1094
+ { unitId: unit.id, distributionType: dist.type },
1095
+ );
1096
+ }
1097
+ if (offline) {
1098
+ throw new ReleaseError(
1099
+ GATE_FAILED,
1100
+ `unit "${unit.id}" ${dist.type} external marketplace form requires online production prepare to freeze the marketplace commit sha`,
1101
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1102
+ );
1103
+ }
1104
+ const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
1105
+ if (observed.status !== 'observed' || !EXTERNAL_MARKETPLACE_SHA_RE.test(observed.sha ?? '') || !observed.defaultBranch) {
1106
+ throw new ReleaseError(
1107
+ GATE_FAILED,
1108
+ `unit "${unit.id}" could not freeze external marketplace "${dist.marketplaceRepo}" HEAD: ${observed.error ?? 'unknown'}`,
1109
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1110
+ );
1111
+ }
1112
+ const sha = observed.sha;
1113
+ const manifestPath = platform.manifestPaths.marketplace;
1114
+ const fetched = await fetchIndexFn(dist.marketplaceRepo, manifestPath, sha, { githubHost });
1115
+ if (fetched.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
1116
+ throw new ReleaseError(
1117
+ GATE_FAILED,
1118
+ `unit "${unit.id}" could not read external marketplace index for "${dist.marketplaceRepo}" at ${sha}: ${fetched.error ?? 'unknown'}`,
1119
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1120
+ );
1121
+ }
1122
+ const marketplaceIndex = fetched.index;
1123
+ if (marketplaceIndex.name !== dist.marketplace) {
1124
+ throw new ReleaseError(
1125
+ GATE_FAILED,
1126
+ `unit "${unit.id}" external marketplace index name "${marketplaceIndex.name}" does not match distribution marketplace "${dist.marketplace}"`,
1127
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1128
+ );
1129
+ }
1130
+ const pluginEntries = Array.isArray(marketplaceIndex.plugins)
1131
+ ? marketplaceIndex.plugins.filter((entry) => entry && entry.name === dist.plugin)
1132
+ : [];
1133
+ if (pluginEntries.length !== 1) {
1134
+ throw new ReleaseError(
1135
+ GATE_FAILED,
1136
+ `unit "${unit.id}" external marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${pluginEntries.length}`,
1137
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1138
+ );
1139
+ }
1140
+ if (platform.marketplaceEntryCarriesVersion && pluginEntries[0].version !== version) {
1141
+ throw new ReleaseError(
1142
+ GATE_FAILED,
1143
+ `unit "${unit.id}" external marketplace index entry version "${pluginEntries[0].version}" does not match target version "${version}"`,
1144
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1145
+ );
1146
+ }
1147
+ const ref = platform.marketplaceRefForm === 'sha' ? sha : observed.defaultBranch;
1148
+ freezes.set(`${unit.id} ${dist.type}`, {
1149
+ repo: dist.marketplaceRepo,
1150
+ ref,
1151
+ marketplaceCommitSha: sha,
1152
+ marketplace: dist.marketplace,
1153
+ });
1154
+ await evidence.append({
1155
+ phase: 'external-marketplace-freeze',
1156
+ unitId: unit.id,
1157
+ distributionType: dist.type,
1158
+ status: 'completed',
1159
+ marketplaceRepo: dist.marketplaceRepo,
1160
+ marketplaceCommitSha: sha,
1161
+ defaultBranch: observed.defaultBranch,
1162
+ addRef: ref,
1163
+ });
1164
+ }
1165
+ }
1166
+ return freezes;
1167
+ }
1168
+
944
1169
  // ---------------------------------------------------------------------------
945
1170
  // External actions generation
946
1171
  // ---------------------------------------------------------------------------
@@ -956,7 +1181,7 @@ async function buildProductionAssets(
956
1181
  * @param {string} realRoot - The project root for relative path calculation.
957
1182
  * @returns {object[]} Array of external action descriptors.
958
1183
  */
959
- function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1184
+ export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map()) {
960
1185
  const actions = [];
961
1186
 
962
1187
  if (!productionAssets) {
@@ -1026,6 +1251,46 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1026
1251
  if (!dist) continue;
1027
1252
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1028
1253
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
1254
+ // External independent marketplace form: the distribution declares
1255
+ // marketplaceRepo, so the install targets the external marketplace repo
1256
+ // and carries the external payload contract. Non-production prepare does
1257
+ // no online resolution, so it carries the external marker + repo shape
1258
+ // but no frozen ref/marketplaceCommitSha (production-only bindings),
1259
+ // keeping the two loops' shapes aligned for plan completeness.
1260
+ const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1261
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1262
+ // bundled-family: marketplace and plugin live in the same repo.
1263
+ // standalone-index: external marketplace repo indexes a separate plugin repo.
1264
+ // Non-automatable platforms (kimi, codebuddy) use human-attestation and
1265
+ // carry no marketplace form.
1266
+ const marketplaceForm = requiresMarketplace
1267
+ ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1268
+ : null;
1269
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1270
+ ? Object.freeze({
1271
+ form: 'standalone-index',
1272
+ marketplaceRepo: dist.marketplaceRepo,
1273
+ marketplaceEntry: dist.plugin,
1274
+ // pluginRepo is the plugin's own public repo, NOT the external
1275
+ // marketplace repo. The marketplace repo contains the index; the
1276
+ // plugin repo contains the actual plugin code.
1277
+ pluginRepo: unit.publicRepo,
1278
+ sourceType: 'marketplace-entry',
1279
+ // Production-only fields: marketplaceCommitSha and ref are frozen
1280
+ // by resolveExternalMarketplaceFreezes in the production path.
1281
+ marketplaceCommitSha: null,
1282
+ ref: null,
1283
+ payloadDigest: null,
1284
+ })
1285
+ : marketplaceForm === 'bundled-family'
1286
+ ? Object.freeze({
1287
+ form: 'bundled-family',
1288
+ repo: unit.publicRepo,
1289
+ marketplaceEntry: dist.plugin,
1290
+ pluginSubpath: '.',
1291
+ payloadDigest: null,
1292
+ })
1293
+ : null;
1029
1294
  actions.push({
1030
1295
  id: `${platform.actionType}-${unit.id}`,
1031
1296
  type: platform.actionType,
@@ -1035,7 +1300,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1035
1300
  consumer: platform.id,
1036
1301
  plugin: dist.plugin,
1037
1302
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1038
- repo: unit.publicRepo,
1303
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1039
1304
  version,
1040
1305
  entrySkill: dist.entrySkill,
1041
1306
  timeoutMs,
@@ -1043,7 +1308,12 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1043
1308
  // payload is verified by declared-manifest containment; host-added
1044
1309
  // files are recorded, not failed. Frozen plans without this marker
1045
1310
  // keep the legacy full-tree equality semantics byte-for-byte.
1046
- payloadContract: 'declared-manifest-v1',
1311
+ // External marketplace form uses the external-marketplace-v1
1312
+ // contract (whole-tree '.' containment; see plugin-marketplace).
1313
+ payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1314
+ ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1315
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1316
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1047
1317
  },
1048
1318
  expected: {
1049
1319
  installed: true,
@@ -1051,6 +1321,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1051
1321
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1052
1322
  version,
1053
1323
  entrySkill: dist.entrySkill,
1324
+ ...(externalMarketplace ? { marketplaceLocation: 'external', repo: dist.marketplaceRepo } : {}),
1054
1325
  },
1055
1326
  status: 'PENDING',
1056
1327
  });
@@ -1207,6 +1478,41 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1207
1478
  if (!dist) continue;
1208
1479
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1209
1480
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
1481
+ // External independent marketplace form: the install targets the external
1482
+ // marketplace repo with the add-ref + marketplaceCommitSha frozen online
1483
+ // by resolveExternalMarketplaceFreezes. snapshotPath/manifestDigest still
1484
+ // bind this unit's own frozen snapshot — the payload authority is the unit
1485
+ // snapshot, unchanged. Inline form (no marketplaceRepo) is byte-identical.
1486
+ const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1487
+ const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1488
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1489
+ const marketplaceForm = requiresMarketplace
1490
+ ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1491
+ : null;
1492
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1493
+ ? Object.freeze({
1494
+ form: 'standalone-index',
1495
+ marketplaceRepo: dist.marketplaceRepo,
1496
+ marketplaceCommitSha: freeze?.marketplaceCommitSha ?? null,
1497
+ marketplaceEntry: dist.plugin,
1498
+ // pluginRepo is the plugin's own public repo, NOT the external
1499
+ // marketplace repo. The marketplace repo contains the index; the
1500
+ // plugin repo contains the actual plugin code.
1501
+ pluginRepo: unit.publicRepo,
1502
+ sourceType: 'marketplace-entry',
1503
+ ref: freeze?.ref ?? null,
1504
+ payloadDigest: asset.manifestDigest,
1505
+ })
1506
+ : marketplaceForm === 'bundled-family'
1507
+ ? Object.freeze({
1508
+ form: 'bundled-family',
1509
+ repo: unit.publicRepo,
1510
+ commit: asset.commit,
1511
+ marketplaceEntry: dist.plugin,
1512
+ pluginSubpath: '.',
1513
+ payloadDigest: asset.manifestDigest,
1514
+ })
1515
+ : null;
1210
1516
  actions.push({
1211
1517
  id: `${platform.actionType}-${unit.id}`,
1212
1518
  type: platform.actionType,
@@ -1216,8 +1522,8 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1216
1522
  consumer: platform.id,
1217
1523
  plugin: dist.plugin,
1218
1524
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1219
- repo: unit.publicRepo,
1220
- ref: resolvedTag,
1525
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1526
+ ref: externalMarketplace ? freeze.ref : resolvedTag,
1221
1527
  version: unitVersion,
1222
1528
  entrySkill: dist.entrySkill,
1223
1529
  snapshotPath: asset.snapshotPath,
@@ -1227,19 +1533,29 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1227
1533
  // payload is verified by declared-manifest containment; host-added
1228
1534
  // files are recorded, not failed. Frozen plans without this marker
1229
1535
  // keep the legacy full-tree equality semantics byte-for-byte.
1230
- payloadContract: 'declared-manifest-v1',
1536
+ // External marketplace form uses the external-marketplace-v1
1537
+ // contract (whole-tree '.' containment; see plugin-marketplace).
1538
+ payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1539
+ ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1540
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1541
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1542
+ // 冻结的插件来源提交,用于 sourceDescriptor.commit 交叉校验。
1543
+ // bundled-family: sourceDescriptor.commit 绑定到此值。
1544
+ // standalone-index: 通过此值绑定插件载荷来源。
1545
+ sourceCommit: asset.commit,
1231
1546
  },
1232
1547
  expected: {
1233
1548
  installed: true,
1234
1549
  consumer: platform.id,
1235
1550
  plugin: dist.plugin,
1236
1551
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1237
- repo: unit.publicRepo,
1552
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1238
1553
  version: unitVersion,
1239
- ref: resolvedTag,
1554
+ ref: externalMarketplace ? freeze.ref : resolvedTag,
1240
1555
  entrySkill: dist.entrySkill,
1241
1556
  entrySkillFound: true,
1242
1557
  manifestDigest: asset.manifestDigest,
1558
+ ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1243
1559
  },
1244
1560
  status: 'PENDING',
1245
1561
  });
@@ -1891,7 +2207,24 @@ export async function prepareRelease(options) {
1891
2207
  };
1892
2208
  });
1893
2209
 
1894
- const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets);
2210
+ // Freeze external independent marketplace HEADs (production + online only):
2211
+ // for each claude/codex distribution declaring marketplaceRepo, resolve the
2212
+ // external repo's HEAD sha + default branch and validate the marketplace
2213
+ // index entry at that sha before freezing the add-ref. Offline production
2214
+ // with a declared marketplaceRepo fails closed inside the resolver. The
2215
+ // remote is only ever read (git ls-remote / gh api), never written.
2216
+ const externalMarketplaceFreezes = production
2217
+ ? await resolveExternalMarketplaceFreezes({
2218
+ unitResults,
2219
+ resolvedVersions,
2220
+ offline,
2221
+ evidence,
2222
+ observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2223
+ fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2224
+ })
2225
+ : new Map();
2226
+
2227
+ const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
1895
2228
 
1896
2229
  // Compute overall snapshot digest
1897
2230
  const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
@@ -85,6 +85,7 @@ const MARKETPLACE_TYPES = new Set([
85
85
  'claude-marketplace-install',
86
86
  'codex-marketplace-install',
87
87
  'kimi-marketplace-install',
88
+ 'codebuddy-marketplace-install',
88
89
  ]);
89
90
 
90
91
  // ---------------------------------------------------------------------------
@@ -79,6 +79,7 @@ const MARKETPLACE_TYPES = new Set([
79
79
  'claude-marketplace-install',
80
80
  'codex-marketplace-install',
81
81
  'kimi-marketplace-install',
82
+ 'codebuddy-marketplace-install',
82
83
  ]);
83
84
 
84
85
  // ---------------------------------------------------------------------------
@@ -89,6 +89,7 @@ async function walkDiscoveryFiles(root, maxDepth = 8) {
89
89
  absolute.endsWith('/.claude-plugin/plugin.json') ||
90
90
  absolute.endsWith('/.codex-plugin/plugin.json') ||
91
91
  absolute.endsWith('/.kimi-plugin/plugin.json') ||
92
+ absolute.endsWith('/.codebuddy-plugin/plugin.json') ||
92
93
  absolute.endsWith('/.claude-plugin/marketplace.json') ||
93
94
  absolute.endsWith('/.codex-plugin/marketplace.json'))
94
95
  ) {
@@ -461,7 +462,8 @@ async function discoverFacts(root) {
461
462
  path: safeRelative(root, path),
462
463
  host: path.includes('/.claude-plugin/') ? 'claude'
463
464
  : path.includes('/.kimi-plugin/') ? 'kimi'
464
- : 'codex',
465
+ : path.includes('/.codebuddy-plugin/') ? 'codebuddy'
466
+ : 'codex',
465
467
  kind: path.endsWith('/marketplace.json') ? 'marketplace' : 'plugin',
466
468
  name: typeof value.name === 'string' ? value.name : null,
467
469
  version: typeof value.version === 'string' ? value.version : null,
@@ -487,7 +489,8 @@ async function discoverFacts(root) {
487
489
  host: relPath.includes('/adapters/claude/') ? 'claude'
488
490
  : relPath.includes('/adapters/codex/') ? 'codex'
489
491
  : relPath.includes('/adapters/kimi/') ? 'kimi'
490
- : 'shared',
492
+ : relPath.includes('/adapters/workbuddy/') ? 'codebuddy'
493
+ : 'shared',
491
494
  };
492
495
  })
493
496
  .filter((item) => item.name)
@@ -655,7 +658,7 @@ function buildCandidates(facts) {
655
658
  const ids = new Set();
656
659
  const knownFiles = new Set(facts.fileDigests.map((file) => file.path));
657
660
  const manifestRoots = facts.manifests.map((manifest) => {
658
- const match = manifest.path.match(/^(.*?)(?:\/)?(?:\.claude-plugin|\.codex-plugin|\.kimi-plugin)\/(?:plugin|marketplace)\.json$/);
661
+ const match = manifest.path.match(/^(.*?)(?:\/)?(?:\.claude-plugin|\.codex-plugin|\.kimi-plugin|\.codebuddy-plugin)\/(?:plugin|marketplace)\.json$/);
659
662
  return { ...manifest, root: match?.[1] || '.' };
660
663
  });
661
664
  const manifestOwners = new Map();
@@ -703,6 +706,7 @@ function buildCandidates(facts) {
703
706
  if (pluginHosts.includes('claude')) distributions.push('claude-plugin');
704
707
  if (pluginHosts.includes('codex')) distributions.push('codex-plugin');
705
708
  if (pluginHosts.includes('kimi')) distributions.push('kimi-plugin');
709
+ if (pluginHosts.includes('codebuddy')) distributions.push('codebuddy-plugin');
706
710
  if (pkg.private && matchingLegacyUnits.length === 0 && facts.legacyReleaseConfigs.length > 0) continue;
707
711
  if (pkg.private && distributions.length === 0) continue;
708
712
 
@@ -69,6 +69,7 @@ const ADAPTER_ACTION_TYPE_MAP = {
69
69
  'claude-marketplace-install': 'claude-marketplace-install',
70
70
  'codex-marketplace-install': 'codex-marketplace-install',
71
71
  'kimi-marketplace-install': 'kimi-marketplace-install',
72
+ 'codebuddy-marketplace-install': 'codebuddy-marketplace-install',
72
73
  };
73
74
 
74
75
  // ---------------------------------------------------------------------------
@@ -735,6 +736,7 @@ export async function verifyRelease(options) {
735
736
  'claude-marketplace-install',
736
737
  'codex-marketplace-install',
737
738
  'kimi-marketplace-install',
739
+ 'codebuddy-marketplace-install',
738
740
  ]);
739
741
 
740
742
  for (const action of actions) {
@@ -847,7 +849,9 @@ export async function verifyRelease(options) {
847
849
  ? 'claude-plugin'
848
850
  : action.type === 'codex-marketplace-install'
849
851
  ? 'codex-plugin'
850
- : 'kimi-plugin';
852
+ : action.type === 'codebuddy-marketplace-install'
853
+ ? 'codebuddy-plugin'
854
+ : 'kimi-plugin';
851
855
  const installPath = verifyResult.observation?.installPath;
852
856
  consumerGateResults.push(...await runConsumerVerificationGates({
853
857
  plan,
@@ -866,10 +870,14 @@ export async function verifyRelease(options) {
866
870
  HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
867
871
  CODEX_HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
868
872
  }
869
- : {
870
- HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
871
- KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
872
- },
873
+ : action.type === 'codebuddy-marketplace-install'
874
+ ? {
875
+ HOME: resolve(runDir, 'consumers', `codebuddy-${action.parameters.plugin}`),
876
+ }
877
+ : {
878
+ HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
879
+ KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
880
+ },
873
881
  }));
874
882
  } else {
875
883
  // --- Non-marketplace: read-only adapter.verify() ---
@@ -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,
@@ -33,6 +33,7 @@ export const CHECKPOINT_ORDER = [
33
33
  'claude-marketplace-install',
34
34
  'codex-marketplace-install',
35
35
  'kimi-marketplace-install',
36
+ 'codebuddy-marketplace-install',
36
37
  ];
37
38
 
38
39
  /**
@@ -52,6 +53,7 @@ export const ADAPTER_ACTION_TYPE_MAP = {
52
53
  'claude-marketplace-install': 'claude-marketplace-install',
53
54
  'codex-marketplace-install': 'codex-marketplace-install',
54
55
  'kimi-marketplace-install': 'kimi-marketplace-install',
56
+ 'codebuddy-marketplace-install': 'codebuddy-marketplace-install',
55
57
  };
56
58
 
57
59
  /**
@@ -67,7 +69,10 @@ export const ADAPTER_ACTION_TYPE_MAP = {
67
69
  * - Tier 2 `github-release` and the claude/codex marketplace installs depend
68
70
  * on Tier 1 `create-tag` (release `--verify-tag`; install ref is the tag).
69
71
  * - Tier 3 `kimi-marketplace-install` depends on Tier 2 `github-release`
70
- * (its install URL points at the Release page).
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).
71
76
  *
72
77
  * Action types not listed in any tier are unknown to the scheduler and fail
73
78
  * closed (see groupActionsByTier); they are never silently scheduled.
@@ -76,7 +81,7 @@ export const TIER_TABLE = [
76
81
  ['push-commit', 'push-snapshot'], // Tier 0
77
82
  ['set-default-branch', 'create-tag', 'npm-publish'], // Tier 1
78
83
  ['github-release', 'claude-marketplace-install', 'codex-marketplace-install'], // Tier 2
79
- ['kimi-marketplace-install'], // Tier 3
84
+ ['kimi-marketplace-install', 'codebuddy-marketplace-install'], // Tier 3
80
85
  ];
81
86
 
82
87
  /** Fast reverse lookup: action type -> tier index (-1 when unknown). */