release-skill 0.9.34 → 0.9.36

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 (53) 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 +41 -0
  8. package/INSTALL.md +2 -2
  9. package/INSTALL.zh-CN.md +2 -2
  10. package/README.md +10 -17
  11. package/README.zh-CN.md +10 -17
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +190 -59
  14. package/adapters/claude/schemas/.render-manifest.json +6 -2
  15. package/adapters/claude/skills/release-finish/SKILL.md +5 -1
  16. package/adapters/claude/skills/release-help/SKILL.md +1 -1
  17. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  18. package/adapters/codex/bin/release-skill.bundle.mjs +190 -59
  19. package/adapters/codex/schemas/.render-manifest.json +6 -2
  20. package/adapters/codex/skills/release-finish/SKILL.md +5 -1
  21. package/adapters/codex/skills/release-help/SKILL.md +1 -1
  22. package/adapters/cursor/.cursor-plugin/plugin.json +1 -1
  23. package/adapters/cursor/bin/release-skill.bundle.mjs +190 -59
  24. package/adapters/cursor/schemas/.render-manifest.json +6 -2
  25. package/adapters/cursor/skills/release-finish/SKILL.md +5 -1
  26. package/adapters/cursor/skills/release-help/SKILL.md +1 -1
  27. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  28. package/adapters/kimi/bin/release-skill.bundle.mjs +190 -59
  29. package/adapters/kimi/schemas/.render-manifest.json +6 -2
  30. package/adapters/kimi/skills/release-finish/SKILL.md +5 -1
  31. package/adapters/kimi/skills/release-help/SKILL.md +1 -1
  32. package/adapters/qoder/.qoder-plugin/plugin.json +1 -1
  33. package/adapters/qoder/bin/release-skill.bundle.mjs +190 -59
  34. package/adapters/qoder/schemas/.render-manifest.json +6 -2
  35. package/adapters/qoder/skills/release-finish/SKILL.md +5 -1
  36. package/adapters/qoder/skills/release-help/SKILL.md +1 -1
  37. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  38. package/adapters/workbuddy/bin/release-skill.bundle.mjs +190 -59
  39. package/adapters/workbuddy/schemas/.render-manifest.json +6 -2
  40. package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -1
  41. package/adapters/workbuddy/skills/release-help/SKILL.md +1 -1
  42. package/bin/release-skill.bundle.mjs +190 -59
  43. package/package.json +1 -1
  44. package/platform-manifest.json +4 -4
  45. package/references/.render-manifest.json +2 -2
  46. package/references/02-project-config.md +5 -1
  47. package/schemas/.render-manifest.json +6 -2
  48. package/skills/release-finish/SKILL.md +5 -1
  49. package/skills/release-help/SKILL.md +1 -1
  50. package/skills-src/release-finish/SKILL.md +5 -1
  51. package/skills-src/release-help/SKILL.md +1 -1
  52. package/src/adapters/plugin-marketplace.mjs +152 -43
  53. package/src/commands/post-release-local.mjs +71 -34
@@ -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,15 +846,30 @@ async function observeQoderPlugin(target, command, env, run) {
846
846
  };
847
847
  }
848
848
 
849
- async function readQoderManifest(root, label) {
850
- const manifest = parseJson(
851
- await readFileContained(root, '.qoder-plugin/plugin.json', { encoding: 'utf8' }),
852
- label,
853
- );
854
- if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
855
- throw new Error(`${label} did not return an object`);
849
+ async function readQoderManifest(root, label, target) {
850
+ const candidates = [];
851
+ for (const manifestPath of ['.qoder-plugin/plugin.json', '.claude-plugin/plugin.json']) {
852
+ let text;
853
+ try {
854
+ text = await readFileContained(root, manifestPath, { encoding: 'utf8' });
855
+ } catch (error) {
856
+ if (error?.code === 'SFC2004' && error?.details?.kind === 'missing-resource') continue;
857
+ throw error;
858
+ }
859
+ const manifest = parseJson(text, `${label} (${manifestPath})`);
860
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
861
+ throw new Error(`${label} (${manifestPath}) did not return an object`);
862
+ }
863
+ await assertQoderPluginManifest(target, manifest, `${label} (${manifestPath})`, root);
864
+ candidates.push({ manifestPath, manifest });
865
+ }
866
+ if (candidates.length === 0) throw new Error(`${label} has no native or compatible plugin manifest`);
867
+ if (candidates.length === 2 && ['name', 'version'].some((field) => (
868
+ candidates[0].manifest[field] !== candidates[1].manifest[field]
869
+ ))) {
870
+ throw new Error(`${label} does not match the frozen Qoder plugin identity and projection path: conflicting native and compatible manifests`);
856
871
  }
857
- return manifest;
872
+ return candidates[0];
858
873
  }
