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
@@ -49,10 +49,16 @@ import {
49
49
  normalizeGitTimestamp,
50
50
  sealFrozenSnapshot,
51
51
  } from '../snapshot/frozen.mjs';
52
- import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE } from '../core/errors.mjs';
52
+ import { ReleaseError, GATE_FAILED, CONFIG_INVALID, CONFIG_MISSING, FORBIDDEN_CONTENT_DETECTED, RELEASE_DOCS_STALE, DIRTY_SOURCE_INPUT } from '../core/errors.mjs';
53
+ import {
54
+ SOURCE_INPUT_ALGORITHM_VERSION,
55
+ computeSourceInputClosure,
56
+ checkSourceInputDirty,
57
+ verifySnapshotSourcesMatchClosure,
58
+ } from '../core/source-authority.mjs';
53
59
  import { acquireProjectLock } from '../artifacts/project-lock.mjs';
54
60
  import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
55
- import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
61
+ import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
56
62
  import { createProductionPrepareRunDir } from '../core/run.mjs';
57
63
  import { PLATFORMS } from '../platforms/registry.mjs';
58
64
  import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
@@ -924,13 +930,13 @@ async function buildProductionAssets(
924
930
  tarballDir: resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id),
925
931
  expectedSnapshotDigest: sealed.digest,
926
932
  });
927
- await verifyFrozenNpmTarballIdentity({
933
+ await verifyFrozenNpmTarballContract({
928
934
  package: npmDistribution.package,
929
935
  version,
930
936
  tarballPath: relative(root, npm.tarballPath),
931
937
  tarballSha256: npm.sha256,
932
938
  integrity: npm.integrity,
933
- }, root);
939
+ }, root, resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id));
934
940
  }
935
941
 
936
942
  assets.push({
@@ -1919,6 +1925,87 @@ export async function prepareRelease(options) {
1919
1925
  });
1920
1926
  }
1921
1927
 
1928
+ // --- Step 3c: Source authority content closure gate ---
1929
+ // After hooks complete, compute the deterministic source-input closure
1930
+ // and verify that closure inputs are clean (no staged/unstaged/untracked
1931
+ // changes). Production configs must declare sourceRepository.
1932
+ const sourceRepository = config.project?.sourceRepository ?? null;
1933
+ const configDefaultBranch = config.project?.defaultBranch ?? null;
1934
+
1935
+ let sourceAuthority = null;
1936
+ let sourceInputClosure = null;
1937
+ if (production) {
1938
+ if (!sourceRepository || typeof sourceRepository !== 'string') {
1939
+ throw new ReleaseError(
1940
+ CONFIG_MISSING,
1941
+ 'production prepare requires project.sourceRepository in configuration; source authority content gate needs a workspace source repository',
1942
+ { configPath },
1943
+ );
1944
+ }
1945
+
1946
+ await evidence.append({ phase: 'source-authority', status: 'started' });
1947
+
1948
+ // Compute source-input closure from resolved unit configs
1949
+ const unitConfigsForClosure = configUnits.map((unit, idx) => ({
1950
+ ...unit,
1951
+ version: { ...unit.version },
1952
+ }));
1953
+ sourceInputClosure = await computeSourceInputClosure({
1954
+ units: unitConfigsForClosure,
1955
+ root: realRoot,
1956
+ });
1957
+
1958
+ await evidence.append({
1959
+ phase: 'source-authority',
1960
+ step: 'closure-computed',
1961
+ entryCount: sourceInputClosure.entries.length,
1962
+ inputDigest: sourceInputClosure.digest,
1963
+ });
1964
+
1965
+ // Check only closure inputs for dirty (not whole workspace)
1966
+ const dirtyResult = await checkSourceInputDirty({
1967
+ closure: sourceInputClosure,
1968
+ root: realRoot,
1969
+ });
1970
+ if (dirtyResult.dirty) {
1971
+ await evidence.append({
1972
+ phase: 'source-authority',
1973
+ status: 'blocking',
1974
+ reason: 'DIRTY_SOURCE_INPUT',
1975
+ dirtyPaths: dirtyResult.dirtyPaths,
1976
+ });
1977
+ throw new ReleaseError(
1978
+ DIRTY_SOURCE_INPUT,
1979
+ `source-input closure files have uncommitted changes: ${dirtyResult.dirtyPaths.join(', ')}`,
1980
+ { dirtyPaths: dirtyResult.dirtyPaths },
1981
+ );
1982
+ }
1983
+
1984
+ await evidence.append({
1985
+ phase: 'source-authority',
1986
+ step: 'dirty-check',
1987
+ status: 'clean',
1988
+ });
1989
+
1990
+ // Build sourceAuthority binding for plan digest
1991
+ sourceAuthority = {
1992
+ sourceRepository,
1993
+ defaultBranch: configDefaultBranch,
1994
+ entries: sourceInputClosure.entries,
1995
+ inputDigest: sourceInputClosure.digest,
1996
+ algorithmVersion: SOURCE_INPUT_ALGORITHM_VERSION,
1997
+ };
1998
+
1999
+ await evidence.append({
2000
+ phase: 'source-authority',
2001
+ status: 'completed',
2002
+ sourceRepository,
2003
+ defaultBranch: configDefaultBranch,
2004
+ inputDigest: sourceInputClosure.digest,
2005
+ remoteObservation: offline ? 'unobserved-offline' : 'deferred-to-publish',
2006
+ });
2007
+ }
2008
+
1922
2009
  // --- Step 4: Capture Git baseline (AFTER hooks, so workspaceDigest
