release-skill 0.4.0 → 0.4.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 (45) hide show
  1. package/.claude-plugin/marketplace.json +6 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codebuddy-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +4 -4
  5. package/.kimi-plugin/plugin.json +3 -3
  6. package/CHANGELOG.md +28 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/LICENSE +183 -18
  10. package/NOTICE +4 -2
  11. package/README.md +37 -21
  12. package/README.zh-CN.md +27 -18
  13. package/adapters/claude/.claude-plugin/marketplace.json +6 -2
  14. package/adapters/claude/.claude-plugin/plugin.json +2 -2
  15. package/adapters/claude/bin/release-skill.bundle.mjs +100 -18
  16. package/adapters/claude/schemas/release-plan.schema.json +5 -0
  17. package/adapters/claude/schemas/release-project.schema.json +5 -0
  18. package/adapters/claude/skills/release-setup/SKILL.md +6 -0
  19. package/adapters/codex/.codex-plugin/plugin.json +4 -4
  20. package/adapters/codex/bin/release-skill.bundle.mjs +100 -18
  21. package/adapters/codex/schemas/release-plan.schema.json +5 -0
  22. package/adapters/codex/schemas/release-project.schema.json +5 -0
  23. package/adapters/codex/skills/release-setup/SKILL.md +6 -0
  24. package/adapters/kimi/.kimi-plugin/plugin.json +3 -3
  25. package/adapters/kimi/bin/release-skill.bundle.mjs +100 -18
  26. package/adapters/kimi/schemas/release-plan.schema.json +5 -0
  27. package/adapters/kimi/schemas/release-project.schema.json +5 -0
  28. package/adapters/kimi/skills/release-setup/SKILL.md +6 -0
  29. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +2 -2
  30. package/adapters/workbuddy/bin/release-skill.bundle.mjs +100 -18
  31. package/adapters/workbuddy/schemas/release-plan.schema.json +5 -0
  32. package/adapters/workbuddy/schemas/release-project.schema.json +5 -0
  33. package/adapters/workbuddy/skills/release-setup/SKILL.md +6 -0
  34. package/bin/release-skill.bundle.mjs +100 -18
  35. package/package.json +2 -2
  36. package/schemas/release-plan.schema.json +5 -0
  37. package/schemas/release-project.schema.json +5 -0
  38. package/skills/release-setup/SKILL.md +6 -0
  39. package/skills-src/release-setup/SKILL.md +6 -0
  40. package/src/adapters/npm.mjs +31 -0
  41. package/src/adapters/plugin-marketplace.mjs +21 -5
  42. package/src/commands/prepare.mjs +19 -13
  43. package/src/core/plan.mjs +4 -3
  44. package/src/platforms/codebuddy.mjs +92 -10
  45. package/src/platforms/registry.mjs +4 -3
@@ -191,6 +191,11 @@
191
191
  "minLength": 1,
192
192
  "pattern": "^[a-z0-9][a-z0-9._-]*$"
193
193
  },
