release-skill 0.9.35 → 0.9.37

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 (95) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.qoder-plugin/plugin.json +1 -1
  7. package/CHANGELOG.md +45 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +24 -9
  11. package/README.zh-CN.md +15 -9
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +270 -58
  14. package/adapters/claude/schemas/.render-manifest.json +6 -2
  15. package/adapters/claude/skills/release-config/SKILL.md +36 -41
  16. package/adapters/claude/skills/release-docs/SKILL.md +38 -35
  17. package/adapters/claude/skills/release-finish/SKILL.md +2 -2
  18. package/adapters/claude/skills/release-help/SKILL.md +51 -41
  19. package/adapters/claude/skills/release-marketplace/SKILL.md +38 -46
  20. package/adapters/claude/skills/release-prepare/SKILL.md +19 -28
  21. package/adapters/claude/skills/release-publish/SKILL.md +29 -45
  22. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  23. package/adapters/codex/bin/release-skill.bundle.mjs +270 -58
  24. package/adapters/codex/schemas/.render-manifest.json +6 -2
  25. package/adapters/codex/skills/release-config/SKILL.md +36 -41
  26. package/adapters/codex/skills/release-docs/SKILL.md +38 -35
  27. package/adapters/codex/skills/release-finish/SKILL.md +2 -2
  28. package/adapters/codex/skills/release-help/SKILL.md +51 -41
  29. package/adapters/codex/skills/release-marketplace/SKILL.md +38 -46
  30. package/adapters/codex/skills/release-prepare/SKILL.md +19 -28
  31. package/adapters/codex/skills/release-publish/SKILL.md +29 -45
  32. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  33. package/adapters/cursor/bin/release-skill.bundle.mjs +270 -58
  34. package/adapters/cursor/schemas/.render-manifest.json +6 -2
  35. package/adapters/cursor/skills/release-config/SKILL.md +36 -41
  36. package/adapters/cursor/skills/release-docs/SKILL.md +38 -35
  37. package/adapters/cursor/skills/release-finish/SKILL.md +2 -2
  38. package/adapters/cursor/skills/release-help/SKILL.md +51 -41
  39. package/adapters/cursor/skills/release-marketplace/SKILL.md +38 -46
  40. package/adapters/cursor/skills/release-prepare/SKILL.md +19 -28
  41. package/adapters/cursor/skills/release-publish/SKILL.md +29 -45
  42. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  43. package/adapters/kimi/bin/release-skill.bundle.mjs +270 -58
  44. package/adapters/kimi/schemas/.render-manifest.json +6 -2
  45. package/adapters/kimi/skills/release-config/SKILL.md +36 -41
  46. package/adapters/kimi/skills/release-docs/SKILL.md +38 -35
  47. package/adapters/kimi/skills/release-finish/SKILL.md +2 -2
  48. package/adapters/kimi/skills/release-help/SKILL.md +51 -41
  49. package/adapters/kimi/skills/release-marketplace/SKILL.md +38 -46
  50. package/adapters/kimi/skills/release-prepare/SKILL.md +19 -28
  51. package/adapters/kimi/skills/release-publish/SKILL.md +29 -45
  52. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  53. package/adapters/qoder/bin/release-skill.bundle.mjs +270 -58
  54. package/adapters/qoder/schemas/.render-manifest.json +6 -2
  55. package/adapters/qoder/skills/release-config/SKILL.md +36 -41
  56. package/adapters/qoder/skills/release-docs/SKILL.md +38 -35
  57. package/adapters/qoder/skills/release-finish/SKILL.md +2 -2
  58. package/adapters/qoder/skills/release-help/SKILL.md +51 -41
  59. package/adapters/qoder/skills/release-marketplace/SKILL.md +38 -46
  60. package/adapters/qoder/skills/release-prepare/SKILL.md +19 -28
  61. package/adapters/qoder/skills/release-publish/SKILL.md +29 -45
  62. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  63. package/adapters/workbuddy/bin/release-skill.bundle.mjs +270 -58
  64. package/adapters/workbuddy/schemas/.render-manifest.json +6 -2
  65. package/adapters/workbuddy/skills/release-config/SKILL.md +36 -41
  66. package/adapters/workbuddy/skills/release-docs/SKILL.md +38 -35
  67. package/adapters/workbuddy/skills/release-finish/SKILL.md +2 -2
  68. package/adapters/workbuddy/skills/release-help/SKILL.md +51 -41
  69. package/adapters/workbuddy/skills/release-marketplace/SKILL.md +38 -46
  70. package/adapters/workbuddy/skills/release-prepare/SKILL.md +19 -28
  71. package/adapters/workbuddy/skills/release-publish/SKILL.md +29 -45
  72. package/bin/release-skill.bundle.mjs +270 -58
  73. package/package.json +1 -1
  74. package/platform-manifest.json +4 -4
  75. package/references/.render-manifest.json +2 -2
  76. package/references/02-project-config.md +91 -8
  77. package/schemas/.render-manifest.json +6 -2
  78. package/skills/release-config/SKILL.md +36 -41
  79. package/skills/release-docs/SKILL.md +38 -35
  80. package/skills/release-finish/SKILL.md +2 -2
  81. package/skills/release-help/SKILL.md +51 -41
  82. package/skills/release-marketplace/SKILL.md +38 -46
  83. package/skills/release-prepare/SKILL.md +19 -28
  84. package/skills/release-publish/SKILL.md +29 -45
  85. package/skills-src/release-config/SKILL.md +36 -41
  86. package/skills-src/release-docs/SKILL.md +38 -35
  87. package/skills-src/release-finish/SKILL.md +2 -2
  88. package/skills-src/release-help/SKILL.md +51 -41
  89. package/skills-src/release-marketplace/SKILL.md +38 -46
  90. package/skills-src/release-prepare/SKILL.md +19 -28
  91. package/skills-src/release-publish/SKILL.md +29 -45
  92. package/src/adapters/plugin-marketplace.mjs +152 -43
  93. package/src/commands/post-release-local.mjs +41 -22
  94. package/src/commands/verify.mjs +123 -17
  95. package/src/core/skill-resource-closure.mjs +1 -1