1923
2010
  // reflects any file changes introduced by hooks) ---
1924
2011
  await evidence.append({ phase: 'baseline', status: 'started' });
@@ -2183,6 +2270,75 @@ export async function prepareRelease(options) {
2183
2270
  gateCount: snapshotGateResults.length,
2184
2271
  });
2185
2272
 
2273
+ // Bind the remote source-authority proof to the exact bytes that entered
2274
+ // the frozen snapshots, not merely to an earlier read of the workspace.
2275
+ // Then re-read the complete closure and dirty state once more so version
2276
+ // sources and non-snapshot closure entries cannot drift during prepare.
2277
+ if (production) {
2278
+ const snapshotSourceResult = verifySnapshotSourcesMatchClosure({
2279
+ closure: sourceInputClosure,
2280
+ unitResults,
2281
+ });
2282
+ if (!snapshotSourceResult.passed) {
2283
+ throw new ReleaseError(
2284
+ DIRTY_SOURCE_INPUT,
2285
+ 'source inputs changed between closure calculation and frozen snapshot construction',
2286
+ {
2287
+ reason: 'SNAPSHOT_SOURCE_DRIFT',
2288
+ dirtyPaths: snapshotSourceResult.error.paths,
2289
+ },
2290
+ );
2291
+ }
2292
+
2293
+ const finalClosure = await computeSourceInputClosure({
2294
+ units: configUnits,
2295
+ root: realRoot,
2296
+ });
2297
+ if (finalClosure.digest !== sourceInputClosure.digest) {
2298
+ const initialByPath = new Map(
2299
+ sourceInputClosure.entries.map((entry) => [entry.path, entry]),
2300
+ );
2301
+ const finalByPath = new Map(
2302
+ finalClosure.entries.map((entry) => [entry.path, entry]),
2303
+ );
2304
+ const changedPaths = [...new Set([
2305
+ ...sourceInputClosure.entries.map((entry) => entry.path),
2306
+ ...finalClosure.entries.map((entry) => entry.path),
2307
+ ])].filter((path) => (
2308
+ JSON.stringify(initialByPath.get(path) ?? null)
2309
+ !== JSON.stringify(finalByPath.get(path) ?? null)
2310
+ )).sort();
2311
+ throw new ReleaseError(
2312
+ DIRTY_SOURCE_INPUT,
2313
+ 'source-input closure changed while preparing frozen snapshots',
2314
+ { reason: 'SOURCE_CLOSURE_DRIFT', dirtyPaths: changedPaths },
2315
+ );
2316
+ }
2317
+
2318
+ const finalDirtyResult = await checkSourceInputDirty({
2319
+ closure: finalClosure,
2320
+ root: realRoot,
2321
+ });
2322
+ if (finalDirtyResult.dirty) {
2323
+ throw new ReleaseError(
2324
+ DIRTY_SOURCE_INPUT,
2325
+ `source-input closure files have uncommitted changes after snapshot construction: ${finalDirtyResult.dirtyPaths.join(', ')}`,
2326
+ {
2327
+ reason: 'DIRTY_AFTER_SNAPSHOT',
2328
+ dirtyPaths: finalDirtyResult.dirtyPaths,
2329
+ },
2330
+ );
2331
+ }
2332
+
2333
+ await evidence.append({
2334
+ phase: 'source-authority',
2335
+ step: 'snapshot-binding',
2336
+ status: 'completed',
2337
+ inputDigest: sourceInputClosure.digest,
2338
+ snapshotSourceCount: snapshotSourceResult.observation.snapshotSourceCount,
2339
+ });
2340
+ }
2341
+
2186
2342
  // --- Step 6: Remote uniqueness (deferred to publish preflight) ---
