release-skill 0.2.5 → 0.2.7

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 (76) 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 +42 -0
  7. package/INSTALL.md +40 -16
  8. package/INSTALL.zh-CN.md +34 -12
  9. package/README.md +42 -15
  10. package/README.zh-CN.md +36 -15
  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 +2671 -1552
  14. package/adapters/claude/schemas/.render-manifest.json +6 -6
  15. package/adapters/claude/schemas/release-plan.schema.json +67 -0
  16. package/adapters/claude/schemas/release-project.schema.json +6 -0
  17. package/adapters/claude/schemas/release-run.schema.json +65 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
  19. package/adapters/claude/skills/release-setup/SKILL.md +10 -1
  20. package/adapters/claude/skills/release-verify/SKILL.md +4 -2
  21. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  22. package/adapters/codex/bin/release-skill.bundle.mjs +2671 -1552
  23. package/adapters/codex/schemas/.render-manifest.json +6 -6
  24. package/adapters/codex/schemas/release-plan.schema.json +67 -0
  25. package/adapters/codex/schemas/release-project.schema.json +6 -0
  26. package/adapters/codex/schemas/release-run.schema.json +65 -0
  27. package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
  28. package/adapters/codex/skills/release-setup/SKILL.md +10 -1
  29. package/adapters/codex/skills/release-verify/SKILL.md +4 -2
  30. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  31. package/adapters/kimi/bin/release-skill.bundle.mjs +2671 -1552
  32. package/adapters/kimi/schemas/.render-manifest.json +6 -6
  33. package/adapters/kimi/schemas/release-plan.schema.json +67 -0
  34. package/adapters/kimi/schemas/release-project.schema.json +6 -0
  35. package/adapters/kimi/schemas/release-run.schema.json +65 -0
  36. package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
  37. package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
  38. package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
  39. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  40. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2671 -1552
  41. package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
  42. package/adapters/workbuddy/schemas/release-plan.schema.json +67 -0
  43. package/adapters/workbuddy/schemas/release-project.schema.json +6 -0
  44. package/adapters/workbuddy/schemas/release-run.schema.json +65 -0
  45. package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
  46. package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
  47. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
  48. package/bin/release-skill.bundle.mjs +2671 -1552
  49. package/package.json +1 -1
  50. package/references/.render-manifest.json +4 -4
  51. package/references/02-project-config.md +28 -2
  52. package/references/05-evidence-and-errors.md +6 -0
  53. package/schemas/.render-manifest.json +6 -6
  54. package/schemas/release-plan.schema.json +67 -0
  55. package/schemas/release-project.schema.json +6 -0
  56. package/schemas/release-run.schema.json +65 -0
  57. package/skills/release-prepare/SKILL.md +5 -0
  58. package/skills/release-setup/SKILL.md +10 -1
  59. package/skills/release-verify/SKILL.md +4 -2
  60. package/skills-src/release-prepare/SKILL.md +5 -0
  61. package/skills-src/release-setup/SKILL.md +10 -1
  62. package/skills-src/release-verify/SKILL.md +4 -2
  63. package/src/adapters/npm.mjs +54 -2
  64. package/src/adapters/plugin-marketplace.mjs +4 -29
  65. package/src/commands/prepare.mjs +161 -4
  66. package/src/commands/publish.mjs +74 -2
  67. package/src/commands/reconcile.mjs +58 -0
  68. package/src/commands/setup.mjs +194 -0
  69. package/src/commands/verify.mjs +60 -1
  70. package/src/core/errors.mjs +12 -0
  71. package/src/core/source-authority.mjs +547 -0
  72. package/src/npm/npm-entry-closure.mjs +195 -0
  73. package/src/platforms/codebuddy.mjs +19 -11
  74. package/src/platforms/codex.mjs +18 -10
  75. package/src/platforms/kimi.mjs +26 -39
  76. package/src/snapshot/frozen.mjs +51 -19
@@ -24,6 +24,7 @@ import addFormats from 'ajv-formats';
24
24
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
25
25
  import { acquireProjectLock } from '../artifacts/project-lock.mjs';
26
26
  import { getPlatform, PLATFORMS } from '../platforms/registry.mjs';