@@ -823,6 +823,88 @@ export function normalizeCodeBuddySkillsRel(skillsRaw) {
823
823
  return rel;
824
824
  }
825
825
 
826
+ /**
827
+ * Resolve a Codex entry skill using the plugin manifest at the actual plugin
828
+ * root. An omitted `skills` field keeps Codex's conventional `skills/` root;
829
+ * an explicit declaration is authoritative and must be valid and present.
830
+ *
831
+ * The short-name layout keeps priority over the plugin-prefixed layout, but
832
+ * fallback is allowed only when the short-name candidate is absent. Existing
833
+ * unsafe candidates are errors and cannot be hidden by a second layout.
834
+ *
835
+ * @param {string} pluginRoot - Real plugin root derived from source or install.
836
+ * @param {string} entrySkill - Frozen entry-skill id.
837
+ * @param {string} pluginName - Frozen plugin id used by the legacy prefix.
838
+ * @returns {Promise<string>} Absolute path to a regular, non-symlink SKILL.md.
839
+ */
840
+ async function resolveCodexEntrySkillFile(pluginRoot, entrySkill, pluginName) {
841
+ const { normalizeGenericSkillsRel } = await import('../core/surface-host-bindings.mjs');
842
+ if (!entrySkill || typeof entrySkill !== 'string' || !SAFE_ID_RE.test(entrySkill)) {
843
+ throw new Error(`unsafe entrySkill: "${entrySkill}"`);
844
+ }
845
+
846
+ const platform = getPlatform('codex');
847
+ const manifestPath = await resolveContained(pluginRoot, platform.manifestPaths.plugin);
848
+ let manifest;
849
+ try {
850
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
851
+ } catch (err) {
852
+ throw new Error(`Codex plugin manifest cannot be read: ${err.message}`);
853
+ }
854
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
855
+ throw new Error('Codex plugin manifest must be an object');
856
+ }
857
+
858
+ const skillsRel = Object.hasOwn(manifest, 'skills')
859
+ ? normalizeGenericSkillsRel(manifest.skills)
860
+ : 'skills';
861
+ const skillsRoot = await resolveContained(pluginRoot, skillsRel);
862
+ let skillsRootStat;
863
+ try {
864
+ skillsRootStat = await lstat(skillsRoot);
865
+ } catch {
866
+ throw new Error(`Codex manifest skills root does not exist: ${manifest.skills ?? skillsRel}`);
867
+ }
868
+ if (skillsRootStat.isSymbolicLink() || !skillsRootStat.isDirectory()) {
869
+ throw new Error(`Codex manifest skills root must be a real directory: ${manifest.skills ?? skillsRel}`);
870
+ }
871
+
872
+ const findCandidate = async (skillDirName) => {
873
+ const skillDir = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}`);
874
+ let skillDirStat;
875
+ try {
876
+ skillDirStat = await lstat(skillDir);
877
+ } catch (err) {
878
+ if (err?.code === 'ENOENT') return null;
879
+ throw err;
880
+ }
881
+ if (skillDirStat.isSymbolicLink() || !skillDirStat.isDirectory()) {
882
+ throw new Error(`Codex entry skill directory must be a real directory: ${skillsRel}/${skillDirName}`);
883
+ }
884
+
885
+ const skillFile = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}/SKILL.md`);
886
+ let skillStat;
887
+ try {
888
+ skillStat = await lstat(skillFile);
889
+ } catch (err) {
890
+ if (err?.code === 'ENOENT') return null;
891
+ throw err;
892
+ }
893
+ if (skillStat.isSymbolicLink() || !skillStat.isFile()) {
894
+ throw new Error(`Codex entry skill must be a regular non-symlink file: ${skillsRel}/${skillDirName}/SKILL.md`);
895
+ }
896
+ return skillFile;
897
+ };
898
+
899
+ const shortNameEntry = await findCandidate(entrySkill);
900
+ if (shortNameEntry) return shortNameEntry;
901
+ if (pluginName && SAFE_ID_RE.test(pluginName)) {
902
+ const prefixedEntry = await findCandidate(`${pluginName}-${entrySkill}`);
903
+ if (prefixedEntry) return prefixedEntry;
904
+ }
905
+ throw new Error(`Codex entry skill not found: ${skillsRel}/${entrySkill}/SKILL.md`);
906
+ }
907
+
826
908
  /**
827
909
  * CodeBuddy equivalent of resolveKimiEntrySkillFile (parallel implementation,
828
910
  * codebuddy wording; the kimi helper above is intentionally left untouched).
@@ -2110,8 +2192,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2110
2192
  // snapshot, so the snapshot marketplace-manifest section is skipped
2111
2193
  // entirely. The plugin manifest is read directly from the snapshot
2112
2194
  // root and the frozen external identity fields are validated. ref was
2113
- // already injection-checked above (step 2); the entry skill reuses the
2114
- // automatable fixed layout validated below.
2195
+ // already injection-checked above (step 2); Codex entry lookup reads
2196
+ // the plugin manifest from this snapshot root.
2115
2197
  if (
2116
2198
  typeof action.marketplaceCommitSha !== 'string'
2117
2199
  || !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
@@ -2315,13 +2397,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2315
2397
  }
2316
2398
  }
2317
2399
 
2318
- // Verify the entry skill exists in the snapshot.
2319
- // Structured-cli platforms' manifests always declare ./skills/, so the
2320
- // fixed skills/<entrySkill>/SKILL.md layout is authoritative for
2321
- // them. A human-attestation platform (kimi, codebuddy) resolves the
2322
- // entry skill via the manifest-declared skills root (MAJOR-4): the
2323
- // root is validated + realpath-contained, and omitted `skills` means
2324
- // the official single-skill root SKILL.md.
2400
+ // Verify the entry skill exists in the snapshot. Kimi/CodeBuddy use
2401
+ // their existing manifest-specific rules; Codex resolves its own
2402
+ // manifest-declared skills root below. Claude keeps its established
2403
+ // conventional skills/ lookup and prefixed fallback.
2325
2404
  if (platformRoute.route === 'human-attestation') {
2326
2405
  const resolveEntrySkillFile = consumer === 'codebuddy'
2327
2406
  ? resolveCodeBuddyEntrySkillFile
@@ -2335,6 +2414,17 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2335
2414
  error: `entry skill not resolvable in snapshot: ${entryErr.message}`,
2336
2415
  });
2337
2416
  }
2417
+ } else if (consumer === 'codex') {
2418
+ const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
2419
+ try {
2420
+ await resolveCodexEntrySkillFile(pluginRootForSkill, action.entrySkill, action.plugin);
2421
+ } catch (entryErr) {
2422
+ return createResult({
2423
+ actionType,
2424
+ status: ActionStatus.PREFLIGHT_FAILED,
2425
+ error: `entry skill not resolvable in snapshot: ${entryErr.message}`,
2426
+ });
2427
+ }
2338
2428
  } else {
2339
2429
  // Entry skill 相对插件根解析(嵌套布局时 pluginRoot != snapshotRoot)。
2340
2430
  // pluginRootForEntrySkill 在非 external 结构化 CLI 路径中已确定;
@@ -3735,48 +3825,67 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3735
3825
  }
3736
3826
 
3737
3827
  // Verify entry skill exists as a regular file in install dir.
3738
- // Prefixed skill-directory fallback (0.5.1 entry-skill-prefix fix):
3739
- // mirror the preflight resolution — `skills/<entrySkill>` first,
3740
- // then `skills/<plugin>-<entrySkill>` when the plugin is known.
3741
- let entrySkillPath = resolve(installPath, 'skills', action.entrySkill, 'SKILL.md');
3742
- let entrySkillFound = false;
3743
- try {
3744
- const skillStat = await lstat(entrySkillPath);
3745
- if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
3746
- entrySkillFound = true;
3828
+ // Codex uses its actual installed plugin manifest for the skills
3829
+ // root, keeping observation aligned with frozen-source preflight.
3830
+ if (consumer === 'codex') {
3831
+ try {
3832
+ await resolveCodexEntrySkillFile(installPath, action.entrySkill, action.plugin);
3833
+ } catch (entryErr) {
3834
+ return createResult({
3835
+ actionType,
3836
+ status: ActionStatus.OBSERVED,
3837
+ observation: {
3838
+ installed: true,
3839
+ installPath,
3840
+ entrySkillFound: false,
3841
+ error: `entry skill not resolvable in installed plugin: ${entryErr.message}`,
3842
+ },
3843
+ });
3747
3844
  }
3748
- } catch {
3749
- // entry skill not found in unprefixed layout
3750
- }
3751
- if (!entrySkillFound && action.plugin) {
3752
- const prefixedPath = resolve(
3753
- installPath,
3754
- 'skills',
3755
- `${action.plugin}-${action.entrySkill}`,
3756
- 'SKILL.md',
3757
- );
3845
+ } else {
3846
+ // Prefixed skill-directory fallback (0.5.1 entry-skill-prefix fix):
3847
+ // mirror the preflight resolution — `skills/<entrySkill>` first,
3848
+ // then `skills/<plugin>-<entrySkill>` when the plugin is known.
3849
+ let entrySkillPath = resolve(installPath, 'skills', action.entrySkill, 'SKILL.md');
3850
+ let entrySkillFound = false;
3758
3851
  try {
3759
- const skillStat = await lstat(prefixedPath);
3852
+ const skillStat = await lstat(entrySkillPath);
3760
3853
  if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
3761
- entrySkillPath = prefixedPath;
3762
3854
  entrySkillFound = true;
3763
3855
  }
3764
3856
  } catch {
3765
- // both layouts miss; report below
3857
+ // entry skill not found in unprefixed layout
3766
3858
  }
3767
- }
3768
-
3769
- if (!entrySkillFound) {
3770
- return createResult({
3771
- actionType,
3772
- status: ActionStatus.OBSERVED,
3773
- observation: {
3774
- installed: true,
3859
+ if (!entrySkillFound && action.plugin) {
3860
+ const prefixedPath = resolve(
3775
3861
  installPath,
3776
- entrySkillFound: false,
3777
- error: `entry skill not found: skills/${action.entrySkill}/SKILL.md`,
3778
- },
3779
- });
3862
+ 'skills',
3863
+ `${action.plugin}-${action.entrySkill}`,
3864
+ 'SKILL.md',
3865
+ );
3866
+ try {
3867
+ const skillStat = await lstat(prefixedPath);
3868
+ if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
3869
+ entrySkillPath = prefixedPath;
3870
+ entrySkillFound = true;
3871
+ }
3872
+ } catch {
3873
+ // both layouts miss; report below
3874
+ }
3875
+ }
3876
+
3877
+ if (!entrySkillFound) {
3878
+ return createResult({
3879
+ actionType,
3880
+ status: ActionStatus.OBSERVED,
3881
+ observation: {
3882
+ installed: true,
3883
+ installPath,
3884
+ entrySkillFound: false,
3885
+ error: `entry skill not found: skills/${action.entrySkill}/SKILL.md`,
3886
+ },
3887
+ });
3888
+ }
3780
3889
  }
3781
3890
 
3782
3891
  // Bind the installed payload back to the sealed authority while
@@ -846,7 +846,7 @@ async function observeQoderPlugin(target, command, env, run) {
846
846
  };
847
847
  }
848
848
 
849
- async function readQoderManifest(root, label) {
849
+ async function readQoderManifest(root, label, target) {
850
850
  const candidates = [];
851
851
  for (const manifestPath of ['.qoder-plugin/plugin.json', '.claude-plugin/plugin.json']) {
852
852
  let text;
@@ -860,10 +860,11 @@ async function readQoderManifest(root, label) {
860
860
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
861
861
  throw new Error(`${label} (${manifestPath}) did not return an object`);
862
862
  }
863
+ await assertQoderPluginManifest(target, manifest, `${label} (${manifestPath})`, root);
863
864
  candidates.push({ manifestPath, manifest });
864
865
  }
865
866
  if (candidates.length === 0) throw new Error(`${label} has no native or compatible plugin manifest`);
866
- if (candidates.length === 2 && ['name', 'version', 'skills'].some((field) => (
867
+ if (candidates.length === 2 && ['name', 'version'].some((field) => (
867
868
  candidates[0].manifest[field] !== candidates[1].manifest[field]
868
869
  ))) {
869
870
  throw new Error(`${label} does not match the frozen Qoder plugin identity and projection path: conflicting native and compatible manifests`);
@@ -894,31 +895,49 @@ async function assertFrozenQoderManifest(target, root) {
894
895
  snapshotPath: target.snapshotPath,
895
896
  expectedDigest: target.manifestDigest,
896
897
  });
897
- const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest');
898
+ const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest', target);
898
899
  const skills = await assertQoderPluginManifest(target, manifest, 'frozen Qoder plugin manifest', snapshotRoot);
899
900
  return { manifestPath, skills };
900
901
  }
901
902
 
902
903
  async function assertQoderHubEntry(target, marketplaceRoot) {
903
- const marketplace = parseJson(
904
- await readFileContained(marketplaceRoot, 'marketplace.json', { encoding: 'utf8' }),
905
- 'Qoder Hub marketplace manifest',
906
- );
907
- if (marketplace?.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
908
- throw new Error('Qoder Hub marketplace manifest does not match the frozen marketplace');
909
- }
910
- const matches = marketplace.plugins.filter((entry) => entry?.name === target.plugin);
911
- if (matches.length !== 1) {
912
- throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
913
- }
914
- const source = matches[0]?.source;
915
- if (
916
- source?.source !== 'url'
917
- || normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo
918
- || source.sha !== target.pluginCommit
919
- ) {
920
- throw new Error('Qoder Hub entry does not match the frozen public source identity');
904
+ let found = false;
905
+ for (const indexPath of ['marketplace.json', '.claude-plugin/marketplace.json']) {
906
+ let text;
907
+ try {
908
+ text = await readFileContained(marketplaceRoot, indexPath, { encoding: 'utf8' });
909
+ } catch (error) {
910
+ if (error?.code === 'SFC2004' && error?.details?.kind === 'missing-resource') continue;
911
+ throw error;
912
+ }
913
+ found = true;
914
+ const marketplace = parseJson(text, `Qoder Hub marketplace manifest (${indexPath})`);
915
+ if (!marketplace || typeof marketplace !== 'object' || Array.isArray(marketplace)
916
+ || marketplace.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
917
+ throw new Error('Qoder Hub marketplace manifest does not match the frozen marketplace');
918
+ }
919
+ if (indexPath === '.claude-plugin/marketplace.json') {
920
+ if (target.hub.githubHost !== 'github.com' || typeof target.frozenTag !== 'string' || target.frozenTag.length === 0) {
921
+ throw new Error('Qoder compatible Hub entry requires a frozen GitHub tag identity');
922
+ }
923
+ await assertHubHostEntry({ ...target, host: 'claude', pluginTag: target.frozenTag, githubHost: target.hub.githubHost }, marketplaceRoot);
924
+ continue;
925
+ }
926
+ const matches = marketplace.plugins.filter((entry) => entry?.name === target.plugin);
927
+ if (matches.length !== 1) {
928
+ throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
929
+ }
930
+ const entry = matches[0];
931
+ const source = entry.source;
932
+ if (source?.source !== 'url'
933
+ || normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo
934
+ || source.sha !== target.pluginCommit
935
+ || (entry.version !== undefined && entry.version !== target.version)
936
+ || (source.ref !== undefined && (typeof target.frozenTag !== 'string' || source.ref !== target.frozenTag))) {
937
+ throw new Error('Qoder Hub entry does not match the frozen public source identity');
938
+ }
921
939
  }
940
+ if (!found) throw new Error('Qoder Hub has no native or compatible marketplace manifest');
922
941
  }
923
942
 
924
943
  async function verifyQoderInstalledPayload({
@@ -928,7 +947,7 @@ async function verifyQoderInstalledPayload({
928
947
  frozenManifest,
929
948
  verifyInstalledPayload,
930
949
  }) {
931
- const { manifestPath, manifest } = await readQoderManifest(installPath, 'installed Qoder plugin manifest');
950
+ const { manifestPath, manifest } = await readQoderManifest(installPath, 'installed Qoder plugin manifest', target);
932
951
  if (manifestPath !== frozenManifest.manifestPath) {
933
952
  throw new Error('installed Qoder plugin manifest path differs from the frozen manifest selection');
934
953
  }
@@ -57,7 +57,7 @@ import {
57
57
  consumePublicSourceAuthorityReceipt,
58
58
  verifySourceAuthorityReceipt,
59
59
  } from '../core/source-authority.mjs';
60
- import { verifyFrozenFile } from '../snapshot/frozen.mjs';
60
+ import { computeFrozenSnapshot, resolveFrozenPath, verifyFrozenFile } from '../snapshot/frozen.mjs';
61
61
  import {
62
62
  deriveBaselineAdvances,
63
63
  applyBaselineAdvances,
@@ -87,6 +87,7 @@ import {
87
87
  checkSkillResourceClosure,
88
88
  createSkillResourceClosureReceipt,
89
89
  evaluateConsumerSkillResourceClosureReceipts,
90
+ TRANSPORT_EXCLUSIONS,
90
91
  } from '../core/skill-resource-closure.mjs';
91
92
  import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
92
93
  import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
@@ -131,6 +132,61 @@ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
131
132
  NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
132
133
  });
133
134
 
135
+ /** Ignore only write bits; retain type, size, content, and non-write mode bits. */
136
+ const WRITE_BITS = 0o222;
137
+ const PAYLOAD_CONFLICT_REPORT_CAP = 10;
138
+
139
+ function transportIdentityEntries(entries) {
140
+ return entries.map(({ path, type, mode, size, contentDigest }) => ({
141
+ path,
142
+ type,
143
+ mode: mode & ~WRITE_BITS,
144
+ size,
145
+ contentDigest,
146
+ }));
147
+ }
148
+
149
+ function compareInstallToFrozenTransport(frozenEntries, installedEntries) {
150
+ const frozen = transportIdentityEntries(frozenEntries);
151
+ const installed = transportIdentityEntries(installedEntries);
152
+ const installedByPath = new Map(installed.map((entry) => [entry.path, entry]));
153
+ const conflicts = [];
154
+ for (const frozenEntry of frozen) {
155
+ const installedEntry = installedByPath.get(frozenEntry.path);
156
+ if (!installedEntry) {
157
+ conflicts.push(`missing: ${frozenEntry.path}`);
158
+ continue;
159
+ }
160
+ if (installedEntry.type !== frozenEntry.type) {
161
+ conflicts.push(`type mismatch: ${frozenEntry.path}`);
162
+ } else if (
163
+ installedEntry.size !== frozenEntry.size
164
+ || installedEntry.contentDigest !== frozenEntry.contentDigest
165
+ ) {
166
+ conflicts.push(`content mismatch: ${frozenEntry.path}`);
167
+ } else if (installedEntry.mode !== frozenEntry.mode) {
168
+ conflicts.push(`mode mismatch: ${frozenEntry.path}`);
169
+ }
170
+ }
171
+ const frozenPaths = new Set(frozen.map((entry) => entry.path));
172
+ for (const installedEntry of installed) {
173
+ if (!frozenPaths.has(installedEntry.path)) {
174
+ conflicts.push(`extra: ${installedEntry.path}`);
175
+ }
176
+ }
177
+ return conflicts;
178
+ }
179
+
180
+ function frozenSnapshotRelativePath(unit, actions, unitId) {
181
+ if (typeof unit?.frozenSnapshot?.path === 'string' && unit.frozenSnapshot.path.length > 0) {
182
+ return unit.frozenSnapshot.path;
183
+ }
184
+ const action = (actions ?? []).find((item) => item.unitId === unitId
185
+ && typeof item.parameters?.snapshotPath === 'string'
186
+ && item.parameters.snapshotPath.length > 0);
187
+ return action?.parameters?.snapshotPath ?? null;
188
+ }
189
+
134
190
  /** Verify actionless Cursor distributions through their frozen host contract. */