2187
2343
  // Prepare only observes the previous public baseline (already done above).
2188
2344
  // Remote uniqueness checks (tag, GitHub Release, npm version) are deferred
@@ -2665,6 +2821,7 @@ export async function prepareRelease(options) {
2665
2821
  } : {}),
2666
2822
  units,
2667
2823
  externalActions,
2824
+ ...(sourceAuthority ? { sourceAuthority } : {}),
2668
2825
  createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
2669
2826
  };
2670
2827
 
@@ -59,10 +59,15 @@ import { appendRunState, createProductionRunDir, writeRunAtomic, resolveDefaultR
59
59
  import {
60
60
  ReleaseError,
61
61
  GATE_FAILED,
62
+ CONFIG_MISSING,
62
63
  BASELINE_CHANGED,
63
64
  PARTIAL_RELEASE,
64
65
  CONSUMER_VERIFICATION_DEFERRED,
65
66
  } from '../core/errors.mjs';
67
+ import {
68
+ verifyRemoteSourceContent,
69
+ createSourceAuthorityReceipt,
70
+ } from '../core/source-authority.mjs';
66
71
  import { assertTransition, PUBLISHING, PUBLISHED, PARTIAL } from '../core/state-machine.mjs';
67
72
  import { matchObservation } from '../adapters/contract.mjs';
68
73
  import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY, isPropagatingMissing } from '../core/observe-retry.mjs';
@@ -72,7 +77,7 @@ import {
72
77
  verifyFrozenGitRepository,
73
78
  verifyFrozenSnapshot,
74
79
  } from '../snapshot/frozen.mjs';
75
- import { verifyFrozenNpmTarballIdentity } from '../adapters/npm.mjs';
80
+ import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
76
81
 
