release-skill 0.9.4 → 0.9.5

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 (58) 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 +29 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +20 -16
  10. package/README.zh-CN.md +19 -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 +866 -166
  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 +3 -3
  18. package/adapters/claude/skills/release-verify/SKILL.md +2 -1
  19. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  20. package/adapters/codex/bin/release-skill.bundle.mjs +866 -166
  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 +3 -3
  25. package/adapters/codex/skills/release-verify/SKILL.md +2 -1
  26. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  27. package/adapters/kimi/bin/release-skill.bundle.mjs +866 -166
  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 +3 -3
  32. package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
  33. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  34. package/adapters/workbuddy/bin/release-skill.bundle.mjs +866 -166
  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 +3 -3
  39. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
  40. package/bin/release-skill-cli.mjs +87 -20
  41. package/bin/release-skill.bundle.mjs +866 -166
  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 +3 -3
  48. package/skills/release-verify/SKILL.md +2 -1
  49. package/skills-src/release-finish/SKILL.md +5 -5
  50. package/skills-src/release-help/SKILL.md +3 -3
  51. package/skills-src/release-verify/SKILL.md +2 -1
  52. package/src/commands/post-release-local.mjs +204 -10
  53. package/src/commands/prepare.mjs +178 -9
  54. package/src/commands/ship.mjs +26 -11
  55. package/src/commands/verify.mjs +175 -2
  56. package/src/core/plan.mjs +45 -1
  57. package/src/platforms/registry.mjs +97 -0
  58. package/src/readme/contract.mjs +23 -3
