release-skill 0.2.1 → 0.2.2

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 +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +21 -0
  7. package/INSTALL.md +183 -21
  8. package/INSTALL.zh-CN.md +160 -16
  9. package/README.md +112 -11
  10. package/README.zh-CN.md +73 -10
  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 +1718 -600
  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 +1718 -600
  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 +1718 -600
  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 +1718 -600
  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 +1718 -600
  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 +536 -23
  43. package/src/commands/assess.mjs +50 -1
  44. package/src/commands/prepare.mjs +273 -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 +2 -0
  49. package/src/core/checkpoints.mjs +7 -2
  50. package/src/core/plan.mjs +97 -4
  51. package/src/core/verification-gates.mjs +1 -1
  52. package/src/platforms/codebuddy.mjs +618 -0
  53. package/src/platforms/registry.mjs +100 -5
  54. package/src/producers/build-adapters.mjs +21 -13
@@ -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,13 @@ 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;
1029
1261
  actions.push({
1030
1262
  id: `${platform.actionType}-${unit.id}`,
1031
1263
  type: platform.actionType,
@@ -1035,7 +1267,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1035
1267
  consumer: platform.id,
1036
1268
  plugin: dist.plugin,
1037
1269
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1038
- repo: unit.publicRepo,
1270
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1039
1271
  version,
1040
1272
  entrySkill: dist.entrySkill,
1041
1273
  timeoutMs,
@@ -1043,7 +1275,10 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1043
1275
  // payload is verified by declared-manifest containment; host-added
1044
1276
  // files are recorded, not failed. Frozen plans without this marker
1045
1277
  // keep the legacy full-tree equality semantics byte-for-byte.
1046
- payloadContract: 'declared-manifest-v1',
1278
+ // External marketplace form uses the external-marketplace-v1
1279
+ // contract (whole-tree '.' containment; see plugin-marketplace).
1280
+ payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1281
+ ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1047
1282
  },
1048
1283
  expected: {
1049
1284
  installed: true,
@@ -1051,6 +1286,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1051
1286
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1052
1287
  version,
1053
1288
  entrySkill: dist.entrySkill,
1289
+ ...(externalMarketplace ? { marketplaceLocation: 'external', repo: dist.marketplaceRepo } : {}),
1054
1290
  },
1055
1291
  status: 'PENDING',
1056
1292
  });
@@ -1207,6 +1443,13 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1207
1443
  if (!dist) continue;
1208
1444
  const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1209
1445
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
1446
+ // External independent marketplace form: the install targets the external
1447
+ // marketplace repo with the add-ref + marketplaceCommitSha frozen online
1448
+ // by resolveExternalMarketplaceFreezes. snapshotPath/manifestDigest still
1449
+ // bind this unit's own frozen snapshot — the payload authority is the unit
1450
+ // snapshot, unchanged. Inline form (no marketplaceRepo) is byte-identical.
1451
+ const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1452
+ const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1210
1453
  actions.push({
1211
1454
  id: `${platform.actionType}-${unit.id}`,
1212
1455
  type: platform.actionType,
@@ -1216,8 +1459,8 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1216
1459
  consumer: platform.id,
1217
1460
  plugin: dist.plugin,
1218
1461
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1219
- repo: unit.publicRepo,
1220
- ref: resolvedTag,
1462
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1463
+ ref: externalMarketplace ? freeze.ref : resolvedTag,
1221
1464
  version: unitVersion,
1222
1465
  entrySkill: dist.entrySkill,
1223
1466
  snapshotPath: asset.snapshotPath,
@@ -1227,19 +1470,23 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
1227
1470
  // payload is verified by declared-manifest containment; host-added
1228
1471
  // files are recorded, not failed. Frozen plans without this marker
1229
1472
  // keep the legacy full-tree equality semantics byte-for-byte.
1230
- payloadContract: 'declared-manifest-v1',
1473
+ // External marketplace form uses the external-marketplace-v1
1474
+ // contract (whole-tree '.' containment; see plugin-marketplace).
1475
+ payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1476
+ ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1231
1477
  },
1232
1478
  expected: {
1233
1479
  installed: true,
1234
1480
  consumer: platform.id,
1235
1481
  plugin: dist.plugin,
1236
1482
  ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1237
- repo: unit.publicRepo,
1483
+ repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1238
1484
  version: unitVersion,
1239
- ref: resolvedTag,
1485
+ ref: externalMarketplace ? freeze.ref : resolvedTag,
1240
1486
  entrySkill: dist.entrySkill,
1241
1487
  entrySkillFound: true,
1242
1488
  manifestDigest: asset.manifestDigest,
1489
+ ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1243
1490
  },
1244
1491
  status: 'PENDING',
1245
1492
  });
@@ -1891,7 +2138,24 @@ export async function prepareRelease(options) {
1891
2138
  };
1892
2139
  });
1893
2140
 
1894
- const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets);
2141
+ // Freeze external independent marketplace HEADs (production + online only):
2142
+ // for each claude/codex distribution declaring marketplaceRepo, resolve the
2143
+ // external repo's HEAD sha + default branch and validate the marketplace
2144
+ // index entry at that sha before freezing the add-ref. Offline production
2145
+ // with a declared marketplaceRepo fails closed inside the resolver. The
2146
+ // remote is only ever read (git ls-remote / gh api), never written.
2147
+ const externalMarketplaceFreezes = production
2148
+ ? await resolveExternalMarketplaceFreezes({
2149
+ unitResults,
2150
+ resolvedVersions,
2151
+ offline,
2152
+ evidence,
2153
+ observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
2154
+ fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
2155
+ })
2156
+ : new Map();
2157
+
2158
+ const externalActions = buildExternalActions(unitResults, resolvedVersions, productionAssets, externalMarketplaceFreezes);
1895
2159
 
1896
2160
  // Compute overall snapshot digest
1897
2161
  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) {
@@ -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). */