77
82
  function assertInsideAssetRoot(assetRoot, candidate, label) {
78
83
  const rel = relative(assetRoot, candidate);
@@ -527,6 +532,13 @@ export async function publishRelease(options) {
527
532
  if (productionMode && !isProductionPlan) {
528
533
  throw new ReleaseError(GATE_FAILED, 'production publish requires a github-npm-v1 frozen plan');
529
534
  }
535
+ if (isProductionPlan && !plan.sourceAuthority) {
536
+ throw new ReleaseError(
537
+ CONFIG_MISSING,
538
+ 'production publish requires a frozen sourceAuthority binding; re-run prepare with project.sourceRepository configured',
539
+ { gate: 'source-authority' },
540
+ );
541
+ }
530
542
  const verifiedFrozenSnapshots = new Map();
531
543
  if (isProductionPlan) {
532
544
  if (!plan.production.assetRoot || plan.production.assetRoot === '.') {
@@ -595,7 +607,7 @@ export async function publishRelease(options) {
595
607
  if (!npmDistribution) {
596
608
  throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
597
609
  }
598
- await verifyFrozenNpmTarballIdentity({
610
+ await verifyFrozenNpmTarballContract({
599
611
  package: npmDistribution.package,
600
612
  version: unit.targetVersion,
601
613
  tarballPath: frozen.npm.tarballPath,
@@ -932,6 +944,65 @@ export async function publishRelease(options) {
932
944
  }
933
945
  }
934
946
 
947
+ // =======================================================================
948
+ // Safety Gate 11: Source authority content closure verification
949
+ // Verifies that the frozen source-input closure content exists in the
950
+ // workspace's remote default branch. Must pass before ANY adapter execute.
951
+ // Failure => zero adapter execute calls.
952
+ // =======================================================================
953
+ let sourceAuthorityReceipt = null;
954
+ if (plan.sourceAuthority) {
955
+ await evidence.append({ phase: 'safety-gate', gate: 'source-authority', status: 'started' });
956
+
957
+ const sa = plan.sourceAuthority;
958
+
959
+ // Verify the remote source tree against the entries frozen into the
960
+ // digest-bound plan. Publish must never rebuild authority from the
961
+ // mutable workspace or current config.
962
+ const frozenClosure = {
963
+ algorithmVersion: sa.algorithmVersion,
964
+ digest: sa.inputDigest,
965
+ entries: sa.entries,
966
+ };
967
+ const remoteResult = await verifyRemoteSourceContent({
968
+ sourceRepository: sa.sourceRepository,
969
+ defaultBranch: sa.defaultBranch,
970
+ closure: frozenClosure,
971
+ readRemoteFn: options.readRemoteSourceFn,
972
+ });
973
+
974
+ if (!remoteResult.passed) {
975
+ const errorCode = remoteResult.error?.code ?? GATE_FAILED;
976
+ await evidence.append({
977
+ phase: 'safety-gate',
978
+ gate: 'source-authority',
979
+ status: 'failed',
980
+ error: remoteResult.error,
981
+ });
982
+ throw new ReleaseError(
983
+ errorCode,
984
+ `source authority content gate failed: ${remoteResult.error?.message}`,
985
+ remoteResult.error,
986
+ );
987
+ }
988
+
989
+ // Create receipt for successful verification
990
+ sourceAuthorityReceipt = createSourceAuthorityReceipt({
991
+ plan,
992
+ result: 'CONSISTENT',
993
+ observation: remoteResult.observation,
994
+ clock: clockFn,
995
+ });
996
+
997
+ await evidence.append({
998
+ phase: 'safety-gate',
999
+ gate: 'source-authority',
1000
+ status: 'passed',
1001
+ sourceRepository: sa.sourceRepository,
1002
+ defaultBranch: sa.defaultBranch,
1003
+ });
1004
+ }
1005
+
935
1006
  // =======================================================================
936
1007
  // All safety gates passed -- prepare for execution
937
1008
  // =======================================================================
@@ -1069,6 +1140,7 @@ export async function publishRelease(options) {
1069
1140
  : {}),
1070
1141
  })),
1071
1142
  startedAt,
1143
+ ...(sourceAuthorityReceipt ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] } : {}),
1072
1144
  ...(finishedAt ? { finishedAt } : {}),
1073
1145
  });
1074
1146
  let stateSequence = 0;
@@ -59,8 +59,10 @@ import {
59
59
  CONSUMER_VERIFICATION_DEFERRED,
60
60
  } from '../core/errors.mjs';
61
61
  import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-machine.mjs';
62
+ import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
62
63
  import { matchObservation } from '../adapters/contract.mjs';
63
64
  import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
65
+ import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
64
66
 
65
67
  // ---------------------------------------------------------------------------
66
68
  // Constants
@@ -209,6 +211,44 @@ export async function reconcileRelease(options) {
209
211
 
210
212
  await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'passed' });
