release-skill 0.9.4 → 0.9.6

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 (59) 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 +56 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +21 -17
  10. package/README.zh-CN.md +18 -16
  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 +1036 -174
  14. package/adapters/claude/schemas/release-plan.schema.json +36 -0
  15. package/adapters/claude/schemas/release-project.schema.json +31 -0
  16. package/adapters/claude/skills/release-finish/SKILL.md +5 -5
  17. package/adapters/claude/skills/release-help/SKILL.md +7 -3
  18. package/adapters/claude/skills/release-verify/SKILL.md +14 -1
  19. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  20. package/adapters/codex/bin/release-skill.bundle.mjs +1036 -174
  21. package/adapters/codex/schemas/release-plan.schema.json +36 -0
  22. package/adapters/codex/schemas/release-project.schema.json +31 -0
  23. package/adapters/codex/skills/release-finish/SKILL.md +5 -5
  24. package/adapters/codex/skills/release-help/SKILL.md +7 -3
  25. package/adapters/codex/skills/release-verify/SKILL.md +14 -1
  26. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  27. package/adapters/kimi/bin/release-skill.bundle.mjs +1036 -174
  28. package/adapters/kimi/schemas/release-plan.schema.json +36 -0
  29. package/adapters/kimi/schemas/release-project.schema.json +31 -0
  30. package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
  31. package/adapters/kimi/skills/release-help/SKILL.md +7 -3
  32. package/adapters/kimi/skills/release-verify/SKILL.md +14 -1
  33. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  34. package/adapters/workbuddy/bin/release-skill.bundle.mjs +1036 -174
  35. package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
  36. package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
  37. package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
  38. package/adapters/workbuddy/skills/release-help/SKILL.md +7 -3
  39. package/adapters/workbuddy/skills/release-verify/SKILL.md +14 -1
  40. package/bin/release-skill-cli.mjs +217 -26
  41. package/bin/release-skill.bundle.mjs +1036 -174
  42. package/package.json +1 -1
  43. package/platform-manifest.json +4 -4
  44. package/schemas/release-plan.schema.json +36 -0
  45. package/schemas/release-project.schema.json +31 -0
  46. package/skills/release-finish/SKILL.md +5 -5
  47. package/skills/release-help/SKILL.md +7 -3
  48. package/skills/release-verify/SKILL.md +14 -1
  49. package/skills-src/release-finish/SKILL.md +5 -5
  50. package/skills-src/release-help/SKILL.md +7 -3
  51. package/skills-src/release-verify/SKILL.md +14 -1
  52. package/src/adapters/plugin-marketplace.mjs +54 -5
  53. package/src/commands/post-release-local.mjs +204 -10
  54. package/src/commands/prepare.mjs +178 -9
  55. package/src/commands/ship.mjs +26 -11
  56. package/src/commands/verify.mjs +175 -2
  57. package/src/core/plan.mjs +45 -1
  58. package/src/platforms/registry.mjs +97 -0
  59. package/src/readme/contract.mjs +23 -3
@@ -68,6 +68,7 @@ import {
68
68
  verifyFrozenPluginWithFoundation,
69
69
  } from '../core/foundation-plugin-verification.mjs';
70
70
  import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