135
191
  export async function verifyCursorHostDistributions({
136
192
  plan,
@@ -2265,25 +2321,75 @@ export async function verifyRelease(options) {
2265
2321
  );
2266
2322
  }
2267
2323
  // For single-plugin units (the root-surface distributions this
2268
- // design covers) the install payload IS the frozen snapshot, so the
2269
- // recomputed receipt must equal the frozen unit receipt field for
2270
- // field (checker version, input digest, surface/skill/reference/
2271
- // finding counts, receipt digest). Record-layer fields (preparedAt,
2272
- // exitCode) are carried from the frozen receipt — the same rule
2273
- // publish's recheck uses.
2324
+ // design covers) the install payload IS the frozen snapshot.
2325
+ // Frozen snapshot + original receipt stay on the strict prepare/
2326
+ // publish rule. Install-versus-frozen comparison follows the
2327
+ // existing transport policy (mode & ~0o222) so a sealed 0444
2328
+ // tree may legally reappear as 0644 after marketplace extract.
2274
2329
  const pluginDistCount = (surface.unit?.distributions ?? [])
2275
2330
  .filter((d) => d.type !== 'npm').length;
2276
2331
  if (pluginDistCount === 1 && surface.frozenPluginRoot === '.') {
2277
- const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
2278
- unitId: surface.unitId,
2279
- preparedAt: expectedUnitReceipt.preparedAt ?? null,
2280
- exitCode: expectedUnitReceipt.exitCode ?? 0,
2281
- });
2282
- assertSkillResourceClosureReceipt(
2283
- expectedUnitReceipt,
2284
- observedReceipt,
2285
- `unit "${surface.unitId}"`,
2286
- );
2332
+ const frozenRel = frozenSnapshotRelativePath(surface.unit, actions, surface.unitId);
2333
+ if (frozenRel) {
2334
+ const frozenDir = await resolveFrozenPath(root, frozenRel, 'frozen snapshot path');
2335
+ const frozenClosure = await checkSkillResourceClosure({
2336
+ snapshotDir: frozenDir,
2337
+ host: surface.host,
2338
+ surfaceHostBindings: [{ surfaceId: surface.surfaceId, host: surface.host }],
2339
+ });
2340
+ const frozenReceipt = createSkillResourceClosureReceipt(frozenClosure, {
2341
+ unitId: surface.unitId,
2342
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
2343
+ exitCode: expectedUnitReceipt.exitCode ?? 0,
2344
+ });
2345
+ assertSkillResourceClosureReceipt(
2346
+ expectedUnitReceipt,
2347
+ frozenReceipt,
2348
+ `unit "${surface.unitId}"`,
2349
+ );
2350
+ const frozenSnap = await computeFrozenSnapshot(frozenDir, {
2351
+ excludeRootEntries: TRANSPORT_EXCLUSIONS,
2352
+ });
2353
+ const installedSnap = await computeFrozenSnapshot(surface.installPath, {
2354
+ excludeRootEntries: TRANSPORT_EXCLUSIONS,
2355
+ });
2356
+ const conflicts = compareInstallToFrozenTransport(
2357
+ frozenSnap.entries,
2358
+ installedSnap.entries,
2359
+ );
2360
+ await evidence.append({
2361
+ phase: 'verify',
2362
+ step: 'skill-resource-closure',
2363
+ status: 'install-comparison',
2364
+ unitId: surface.unitId,
2365
+ frozenInputDigest: frozenSnap.digest,
2366
+ installedInputDigest: installedSnap.digest,
2367
+ frozenModes: frozenSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
2368
+ installedModes: installedSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
2369
+ });
2370
+ if (conflicts.length > 0) {
2371
+ const listed = conflicts.slice(0, PAYLOAD_CONFLICT_REPORT_CAP).join('; ');
2372
+ const overflow = conflicts.length > PAYLOAD_CONFLICT_REPORT_CAP
2373
+ ? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP} more conflicting path(s)`
2374
+ : '';
2375
+ throw new ReleaseError(
2376
+ POST_PUBLISH_VERIFY_FAILED,
2377
+ `installed plugin root differs in path, bytes, size, or non-write mode bits for unit "${surface.unitId}": ${listed}${overflow}`,
2378
+ { unitId: surface.unitId, conflicts },
2379
+ );
2380
+ }
2381
+ } else {
2382
+ const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
2383
+ unitId: surface.unitId,
2384
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
2385
+ exitCode: expectedUnitReceipt.exitCode ?? 0,
2386
+ });
2387
+ assertSkillResourceClosureReceipt(
2388
+ expectedUnitReceipt,
2389
+ observedReceipt,
2390
+ `unit "${surface.unitId}"`,
2391
+ );
2392
+ }
2287
2393
  }
2288
2394
  }
2289
2395
  skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
@@ -51,7 +51,7 @@ const GLOB_PATTERN = /[*?\[]/u;
51
51
  // the token was extracted. A prose marker elsewhere on the line exempts
52
52
  // nothing (fail-closed over a line-wide false pass).
53
53
  const SOURCE_ONLY_MARKER = /(?:\bsource-only\b|仅源码包)/iu;
54
- const TRANSPORT_EXCLUSIONS = Object.freeze([
54
+ export const TRANSPORT_EXCLUSIONS = Object.freeze([
55
55
  '.git',
56
56
  'node_modules',
57
57
  '.in_use',