@@ -77,7 +77,11 @@ import { acquireProjectLock } from '../artifacts/project-lock.mjs';
77
77
  import { observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
78
78
  import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
79
79
  import { createProductionPrepareRunDir } from '../core/run.mjs';
80
- import { PLATFORMS, normalizeHostId } from '../platforms/registry.mjs';
80
+ import {
81
+ PLATFORMS,
82
+ normalizeHostId,
83
+ buildExpectedStandaloneIndexInstallIdentity,
84
+ } from '../platforms/registry.mjs';
81
85
  import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
82
86
  import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
83
87
  import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
@@ -97,6 +101,7 @@ import { digestReleaseAssetIdentities } from '../core/release-assets.mjs';
97
101
 
98
102
  /** 消费端安装验证配方版本。算法变更时递增。 */
99
103
  const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
104
+ const FIRST_RELEASE_BOOTSTRAP_MODE = 'manual-index-checkpoint';
100
105
 
101
106
  // ---------------------------------------------------------------------------
102
107
  // Version resolution
@@ -1498,6 +1503,72 @@ async function defaultFetchExternalMarketplaceIndex(repo, manifestPath, ref, { g
1498
1503
  }
1499
1504
  }
1500
1505
 
1506
+ /**
1507
+ * Observe the plugin repository before a first standalone-index release.
1508
+ * This is deliberately a read-only, fail-closed observation: an empty
1509
+ * repository, no target tag, and no target GitHub Release are all required.
1510
+ */
1511
+ export function parseFirstReleasePluginRepositoryRefs(stdout, tag) {
1512
+ if (typeof stdout !== 'string' || typeof tag !== 'string' || tag.length === 0) {
1513
+ return { hasRefs: false, tagExists: false };
1514
+ }
1515
+ const refs = [];
1516
+ let hasRefs = false;
1517
+ for (const line of stdout.trim().split('\n').filter(Boolean)) {
1518
+ const tabIndex = line.indexOf('\t');
1519
+ if (tabIndex < 0) continue;
1520
+ const sha = line.slice(0, tabIndex);
1521
+ const ref = line.slice(tabIndex + 1);
1522
+ if (EXTERNAL_MARKETPLACE_SHA_RE.test(sha)) {
1523
+ hasRefs = true;
1524
+ if (ref && ref !== 'HEAD') refs.push(ref);
1525
+ }
1526
+ }
1527
+ const target = `refs/tags/${tag}`;
1528
+ return {
1529
+ hasRefs,
1530
+ tagExists: refs.includes(target) || refs.includes(`${target}^{}`),
1531
+ };
1532
+ }
1533
+
1534
+ async function defaultObserveFirstReleasePluginRepository(repo, tag, { githubHost = 'github.com' } = {}) {
1535
+ const url = `https://${githubHost}/${repo}.git`;
1536
+ let repositoryRefs;
1537
+ try {
1538
+ const { stdout } = await execFile(
1539
+ 'git', ['ls-remote', '--symref', url],
1540
+ { shell: false, encoding: 'utf8', timeout: 30000 },
1541
+ );
1542
+ repositoryRefs = parseFirstReleasePluginRepositoryRefs(String(stdout ?? ''), tag);
1543
+ } catch (error) {
1544
+ return { status: 'unknown', error: error.message };
1545
+ }
1546
+
1547
+ let releaseExists = false;
1548
+ try {
1549
+ await execFile(
1550
+ 'gh', ['api', `repos/${repo}/releases/tags/${tag}`],
1551
+ {
1552
+ shell: false,
1553
+ encoding: 'utf8',
1554
+ timeout: 30000,
1555
+ env: { ...process.env, GH_HOST: githubHost },
1556
+ },
1557
+ );
1558
+ releaseExists = true;
1559
+ } catch (error) {
1560
+ const diagnostic = `${error?.stderr ?? ''} ${error?.message ?? ''}`;
1561
+ if (!/\b404\b|not found/i.test(diagnostic)) {
1562
+ return { status: 'unknown', error: error.message };
1563
+ }
1564
+ }
1565
+ return { status: repositoryRefs.hasRefs ? 'non-empty' : 'empty', tagExists: repositoryRefs.tagExists, releaseExists };
1566
+ }
1567
+
1568
+ function isFirstReleaseBootstrap(dist) {
1569
+ return dist?.firstReleaseBootstrap === FIRST_RELEASE_BOOTSTRAP_MODE;
1570
+ }
1571
+
1501
1572
  /**
1502
1573
  * Freeze the external marketplace HEAD for every claude/codex distribution
1503
1574
  * that declares `marketplaceRepo` (production + online only). For each such
@@ -1518,6 +1589,8 @@ export async function resolveExternalMarketplaceFreezes({
1518
1589
  evidence,
1519
1590
  observeHeadFn,
1520
1591
  fetchIndexFn,
1592
+ productionAssets = null,
1593
+ observePluginRepositoryFn = defaultObserveFirstReleasePluginRepository,
1521
1594
  }) {
1522
1595
  const freezes = new Map();
1523
1596
  for (let index = 0; index < unitResults.length; index += 1) {
@@ -1525,10 +1598,26 @@ export async function resolveExternalMarketplaceFreezes({
1525
1598
  const version = resolvedVersions[index];
1526
1599
  const githubHost = unit.production?.githubHost ?? 'github.com';
1527
1600
  for (const dist of unit.distributions ?? []) {
1601
+ if (isFirstReleaseBootstrap(dist) && dist.marketplaceSourceType !== 'standalone-index') {
1602
+ throw new ReleaseError(
1603
+ GATE_FAILED,
1604
+ `unit "${unit.id}" ${dist.type} first-release bootstrap requires marketplaceSourceType standalone-index`,
1605
+ { unitId: unit.id, distributionType: dist.type },
1606
+ );
1607
+ }
1528
1608
  // 只处理 standalone-index 来源;bundled-family 不需要外部冻结。
1529
1609
  if (dist.marketplaceSourceType !== 'standalone-index') continue;
1530
1610
  // standalone-index 必须有 marketplaceRepo;没有则跳过(兼容旧配置)。
1531
- if (!dist.marketplaceRepo) continue;
1611
+ if (!dist.marketplaceRepo) {
1612
+ if (isFirstReleaseBootstrap(dist)) {
1613
+ throw new ReleaseError(
1614
+ GATE_FAILED,
1615
+ `unit "${unit.id}" ${dist.type} first-release bootstrap requires marketplaceRepo`,
1616
+ { unitId: unit.id, distributionType: dist.type },
1617
+ );
1618
+ }
1619
+ continue;
1620
+ }
1532
1621
  const platform = PLATFORMS.find((p) => p.distributionType === dist.type);
1533
1622
  if (!platform) {
1534
1623
  throw new ReleaseError(
@@ -1545,6 +1634,13 @@ export async function resolveExternalMarketplaceFreezes({
1545
1634
  { unitId: unit.id, marketplaceSourceType: dist.marketplaceSourceType },
1546
1635
  );
1547
1636
  }
1637
+ if (isFirstReleaseBootstrap(dist) && dist.type !== 'claude-plugin' && dist.type !== 'codex-plugin') {
1638
+ throw new ReleaseError(
1639
+ GATE_FAILED,
1640
+ `unit "${unit.id}" ${dist.type} first-release bootstrap is supported only for claude-plugin or codex-plugin standalone-index distributions`,
1641
+ { unitId: unit.id, distributionType: dist.type },
1642
+ );
1643
+ }
1548
1644
  const observed = await observeHeadFn(dist.marketplaceRepo, { githubHost });
1549
1645
  if (observed.status !== 'observed' || !EXTERNAL_MARKETPLACE_SHA_RE.test(observed.sha ?? '') || !observed.defaultBranch) {
1550
1646
  throw new ReleaseError(
@@ -1581,23 +1677,65 @@ export async function resolveExternalMarketplaceFreezes({
1581
1677
  { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1582
1678
  );
1583
1679
  }
1584
- const pluginEntries = Array.isArray(marketplaceIndex.plugins)
1585
- ? marketplaceIndex.plugins.filter((entry) => entry && entry.name === dist.plugin)
1586
- : [];
1587
- if (pluginEntries.length !== 1) {
1680
+ if (!Array.isArray(marketplaceIndex.plugins)) {
1681
+ throw new ReleaseError(
1682
+ GATE_FAILED,
1683
+ `unit "${unit.id}" ${dist.type} external marketplace index plugins must be an array`,
1684
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1685
+ );
1686
+ }
1687
+ const pluginEntries = marketplaceIndex.plugins.filter((entry) => entry && entry.name === dist.plugin);
1688
+ if (isFirstReleaseBootstrap(dist) && pluginEntries.length > 0) {
1689
+ throw new ReleaseError(
1690
+ GATE_FAILED,
1691
+ `unit "${unit.id}" first-release bootstrap requires the marketplace entry to be absent, found ${pluginEntries.length} matching entries`,
1692
+ { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1693
+ );
1694
+ }
1695
+ if (pluginEntries.length !== 1 && !isFirstReleaseBootstrap(dist)) {
1588
1696
  throw new ReleaseError(
1589
1697
  GATE_FAILED,
1590
1698
  `unit "${unit.id}" external marketplace index must contain exactly one plugin entry named "${dist.plugin}", found ${pluginEntries.length}`,
1591
1699
  { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1592
1700
  );
1593
1701
  }
1594
- if (platform.marketplaceEntryCarriesVersion && pluginEntries[0].version !== version) {
1702
+ let selectedEntry = pluginEntries[0];
1703
+ if (!isFirstReleaseBootstrap(dist) && platform.marketplaceEntryCarriesVersion && selectedEntry.version !== version) {
1595
1704
  throw new ReleaseError(
1596
1705
  GATE_FAILED,
1597
1706
  `unit "${unit.id}" external marketplace index entry version "${pluginEntries[0].version}" does not match target version "${version}"`,
1598
1707
  { unitId: unit.id, marketplaceRepo: dist.marketplaceRepo },
1599
1708
  );
1600
1709
  }
1710
+ if (isFirstReleaseBootstrap(dist)) {
1711
+ const asset = productionAssets?.[index];
1712
+ if (!asset || !EXTERNAL_MARKETPLACE_SHA_RE.test(asset.commit ?? '') || !asset.tag) {
1713
+ throw new ReleaseError(
1714
+ GATE_FAILED,
1715
+ `unit "${unit.id}" first-release bootstrap requires a frozen plugin tag and commit`,
1716
+ { unitId: unit.id, distributionType: dist.type },
1717
+ );
1718
+ }
1719
+ const pluginState = await observePluginRepositoryFn(unit.publicRepo, asset.tag, { githubHost });
1720
+ if (
1721
+ pluginState?.status !== 'empty'
1722
+ || pluginState.tagExists !== false
1723
+ || pluginState.releaseExists !== false
1724
+ ) {
1725
+ throw new ReleaseError(
1726
+ GATE_FAILED,
1727
+ `unit "${unit.id}" first-release bootstrap requires an empty plugin repository without the target tag or release`,
1728
+ { unitId: unit.id, publicRepo: unit.publicRepo, tag: asset.tag, observed: pluginState },
1729
+ );
1730
+ }
1731
+ selectedEntry = buildExpectedStandaloneIndexInstallIdentity(platform, {
1732
+ name: dist.plugin,
1733
+ repo: unit.publicRepo,
1734
+ tag: asset.tag,
1735
+ sha: asset.commit,
1736
+ version,
1737
+ });
1738
+ }
1601
1739
  // marketplaceRef:Claude 使用默认分支名(name-ref),其余平台使用提交 SHA。
1602
1740
  // 无 CLI 的平台(kimi、codebuddy)也必须能冻结,ref 用 SHA。
1603
1741
  const marketplaceRef = platform.marketplaceRefForm === 'name' ? observed.defaultBranch : sha;
@@ -1612,7 +1750,10 @@ export async function resolveExternalMarketplaceFreezes({
1612
1750
  marketplaceRef,
1613
1751
  marketplaceIndexPath: manifestPath,
1614
1752
  marketplaceName: marketplaceIndex.name,
1615
- selectedEntry: pluginEntries[0],
1753
+ selectedEntry,
1754
+ ...(isFirstReleaseBootstrap(dist)
1755
+ ? { firstReleaseBootstrap: FIRST_RELEASE_BOOTSTRAP_MODE, marketplaceIndexSha: null }
1756
+ : {}),
1616
1757
  });
1617
1758
  await evidence.append({
1618
1759
  phase: 'external-marketplace-freeze',
@@ -1624,7 +1765,10 @@ export async function resolveExternalMarketplaceFreezes({
1624
1765
  marketplaceRef,
1625
1766
  marketplaceIndexPath: manifestPath,
1626
1767
  marketplaceName: marketplaceIndex.name,
1627
- selectedEntry: pluginEntries[0],
1768
+ selectedEntry,
1769
+ ...(isFirstReleaseBootstrap(dist)
1770
+ ? { firstReleaseBootstrap: FIRST_RELEASE_BOOTSTRAP_MODE, marketplaceIndexSha: null }
1771
+ : {}),
1628
1772
  defaultBranch: observed.defaultBranch,
1629
1773
  });
1630
1774
  }
@@ -2043,6 +2187,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2043
2187
  marketplaceIndexPath: freeze.marketplaceIndexPath,
2044
2188
  marketplaceName: freeze.marketplaceName,
2045
2189
  selectedEntry: freeze.selectedEntry,
2190
+ ...(freeze.firstReleaseBootstrap ? {
2191
+ firstReleaseBootstrap: freeze.firstReleaseBootstrap,
2192
+ marketplaceIndexSha: null,
2193
+ } : {}),
2046
2194
  } : {}),
2047
2195
  },
2048
2196
  expected: {
@@ -2058,6 +2206,10 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
2058
2206
  entrySkillFound: true,
2059
2207
  manifestDigest: asset.manifestDigest,
2060
2208
  ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
2209
+ ...(externalMarketplace && freeze?.firstReleaseBootstrap ? {
2210
+ firstReleaseBootstrap: freeze.firstReleaseBootstrap,
2211
+ marketplaceIndexSha: null,
2212
+ } : {}),
2061
2213
  },
2062
2214
  status: 'PENDING',
2063
2215
  });
@@ -4242,6 +4394,17 @@ export async function prepareRelease(options) {
4242
4394
  // and validate the marketplace index entry at that sha before freezing the
4243
4395
  // add-ref. The remote is only ever read (git ls-remote / gh api), never
4244
4396
  // written, and it is only ever reached AFTER the local closure passed.
4397
+ for (const { unit } of unitResults) {
4398
+ for (const dist of unit.distributions ?? []) {
4399
+ if (isFirstReleaseBootstrap(dist) && (!production || offline)) {
4400
+ throw new ReleaseError(
4401
+ GATE_FAILED,
4402
+ `unit "${unit.id}" ${dist.type} first-release bootstrap requires online production prepare`,
4403
+ { unitId: unit.id, distributionType: dist.type },
4404
+ );
4405
+ }
4406
+ }
4407
+ }
4245
4408
  const externalMarketplaceFreezes = production
4246
4409
  ? await resolveExternalMarketplaceFreezes({
4247
4410
  unitResults,
@@ -4250,6 +4413,8 @@ export async function prepareRelease(options) {
4250
4413
  evidence,
4251
4414
  observeHeadFn: options.observeExternalMarketplaceHeadFn ?? defaultObserveExternalMarketplaceHead,
4252
4415
  fetchIndexFn: options.fetchExternalMarketplaceIndexFn ?? defaultFetchExternalMarketplaceIndex,
4416
+ productionAssets,
4417
+ observePluginRepositoryFn: options.observeFirstReleasePluginRepositoryFn ?? defaultObserveFirstReleasePluginRepository,
4253
4418
  })
4254
4419
  : new Map();
4255
4420
 
@@ -4353,6 +4518,10 @@ export async function prepareRelease(options) {
4353
4518
  frozenDist.marketplaceIndexPath = freeze.marketplaceIndexPath;
4354
4519
  frozenDist.marketplaceName = freeze.marketplaceName;
4355
4520
  frozenDist.selectedEntry = freeze.selectedEntry;
4521
+ if (freeze.firstReleaseBootstrap) {
4522
+ frozenDist.firstReleaseBootstrap = freeze.firstReleaseBootstrap;
4523
+ frozenDist.marketplaceIndexSha = null;
4524
+ }
4356
4525
  }
4357
4526
  }
4358
4527
 
@@ -174,7 +174,8 @@ async function defaultDependencies() {
174
174
  };
175
175
  }
176
176
 
177
- function publicState(state) {
177
+ function publicState(state, { postRelease } = {}) {
178
+ const visiblePostRelease = postRelease === undefined ? state.postRelease : postRelease;
178
179
  return {
179
180
  command: 'ship',
180
181
  status: state.status,
@@ -199,7 +200,7 @@ function publicState(state) {
199
200
  ...(state.requirements ? { requirements: state.requirements } : {}),
200
201
  ...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
201
202
  ...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
202
- ...(state.postRelease ? { postRelease: state.postRelease } : {}),
203
+ ...(visiblePostRelease ? { postRelease: visiblePostRelease } : {}),
203
204
  externalActionsIncludedInPlanApproval: true,
204
205
  verificationGatesIncludedInPlanApproval: true,
205
206
  postPublishCheckpointApprovalsIncludedInPlanApproval: false,
@@ -718,15 +719,6 @@ export async function advanceShip(options = {}, injected = {}) {
718
719
  updatedAt: new Date().toISOString(),
719
720
  };
720
721
  await writeJsonAtomic(statePath, state);
721
- if (verified.status === 'VERIFIED') {
722
- try {
723
- state.postRelease = derivePostReleaseChecklist(plan);
724
- } catch (error) {
725
- state.postRelease = unavailablePostReleaseChecklist(plan, error);
726
- }
727
- state.updatedAt = new Date().toISOString();
728
- await writeJsonAtomic(statePath, state);
729
- }
730
722
  } catch (error) {
731
723
  if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
732
724
  state = {
@@ -835,5 +827,28 @@ export async function advanceShip(options = {}, injected = {}) {
835
827
  }
836
828
  }
837
829
 
830
+ if (state.status === 'VERIFIED') {
831
+ const finalPlan = JSON.parse(await readFile(state.planPath, 'utf8'));
832
+ const hasPostVerify = normalizePostPublishView(finalPlan).some((declaration) => (
833
+ (declaration.hooks ?? []).some((hook) => hook.phase === 'postVerify')
834
+ ));
835
+ const localFinishRunPath = hasPostVerify
836
+ ? (state.postVerify?.status === 'DISTRIBUTED' ? state.postVerify.runPath : undefined)
837
+ : state.verifyRunPath;
838
+ let postRelease;
839
+ try {
840
+ postRelease = derivePostReleaseChecklist(finalPlan, {
841
+ root,
842
+ statePath,
843
+ unitIds: state.selectedUnitIds,
844
+ runPath: localFinishRunPath,
845
+ postVerifyComplete: !hasPostVerify || state.postVerify?.status === 'DISTRIBUTED',
846
+ });
847
+ } catch (error) {
848
+ postRelease = unavailablePostReleaseChecklist(finalPlan, error);
849
+ }
850
+ return publicState(state, { postRelease });
851
+ }
852
+
838
853
  return publicState(state);
839
854
  }
@@ -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') {