27
+ import { checkNpmEntryClosure } from '../npm/npm-entry-closure.mjs';
27
28
  import {
28
29
  CONFIG_EXISTS,
29
30
  CONFIG_INVALID,
@@ -201,6 +202,8 @@ function summarizeLegacyReleaseConfig(value, path) {
201
202
  tagPrefix: optionalString(repo.tagPrefix),
202
203
  npmPackage: optionalString(repo.npmPackage),
203
204
  npmPackageDeclared: Object.hasOwn(repo, 'npmPackage'),
205
+ npmRequiredPathCandidates: stringList(repo.npmRequiredPackagePaths),
206
+ npmRequiredPathsDeclared: Object.hasOwn(repo, 'npmRequiredPackagePaths'),
204
207
  docsSource: optionalString(repo.docsSource),
205
208
  requiredPathCandidates: stringList(repo.requiredPackagePaths),
206
209
  snapshotCommands: Array.isArray(repo.snapshotCommands) ? repo.snapshotCommands : [],
@@ -223,6 +226,8 @@ function summarizeLegacyReleaseConfig(value, path) {
223
226
  npmPackageDeclared: plugins.some((plugin) => (
224
227
  plugin && typeof plugin === 'object' && !Array.isArray(plugin) && Object.hasOwn(plugin, 'npmPackage')
225
228
  )),
229
+ npmRequiredPathCandidates: stringList(value.npmRequiredPackagePaths),
230
+ npmRequiredPathsDeclared: Object.hasOwn(value, 'npmRequiredPackagePaths'),
226
231
  docsSource: null,
227
232
  requiredPathCandidates: stringList(value.requiredPaths),
228
233
  snapshotCommands: Array.isArray(value.snapshotCommands) ? value.snapshotCommands : [],
@@ -467,6 +472,111 @@ async function discoverUnitGit(unitAbsDir, parentRoot) {
467
472
  };
468
473
  }
469
474
 
475
+ async function pathIsGitIgnored(unitAbsDir, target) {
476
+ // --no-index also classifies generated paths that do not exist yet.
477
+ try {
478
+ await execFile('git', ['check-ignore', '--no-index', '--quiet', '--', target], {
479
+ cwd: unitAbsDir,
480
+ shell: false,
481
+ timeout: 5000,
482
+ });
483
+ return true;
484
+ } catch {
485
+ return false;
486
+ }
487
+ }
488
+
489
+ async function classifyNpmEntryCandidate(unitAbsDir, target, trackedFiles) {
490
+ const tracked = trackedFiles.includes(target);
491
+ const ignored = await pathIsGitIgnored(unitAbsDir, target);
492
+ let stat = null;
493
+ try {
494
+ stat = await lstat(join(unitAbsDir, target));
495
+ } catch (error) {
496
+ if (error.code !== 'ENOENT') throw error;
497
+ }
498
+ const exists = stat !== null;
499
+ const regular = stat?.isFile() === true && stat?.isSymbolicLink() !== true;
500
+ const state = exists && !regular
501
+ ? 'NON_REGULAR'
502
+ : exists && tracked
503
+ ? 'TRACKED_PRESENT'
504
+ : exists && ignored
505
+ ? 'IGNORED_PRESENT'
506
+ : exists
507
+ ? 'UNTRACKED_PRESENT'
508
+ : ignored
509
+ ? 'IGNORED_MISSING'
510
+ : 'MISSING';
511
+ return { path: target, state, exists, tracked, ignored };
512
+ }
513
+
514
+ async function discoverNpmEntryCandidates(root, pkg, matchingLegacyUnits, unitGitEntry) {
515
+ const emptyIndex = new Map();
516
+ const semantic = checkNpmEntryClosure(pkg.entryManifest, emptyIndex);
517
+ const byPath = new Map();
518
+ const add = (target, source) => {
519
+ const current = byPath.get(target) ?? { path: target, sources: new Set() };
520
+ current.sources.add(source);
521
+ byPath.set(target, current);
522
+ };
523
+ for (const entry of semantic.entries) add(entry.target, `package.json:${entry.field}`);
524
+
525
+ const diagnostics = [
526
+ ...semantic.errors
527
+ .filter((error) => error.reason !== 'entry_missing')
528
+ .map((error) => ({ code: 'NPM_ENTRY_DECLARATION_INVALID', ...error })),
529
+ ...semantic.diagnostics.map((diagnostic) => ({
530
+ code: 'NPM_ENTRY_WILDCARD_REQUIRES_REVIEW',
531
+ ...diagnostic,
532
+ })),
533
+ ];
534
+ const declaredLegacyPaths = new Set();
535
+ const legacyCoverageDeclared = matchingLegacyUnits.some((unit) => unit.npmRequiredPathsDeclared);
536
+ for (const legacy of matchingLegacyUnits) {
537
+ for (const target of legacy.npmRequiredPathCandidates) {
538
+ const checked = checkNpmEntryClosure({ main: target }, emptyIndex);
539
+ const normalized = checked.entries[0]?.target;
540
+ if (!normalized) {
541
+ diagnostics.push({
542
+ code: 'LEGACY_NPM_REQUIRED_PATH_INVALID',
543
+ path: target,
544
+ reason: checked.errors[0]?.reason ?? 'invalid_path',
545
+ });
546
+ continue;
547
+ }
548
+ declaredLegacyPaths.add(normalized);
549
+ add(normalized, 'public-release.json:npmRequiredPackagePaths');
550
+ }
551
+ }
552
+ if (legacyCoverageDeclared) {
553
+ for (const entry of semantic.entries) {
554
+ if (!declaredLegacyPaths.has(entry.target)) {
555
+ diagnostics.push({
556
+ code: 'LEGACY_NPM_ENTRY_COVERAGE_MISSING',
557
+ path: entry.target,
558
+ field: entry.field,
559
+ });
560
+ }
561
+ }
562
+ }
563
+
564
+ const unitAbsDir = resolve(root, pkg.directory);
565
+ const candidates = [];
566
+ for (const item of [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path))) {
567
+ candidates.push({
568
+ ...await classifyNpmEntryCandidate(
569
+ unitAbsDir,
570
+ item.path,
571
+ unitGitEntry?.trackedFiles ?? [],
572
+ ),
573
+ sources: [...item.sources].sort(),
574
+ });
575
+ }
576
+ diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
577
+ return { candidates, diagnostics };
578
+ }
579
+
470
580
  async function discoverFacts(root) {
471
581
  const files = await walkDiscoveryFiles(root);
472
582
  const packageFiles = files.filter((path) => (
@@ -494,6 +604,11 @@ async function discoverFacts(root) {
494
604
  private: pkg.private === true,
495
605
  repository: parseGithubRepo(pkg.repository),
496
606
  publishRegistry: typeof pkg.publishConfig?.registry === 'string' ? pkg.publishConfig.registry : null,
607
+ entryManifest: Object.fromEntries(
608
+ ['bin', 'main', 'module', 'types', 'typings', 'exports']
609
+ .filter((field) => Object.hasOwn(pkg, field))
610
+ .map((field) => [field, pkg[field]]),
611
+ ),
497
612
  files: Array.isArray(pkg.files) ? pkg.files.filter((item) => typeof item === 'string').sort() : [],
498
613
  scripts: Object.fromEntries(Object.entries(pkg.scripts ?? {})
499
614
  .filter(([, value]) => typeof value === 'string')
@@ -557,6 +672,22 @@ async function discoverFacts(root) {
557
672
  for (const pkg of packages) {
558
673
  const unitAbsDir = resolve(root, pkg.directory);
559
674
  unitGit[pkg.directory] = await discoverUnitGit(unitAbsDir, root);
675
+ const inferredId = safeUnitId(pkg, pkg.directory);
676
+ const matchingLegacyUnits = legacyReleaseConfigs
677
+ .flatMap((config) => config.releaseUnits)
678
+ .filter((unit) => (
679
+ unit.id === inferredId ||
680
+ unit.source === pkg.directory ||
681
+ unit.source === dirname(pkg.path)
682
+ ));
683
+ const npmDiscovery = await discoverNpmEntryCandidates(
684
+ root,
685
+ pkg,
686
+ matchingLegacyUnits,
687
+ unitGit[pkg.directory],
688
+ );
689
+ pkg.npmEntryCandidates = npmDiscovery.candidates;
690
+ pkg.npmEntryDiagnostics = npmDiscovery.diagnostics;
560
691
  }
561
692
 
562
693
  return { git, packages, manifests, skills, legacyReleaseConfigs, fileDigests, unitGit };
@@ -871,6 +1002,8 @@ function buildCandidates(facts) {
871
1002
  publicFileMappingCandidates: mappingResult.mappings,
872
1003
  publicFileMappingConflicts: mappingResult.conflicts,
873
1004
  requiredPublicFileCandidates,
1005
+ npmEntryCandidates: pkg.npmEntryCandidates ?? [],
1006
+ npmEntryDiagnostics: pkg.npmEntryDiagnostics ?? [],
874
1007
  entrySkillCandidates: entrySkillCandidatesForUnit(facts, pkg, id),
875
1008
  ...(hasAuthorityConflict ? {
876
1009
  authorityConflict: {
@@ -976,6 +1109,21 @@ function buildRecommendedProposal(facts, candidates) {
976
1109
  const legacyOwner = facts.legacyReleaseConfigs
977
1110
  .map((c) => c.owner)
978
1111
  .find(Boolean);
1112
+ const sourceRepositoryCandidates = [...new Set(
1113
+ facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
1114
+ )].sort();
1115
+ if (sourceRepositoryCandidates.length !== 1) {
1116
+ return {
1117
+ answers: null,
1118
+ conflicts: [{
1119
+ code: sourceRepositoryCandidates.length === 0
1120
+ ? 'SOURCE_REPOSITORY_MISSING'
1121
+ : 'SOURCE_REPOSITORY_AMBIGUOUS',
1122
+ candidates: sourceRepositoryCandidates,
1123
+ }],
1124
+ assumptions,
1125
+ };
1126
+ }
979
1127
 
980
1128
  const units = [];
981
1129
  for (const unit of candidates.units) {
@@ -1012,6 +1160,24 @@ function buildRecommendedProposal(facts, candidates) {
1012
1160
  assumptions,
1013
1161
  };
1014
1162
  }
1163
+ if (
1164
+ unit.distributionCandidates.includes('npm') &&
1165
+ (
1166
+ (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
1167
+ (unit.npmEntryDiagnostics ?? []).length > 0
1168
+ )
1169
+ ) {
1170
+ return {
1171
+ answers: null,
1172
+ conflicts: [{
1173
+ code: 'NPM_ENTRY_REVIEW_REQUIRED',
1174
+ unit: unit.id,
1175
+ candidates: unit.npmEntryCandidates,
1176
+ diagnostics: unit.npmEntryDiagnostics,
1177
+ }],
1178
+ assumptions,
1179
+ };
1180
+ }
1015
1181
 
1016
1182
  // Infer npm publisher: prefer legacy owner, then parse from repo slug
1017
1183
  const repoOwner = publicRepo.includes('/') ? publicRepo.split('/')[0] : null;
@@ -1156,6 +1322,7 @@ function buildRecommendedProposal(facts, candidates) {
1156
1322
  project: {
1157
1323
  name: facts.packages[0]?.name ?? 'project',
1158
1324
  defaultBranch: facts.legacyReleaseConfigs[0]?.defaultBranch ?? facts.git.branch ?? 'main',
1325
+ sourceRepository: sourceRepositoryCandidates[0],
1159
1326
  },
1160
1327
  releaseUnits: units,
1161
1328
  ...(selectedGateIds.length > 0 ? {
@@ -1229,6 +1396,15 @@ function buildDecisionsRequired(candidates, localOnly) {
1229
1396
  id: `unit:${unit.id}:distributions-and-files`,
1230
1397
  description: `逐项确认渠道 ${JSON.stringify(unit.distributionCandidates)}、公开文件边界和 requiredPublicFiles;候选不是授权。`,
1231
1398
  });
1399
+ if (
1400
+ (unit.npmEntryCandidates ?? []).length > 0 ||
1401
+ (unit.npmEntryDiagnostics ?? []).length > 0
1402
+ ) {
1403
+ decisions.push({
1404
+ id: `unit:${unit.id}:npm-entry-closure`,
1405
+ description: '审阅 package.json 入口及旧 npmRequiredPackagePaths 的 tracked/untracked/ignored/missing 状态;setup 只报告候选,不自动写入 publicFiles 或 requiredPublicFiles。',
1406
+ });
1407
+ }
1232
1408
  }
1233
1409
  decisions.push({
1234
1410
  id: 'verification-gates',
@@ -1255,6 +1431,8 @@ function buildCompactSummary(report) {
1255
1431
  distributionCandidates: [...unit.distributionCandidates].sort(),
1256
1432
  publicFileMappingCount: (unit.publicFileMappingCandidates ?? []).length,
1257
1433
  requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length,
1434
+ npmEntryCandidates: unit.npmEntryCandidates ?? [],
1435
+ npmEntryDiagnostics: unit.npmEntryDiagnostics ?? [],
1258
1436
  })),
1259
1437
  gateCandidates: (report.gateCandidates ?? []).map((gate) => ({
1260
1438
  id: gate.id,
@@ -1503,6 +1681,12 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1503
1681
  ...(unconfiguredGateCandidateIds.length > 0
1504
1682
  ? ['存在未配置的验证候选;逐项审阅副作用后决定是否人工注册。']
1505
1683
  : []),
1684
+ ...(candidates.units.some((unit) => (
1685
+ (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== 'TRACKED_PRESENT') ||
1686
+ (unit.npmEntryDiagnostics ?? []).length > 0
1687
+ ))
1688
+ ? ['npm 入口候选存在未跟踪、被忽略、缺失、非普通文件或声明覆盖疑点;人工增量修复配置或构建流程,setup 不自动改写。']
1689
+ : []),
1506
1690
  ],
1507
1691
  },
1508
1692
  recommendedGateIds: [],
@@ -1517,6 +1701,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1517
1701
 
1518
1702
  const facts = await discoverFacts(rootReal);
1519
1703
  const candidates = buildCandidates(facts);
1704
+ const sourceRepositoryCandidates = [...new Set(
1705
+ facts.git.remotes.map((remote) => remote.repo).filter(Boolean),
1706
+ )].sort();
1520
1707
  let answers = null;
1521
1708
  if (answersPath) {
1522
1709
  const resolvedAnswers = isAbsolute(answersPath) ? answersPath : resolve(rootReal, answersPath);
@@ -1527,6 +1714,7 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1527
1714
  const digestAuthority = {
1528
1715
  setupVersion: 1,
1529
1716
  facts,
1717
+ sourceRepositoryCandidates,
1530
1718
  releaseUnitCandidates: candidates.units,
1531
1719
  gateCandidates: candidates.gates,
1532
1720
  selectedGateIds,
@@ -1599,6 +1787,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1599
1787
  const lockedAuthority = {
1600
1788
  setupVersion: 1,
1601
1789
  facts: lockedFacts,
1790
+ sourceRepositoryCandidates: [...new Set(
1791
+ lockedFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
1792
+ )].sort(),
1602
1793
  releaseUnitCandidates: lockedCandidates.units,
1603
1794
  gateCandidates: lockedCandidates.gates,
1604
1795
  selectedGateIds: lockedAnswers.selectedGateIds,
@@ -1622,6 +1813,9 @@ export async function setupProject({ root, answersPath, write = false, confirmSe
1622
1813
  const finalAuthority = {
1623
1814
  setupVersion: 1,
1624
1815
  facts: finalFacts,
1816
+ sourceRepositoryCandidates: [...new Set(
1817
+ finalFacts.git.remotes.map((remote) => remote.repo).filter(Boolean),
1818
+ )].sort(),
1625
1819
  releaseUnitCandidates: finalCandidates.units,
1626
1820
  gateCandidates: finalCandidates.gates,
1627
1821
  selectedGateIds: finalAnswers.selectedGateIds,
@@ -41,8 +41,10 @@ import {
41
41
  import {
42
42
  ReleaseError,
43
43
  GATE_FAILED,
44
+ CONFIG_MISSING,
44
45
  POST_PUBLISH_VERIFY_FAILED,
45
46
  } from '../core/errors.mjs';
47
+ import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
46
48
  import { assertTransition, PUBLISHED, VERIFIED } from '../core/state-machine.mjs';
47
49
  import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
48
50
  import {
@@ -61,6 +63,10 @@ import {
61
63
  shouldSkipVerification,
62
64
  INSTALLATION_CONTRACT_ALGORITHM_VERSION,
63
65
  } from '../core/installation-contract.mjs';
66
+ import {
67
+ buildDirectoryFileIndex,
68
+ checkNpmEntryClosure,
69
+ } from '../npm/npm-entry-closure.mjs';
64
70
 
65
71
  // ---------------------------------------------------------------------------
66
72
  // Constants
@@ -292,6 +298,30 @@ export async function runSmokeTest(plan, root, options = {}) {
292
298
  }
293
299
 
294
300
  const pkgRoot = join(installDir, 'node_modules', pkgName);
301
+
302
+ // Entry closure check: verify all declared entry points (bin, main,
303
+ // module, types, typings, exports) exist as regular files in the
304
+ // installed package. This catches incident-class tarballs where
305
+ // name/version are correct but distribution files are missing.
306
+ {
307
+ const dirIndex = await buildDirectoryFileIndex(pkgRoot);
308
+ const closureResult = checkNpmEntryClosure(installedPkg, dirIndex);
309
+ if (closureResult.errors.length > 0) {
310
+ return {
311
+ passed: false,
312
+ details: {
313
+ gate: 'npm-entry-closure',
314
+ error: `npm entry closure check failed for ${packageAtVersion}: ${closureResult.errors.map((e) => e.message).join('; ')}`,
315
+ packageAtVersion,
316
+ unitId,
317
+ entries: closureResult.entries,
318
+ errors: closureResult.errors,
319
+ diagnostics: closureResult.diagnostics,
320
+ },
321
+ };
322
+ }
323
+ }
324
+
295
325
  if (plan.skillResourceClosure) {
296
326
  const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts
297
327
  .find((item) => item.unitId === unitId);
@@ -654,6 +684,13 @@ export async function verifyRelease(options) {
654
684
  );
655
685
  }
656
686
  validatePlan(plan);
687
+ if (plan.production?.mode === 'github-npm-v1' && !plan.sourceAuthority) {
688
+ throw new ReleaseError(
689
+ CONFIG_MISSING,
690
+ 'production verify requires a frozen sourceAuthority binding; the release must be re-prepared before publish',
691
+ { gate: 'source-authority' },
692
+ );
693
+ }
657
694
 
658
695
  // --- Set up directories ---
659
696
  const runId = `verify-${Date.now()}`;
@@ -777,6 +814,29 @@ export async function verifyRelease(options) {
777
814
  // Validate checkpoint mapping
778
815
  validateRunCheckpointMapping(sourceRun, plan.externalActions ?? []);
779
816
 
817
+ // --- Source authority receipt verification ---
818
+ // If the plan declares sourceAuthority, the source publish run must
819
+ // contain a matching CONSISTENT receipt bound to this planDigest.
820
+ if (plan.sourceAuthority) {
821
+ await evidence.append({ phase: 'source-authority-receipt', status: 'started' });
822
+
823
+ const receiptResult = verifySourceAuthorityReceipt({ plan, run: sourceRun });
824
+ if (!receiptResult.passed) {
825
+ await evidence.append({
826
+ phase: 'source-authority-receipt',
827
+ status: 'failed',
828
+ reason: receiptResult.reason,
829
+ });
830
+ throw new ReleaseError(
831
+ GATE_FAILED,
832
+ `source authority receipt verification failed: ${receiptResult.reason}`,
833
+ { reason: receiptResult.reason },
834
+ );
835
+ }
836
+
837
+ await evidence.append({ phase: 'source-authority-receipt', status: 'passed' });
838
+ }
839
+
780
840
  // All checkpoints must be succeeded or skipped (no failed/pending),
781
841
  // except marketplace install checkpoints which are re-verified by verify
782
842
  // itself via consumer verification (human attestation or automatic).
@@ -1193,7 +1253,6 @@ export async function verifyRelease(options) {
1193
1253
  }
1194
1254
  : {
1195
1255
  HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1196
- KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
1197
1256
  },
1198
1257
  }));
1199
1258
  } else {
@@ -88,6 +88,12 @@ const EXIT_CODE_MAP = Object.freeze({
88
88
  RELEASE_DOCS_REFRESH_STALE: 45,
89
89
  RELEASE_DOCS_STALE: 46,
90
90
  CONSUMER_VERIFICATION_DEFERRED: 47,
91
+ CONFIG_MISSING: 48,
92
+ REMOTE_UNAVAILABLE: 49,
93
+ REF_MISSING: 50,
94
+ NOT_DEFAULT: 51,
95
+ CONTENT_MISMATCH: 52,
96
+ DIRTY_SOURCE_INPUT: 53,
91
97
  });
92
98
 
93
99
  // ---- Error code constants ----
@@ -130,6 +136,12 @@ export const RELEASE_DOCS_CONFLICT = 'RELEASE_DOCS_CONFLICT';
130
136
  export const RELEASE_DOCS_REFRESH_STALE = 'RELEASE_DOCS_REFRESH_STALE';
131
137
  export const RELEASE_DOCS_STALE = 'RELEASE_DOCS_STALE';
132
138
  export const CONSUMER_VERIFICATION_DEFERRED = 'CONSUMER_VERIFICATION_DEFERRED';
139
+ export const CONFIG_MISSING = 'CONFIG_MISSING';
140
+ export const REMOTE_UNAVAILABLE = 'REMOTE_UNAVAILABLE';
141
+ export const REF_MISSING = 'REF_MISSING';
142
+ export const NOT_DEFAULT = 'NOT_DEFAULT';
143
+ export const CONTENT_MISMATCH = 'CONTENT_MISMATCH';
144
+ export const DIRTY_SOURCE_INPUT = 'DIRTY_SOURCE_INPUT';
133
145
 
134
146
  /**
135
147
  * Typed error for release-skill operations.