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
@@ -9,9 +9,9 @@ const __bundlePkgRoot = __bundleResolve(__bundleDirname(__bundleFileURLToPath(im
9
9
  // Provide a real require() for CJS packages bundled into ESM (e.g. yaml, ajv).
10
10
  const __bundleRealRequire = __bundleCreateRequire(import.meta.url);
11
11
  // Package identity injected at build time — closure-independent --version probe.
12
- const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.9.35"});
12
+ const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.9.37"});
13
13
  // Build-time source digest for the BUNDLE_STALE freshness gate (see above).
14
- const __bundleSourceDigest = "e14a5aba2f50cba3982bbe413cc9faebf0d45629e53f410e4942f562d7161754";
14
+ const __bundleSourceDigest = "6eb7b6560da84252426dc5cfc81297a3d5d6d6101aecb1a8fbf3f35402892715";
15
15
 
16
16
  var __create = Object.create;
17
17
  var __defProp = Object.defineProperty;
@@ -113912,6 +113912,66 @@ function normalizeCodeBuddySkillsRel(skillsRaw) {
113912
113912
  }
113913
113913
  return rel;
113914
113914
  }
113915
+ async function resolveCodexEntrySkillFile(pluginRoot, entrySkill, pluginName) {
113916
+ const { normalizeGenericSkillsRel: normalizeGenericSkillsRel2 } = await init_surface_host_bindings().then(() => surface_host_bindings_exports);
113917
+ if (!entrySkill || typeof entrySkill !== "string" || !SAFE_ID_RE.test(entrySkill)) {
113918
+ throw new Error(`unsafe entrySkill: "${entrySkill}"`);
113919
+ }
113920
+ const platform = getPlatform("codex");
113921
+ const manifestPath = await resolveContained(pluginRoot, platform.manifestPaths.plugin);
113922
+ let manifest;
113923
+ try {
113924
+ manifest = JSON.parse(await readFile26(manifestPath, "utf8"));
113925
+ } catch (err) {
113926
+ throw new Error(`Codex plugin manifest cannot be read: ${err.message}`);
113927
+ }
113928
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
113929
+ throw new Error("Codex plugin manifest must be an object");
113930
+ }
113931
+ const skillsRel = Object.hasOwn(manifest, "skills") ? normalizeGenericSkillsRel2(manifest.skills) : "skills";
113932
+ const skillsRoot = await resolveContained(pluginRoot, skillsRel);
113933
+ let skillsRootStat;
113934
+ try {
113935
+ skillsRootStat = await lstat37(skillsRoot);
113936
+ } catch {
113937
+ throw new Error(`Codex manifest skills root does not exist: ${manifest.skills ?? skillsRel}`);
113938
+ }
113939
+ if (skillsRootStat.isSymbolicLink() || !skillsRootStat.isDirectory()) {
113940
+ throw new Error(`Codex manifest skills root must be a real directory: ${manifest.skills ?? skillsRel}`);
113941
+ }
113942
+ const findCandidate = /* @__PURE__ */ __name(async (skillDirName) => {
113943
+ const skillDir = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}`);
113944
+ let skillDirStat;
113945
+ try {
113946
+ skillDirStat = await lstat37(skillDir);
113947
+ } catch (err) {
113948
+ if (err?.code === "ENOENT") return null;
113949
+ throw err;
113950
+ }
113951
+ if (skillDirStat.isSymbolicLink() || !skillDirStat.isDirectory()) {
113952
+ throw new Error(`Codex entry skill directory must be a real directory: ${skillsRel}/${skillDirName}`);
113953
+ }
113954
+ const skillFile = await resolveContained(pluginRoot, `${skillsRel}/${skillDirName}/SKILL.md`);
113955
+ let skillStat;
113956
+ try {
113957
+ skillStat = await lstat37(skillFile);
113958
+ } catch (err) {
113959
+ if (err?.code === "ENOENT") return null;
113960
+ throw err;
113961
+ }
113962
+ if (skillStat.isSymbolicLink() || !skillStat.isFile()) {
113963
+ throw new Error(`Codex entry skill must be a regular non-symlink file: ${skillsRel}/${skillDirName}/SKILL.md`);
113964
+ }
113965
+ return skillFile;
113966
+ }, "findCandidate");
113967
+ const shortNameEntry = await findCandidate(entrySkill);
113968
+ if (shortNameEntry) return shortNameEntry;
113969
+ if (pluginName && SAFE_ID_RE.test(pluginName)) {
113970
+ const prefixedEntry = await findCandidate(`${pluginName}-${entrySkill}`);
113971
+ if (prefixedEntry) return prefixedEntry;
113972
+ }
113973
+ throw new Error(`Codex entry skill not found: ${skillsRel}/${entrySkill}/SKILL.md`);
113974
+ }
113915
113975
  async function resolveCodeBuddyEntrySkillFile(pluginRootReal, manifest, entrySkill) {
113916
113976
  if (!entrySkill || typeof entrySkill !== "string" || !SAFE_ID_RE.test(entrySkill)) {
113917
113977
  throw new Error(`unsafe entrySkill: "${entrySkill}"`);
@@ -115067,6 +115127,17 @@ function createPluginMarketplaceAdapter(deps = {}) {
115067
115127
  error: `entry skill not resolvable in snapshot: ${entryErr.message}`
115068
115128
  });
115069
115129
  }
115130
+ } else if (consumer === "codex") {
115131
+ const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
115132
+ try {
115133
+ await resolveCodexEntrySkillFile(pluginRootForSkill, action.entrySkill, action.plugin);
115134
+ } catch (entryErr) {
115135
+ return createResult({
115136
+ actionType,
115137
+ status: ActionStatus.PREFLIGHT_FAILED,
115138
+ error: `entry skill not resolvable in snapshot: ${entryErr.message}`
115139
+ });
115140
+ }
115070
115141
  } else {
115071
115142
  const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
115072
115143
  let entrySkillFile = resolve22(pluginRootForSkill, "skills", action.entrySkill, "SKILL.md");
@@ -116181,42 +116252,59 @@ function createPluginMarketplaceAdapter(deps = {}) {
116181
116252
  }
116182
116253
  });
116183
116254
  }
116184
- let entrySkillPath = resolve22(installPath, "skills", action.entrySkill, "SKILL.md");
116185
- let entrySkillFound = false;
116186
- try {
116187
- const skillStat = await lstat37(entrySkillPath);
116188
- if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
116189
- entrySkillFound = true;
116255
+ if (consumer === "codex") {
116256
+ try {
116257
+ await resolveCodexEntrySkillFile(installPath, action.entrySkill, action.plugin);
116258
+ } catch (entryErr) {
116259
+ return createResult({
116260
+ actionType,
116261
+ status: ActionStatus.OBSERVED,
116262
+ observation: {
116263
+ installed: true,
116264
+ installPath,
116265
+ entrySkillFound: false,
116266
+ error: `entry skill not resolvable in installed plugin: ${entryErr.message}`
116267
+ }
116268
+ });
116190
116269
  }
116191
- } catch {
116192
- }
116193
- if (!entrySkillFound && action.plugin) {
116194
- const prefixedPath = resolve22(
116195
- installPath,
116196
- "skills",
116197
- `${action.plugin}-${action.entrySkill}`,
116198
- "SKILL.md"
116199
- );
116270
+ } else {
116271
+ let entrySkillPath = resolve22(installPath, "skills", action.entrySkill, "SKILL.md");
116272
+ let entrySkillFound = false;
116200
116273
  try {
116201
- const skillStat = await lstat37(prefixedPath);
116274
+ const skillStat = await lstat37(entrySkillPath);
116202
116275
  if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
116203
- entrySkillPath = prefixedPath;
116204
116276
  entrySkillFound = true;
116205
116277
  }
116206
116278
  } catch {
116207
116279
  }
116208
- }
116209
- if (!entrySkillFound) {
116210
- return createResult({
116211
- actionType,
116212
- status: ActionStatus.OBSERVED,
116213
- observation: {
116214
- installed: true,
116280
+ if (!entrySkillFound && action.plugin) {
116281
+ const prefixedPath = resolve22(
116215
116282
  installPath,
116216
- entrySkillFound: false,
116217
- error: `entry skill not found: skills/${action.entrySkill}/SKILL.md`
116283
+ "skills",
116284
+ `${action.plugin}-${action.entrySkill}`,
116285
+ "SKILL.md"
116286
+ );
116287
+ try {
116288
+ const skillStat = await lstat37(prefixedPath);
116289
+ if (skillStat.isFile() && !skillStat.isSymbolicLink()) {
116290
+ entrySkillPath = prefixedPath;
116291
+ entrySkillFound = true;
116292
+ }
116293
+ } catch {
116218
116294
  }
116219
- });
116295
+ }
116296
+ if (!entrySkillFound) {
116297
+ return createResult({
116298
+ actionType,
116299
+ status: ActionStatus.OBSERVED,
116300
+ observation: {
116301
+ installed: true,
116302
+ installPath,
116303
+ entrySkillFound: false,
116304
+ error: `entry skill not found: skills/${action.entrySkill}/SKILL.md`
116305
+ }
116306
+ });
116307
+ }
116220
116308
  }
116221
116309
  let manifestDigest;
116222
116310
  let manifestError = null;
@@ -116418,6 +116506,7 @@ var init_plugin_marketplace = __esm({
116418
116506
  __name(normalizeKimiSkillsRel, "normalizeKimiSkillsRel");
116419
116507
  __name(resolveKimiEntrySkillFile, "resolveKimiEntrySkillFile");
116420
116508
  __name(normalizeCodeBuddySkillsRel, "normalizeCodeBuddySkillsRel");
116509
+ __name(resolveCodexEntrySkillFile, "resolveCodexEntrySkillFile");
116421
116510
  __name(resolveCodeBuddyEntrySkillFile, "resolveCodeBuddyEntrySkillFile");
116422
116511
  SUPPORTED_TYPES = [
116423
116512
  ActionType.PLUGIN_MANIFEST_VALIDATE,
@@ -116458,6 +116547,14 @@ var init_plugin_marketplace = __esm({
116458
116547
  });
116459
116548
 
116460
116549
  // src/core/surface-host-bindings.mjs
116550
+ var surface_host_bindings_exports = {};
116551
+ __export(surface_host_bindings_exports, {
116552
+ deriveSurfaceHostBinding: () => deriveSurfaceHostBinding,
116553
+ groupSurfaceHostBindings: () => groupSurfaceHostBindings,
116554
+ normalizeGenericSkillsRel: () => normalizeGenericSkillsRel,
116555
+ normalizeSurfacePath: () => normalizeSurfacePath,
116556
+ pluginRootFromManifestRelativePath: () => pluginRootFromManifestRelativePath
116557
+ });
116461
116558
  import { dirname as dirname10, isAbsolute as isAbsolute18, join as join19, normalize as normalize3, resolve as resolve23, sep as sep9 } from "node:path";
116462
116559
  import { lstat as lstat38 } from "node:fs/promises";
116463
116560
  function normalizeSurfacePath(rawPath) {
@@ -116586,6 +116683,49 @@ import { dirname as dirname11, join as join20, relative as relative18, isAbsolut
116586
116683
  import { tmpdir as tmpdir3 } from "node:os";
116587
116684
  import { execFile as execFileCb7 } from "node:child_process";
116588
116685
  import { promisify as promisify7 } from "node:util";
116686
+ function transportIdentityEntries(entries) {
116687
+ return entries.map(({ path: path43, type, mode, size, contentDigest }) => ({
116688
+ path: path43,
116689
+ type,
116690
+ mode: mode & ~WRITE_BITS,
116691
+ size,
116692
+ contentDigest
116693
+ }));
116694
+ }
116695
+ function compareInstallToFrozenTransport(frozenEntries, installedEntries) {
116696
+ const frozen = transportIdentityEntries(frozenEntries);
116697
+ const installed = transportIdentityEntries(installedEntries);
116698
+ const installedByPath = new Map(installed.map((entry) => [entry.path, entry]));
116699
+ const conflicts = [];
116700
+ for (const frozenEntry of frozen) {
116701
+ const installedEntry = installedByPath.get(frozenEntry.path);
116702
+ if (!installedEntry) {
116703
+ conflicts.push(`missing: ${frozenEntry.path}`);
116704
+ continue;
116705
+ }
116706
+ if (installedEntry.type !== frozenEntry.type) {
116707
+ conflicts.push(`type mismatch: ${frozenEntry.path}`);
116708
+ } else if (installedEntry.size !== frozenEntry.size || installedEntry.contentDigest !== frozenEntry.contentDigest) {
116709
+ conflicts.push(`content mismatch: ${frozenEntry.path}`);
116710
+ } else if (installedEntry.mode !== frozenEntry.mode) {
116711
+ conflicts.push(`mode mismatch: ${frozenEntry.path}`);
116712
+ }
116713
+ }
116714
+ const frozenPaths = new Set(frozen.map((entry) => entry.path));
116715
+ for (const installedEntry of installed) {
116716
+ if (!frozenPaths.has(installedEntry.path)) {
116717
+ conflicts.push(`extra: ${installedEntry.path}`);
116718
+ }
116719
+ }
116720
+ return conflicts;
116721
+ }
116722
+ function frozenSnapshotRelativePath(unit, actions, unitId) {
116723
+ if (typeof unit?.frozenSnapshot?.path === "string" && unit.frozenSnapshot.path.length > 0) {
116724
+ return unit.frozenSnapshot.path;
116725
+ }
116726
+ const action = (actions ?? []).find((item) => item.unitId === unitId && typeof item.parameters?.snapshotPath === "string" && item.parameters.snapshotPath.length > 0);
116727
+ return action?.parameters?.snapshotPath ?? null;
116728
+ }
116589
116729
  async function verifyCursorHostDistributions({
116590
116730
  plan,
116591
116731
  root,
@@ -118161,16 +118301,65 @@ async function verifyRelease(options) {
118161
118301
  }
118162
118302
  const pluginDistCount = (surface.unit?.distributions ?? []).filter((d) => d.type !== "npm").length;
118163
118303
  if (pluginDistCount === 1 && surface.frozenPluginRoot === ".") {
118164
- const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
118165
- unitId: surface.unitId,
118166
- preparedAt: expectedUnitReceipt.preparedAt ?? null,
118167
- exitCode: expectedUnitReceipt.exitCode ?? 0
118168
- });
118169
- assertSkillResourceClosureReceipt(
118170
- expectedUnitReceipt,
118171
- observedReceipt,
118172
- `unit "${surface.unitId}"`
118173
- );
118304
+ const frozenRel = frozenSnapshotRelativePath(surface.unit, actions, surface.unitId);
118305
+ if (frozenRel) {
118306
+ const frozenDir = await resolveFrozenPath(root, frozenRel, "frozen snapshot path");
118307
+ const frozenClosure = await checkSkillResourceClosure({
118308
+ snapshotDir: frozenDir,
118309
+ host: surface.host,
118310
+ surfaceHostBindings: [{ surfaceId: surface.surfaceId, host: surface.host }]
118311
+ });
118312
+ const frozenReceipt = createSkillResourceClosureReceipt(frozenClosure, {
118313
+ unitId: surface.unitId,
118314
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
118315
+ exitCode: expectedUnitReceipt.exitCode ?? 0
118316
+ });
118317
+ assertSkillResourceClosureReceipt(
118318
+ expectedUnitReceipt,
118319
+ frozenReceipt,
118320
+ `unit "${surface.unitId}"`
118321
+ );
118322
+ const frozenSnap = await computeFrozenSnapshot(frozenDir, {
118323
+ excludeRootEntries: TRANSPORT_EXCLUSIONS
118324
+ });
118325
+ const installedSnap = await computeFrozenSnapshot(surface.installPath, {
118326
+ excludeRootEntries: TRANSPORT_EXCLUSIONS
118327
+ });
118328
+ const conflicts = compareInstallToFrozenTransport(
118329
+ frozenSnap.entries,
118330
+ installedSnap.entries
118331
+ );
118332
+ await evidence.append({
118333
+ phase: "verify",
118334
+ step: "skill-resource-closure",
118335
+ status: "install-comparison",
118336
+ unitId: surface.unitId,
118337
+ frozenInputDigest: frozenSnap.digest,
118338
+ installedInputDigest: installedSnap.digest,
118339
+ frozenModes: frozenSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode })),
118340
+ installedModes: installedSnap.entries.map((entry) => ({ path: entry.path, mode: entry.mode }))
118341
+ });
118342
+ if (conflicts.length > 0) {
118343
+ const listed = conflicts.slice(0, PAYLOAD_CONFLICT_REPORT_CAP2).join("; ");
118344
+ const overflow = conflicts.length > PAYLOAD_CONFLICT_REPORT_CAP2 ? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP2} more conflicting path(s)` : "";
118345
+ throw new ReleaseError(
118346
+ POST_PUBLISH_VERIFY_FAILED,
118347
+ `installed plugin root differs in path, bytes, size, or non-write mode bits for unit "${surface.unitId}": ${listed}${overflow}`,
118348
+ { unitId: surface.unitId, conflicts }
118349
+ );
118350
+ }
118351
+ } else {
118352
+ const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
118353
+ unitId: surface.unitId,
118354
+ preparedAt: expectedUnitReceipt.preparedAt ?? null,
118355
+ exitCode: expectedUnitReceipt.exitCode ?? 0
118356
+ });
118357
+ assertSkillResourceClosureReceipt(
118358
+ expectedUnitReceipt,
118359
+ observedReceipt,
118360
+ `unit "${surface.unitId}"`
118361
+ );
118362
+ }
118174
118363
  }