211
213
 
214
+ // =======================================================================
215
+ // Safety Gate 2c: Unconditional frozen npm entry-closure recheck
216
+ // This is plan-global and intentionally precedes source-run observation:
217
+ // a previously consistent npm checkpoint must not exempt a bad frozen
218
+ // tarball while another action is being reconciled.
219
+ // =======================================================================
220
+ for (const unit of plan.units ?? []) {
221
+ const frozen = unit.frozenSnapshot;
222
+ if (!frozen?.npm) continue;
223
+ const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
224
+ if (!npmDistribution) {
225
+ throw new ReleaseError(
226
+ GATE_FAILED,
227
+ `unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
228
+ { gate: 'npm-entry-closure', unitId: unit.id },
229
+ );
230
+ }
231
+ await evidence.append({
232
+ phase: 'safety-gate',
233
+ gate: 'npm-entry-closure',
234
+ unitId: unit.id,
235
+ status: 'started',
236
+ });
237
+ await verifyFrozenNpmTarballContract({
238
+ package: npmDistribution.package,
239
+ version: unit.targetVersion,
240
+ tarballPath: frozen.npm.tarballPath,
241
+ tarballSha256: frozen.npm.tarballSha256,
242
+ integrity: frozen.npm.integrity,
243
+ }, root);
244
+ await evidence.append({
245
+ phase: 'safety-gate',
246
+ gate: 'npm-entry-closure',
247
+ unitId: unit.id,
248
+ status: 'passed',
249
+ });
250
+ }
251
+
212
252
  // =======================================================================
213
253
  // Safety Gate 3: Load and validate source run
214
254
  // =======================================================================
@@ -233,6 +273,18 @@ export async function reconcileRelease(options) {
233
273
  `reconcile source command must be publish or reconcile, got "${sourceRun.command}"`,
234
274
  );
235
275
  }
276
+ let sourceAuthorityReceipt = null;
277
+ if (plan.sourceAuthority) {
278
+ const receiptResult = verifySourceAuthorityReceipt({ plan, run: sourceRun });
279
+ if (!receiptResult.passed) {
280
+ throw new ReleaseError(
281
+ GATE_FAILED,
282
+ `reconcile source run has no valid source-authority receipt: ${receiptResult.reason}`,
283
+ { gate: 'source-authority', sourceRunId: sourceRun.runId },
284
+ );
285
+ }
286
+ sourceAuthorityReceipt = receiptResult.receipt;
287
+ }
236
288
 
237
289
  let consumedApprovalPath = sourceRun.approvalPath;
238
290
  let consumedApprovalDigest = sourceRun.approvalDigest;
@@ -956,6 +1008,9 @@ export async function reconcileRelease(options) {
956
1008
  sourceRunDigest: sourceAuthorityDigest,
957
1009
  sourceRunPath,
958
1010
  status,
1011
+ ...(sourceAuthorityReceipt
1012
+ ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
1013
+ : {}),
959
1014
  checkpoints: planActions.map((action) => {
960
1015
  const value = actionResults.get(action.id);
961
1016
  const normalized = value === 'deferred' ? 'deferred'
@@ -1303,6 +1358,9 @@ export async function reconcileRelease(options) {
1303
1358
  sourceRunDigest,
1304
1359
  sourceRunPath,
1305
1360
  status: overallStatus,
1361
+ ...(sourceAuthorityReceipt
1362
+ ? { sourceAuthorityReceipts: [sourceAuthorityReceipt] }
1363
+ : {}),
1306
1364
  checkpoints: planActions.map((a) => {
1307
1365
  const status = actionResults.get(a.id) ?? 'pending';
1308
1366
  const normalized = status === 'deferred' ? 'deferred'