71
+ import { canonicalJson } from '../core/digest.mjs';
71
72
  import {
72
73
  normalizeRegistry,
73
74
  registryTokenKey,
@@ -83,7 +84,7 @@ import {
83
84
  } from '../core/skill-resource-closure.mjs';
84
85
  import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
85
86
  import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
86
- import { PLATFORMS, normalizeHostId } from '../platforms/registry.mjs';
87
+ import { PLATFORMS, normalizeHostId, projectObservedStandaloneIndexInstallIdentity } from '../platforms/registry.mjs';
87
88
  import {
88
89
  shouldSkipVerification,
89
90
  INSTALLATION_CONTRACT_ALGORITHM_VERSION,
@@ -183,6 +184,140 @@ function defaultClock() {
183
184
  return new Date().toISOString();
184
185
  }
185
186
 
187
+ const FIRST_RELEASE_BOOTSTRAP_MODE = 'manual-index-checkpoint';
188
+ const EXTERNAL_MARKETPLACE_SHA_RE = /^[0-9a-f]{40}$/;
189
+
190
+ function parseMarketplaceHead(stdout) {
191
+ if (typeof stdout !== 'string') return null;
192
+ let sha = null;
193
+ let defaultBranch = null;
194
+ for (const line of stdout.trim().split('\n').filter(Boolean)) {
195
+ const tab = line.indexOf('\t');
196
+ if (tab < 0) continue;
197
+ const left = line.slice(0, tab);
198
+ const right = line.slice(tab + 1);
199
+ if (right !== 'HEAD') continue;
200
+ if (left.startsWith('ref: refs/heads/')) defaultBranch = left.slice('ref: refs/heads/'.length);
201
+ else if (EXTERNAL_MARKETPLACE_SHA_RE.test(left)) sha = left;
202
+ }
203
+ return sha && defaultBranch ? { sha, defaultBranch } : null;
204
+ }
205
+
206
+ async function defaultObserveMarketplaceHead(repo, { githubHost = 'github.com' } = {}) {
207
+ try {
208
+ const { stdout } = await execFile('git', [
209
+ 'ls-remote', '--symref', `https://${githubHost}/${repo}.git`, 'HEAD',
210
+ ], { shell: false, encoding: 'utf8', timeout: 30000 });
211
+ const parsed = parseMarketplaceHead(stdout);
212
+ return parsed ? { status: 'observed', ...parsed } : { status: 'unknown', error: 'could not resolve marketplace HEAD' };
213
+ } catch (error) {
214
+ return { status: 'unknown', error: error.message };
215
+ }
216
+ }
217
+
218
+ function decodeMarketplaceIndex(content) {
219
+ if (typeof content !== 'string') return null;
220
+ try {
221
+ return JSON.parse(Buffer.from(content.replace(/\s/g, ''), 'base64').toString('utf8'));
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ async function defaultFetchMarketplaceIndex(repo, path, ref, { githubHost = 'github.com' } = {}) {
228
+ try {
229
+ const { stdout } = await execFile('gh', [
230
+ 'api', `repos/${repo}/contents/${path}?ref=${ref}`, '--jq', '.content',
231
+ ], {
232
+ shell: false,
233
+ encoding: 'utf8',
234
+ timeout: 30000,
235
+ env: { ...process.env, GH_HOST: githubHost },
236
+ });
237
+ const index = decodeMarketplaceIndex(stdout);
238
+ return index && typeof index === 'object'
239
+ ? { status: 'fetched', index }
240
+ : { status: 'unknown', error: 'could not decode marketplace index' };
241
+ } catch (error) {
242
+ return { status: 'unknown', error: error.message };
243
+ }
244
+ }
245
+
246
+ async function verifyFirstReleaseBootstrapIndexes({
247
+ actions,
248
+ plan,
249
+ evidence,
250
+ observeHeadFn,
251
+ fetchIndexFn,
252
+ }) {
253
+ const bindings = new Map();
254
+ for (const action of actions) {
255
+ if (action.parameters?.firstReleaseBootstrap !== FIRST_RELEASE_BOOTSTRAP_MODE) continue;
256
+ const params = action.parameters;
257
+ const githubHost = params.githubHost ?? 'github.com';
258
+ const failDeferred = async (reason, observed = null) => {
259
+ await evidence.append({
260
+ phase: 'verify-marketplace-bootstrap-index',
261
+ actionId: action.id,
262
+ actionType: action.type,
263
+ status: 'needs-input',
264
+ reason,
265
+ ...(observed ? { observed } : {}),
266
+ });
267
+ throw new ReleaseError(
268
+ CONSUMER_VERIFICATION_DEFERRED,
269
+ `first-release bootstrap marketplace index is not an exact match for action "${action.id}"; complete the manual index checkpoint and retry verify with the same plan`,
270
+ { actionId: action.id, reason, observed, nextState: 'NEEDS_MANUAL_ATTESTATIONS' },
271
+ );
272
+ };
273
+ const observed = await observeHeadFn(params.repo, { githubHost });
274
+ if (observed?.status !== 'observed' || !EXTERNAL_MARKETPLACE_SHA_RE.test(observed.sha ?? '') || !observed.defaultBranch) {
275
+ await failDeferred(`could not observe current marketplace HEAD: ${observed?.error ?? 'unknown'}`, observed);
276
+ }
277
+ const path = params.marketplaceIndexPath;
278
+ const fetched = await fetchIndexFn(params.repo, path, observed.sha, { githubHost });
279
+ if (fetched?.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
280
+ await failDeferred(`could not read marketplace index at ${observed.sha}: ${fetched?.error ?? 'unknown'}`, observed);
281
+ }
282
+ const index = fetched.index;
283
+ if (index.name !== params.marketplaceName) {
284
+ await failDeferred(`marketplace name mismatch: expected ${params.marketplaceName}, got ${index.name}`, { ...observed, name: index.name });
285
+ }
286
+ if (!Array.isArray(index.plugins)) {
287
+ await failDeferred('marketplace index plugins must be an array', { ...observed, pluginsType: typeof index.plugins });
288
+ }
289
+ const entries = index.plugins.filter((entry) => entry && entry.name === params.plugin);
290
+ let projectedEntry = null;
291
+ if (entries.length === 1) {
292
+ try {
293
+ projectedEntry = projectObservedStandaloneIndexInstallIdentity(params.consumer, entries[0]);
294
+ } catch {
295
+ projectedEntry = null;
296
+ }
297
+ }
298
+ if (entries.length !== 1 || !projectedEntry || canonicalJson(projectedEntry) !== canonicalJson(params.selectedEntry)) {
299
+ await failDeferred('marketplace selectedEntry is missing, duplicated, or does not exactly match the plan-bound expected entry', {
300
+ ...observed,
301
+ entryCount: entries.length,
302
+ selectedEntry: projectedEntry ?? entries[0] ?? null,
303
+ });
304
+ }
305
+ bindings.set(action.id, {
306
+ marketplaceIndexSha: observed.sha,
307
+ marketplaceIndexRef: action.type === 'claude-marketplace-install' ? observed.defaultBranch : observed.sha,
308
+ });
309
+ await evidence.append({
310
+ phase: 'verify-marketplace-bootstrap-index',
311
+ actionId: action.id,
312
+ actionType: action.type,
313
+ status: 'observed',
314
+ marketplaceIndexSha: observed.sha,
315
+ marketplaceIndexRef: action.type === 'claude-marketplace-install' ? observed.defaultBranch : observed.sha,
316
+ });
317
+ }
318
+ return bindings;
319
+ }
320
+
186
321
  /**
187
322
  * 验证摘要格式是否为合法的 64 位十六进制字符串。
188
323
  *
@@ -957,6 +1092,8 @@ export async function verifyRelease(options) {
957
1092
  execFn,
958
1093
  configPath: configPathOpt,
959
1094
  runPluginVerificationFn,
1095
+ observeFirstReleaseMarketplaceHeadFn,
1096
+ fetchFirstReleaseMarketplaceIndexFn,
960
1097
  } = options ?? {};
961
1098
 
962
1099
  const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
@@ -1265,6 +1402,19 @@ export async function verifyRelease(options) {
1265
1402
  const consumerVerificationReceipts = [];
1266
1403
  const actions = plan.externalActions ?? [];
1267
1404
 
1405
+ // A bootstrap plan intentionally leaves the final marketplace index SHA
1406
+ // pending. Before any marketplace adapter call, observe the current
1407
+ // remote index and require an exact match for the plan-bound entry. A
1408
+ // mismatch is the existing deferred/manual-attestation state; no add or
1409
+ // install command is reached on this path.
1410
+ const bootstrapIndexBindings = await verifyFirstReleaseBootstrapIndexes({
1411
+ actions,
1412
+ plan,
1413
+ evidence,
1414
+ observeHeadFn: observeFirstReleaseMarketplaceHeadFn ?? defaultObserveMarketplaceHead,
1415
+ fetchIndexFn: fetchFirstReleaseMarketplaceIndexFn ?? defaultFetchMarketplaceIndex,
1416
+ });
1417
+
1268
1418
  // --- 自动发现可信消费端验证收据 ---
1269
1419
  // 收据选择逻辑(per-action 从所有候选中选最新匹配):
1270
1420
  // - 只读取同一权威 .release-skill/runs 的真实直接子目录
@@ -1533,10 +1683,21 @@ export async function verifyRelease(options) {
1533
1683
  runDir,
1534
1684
  };
1535
1685
 
1686
+ const bootstrapBinding = bootstrapIndexBindings.get(action.id) ?? null;
1536
1687
  const actionInput = {
1537
1688
  actionType: adapterActionType,
1538
1689
  ...action.parameters,
1539
1690
  };
1691
+ if (bootstrapBinding) {
1692
+ actionInput.marketplaceCommitSha = bootstrapBinding.marketplaceIndexSha;
1693
+ actionInput.marketplaceIndexSha = bootstrapBinding.marketplaceIndexSha;
1694
+ actionInput.ref = bootstrapBinding.marketplaceIndexRef;
1695
+ actionInput.sourceDescriptor = {
1696
+ ...actionInput.sourceDescriptor,
1697
+ marketplaceCommitSha: bootstrapBinding.marketplaceIndexSha,
1698
+ ref: bootstrapBinding.marketplaceIndexRef,
1699
+ };
1700
+ }
1540
1701
 
1541
1702
  // --- 安装契约摘要免验检查 ---
1542
1703
  // 从计划中读取冻结的安装契约摘要。
@@ -1614,6 +1775,7 @@ export async function verifyRelease(options) {
1614
1775
  actionType: action.type,
1615
1776
  status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1616
1777
  installationContractDigest: currentDigest,
1778
+ ...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
1617
1779
  reason: '安装契约摘要未变化,跳过验证',
1618
1780
  });
1619
1781
 
@@ -1634,6 +1796,7 @@ export async function verifyRelease(options) {
1634
1796
  actionType: action.type,
1635
1797
  status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
1636
1798
  installationContractDigest: currentDigest,
1799
+ ...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
1637
1800
  });
1638
1801
 
1639
1802
  return;
@@ -1657,8 +1820,16 @@ export async function verifyRelease(options) {
1657
1820
  }
1658
1821
 
1659
1822
  // Step 3c: Verify (observe + match against plan expected state)
1823
+ const expectedInput = bootstrapBinding
1824
+ ? {
1825
+ ...action.expected,
1826
+ marketplaceCommitSha: bootstrapBinding.marketplaceIndexSha,
1827
+ marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha,
1828
+ ref: bootstrapBinding.marketplaceIndexRef,
1829
+ }
1830
+ : action.expected;
1660
1831
  const verifyResult = await adapter.verify(
1661
- { ...actionInput, expected: action.expected },
1832
+ { ...actionInput, expected: expectedInput },
1662
1833
  marketplaceContext,
1663
1834
  );
1664
1835
 
@@ -1676,6 +1847,7 @@ export async function verifyRelease(options) {
1676
1847
  status: resolvedStatus,
1677
1848
  observation: verifyResult.observation,
1678
1849
  error: verifyResult.error,
1850
+ ...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
1679
1851
  ...(dist?.installationContractDigest ? { installationContractDigest: dist.installationContractDigest } : {}),
1680
1852
  };
1681
1853
  adapterChecks.push(check);
@@ -1700,6 +1872,7 @@ export async function verifyRelease(options) {
1700
1872
  actionId: action.id,
1701
1873
  actionType: action.type,
1702
1874
  status: check.status,
1875
+ ...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
1703
1876
  });
1704
1877
 
1705
1878
  if (check.status === 'FAILED') {
package/src/core/plan.mjs CHANGED
@@ -34,7 +34,7 @@ import { digestReleaseAssetIdentities, normalizeReleaseAssets } from './release-
34
34
  // function bodies (after the import graph has settled), never at plan.mjs
35
35
  // module-init time — a top-level read would hit the registry's
36
36
  // not-yet-initialized bindings when the cycle is entered registry-first.
37
- import { PLATFORMS } from '../platforms/registry.mjs';
37
+ import { PLATFORMS, buildExpectedStandaloneIndexInstallIdentity } from '../platforms/registry.mjs';
38
38
 
39
39
  // ---------------------------------------------------------------------------
40
40
  // Schema loaded from the authoritative JSON file (single source of truth)
@@ -1278,6 +1278,7 @@ export function validatePlanActionCompleteness(plan, options = {}) {
1278
1278
  // 也不得接受"用 null 表示存在"的方案。
1279
1279
  if (production && externalMarketplace && dist.marketplaceSourceType === 'standalone-index') {
1280
1280
  const params = action.parameters;
1281
+ const bootstrap = dist.firstReleaseBootstrap === 'manual-index-checkpoint';
1281
1282
  if (params) {
1282
1283
  if (!params.marketplaceIndexPath) {
1283
1284
  failures.push(
@@ -1294,6 +1295,41 @@ export function validatePlanActionCompleteness(plan, options = {}) {
1294
1295
  `unit "${unitId}", action "${action.id}": parameters.selectedEntry is required for production standalone-index`,
1295
1296
  );
1296
1297
  }
1298
+ if (bootstrap) {
1299
+ if (params.firstReleaseBootstrap !== 'manual-index-checkpoint') {
1300
+ failures.push(
1301
+ `unit "${unitId}", action "${action.id}": parameters.firstReleaseBootstrap must be manual-index-checkpoint for a bootstrap distribution`,
1302
+ );
1303
+ }
1304
+ if (params.marketplaceIndexSha !== null) {
1305
+ failures.push(
1306
+ `unit "${unitId}", action "${action.id}": parameters.marketplaceIndexSha must remain null until the manual index checkpoint`,
1307
+ );
1308
+ }
1309
+ let expectedEntry = null;
1310
+ try {
1311
+ expectedEntry = buildExpectedStandaloneIndexInstallIdentity(platform, {
1312
+ name: plugin,
1313
+ version: targetVersion,
1314
+ repo: publicRepo,
1315
+ tag: expectedTag,
1316
+ sha: frozen?.commit,
1317
+ });
1318
+ } catch {
1319
+ // Missing frozen identity is reported by the surrounding
1320
+ // production-field checks; do not turn completeness into an
1321
+ // uncaught implementation error.
1322
+ }
1323
+ if (!expectedEntry || canonicalJson(params.selectedEntry) !== canonicalJson(expectedEntry)) {
1324
+ failures.push(
1325
+ `unit "${unitId}", action "${action.id}": bootstrap selectedEntry does not match the frozen plugin repository, tag, commit, and version`,
1326
+ );
1327
+ }
1328
+ } else if (params.firstReleaseBootstrap !== undefined || params.marketplaceIndexSha !== undefined) {
1329
+ failures.push(
1330
+ `unit "${unitId}", action "${action.id}": bootstrap-only fields are not allowed without firstReleaseBootstrap`,
1331
+ );
1332
+ }
1297
1333
  }
1298
1334
  }
1299
1335
 
@@ -1329,6 +1365,14 @@ export function validatePlanActionCompleteness(plan, options = {}) {
1329
1365
  _checkRequired(action, 'expected.ref', action.expected?.ref, action.parameters?.ref, unitId, failures);
1330
1366
  _checkRequired(action, 'expected.marketplaceLocation', action.expected?.marketplaceLocation, 'external', unitId, failures);
1331
1367
  _checkRequired(action, 'expected.marketplaceCommitSha', action.expected?.marketplaceCommitSha, action.parameters?.marketplaceCommitSha, unitId, failures);
1368
+ if (dist.firstReleaseBootstrap === 'manual-index-checkpoint') {
1369
+ _checkRequired(action, 'expected.firstReleaseBootstrap', action.expected?.firstReleaseBootstrap, 'manual-index-checkpoint', unitId, failures);
1370
+ if (action.expected?.marketplaceIndexSha !== null) {
1371
+ failures.push(
1372
+ `unit "${unitId}", action "${action.id}": expected.marketplaceIndexSha must be null until the manual index checkpoint`,
1373
+ );
1374
+ }
1375
+ }
1332
1376
  } else {
1333
1377
  _checkRequired(action, 'expected.repo', action.expected?.repo, publicRepo, unitId, failures);
1334
1378
  _checkRequired(action, 'expected.ref', action.expected?.ref, expectedTag, unitId, failures);
@@ -438,6 +438,103 @@ export function getPlatform(id) {
438
438
  return platform;
439
439
  }
440
440
 
441
+ const STANDALONE_SHA_RE = /^[0-9a-f]{40}$/u;
442
+
443
+ function standaloneIdentityCore(platformOrId, fields) {
444
+ const platform = typeof platformOrId === 'string' ? getPlatform(platformOrId) : platformOrId;
445
+ const id = platform?.id;
446
+ if (id !== 'claude' && id !== 'codex') {
447
+ throw new Error(`standalone-index install identity is unsupported for platform "${id ?? '<unknown>'}"`);
448
+ }
449
+ const { name, source, version } = fields;
450
+ if (typeof name !== 'string' || name.length === 0
451
+ || !source || typeof source !== 'object' || Array.isArray(source)
452
+ || typeof source.source !== 'string'
453
+ || typeof source.ref !== 'string' || source.ref.length === 0
454
+ || typeof source.sha !== 'string' || !STANDALONE_SHA_RE.test(source.sha)) {
455
+ throw new Error(`${id} standalone-index install identity has invalid owned fields`);
456
+ }
457
+ if (id === 'claude') {
458
+ if (source.source !== 'github' || typeof source.repo !== 'string' || source.repo.length === 0
459
+ || typeof version !== 'string' || version.length === 0) {
460
+ throw new Error('Claude standalone-index install identity requires github repo, ref, sha, and version');
461
+ }
462
+ return {
463
+ name,
464
+ source: { source: 'github', repo: source.repo, ref: source.ref, sha: source.sha },
465
+ version,
466
+ };
467
+ }
468
+ if (source.source !== 'url' || typeof source.url !== 'string' || source.url.length === 0) {
469
+ throw new Error('Codex standalone-index install identity requires url, ref, and sha');
470
+ }
471
+ return {
472
+ name,
473
+ source: { source: 'url', url: source.url, ref: source.ref, sha: source.sha },
474
+ };
475
+ }
476
+
477
+ /** Build the expected install identity from the frozen plugin contract. */
478
+ export function buildExpectedStandaloneIndexInstallIdentity(platformOrId, frozenIdentity = {}) {
479
+ const platform = typeof platformOrId === 'string' ? getPlatform(platformOrId) : platformOrId;
480
+ const id = platform?.id;
481
+ const { name, repo, tag, sha, version } = frozenIdentity;
482
+ if (typeof repo !== 'string' || repo.length === 0 || typeof tag !== 'string' || tag.length === 0) {
483
+ throw new Error('standalone-index expected identity requires a plugin repository and tag');
484
+ }
485
+ if (id === 'claude') {
486
+ return standaloneIdentityCore(platform, {
487
+ name,
488
+ version,
489
+ source: { source: 'github', repo, ref: tag, sha },
490
+ });
491
+ }
492
+ if (id === 'codex') {
493
+ return standaloneIdentityCore(platform, {
494
+ name,
495
+ source: { source: 'url', url: `https://github.com/${repo}.git`, ref: `refs/tags/${tag}`, sha },
496
+ });
497
+ }
498
+ return standaloneIdentityCore(platform, { name, source: {}, version });
499
+ }
500
+
501
+ /** Project only the actual remote entry; frozen plan fields are never accepted. */
502
+ export function projectObservedStandaloneIndexInstallIdentity(platformOrId, entry) {
503
+ const platform = typeof platformOrId === 'string' ? getPlatform(platformOrId) : platformOrId;
504
+ const id = platform?.id;
505
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
506
+ throw new Error('standalone-index observed identity requires an entry object');
507
+ }
508
+ const source = entry.source;
509
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
510
+ throw new Error('standalone-index observed identity requires a source object');
511
+ }
512
+ if (id === 'claude') {
513
+ return standaloneIdentityCore(platform, {
514
+ name: entry.name,
515
+ version: entry.version,
516
+ source: {
517
+ source: source.source,
518
+ repo: source.repo,
519
+ ref: source.ref,
520
+ sha: source.sha,
521
+ },
522
+ });
523
+ }
524
+ if (id === 'codex') {
525
+ return standaloneIdentityCore(platform, {
526
+ name: entry.name,
527
+ source: {
528
+ source: source.source,
529
+ url: source.url,
530
+ ref: source.ref,
531
+ sha: source.sha,
532
+ },
533
+ });
534
+ }
535
+ return standaloneIdentityCore(platform, { name: entry.name, source });
536
+ }
537
+
441
538
  /**
442
539
  * Resolve a declared skill projection surface to its public host name.
443
540
  * The host is always derived from the descriptor's authoritative
@@ -169,6 +169,28 @@ function findSkillNames(content, manifestSkillNames) {
169
169
  return manifestSkillNames.filter((name) => content.includes(name));
170
170
  }
171
171
 
172
+ /**
173
+ * Determine whether README content contains a supported installation command.
174
+ *
175
+ * npm and npx remain compatible with the original whole-document check. The
176
+ * platform plugin commands are checked only inside Markdown code so that a
177
+ * marketplace-source explanation cannot be mistaken for an installation.
178
+ * @param {string} content
179
+ * @returns {boolean}
180
+ */
181
+ function hasInstallCommand(content) {
182
+ if (/npm\s+install|npx\s+release-skill|npm\s+i\s+release-skill/i.test(content)) {
183
+ return true;
184
+ }
185
+
186
+ const codeBlocks = [
187
+ ...[...content.matchAll(/```[^\n]*\n([\s\S]*?)```/g)].map((match) => match[1]),
188
+ ...[...content.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]),
189
+ ];
190
+ const pluginInstall = /^(?:[$>]\s*)?(?:codex\s+plugin\s+add|claude\s+plugin\s+install)\s+\S+/i;
191
+ return codeBlocks.some((block) => block.split('\n').some((line) => pluginInstall.test(line.trim())));
192
+ }
193
+
172
194
  // ---------------------------------------------------------------------------
173
195
  // Public API
174
196
  // ---------------------------------------------------------------------------
@@ -250,9 +272,7 @@ export async function evaluateReadme({ snapshotDir, pluginManifest }) {
250
272
 
251
273
  // Readability checks: installation, minimal example, failure diagnosis
252
274
  const readabilityChecks = {
253
- hasInstall: enContent
254
- ? /npm\s+install|npx\s+release-skill|npm\s+i\s+release-skill/i.test(enContent)
255
- : false,
275
+ hasInstall: enContent ? hasInstallCommand(enContent) : false,
256
276
  hasMinimalExample: enContent
257
277
  ? /```[\s\S]*?(release-skill|assess|prepare|help)[\s\S]*?```/i.test(enContent)
258
278
  : false,