194
+ "marketplaceSource": {
195
+ "type": "string",
196
+ "minLength": 1,
197
+ "description": "消费者添加统一市场所用的来源 URL(如 codebuddy plugin marketplace add);可选,仅在可知时声明"
198
+ },
194
199
  "marketplaceRepo": {
195
200
  "type": "string",
196
201
  "pattern": "^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$",
@@ -91,6 +91,12 @@ node -e 'const fs=require("node:fs");const [c,p,a]=process.argv.slice(1).map(x=>
91
91
  node -e 'require("node:fs").rmSync(process.argv[1],{recursive:true,force:false})' "$SETUP_SESSION"
92
92
  ```
93
93
 
94
+ ## publisher 字段与个人片段泄漏策略
95
+
96
+ setup 生成的 `.release-skill/project.yaml` 中 `publisher` 是**已批准的公开发布身份**:它是人工确认的对外发布署名,属于公开面的一部分,而非需要隐藏的私有信息。
97
+
98
+ 若目标仓库用“个人片段”类泄漏策略扫描仓树,而该策略按片段匹配恰好覆盖到 `publisher` 字段,正确做法不是删除或改写策略,而是由贡献者在**本地、gitignore 的个人片段 overlay** 中为该规则声明位置豁免(如 `approvedPlacements`:限定 `publisher` 所在的确切文件路径与行键前缀)。豁免只放行已批准的放置位置,其余出现照常命中;overlay 不入库,个人片段本身不进 committed 策略。
99
+
94
100
  ## 故障路由
95
101
 
96
102
  - `NEEDS_INPUT`:审阅紧凑摘要;有冲突先修正权威事实并重跑。
@@ -91,6 +91,12 @@ node -e 'const fs=require("node:fs");const [c,p,a]=process.argv.slice(1).map(x=>
91
91
  node -e 'require("node:fs").rmSync(process.argv[1],{recursive:true,force:false})' "$SETUP_SESSION"
92
92
  ```
93
93
 
94
+ ## publisher 字段与个人片段泄漏策略
95
+
96
+ setup 生成的 `.release-skill/project.yaml` 中 `publisher` 是**已批准的公开发布身份**:它是人工确认的对外发布署名,属于公开面的一部分,而非需要隐藏的私有信息。
97
+
98
+ 若目标仓库用“个人片段”类泄漏策略扫描仓树,而该策略按片段匹配恰好覆盖到 `publisher` 字段,正确做法不是删除或改写策略,而是由贡献者在**本地、gitignore 的个人片段 overlay** 中为该规则声明位置豁免(如 `approvedPlacements`:限定 `publisher` 所在的确切文件路径与行键前缀)。豁免只放行已批准的放置位置,其余出现照常命中;overlay 不入库,个人片段本身不进 committed 策略。
99
+
94
100
  ## 故障路由
95
101
 
96
102
  - `NEEDS_INPUT`:审阅紧凑摘要;有冲突先修正权威事实并重跑。
@@ -257,6 +257,13 @@ async function readVerifiedTarballBytes(action, root) {
257
257
  * is missing, not valid JSON, or the extracted name/version do not match the
258
258
  * expected action values.
259
259
  *
260
+ * When the tarball contains a package/README.md entry (matched
261
+ * case-insensitively), its content is injected as manifest.readme and its
262
+ * real entry file name as manifest.readmeFilename, so the registry listing
263
+ * displays the package README. Ambiguous or unsafe README entries
264
+ * (duplicate or case-colliding names, non-regular files, out-of-bounds or
265
+ * oversized bodies) fail closed instead of being treated as metadata.
266
+ *
260
267
  * @param {Buffer} tarballBuffer - the raw tarball bytes (gzipped or plain tar)
261
268
  * @param {{ name: string, version: string }} expected - must match manifest
262
269
  * @returns {object} the parsed package.json manifest
@@ -275,6 +282,7 @@ function extractManifestFromTarball(tarballBuffer, expected) {
275
282
  // Walk tar entries to find package/package.json
276
283
  let offset = 0;
277
284
  let manifest = null;
285
+ let readme = null;
278
286
  while (offset + 512 <= data.length) {
279
287
  // Check for all-zero end-of-archive block
280
288
  let allZero = true;
@@ -330,6 +338,25 @@ function extractManifestFromTarball(tarballBuffer, expected) {
330
338
  }
331
339
  }
332
340
 
341
+ // Match package/README.md case-insensitively (npm registry metadata).
342
+ // The real entry file name is preserved in readmeFilename, and any
343
+ // duplicate, case-colliding, or non-regular README entry fails closed,
344
+ // mirroring the package/package.json checks above.
345
+ if (entryName.toLowerCase() === 'package/readme.md') {
346
+ if (readme) throw new Error('tarball contains conflicting package README entries');
347
+ if (!(typeFlag === 0 || typeFlag === 48 /* '0' */)) {
348
+ throw new Error('tarball package README must be a regular file');
349
+ }
350
+ const bodyStart = offset + 512;
351
+ const bodyEnd = bodyStart + size;
352
+ if (bodyEnd > data.length) throw new Error('frozen npm tarball entry exceeds archive bounds');
353
+ if (size > 10 * 1024 * 1024) throw new Error('tarball package README is unreasonably large');
354
+ readme = {
355
+ content: data.subarray(bodyStart, bodyEnd).toString('utf8'),
356
+ filename: entryName.slice('package/'.length),
357
+ };
358
+ }
359
+
333
360
  // Advance to next header: 512 header + ceil(size/512)*512 data
334
361
  const nextOffset = offset + 512 + Math.ceil(size / 512) * 512;
335
362
  if (!Number.isSafeInteger(nextOffset) || nextOffset > data.length) {
@@ -338,6 +365,10 @@ function extractManifestFromTarball(tarballBuffer, expected) {
338
365
  offset = nextOffset;
339
366
  }
340
367
  if (!manifest) throw new Error('tarball does not contain package/package.json');
368
+ if (readme) {
369
+ manifest.readme = readme.content;
370
+ manifest.readmeFilename = readme.filename;
371
+ }
341
372
  return manifest;
342
373
  }
343
374
 
@@ -48,8 +48,8 @@ import {
48
48
  import {
49
49
  CODEBUDDY_REQUIREMENT_FILE,
50
50
  CODEBUDDY_ATTESTATION_FILE,
51
- CODEBUDDY_MARKETPLACE_NAME,
52
- CODEBUDDY_MARKETPLACE_SOURCE,
51
+ resolveCodeBuddyMarketplace,
52
+ resolveCodeBuddyMarketplaceSource,
53
53
  resolveCodeBuddyBoundPlanDigest,
54
54
  codebuddyAuthorityDir,
55
55
  validateCodeBuddyAttestation,
@@ -3095,15 +3095,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3095
3095
  try {
3096
3096
  attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
3097
3097
  } catch {
3098
+ // The marketplace source hint follows the frozen action's
3099
+ // resolved marketplace; unknown configured marketplaces cite no
3100
+ // fabricated URL.
3101
+ const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
3098
3102
  return createResult({
3099
3103
  actionType,
3100
3104
  status: ActionStatus.OBSERVED,
3101
3105
  observation: {
3102
3106
  installed: false,
3103
3107
  manualInstallRequired: true,
3104
- marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
3108
+ ...(marketplaceSource ? { marketplaceSource } : {}),
3105
3109
  attestationDir,
3106
- error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${CODEBUDDY_MARKETPLACE_SOURCE})`,
3110
+ error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install${marketplaceSource ? ` (marketplace ${marketplaceSource})` : ''}`,
3107
3111
  },
3108
3112
  });
3109
3113
  }
@@ -3162,7 +3166,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3162
3166
  });
3163
3167
  }
3164
3168
  const normalizedPath = normalizedAttestation.installPath.replaceAll('\\', '/');
3165
- const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
3169
+ // The expected suffix follows the frozen action's resolved
3170
+ // marketplace (default constant for legacy plans).
3171
+ let resolvedMarketplace;
3172
+ try {
3173
+ resolvedMarketplace = resolveCodeBuddyMarketplace(action);
3174
+ } catch (resolveErr) {
3175
+ return createResult({
3176
+ actionType,
3177
+ status: ActionStatus.OBSERVED,
3178
+ observation: { installed: false, error: resolveErr.message },
3179
+ });
3180
+ }
3181
+ const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
3166
3182
  if (!normalizedPath.endsWith(expectedSuffix)) {
3167
3183
  return createResult({
3168
3184
  actionType,
@@ -1289,19 +1289,17 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1289
1289
  });
1290
1290
  // Consumer marketplace install actions (only when distribution
1291
1291
  // declared), driven by the platform registry (T2.2 step 3): one loop
1292
- // body for every platform; the registry declares the per-platform
1293
- // differences (actionType, distributionType, adapter, and via the
1294
- // schema required fields marketplace identity, which kimi does not
1295
- // carry: Kimi Code has no non-interactive install/marketplace API, so
1296
- // the kimi action carries no marketplace identity (MINOR-1); plugin +
1297
- // entrySkill are the meaningful identity fields there).
1292
+ // body for every platform. Marketplace identity follows the distribution
1293
+ // declaration: claude/codex require it via the registry schema fields;
1294
+ // kimi/codebuddy tolerate an optional declaration (codebuddy defaults to
1295
+ // its unified marketplace constant downstream when undeclared).
1296
+ // Undeclared stays absent so legacy frozen plans remain byte-identical.
1298
1297
  const frozenUnitDists = frozenDistributions?.get(unit.id) ?? null;
1299
1298
  for (const platform of PLATFORMS) {
1300
1299
  const dist = frozenUnitDists
1301
1300
  ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1302
1301
  : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1303
1302
  if (!dist) continue;
1304
- const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1305
1303
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
1306
1304
  // External independent marketplace form: the distribution declares
1307
1305
  // marketplaceRepo, so the install targets the external marketplace repo
@@ -1350,7 +1348,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1350
1348
  parameters: {
1351
1349
  consumer: platform.id,
1352
1350
  plugin: dist.plugin,
1353
- ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1351
+ ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1352
+ ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1354
1353
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1355
1354
  version,
1356
1355
  entrySkill: dist.entrySkill,
@@ -1377,7 +1376,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1377
1376
  expected: {
1378
1377
  installed: true,
1379
1378
  plugin: dist.plugin,
1380
- ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1379
+ ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1380
+ ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1381
1381
  version,
1382
1382
  entrySkill: dist.entrySkill,
1383
1383
  ...(externalMarketplace ? { marketplaceLocation: 'external', repo: dist.marketplaceRepo } : {}),
@@ -1532,13 +1532,17 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1532
1532
  // non-production loop above plus the production-only bindings
1533
1533
  // (ref/snapshotPath/manifestDigest parameters; consumer/repo/ref/
1534
1534
  // entrySkillFound/manifestDigest expected). Marketplace identity follows
1535
- // the registry's schema required fields kimi carries none (MINOR-1).
1535
+ // the distribution declaration, same as the non-production loop.
1536
1536
  for (const platform of PLATFORMS) {
1537
1537
  const dist = frozenUnitDists
1538
1538
  ? frozenUnitDists.find((d) => d.type === platform.distributionType)
1539
1539
  : (unit.distributions ?? []).find((d) => d.type === platform.distributionType);
1540
1540
  if (!dist) continue;
1541
- const requiresMarketplace = platform.schemaRequiredFields.includes('marketplace');
1541
+ // Marketplace identity follows the distribution declaration: claude/codex
1542
+ // require it via the registry schema fields; kimi/codebuddy tolerate an
1543
+ // optional declaration (codebuddy defaults to its unified marketplace
1544
+ // constant downstream when undeclared). Undeclared stays absent so
1545
+ // legacy frozen plans remain byte-identical.
1542
1546
  const timeoutMs = Number.isInteger(dist.timeoutMs) ? dist.timeoutMs : 300000;
1543
1547
  // External independent marketplace form: the install targets the external
1544
1548
  // marketplace repo with the add-ref + marketplaceCommitSha frozen online
@@ -1583,7 +1587,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1583
1587
  parameters: {
1584
1588
  consumer: platform.id,
1585
1589
  plugin: dist.plugin,
1586
- ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1590
+ ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1591
+ ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1587
1592
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1588
1593
  ref: externalMarketplace ? freeze.ref : resolvedTag,
1589
1594
  version: unitVersion,
@@ -1622,7 +1627,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1622
1627
  installed: true,
1623
1628
  consumer: platform.id,
1624
1629
  plugin: dist.plugin,
1625
- ...(requiresMarketplace ? { marketplace: dist.marketplace } : {}),
1630
+ ...(dist.marketplace !== undefined ? { marketplace: dist.marketplace } : {}),
1631
+ ...(dist.marketplaceSource !== undefined ? { marketplaceSource: dist.marketplaceSource } : {}),
1626
1632
  repo: externalMarketplace ? dist.marketplaceRepo : unit.publicRepo,
1627
1633
  version: unitVersion,
1628
1634
  ref: externalMarketplace ? freeze.ref : resolvedTag,
package/src/core/plan.mjs CHANGED
@@ -751,9 +751,10 @@ export function validatePlanActionCompleteness(plan, options = {}) {
751
751
  // consumer id, and — via the schema required fields — whether marketplace
752
752
  // identity is a required binding). Platforms without a required
753
753
  // marketplace (kimi, MINOR-1: Kimi Code has an interactive marketplace
754
- // but no non-interactive install API) tolerate a declared marketplace as
755
- // an optional legacy value but never bind it as a required condition, in
756
- // parameters or in expected.
754
+ // but no non-interactive install API; codebuddy, whose unified marketplace
755
+ // defaults to a constant but may be overridden by the distribution)
756
+ // tolerate a declared marketplace as an optional value but never bind it
757
+ // as a required condition, in parameters or in expected.
757
758
  for (const platform of PLATFORMS) {
758
759
  const dist = distributions.find((d) => d.type === platform.distributionType);
759
760
  if (!dist) continue;
@@ -88,15 +88,55 @@ export const CODEBUDDY_REQUIREMENT_FILE = 'release-skill-codebuddy-manual-instal
88
88
  export const CODEBUDDY_ATTESTATION_FILE = 'release-skill-codebuddy-attestation.json';
89
89
 
90
90
  /**
91
- * Unified marketplace the WorkBuddy desktop app and the codebuddy CLI install
92
- * release-skill from (verified fact). The attestation `marketplace` field must
93
- * equal this value; the install path is validated against this marketplace
94
- * segment in both channels.
91
+ * Default unified marketplace the WorkBuddy desktop app and the codebuddy CLI
92
+ * install from (verified fact). A distribution may override this by declaring
93
+ * `marketplace` (see resolveCodeBuddyMarketplace); the install path is
94
+ * validated against the resolved marketplace segment in both channels, and a
95
+ * declared attestation `marketplace` field must equal the resolved name.
95
96
  */
96
97
  export const CODEBUDDY_MARKETPLACE_NAME = 'artifact-skill-set';
97
- /** Marketplace source URL used by the isolated-CLI install path. */
98
+ /** Default marketplace source URL (used only when no marketplace override is declared). */
98
99
  export const CODEBUDDY_MARKETPLACE_SOURCE = 'https://github.com/ifoohoo/artifact-skill-set';
99
100
 
101
+ /**
102
+ * Resolve the unified marketplace name for a codebuddy action.
103
+ *
104
+ * Falls back to CODEBUDDY_MARKETPLACE_NAME when the action carries no
105
+ * `marketplace` (legacy frozen plans, byte-identical behavior). A declared
106
+ * marketplace must be a non-empty string; anything else fails closed.
107
+ *
108
+ * @param {object} [action] - expanded codebuddy action (may be undefined).
109
+ * @returns {string} the resolved marketplace name.
110
+ * @throws {Error} when a declared marketplace is not a non-empty string.
111
+ */
112
+ export function resolveCodeBuddyMarketplace(action) {
113
+ const marketplace = action?.marketplace ?? CODEBUDDY_MARKETPLACE_NAME;
114
+ if (typeof marketplace !== 'string' || marketplace.length === 0) {
115
+ throw new Error(`codebuddy marketplace must be a non-empty string when declared, got ${JSON.stringify(action?.marketplace)}`);
116
+ }
117
+ return marketplace;
118
+ }
119
+
120
+ /**
121
+ * Resolve the marketplace source URL for a codebuddy action, when knowable.
122
+ *
123
+ * The default marketplace has a verified source constant; a configured
124
+ * marketplace only has a source when the distribution declared
125
+ * `marketplaceSource`. When neither applies, null is returned and callers
126
+ * must NOT fabricate a URL.
127
+ *
128
+ * @param {object} [action] - expanded codebuddy action (may be undefined).
129
+ * @returns {string|null} the source URL, or null when unknown.
130
+ */
131
+ export function resolveCodeBuddyMarketplaceSource(action) {
132
+ if (action?.marketplace === undefined || action?.marketplace === null) {
133
+ return CODEBUDDY_MARKETPLACE_SOURCE;
134
+ }
135
+ return typeof action.marketplaceSource === 'string' && action.marketplaceSource.length > 0
136
+ ? action.marketplaceSource
137
+ : null;
138
+ }
139
+
100
140
  /** Authoritative codebuddy plugin manifest (single candidate, no precedence). */
101
141
  export const CODEBUDDY_PLUGIN_MANIFEST_RELATIVE = join('.codebuddy-plugin', 'plugin.json');
102
142
 
@@ -185,7 +225,7 @@ export function codebuddyAuthorityDir(context, planDigest, plugin) {
185
225
  /**
186
226
  * 统一人工安装说明:面向 CodeBuddy / WorkBuddy 的人工结果流程。
187
227
  *
188
- * @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean}} p
228
+ * @param {{plugin:string, version:string, ref:string, attestationDir:string, requiresInstalledClosure:boolean, marketplace:string, marketplaceSource:string|null}} p
189
229
  * @returns {string[]}
190
230
  */
191
231
  function buildCodeBuddyManualInstructions({
@@ -194,11 +234,13 @@ function buildCodeBuddyManualInstructions({
194
234
  ref,
195
235
  attestationDir,
196
236
  requiresInstalledClosure,
237
+ marketplace,
238
+ marketplaceSource,
197
239
  }) {
198
240
  return [
199
241
  `CodeBuddy/WorkBuddy 插件安装无法锁定冻结 ref(codebuddy CLI marketplace add/install 没有 ref 选项,跟踪默认分支),因此安装是需要人工结果证明的手动步骤。`,
200
242
  `1) publish 完成所有远端写入后进入 PUBLISHED 状态(自动化 Git 分支/标签、npm 和 GitHub Release 写入已完成)。此 codebuddy 检查点标记为需要人工安装。`,
201
- `2) 从统一市场 "${CODEBUDDY_MARKETPLACE_NAME}" (${CODEBUDDY_MARKETPLACE_SOURCE}) 安装 release-skill。确认安装的插件版本等于冻结版本 ${version}。`,
243
+ `2) 从统一市场 "${marketplace}"${marketplaceSource ? ` (${marketplaceSource})` : ''} 安装 ${plugin}。确认安装的插件版本等于冻结版本 ${version}。`,
202
244
  `3) 将人工结果 JSON 写入: ${attestationDir}/${CODEBUDDY_ATTESTATION_FILE}`,
203
245
  ` 必填字段: platform="codebuddy", version, planDigest(冻结计划摘要), result("passed" 或 "failed"), actor(确认人), confirmedAt(ISO 8601 时间戳)${requiresInstalledClosure ? ',installChannel("desktop" 或 "cli"),installPath(实际安装后的插件目录)' : ''}`,
204
246
  ` 可选字段: note(备注)`,
@@ -312,15 +354,35 @@ export function validateCodeBuddyAttestation(attestation, action, isoNow, boundP
312
354
  if (normalized.installPath.includes('..')) {
313
355
  return { valid: false, error: 'codebuddy attestation installPath must not contain path traversal ("..")', normalized: null };
314
356
  }
315
- // CLI 通道:installPath 必须以预期后缀结尾
357
+ // CLI 通道:installPath 必须以预期后缀结尾(市场名解析自冻结动作,
358
+ // 未声明时回落到默认统一市场常量)
316
359
  if (normalized.installChannel === 'cli') {
317
- const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
360
+ let resolvedMarketplace;
361
+ try {
362
+ resolvedMarketplace = resolveCodeBuddyMarketplace(action);
363
+ } catch (resolveErr) {
364
+ return { valid: false, error: resolveErr.message, normalized: null };
365
+ }
366
+ const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
318
367
  if (!normalized.installPath.endsWith(expectedSuffix)) {
319
368
  return { valid: false, error: `codebuddy CLI attestation installPath must end with "${expectedSuffix}", got "${normalized.installPath}"`, normalized: null };
320
369
  }
321
370
  }
322
371
  }
323
372
 
373
+ // 可选 marketplace 字段:一旦声明必须等于冻结动作解析出的市场名
374
+ if (normalized.marketplace !== undefined) {
375
+ let resolvedMarketplace;
376
+ try {
377
+ resolvedMarketplace = resolveCodeBuddyMarketplace(action);
378
+ } catch (resolveErr) {
379
+ return { valid: false, error: resolveErr.message, normalized: null };
380
+ }
381
+ if (normalized.marketplace !== resolvedMarketplace) {
382
+ return { valid: false, error: `codebuddy attestation marketplace "${normalized.marketplace}" does not match the resolved marketplace "${resolvedMarketplace}"`, normalized: null };
383
+ }
384
+ }
385
+
324
386
  // result 只接受 passed 或 failed
325
387
  if (normalized.result !== 'passed' && normalized.result !== 'failed') {
326
388
  return { valid: false, error: `codebuddy attestation result "${normalized.result}" must be "passed" or "failed"`, normalized: null };
@@ -454,6 +516,22 @@ export async function executeCodeBuddyManualRequirement(action, context) {
454
516
 
455
517
  const ref = action.ref ?? `v${action.version}`;
456
518
 
519
+ // Unified marketplace identity: resolved from the frozen action (declared
520
+ // `marketplace`/`marketplaceSource`), falling back to the default constants
521
+ // for legacy plans. A fabricated source URL is never emitted for a
522
+ // configured marketplace without a declared marketplaceSource.
523
+ let marketplace;
524
+ try {
525
+ marketplace = resolveCodeBuddyMarketplace(action);
526
+ } catch (marketplaceErr) {
527
+ return createResult({
528
+ actionType,
529
+ status: ActionStatus.EXECUTE_FAILED,
530
+ error: marketplaceErr.message,
531
+ });
532
+ }
533
+ const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
534
+
457
535
  // (B) Stable plugin-level authority dir, shared across
458
536
  // publish/reconcile/verify run dirs.
459
537
  let attestationDir;
@@ -473,6 +551,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
473
551
  ref,
474
552
  attestationDir,
475
553
  requiresInstalledClosure: Boolean(context.plan?.skillResourceClosure),
554
+ marketplace,
555
+ marketplaceSource,
476
556
  });
477
557
 
478
558
  // 统一 requirement 结构:不再包含隔离目录信息
@@ -484,6 +564,8 @@ export async function executeCodeBuddyManualRequirement(action, context) {
484
564
  repo: action.repo,
485
565
  ref,
486
566
  entrySkill: action.entrySkill,
567
+ marketplace,
568
+ ...(marketplaceSource ? { marketplaceSource } : {}),
487
569
  planDigest,
488
570
  attestationDir,
489
571
  attestationFile: CODEBUDDY_ATTESTATION_FILE,
@@ -497,7 +579,7 @@ export async function executeCodeBuddyManualRequirement(action, context) {
497
579
  ...(context.plan?.skillResourceClosure
498
580
  ? {
499
581
  installChannel: '<"desktop" or "cli">',
500
- installPath: `<actual .workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin} directory>`,
582
+ installPath: `<actual .workbuddy/plugins/marketplaces/${marketplace}/plugins/${action.plugin} directory>`,
501
583
  }
502
584
  : {}),
503
585
  note: '<optional note>',
@@ -359,9 +359,10 @@ const CODEBUDDY = Object.freeze({
359
359
  marketplace: '.claude-plugin/marketplace.json',
360
360
  }),
361
361
  marketplaceSourceForm: null,
362
- // codebuddy installs from a unified marketplace but carries no marketplace
363
- // identity in the action (the marketplace is a fixed constant in
364
- // ./codebuddy.mjs); the entry-version binding does not apply (null = N/A).
362
+ // codebuddy installs from a unified marketplace whose identity is optional
363
+ // on the action: a distribution may declare `marketplace` to override the
364
+ // default constant in ./codebuddy.mjs (resolveCodeBuddyMarketplace); the
365
+ // entry-version binding does not apply either way (null = N/A).
365
366
  marketplaceEntryCarriesVersion: null,
366
367
  // codebuddy has no marketplace add that can pin a frozen ref (attestation
367
368
  // loop instead), so the marketplace ref form does not apply (null = N/A).