118175
118364
  }
118176
118365
  skillResourceClosureReceipts.push(createSkillResourceClosureReceipt(
@@ -118381,7 +118570,7 @@ async function verifyRelease(options) {
118381
118570
  throw err;
118382
118571
  }
118383
118572
  }
118384
- var execFile6, VERIFICATION_RESOLVED_TYPES, ADAPTER_ACTION_TYPE_MAP2, FIRST_RELEASE_BOOTSTRAP_MODE, EXTERNAL_MARKETPLACE_SHA_RE2, defaultNpmExecutor;
118573
+ var execFile6, VERIFICATION_RESOLVED_TYPES, WRITE_BITS, PAYLOAD_CONFLICT_REPORT_CAP2, ADAPTER_ACTION_TYPE_MAP2, FIRST_RELEASE_BOOTSTRAP_MODE, EXTERNAL_MARKETPLACE_SHA_RE2, defaultNpmExecutor;
118385
118574
  var init_verify = __esm({
118386
118575
  async "src/commands/verify.mjs"() {
118387
118576
  init_src2();
@@ -118419,6 +118608,11 @@ var init_verify = __esm({
118419
118608
  PASSED_MANUAL: "PASSED_MANUAL",
118420
118609
  NOT_REQUIRED_UNCHANGED: "NOT_REQUIRED_UNCHANGED"
118421
118610
  });
118611
+ WRITE_BITS = 146;
118612
+ PAYLOAD_CONFLICT_REPORT_CAP2 = 10;
118613
+ __name(transportIdentityEntries, "transportIdentityEntries");
118614
+ __name(compareInstallToFrozenTransport, "compareInstallToFrozenTransport");
118615
+ __name(frozenSnapshotRelativePath, "frozenSnapshotRelativePath");
118422
118616
  __name(verifyCursorHostDistributions, "verifyCursorHostDistributions");
118423
118617
  __name(verifyPublicSourceAuthorityReceiptOffline, "verifyPublicSourceAuthorityReceiptOffline");
118424
118618
  ADAPTER_ACTION_TYPE_MAP2 = {
@@ -139649,7 +139843,7 @@ async function observeQoderPlugin(target, command2, env, run6) {
139649
139843
  found
139650
139844
  };
139651
139845
  }
139652
- async function readQoderManifest(root, label) {
139846
+ async function readQoderManifest(root, label, target) {
139653
139847
  const candidates = [];
139654
139848
  for (const manifestPath of [".qoder-plugin/plugin.json", ".claude-plugin/plugin.json"]) {
139655
139849
  let text;
@@ -139663,10 +139857,11 @@ async function readQoderManifest(root, label) {
139663
139857
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
139664
139858
  throw new Error(`${label} (${manifestPath}) did not return an object`);
139665
139859
  }
139860
+ await assertQoderPluginManifest(target, manifest, `${label} (${manifestPath})`, root);
139666
139861
  candidates.push({ manifestPath, manifest });
139667
139862
  }
139668
139863
  if (candidates.length === 0) throw new Error(`${label} has no native or compatible plugin manifest`);
139669
- if (candidates.length === 2 && ["name", "version", "skills"].some((field) => candidates[0].manifest[field] !== candidates[1].manifest[field])) {
139864
+ if (candidates.length === 2 && ["name", "version"].some((field) => candidates[0].manifest[field] !== candidates[1].manifest[field])) {
139670
139865
  throw new Error(`${label} does not match the frozen Qoder plugin identity and projection path: conflicting native and compatible manifests`);
139671
139866
  }
139672
139867
  return candidates[0];
@@ -139687,26 +139882,43 @@ async function assertFrozenQoderManifest(target, root) {
139687
139882
  snapshotPath: target.snapshotPath,
139688
139883
  expectedDigest: target.manifestDigest
139689
139884
  });
139690
- const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, "frozen Qoder plugin manifest");
139885
+ const { manifestPath, manifest } = await readQoderManifest(snapshotRoot, "frozen Qoder plugin manifest", target);
139691
139886
  const skills = await assertQoderPluginManifest(target, manifest, "frozen Qoder plugin manifest", snapshotRoot);
139692
139887
  return { manifestPath, skills };
139693
139888
  }
139694
139889
  async function assertQoderHubEntry(target, marketplaceRoot) {
139695
- const marketplace = parseJson2(
139696
- await readFileContained(marketplaceRoot, "marketplace.json", { encoding: "utf8" }),
139697
- "Qoder Hub marketplace manifest"
139698
- );
139699
- if (marketplace?.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
139700
- throw new Error("Qoder Hub marketplace manifest does not match the frozen marketplace");
139701
- }
139702
- const matches = marketplace.plugins.filter((entry) => entry?.name === target.plugin);
139703
- if (matches.length !== 1) {
139704
- throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
139705
- }
139706
- const source = matches[0]?.source;
139707
- if (source?.source !== "url" || normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo || source.sha !== target.pluginCommit) {
139708
- throw new Error("Qoder Hub entry does not match the frozen public source identity");
139890
+ let found = false;
139891
+ for (const indexPath of ["marketplace.json", ".claude-plugin/marketplace.json"]) {
139892
+ let text;
139893
+ try {
139894
+ text = await readFileContained(marketplaceRoot, indexPath, { encoding: "utf8" });
139895
+ } catch (error) {
139896
+ if (error?.code === "SFC2004" && error?.details?.kind === "missing-resource") continue;
139897
+ throw error;
139898
+ }
139899
+ found = true;
139900
+ const marketplace = parseJson2(text, `Qoder Hub marketplace manifest (${indexPath})`);
139901
+ if (!marketplace || typeof marketplace !== "object" || Array.isArray(marketplace) || marketplace.name !== target.marketplace || !Array.isArray(marketplace.plugins)) {
139902
+ throw new Error("Qoder Hub marketplace manifest does not match the frozen marketplace");
139903
+ }
139904
+ if (indexPath === ".claude-plugin/marketplace.json") {
139905
+ if (target.hub.githubHost !== "github.com" || typeof target.frozenTag !== "string" || target.frozenTag.length === 0) {
139906
+ throw new Error("Qoder compatible Hub entry requires a frozen GitHub tag identity");
139907
+ }
139908
+ await assertHubHostEntry({ ...target, host: "claude", pluginTag: target.frozenTag, githubHost: target.hub.githubHost }, marketplaceRoot);
139909
+ continue;
139910
+ }
139911
+ const matches = marketplace.plugins.filter((entry2) => entry2?.name === target.plugin);
139912
+ if (matches.length !== 1) {
139913
+ throw new Error(`Qoder Hub marketplace manifest must contain exactly one ${target.plugin} entry`);
139914
+ }
139915
+ const entry = matches[0];
139916
+ const source = entry.source;
139917
+ if (source?.source !== "url" || normalizeHubGitSource(source.url, target.hub.githubHost) !== target.pluginRepo || source.sha !== target.pluginCommit || entry.version !== void 0 && entry.version !== target.version || source.ref !== void 0 && (typeof target.frozenTag !== "string" || source.ref !== target.frozenTag)) {
139918
+ throw new Error("Qoder Hub entry does not match the frozen public source identity");
139919
+ }
139709
139920
  }
139921
+ if (!found) throw new Error("Qoder Hub has no native or compatible marketplace manifest");
139710
139922
  }
139711
139923
  async function verifyQoderInstalledPayload({
139712
139924
  target,
@@ -139715,7 +139927,7 @@ async function verifyQoderInstalledPayload({
139715
139927
  frozenManifest,
139716
139928
  verifyInstalledPayload
139717
139929
  }) {
139718
- const { manifestPath, manifest } = await readQoderManifest(installPath, "installed Qoder plugin manifest");
139930
+ const { manifestPath, manifest } = await readQoderManifest(installPath, "installed Qoder plugin manifest", target);
139719
139931
  if (manifestPath !== frozenManifest.manifestPath) {
139720
139932
  throw new Error("installed Qoder plugin manifest path differs from the frozen manifest selection");
139721
139933
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.9.35",
3
+ "version": "0.9.37",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": "1.0.0",
3
3
  "platformId": "release-skill",
4
4
  "platformName": "release-skill Plugin Platform",
5
- "version": "0.9.35",
5
+ "version": "0.9.37",
6
6
  "logicalRoot": "skills-src/",
7
- "logicalRootDigest": "05adb1b81257f99a74b7736f13a174095f79b0745f4c1a5947bb7d95ee9cf8ad",
8
- "projectionDigest": "9ae26428567a5d7eaf2586626dcd503e0035205e99c3ec3c335f13b94ffa58c4",
9
- "projectionId": "release-skill:platform:0.9.35",
7
+ "logicalRootDigest": "b76c623a055c6998a6909fc041b2646356f0d69cace4a43cb8edacefc248fb97",
8
+ "projectionDigest": "0d4b9972ee2ce6ce8eadfb075a90e89752a8f2b5f2becd53a6ae034029da8bad",
9
+ "projectionId": "release-skill:platform:0.9.37",
10
10
  "manifestRequired": true,
11
11
  "projectionSources": {
12
12
  "skills-src-root": {
@@ -10,8 +10,8 @@
10
10
  "bytes": 16742
11
11
  },
12
12
  "02-project-config.md": {
13
- "digest": "95ef58bcf788636c2b0300955e095a050829b51f6e5b4fc1a8d618d6b8a18021",
14
- "bytes": 34314
13
+ "digest": "3e19eedbabb7e17d8afd4d9efc6a43309b7cc04e72866d44bd1afac787c8b462",
14
+ "bytes": 34998
15
15
  },
16
16
  "03-readme-quality.md": {
17
17
  "digest": "d74e41052592443ebbd450d0ace5f9b9b19c7a94db50f952b17c1c5a4c5dc230",
@@ -124,19 +124,100 @@ policy: # 可选,安全策略
124
124
 
125
125
  ### 2.0 显式测试接入
126
126
 
127
- `testIntake` 可选。只含两个项目相对普通文件路径:`requirementsFile` 与 `resultFile`。两者归一化后必须不同;`a/./b` 与 `a/b` 视为同一路径。未知字段、绝对路径、`..` 和结尾斜杠在配置加载时拒绝。
127
+ `testIntake` 为可选配置项,仅包含两个项目相对普通文件路径字段:`requirementsFile` 与 `resultFile`。路径经归一化后必须互异;`a/./b` 与 `a/b` 视为同一路径。配置加载时拒绝以下内容:
128
+ - 未知字段;
129
+ - 绝对路径;
130
+ - 包含 `..` 路径组件的路径;
131
+ - 以斜杠结尾的路径。
128
132
 
129
- 未声明 `testIntake` 的项目保持原有 hook、完整门、缓存和发布路径,不因此增加 TEST、Graph 或 Registry 依赖。`--test-selection incremental` 仍在冻结时拒绝。
133
+ 未声明 `testIntake` 的项目维持原有 hook 机制、完整门控、缓存策略与发布路径,不引入 TEST、Graph 或 Registry 依赖。
130
134
 
131
- 声明之后只允许生产 `prepare`。非生产 prepare 返回 `CONFIG_INVALID`,原因 `TEST_INTAKE_NON_PRODUCTION`。assess 等其他命令仍可加载这份配置。docs、config、marketplace 轻量工作流、`selectedUnits` 和无需发布的配置分支都不免除项目级要求:未选中单元的单元级要求可以不进入本轮,项目级要求始终适用。
135
+ `--test-selection incremental` 在 `prepare` 或冻结过程中均被拒绝,不得在任何准备阶段启用。该拒绝行为不延迟至冻结完成后。
132
136
 
133
- 要求文件与结果文件的字段以 `schemas/test-intake.schema.json` 为准。release 只核收原件:预冻结项必须有一条可核验结果,处置为 `executed`、`reused`,或由 owner 确认的 `not-applicable`;结论为 `passed`;`acceptedBy` 与声明的 acceptor 一致,`not-applicable` 时与 owner 一致;引用文件可读且摘要一致;结果里的 `subjectDigest` 与 `requirementsSha256` 绑定当前对象。退出码 0、全局 PASS、空证据、`failed` / `incomplete` / `uncovered`、未知字段,或把后续阶段项提前标成完成,都不能通过。未知失败原因保持 `unknown`。安装、加载和业务证据继续分开保存,不合并成一个类别。
137
+ #### 交互式 Skill 的接入职责
134
138
 
135
- 首次生产 prepare 生成适用快照和 tarball 后,只保存一份非正式候选 `.release-skill/runs/<run>/test-candidate.json`,返回 `NEEDS_INPUT`(`GATE_FAILED`,退出码 13),并给出 `candidatePath`、`candidateSha256`、`subjectDigest`。此时不写正式计划,也不进入 `PREPARED` 或 `FROZEN`。之后用 `prepare --candidate <path>` 续接:新的运行目录,不覆盖旧运行目录、旧证据或旧候选。结果补齐并核验通过后才写正式计划。对象、配置、权威要求或其他实质输入变化时返回 `TEST_CANDIDATE_REPREPARE`,原候选字节保持不变。只往结果文件补入仍绑定该对象的有效内容,不算输入漂移。
139
+ 生产准备前,Skill 必须核对接入状态。若未声明 `testIntake`,系统默认触发公开 TEST 0.1.0 的 `skill-family-test:skill-family-test-plan`,随后在授权范围内调用 `skill-family-test:skill-family-test-run`。
136
140
 
137
- 候选不能传给 approve 或 publish,也不能把 `PARTIAL`、`PUBLISHED`、`VERIFIED` 或任何已成功的外部检查点改成完成。`ship` 在接受前保持持久状态 `NEW` 与 `testCandidate`,不重建候选。没有 `testCandidate` 的 `NEW` 记录仍走原来的准备路径。
141
+ 跳过接入须满足以下条件:
142
+ - 基于本轮适用且范围匹配的明确同意;
143
+ - 必须先明确征询再作决定;
144
+ - 沉默、拒绝、仅普通发布预授权、旧轮次、其他项目、版本或范围不匹配均不可跳过;
145
+ - 本轮匹配的答复可复用。
138
146
 
139
- 正式计划上的可选 `testAcceptance` 进入当前计划版本的绑定摘要,记录层剥离不移除它。替换结果、准则、范围或允许的限制会改变 plan digest,原批准不再适用。不改变这些原件的注释不会使批准失效。没有 `testAcceptance` 的计划保持原摘要路径。approve、publish 和 reconcile 在动作前复核绑定原件;原件缺失或被改写时失败关闭,不重新批准,也不重放已经成功的外部检查点。
147
+ 不得将跳过表述为测试已达标,亦不得以跳过清除已声明的适用要求。
148
+
149
+ 以下请求不新增接入义务:
150
+ - 只读请求;
151
+ - 非生产路径;
152
+ - marketplace-only 委托发布。
153
+ - 已冻结计划的批准、发布、对账与正式验证不追加接入义务。
154
+
155
+ 直接非交互 CLI 支持可选 `testIntake`,不新增选择参数、同意文件 Schema 或身份认证。选择责任由实际交互的 Skill 承担,答复记入既有任务或执行计划。
156
+
157
+ #### 声明后的命令与要求适用范围
158
+
159
+ 声明 `testIntake` 后,仅允许执行生产 `prepare` 命令。非生产 `prepare` 返回 `CONFIG_INVALID`,错误码为 `TEST_INTAKE_NON_PRODUCTION`。
160
+
161
+ `assess` 等其他命令仍可加载该配置。
162
+
163
+ 轻量工作流(如 docs、config、marketplace)、`selectedUnits` 与无需发布的配置分支不免除项目级要求:未选中单元的单元级要求可不进入本轮,但项目级要求始终适用。
164
+
165
+ #### 核收原件与失败条件
166
+
167
+ `release` 仅核收原件,要求如下:
168
+ - 预冻结项必须存在一条可核验结果;
169
+ - 结果状态必须为 `executed`、`reused` 或由 owner 确认的 `not-applicable`;
170
+ - 结论必须为 `passed`;
171
+ - `acceptedBy` 与声明的 acceptor 一致;`not-applicable` 时与 owner 一致;
172
+ - 引用文件可读且摘要一致;
173
+ - 结果中的 `subjectDigest` 与 `requirementsSha256` 均必须绑定当前对象。
174
+
175
+ 以下情况均不通过核收:
176
+ - 退出码为 0;
177
+ - 全局 PASS;
178
+ - 空证据;
179
+ - 状态为 `failed`、`incomplete`、`uncovered`;
180
+ - 未知字段;
181
+ - 提前将后续阶段项标记为完成;
182
+ - 未知失败原因(保持 `unknown`)。
183
+
184
+ 安装、加载与业务证据须分开保存,不得合并为单一类别。
185
+
186
+ 核收依据 `schemas/test-intake.schema.json` 定义的字段结构。
187
+
188
+ #### 首次生产 prepare、候选续接和输入漂移
189
+
190
+ 首次生产 `prepare` 执行后,生成适用快照与 tarball,仅保存一份非正式候选:
191
+ - 路径:`.release-skill/runs/<run>/test-candidate.json`;
192
+ - 返回状态:`NEEDS_INPUT`(`GATE_FAILED`,退出码 13);
193
+ - 附带字段:`candidatePath`、`candidateSha256` 与 `subjectDigest`。
194
+
195
+ 此时不写正式计划,也不进入 `PREPARED` 或 `FROZEN` 状态。
196
+
197
+ 后续使用 `prepare --candidate <path>` 续接:
198
+ - 创建新运行目录;
199
+ - 不覆盖旧运行目录、旧证据或旧候选;
200
+ - 结果补齐并核验通过后,才写入正式计划。
201
+
202
+ 当对象、配置、权威要求或其他实质输入发生变更时,返回 `TEST_CANDIDATE_REPREPARE`,原候选字节保持不变。
203
+
204
+ **仅当向仍绑定原对象的 `resultFile` 补入有效结果内容时,不视为输入漂移。**
205
+
206
+ 首次候选、有效结果、正式计划与正式业务接受为不同阶段,不得将 `NEEDS_INPUT` 或工具成功改写为通过。
207
+
208
+ #### 后续计划与批准绑定
209
+
210
+ 候选不可传给 `approve` 或 `publish`,亦不可将 `PARTIAL`、`PUBLISHED`、`VERIFIED` 或任何已成功的外部检查点修改为完成状态。
211
+
212
+ `ship` 在接受前保持持久状态 `NEW` 与 `testCandidate`,不重建候选。无 `testCandidate` 的 `NEW` 记录仍沿用原有准备路径。
213
+
214
+ 正式计划中的可选字段 `testAcceptance` 进入当前计划版本的绑定摘要,记录层剥离不移除该字段。
215
+
216
+ 替换结果、准则、范围或允许的限制将改变 plan digest,原批准不再适用。未改变这些原件的注释不会使批准失效。
217
+
218
+ 无 `testAcceptance` 的计划保持原摘要路径。
219
+
220
+ `approve`、`publish` 与 `reconcile` 在执行前复核绑定原件;原件缺失或被改写时,动作失败并关闭,不重新批准,也不重放已成功的外部检查点。
140
221
 
141
222
  ### 2.1 本地发布收尾
142
223
 
@@ -247,7 +328,9 @@ releaseUnits:
247
328
 
248
329
  Qoder 保持 build-only 分发边界,不新增发布计划内的 distribution 或安装检查点。本机收尾更新 Qoder 有四个前提:冻结计划已声明、用户显式选择、Hub 来源匹配、既有用户范围安装核对通过。流程不添加市场或执行首次安装。更新完成仅表示安装载荷已变更,不表示当前会话已加载新版,后续仍须重新加载或新开会话,并完成一次真实的只读业务调用。
249
330
 
250
- 冻结与安装两侧均核验原生 `.qoder-plugin/plugin.json` 与 Claude 兼容 `.claude-plugin/plugin.json`:原生清单缺失时方可启用兼容清单,原生清单存在但无效时不得回退;两清单同时存在时,均须可解析为有效对象,且 `name/version/skills` 完全一致,以原生清单为优先。安装侧结果须与冻结侧选定的相对清单路径及 `skills` 一致,不得忽略额外出现的损坏或冲突清单。写入前复验冻结快照摘要,安装后须完整比较声明载荷与冻结来源,现有 `external-marketplace-v1` 合同允许宿主额外文件,因此完整载荷比较不得替代双清单的独立核验。
331
+ 在已核对的市场安装根内,分别读取 `marketplace.json` 的 `url` 来源与 `.claude-plugin/marketplace.json` 的 `github` 来源;至少一个存在,且所有存在候选都须有效并绑定同一冻结目标。根目录来源核对 `source.url/source.sha`;兼容来源沿现有 Claude 断言核对仓库、冻结标签、提交和版本。任一损坏或冲突均拒绝,不猜测优先级。
332
+
333
+ 冻结与安装两侧均核验原生 `.qoder-plugin/plugin.json` 与 Claude 兼容 `.claude-plugin/plugin.json`:原生清单缺失时方可启用兼容清单,原生清单存在但无效时不得回退;两清单同时存在时,均须可解析为有效对象,以原生清单为优先。每个已存在候选(包括未选候选)都须核对目标 `name/version`;其 `skills` 必须为非空字符串,经 `Foundation 0.25.0` 的 `resolveContained` 解析为根内真实目录。跨候选只比较 `name/version`,允许各自合法 `skills` 不同;原生正常不能遮蔽兼容无效。安装侧结果须与冻结侧选定的相对清单路径及 `skills` 一致,不得忽略额外出现的损坏或冲突清单。写入前复验冻结快照摘要,安装后须完整比较声明载荷与冻结来源,现有 `external-marketplace-v1` 合同允许宿主额外文件,因此完整载荷比较不得替代双清单的独立核验。
251
334
 
252
335
  0.9.21 起,同一声明也能执行 Claude、Codex、Kimi、CodeBuddy 和 WorkBuddy 的本机更新,无需恢复旧宿主 distribution。插件仓库、标签、提交和快照摘要必须来自冻结 unit 与对应标签动作。Claude、Codex 和 CodeBuddy 家族使用已有同源 Hub 市场;刷新前后分别核对宿主专用条目,安装后比较完整冻结载荷。CodeBuddy/WorkBuddy 只升级已有插件,并在插件写入前再次确认 Hub 分支未漂移。Kimi 直接使用冻结 GitHub Release 和终端交互,其只有仓库 URL 的 Hub 文件不承担标签或提交证明。启动进程保留三个代理变量 `http_proxy`、`https_proxy`、`all_proxy`。
253
336
 
@@ -17,6 +17,10 @@
17
17
  "digest": "86c9cab024ebe9c7f8ec3358fcc12d05b8ed246ab59da2b0acc6a517f31274ea",
18
18
  "bytes": 2488
19
19
  },
20
+ "evidence-event-v2.schema.json": {
21
+ "digest": "693430e636afc85bf4a79e3f7bf3fb87254cbd62f2e3f35c35d6f84e5e8e0be1",
22
+ "bytes": 6058
23
+ },
20
24
  "evidence-event.schema.json": {
21
25
  "digest": "b7e14522a5aba818cab191545f23d2323be0e4c80566e9d3e4da576103a00490",
22
26
  "bytes": 2390
@@ -34,8 +38,8 @@
34
38
  "bytes": 57111
35
39
  },
36
40
  "release-run.schema.json": {
37
- "digest": "4a041b8f6488e7d61b7eb271ebdbc2e043731df41c4437783ef63e77f664c192",
38
- "bytes": 19002
41
+ "digest": "26c44ddf1ed42f266a2ddee7f6132cee94c59593e37f24359c9c48f12612ec22",
42
+ "bytes": 21874
39
43
  },
40
44
  "test-intake.schema.json": {
41
45
  "digest": "654e91ce518cb2b8aca84947d0c3bb8c4262ab047d86081e7e93dc07a9c6c739",