859
874
 
860
875
  async function assertQoderPluginManifest(target, manifest, label, pluginRoot, frozenSkills) {
@@ -880,41 +895,63 @@ async function assertFrozenQoderManifest(target, root) {
880
895
  snapshotPath: target.snapshotPath,
881
896
  expectedDigest: target.manifestDigest,
882
897
  });
883
- const manifest = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest');
884
- return assertQoderPluginManifest(target, manifest, 'frozen Qoder plugin manifest', snapshotRoot);
898
+ const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, 'frozen Qoder plugin manifest', target);
899
+ const skills = await assertQoderPluginManifest(target, manifest, 'frozen Qoder plugin manifest', snapshotRoot);
900
+ return { manifestPath, skills };
885
901
  }
886
902
 
887
903
  async function assertQoderHubEntry(target, marketplaceRoot) {
888
- const marketplace = parseJson(
889
- await readFileContained(marketplaceRoot, 'marketplace.json', { encoding: 'utf8' }),
890
- 'Qoder Hub marketplace manifest',
891
- );
892
- if (marketplace?.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
893
- throw new Error('Qoder Hub marketplace manifest does not match the frozen marketplace');
894
- }
895
- const matches = marketplace.plugins.filter((entry) => entry?.name === target.plugin);
896
- if (matches.length !== 1) {
897
- throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
898
- }
899
- const source = matches[0]?.source;
900
- if (
901
- source?.source !== 'url'
902
- || normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo
903
- || source.sha !== target.pluginCommit
904
- ) {
905
- 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
+ }
906
939
  }
940
+ if (!found) throw new Error('Qoder Hub has no native or compatible marketplace manifest');
907
941
  }
908
942
 
909
943
  async function verifyQoderInstalledPayload({
910
944
  target,
911
945
  root,
912
946
  installPath,
913
- frozenSkills,
947
+ frozenManifest,
914
948
  verifyInstalledPayload,
915
949
  }) {
916
- const manifest = await readQoderManifest(installPath, 'installed Qoder plugin manifest');
917
- await assertQoderPluginManifest(target, manifest, 'installed Qoder plugin manifest', installPath, frozenSkills);
950
+ const { manifestPath, manifest } = await readQoderManifest(installPath, 'installed Qoder plugin manifest', target);
951
+ if (manifestPath !== frozenManifest.manifestPath) {
952
+ throw new Error('installed Qoder plugin manifest path differs from the frozen manifest selection');
953
+ }
954
+ await assertQoderPluginManifest(target, manifest, 'installed Qoder plugin manifest', installPath, frozenManifest.skills);
918
955
  await verifyInstalledPayload({
919
956
  snapshotPath: target.snapshotPath,
920
957
  manifestDigest: target.manifestDigest,
@@ -928,7 +965,7 @@ async function runQoderUpdate(target, detected, run, {
928
965
  verifyInstalledPayload,
929
966
  }) {
930
967
  const env = hostEnvironment('qoder');
931
- const frozenSkills = await assertFrozenQoderManifest(target, root);
968
+ const frozenManifest = await assertFrozenQoderManifest(target, root);
932
969
  const marketplace = await observeQoderMarketplace(target, detected.command, env, run);
933
970
  if (!marketplace.installed) {
934
971
  return {
@@ -949,7 +986,7 @@ async function runQoderUpdate(target, detected, run, {
949
986
  target,
950
987
  root,
951
988
  installPath: before.installPath,
952
- frozenSkills,
989
+ frozenManifest,
953
990
  verifyInstalledPayload,
954
991
  });
955
992
  return { status: 'ALREADY_CURRENT', version: target.version };
@@ -973,7 +1010,7 @@ async function runQoderUpdate(target, detected, run, {
973
1010
  target,
974
1011
  root,
975
1012
  installPath: after.installPath,
976
- frozenSkills,
1013
+ frozenManifest,
977
1014
  verifyInstalledPayload,
978
1015
  });
979
1016
  return {