release-skill 0.1.3 → 0.1.5

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 (97) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -3
  4. package/CHANGELOG.md +56 -0
  5. package/INSTALL.md +69 -32
  6. package/INSTALL.zh-CN.md +64 -26
  7. package/README.md +111 -37
  8. package/README.zh-CN.md +95 -28
  9. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  10. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  11. package/adapters/claude/bin/release-skill.bundle.mjs +79371 -0
  12. package/adapters/claude/bin/release-skill.mjs +34 -0
  13. package/adapters/claude/native/safe-write/binding.gyp +40 -0
  14. package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  15. package/adapters/claude/native/safe-write/prebuilds.json +24 -0
  16. package/adapters/claude/native/safe-write/src/safe_write.cc +2032 -0
  17. package/adapters/claude/schemas/.render-manifest.json +37 -0
  18. package/adapters/claude/schemas/approval-record.schema.json +115 -0
  19. package/adapters/claude/schemas/artifact-lock.schema.json +111 -0
  20. package/adapters/claude/schemas/artifact-plan.schema.json +52 -0
  21. package/adapters/claude/schemas/artifact-policy.schema.json +76 -0
  22. package/adapters/claude/schemas/evidence-event.schema.json +89 -0
  23. package/adapters/claude/schemas/release-plan.schema.json +860 -0
  24. package/adapters/claude/schemas/release-project.schema.json +759 -0
  25. package/adapters/claude/schemas/release-run.schema.json +342 -0
  26. package/adapters/claude/skills/release-assess/SKILL.md +5 -6
  27. package/adapters/claude/skills/release-help/SKILL.md +11 -18
  28. package/adapters/claude/skills/release-prepare/SKILL.md +6 -6
  29. package/adapters/claude/skills/release-publish/SKILL.md +1 -4
  30. package/adapters/claude/skills/release-reconcile/SKILL.md +5 -5
  31. package/adapters/claude/skills/release-setup/SKILL.md +64 -80
  32. package/adapters/claude/skills/release-verify/SKILL.md +4 -7
  33. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  34. package/adapters/codex/bin/release-skill.bundle.mjs +79371 -0
  35. package/adapters/codex/bin/release-skill.mjs +34 -0
  36. package/adapters/codex/native/safe-write/binding.gyp +40 -0
  37. package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  38. package/adapters/codex/native/safe-write/prebuilds.json +24 -0
  39. package/adapters/codex/native/safe-write/src/safe_write.cc +2032 -0
  40. package/adapters/codex/schemas/.render-manifest.json +37 -0
  41. package/adapters/codex/schemas/approval-record.schema.json +115 -0
  42. package/adapters/codex/schemas/artifact-lock.schema.json +111 -0
  43. package/adapters/codex/schemas/artifact-plan.schema.json +52 -0
  44. package/adapters/codex/schemas/artifact-policy.schema.json +76 -0
  45. package/adapters/codex/schemas/evidence-event.schema.json +89 -0
  46. package/adapters/codex/schemas/release-plan.schema.json +860 -0
  47. package/adapters/codex/schemas/release-project.schema.json +759 -0
  48. package/adapters/codex/schemas/release-run.schema.json +342 -0
  49. package/adapters/codex/skills/release-assess/SKILL.md +12 -6
  50. package/adapters/codex/skills/release-help/SKILL.md +18 -18
  51. package/adapters/codex/skills/release-prepare/SKILL.md +13 -6
  52. package/adapters/codex/skills/release-publish/SKILL.md +8 -4
  53. package/adapters/codex/skills/release-reconcile/SKILL.md +12 -5
  54. package/adapters/codex/skills/release-setup/SKILL.md +71 -80
  55. package/adapters/codex/skills/release-verify/SKILL.md +11 -7
  56. package/bin/release-skill-cli.mjs +807 -0
  57. package/bin/release-skill.bundle.mjs +79371 -0
  58. package/bin/release-skill.mjs +23 -788
  59. package/package.json +6 -2
  60. package/references/02-project-config.md +2 -2
  61. package/schemas/release-plan.schema.json +7 -1
  62. package/schemas/release-project.schema.json +31 -0
  63. package/scripts/build-bundle.mjs +133 -0
  64. package/skills/release-assess/SKILL.md +5 -6
  65. package/skills/release-help/SKILL.md +11 -18
  66. package/skills/release-prepare/SKILL.md +6 -6
  67. package/skills/release-publish/SKILL.md +1 -4
  68. package/skills/release-reconcile/SKILL.md +5 -5
  69. package/skills/release-setup/SKILL.md +64 -80
  70. package/skills/release-verify/SKILL.md +4 -7
  71. package/skills-src/release-assess/SKILL.md +5 -6
  72. package/skills-src/release-help/SKILL.md +11 -18
  73. package/skills-src/release-prepare/SKILL.md +6 -6
  74. package/skills-src/release-publish/SKILL.md +1 -4
  75. package/skills-src/release-reconcile/SKILL.md +5 -5
  76. package/skills-src/release-setup/SKILL.md +64 -80
  77. package/skills-src/release-verify/SKILL.md +4 -7
  78. package/src/adapters/npm.mjs +5 -13
  79. package/src/adapters/plugin-marketplace.mjs +137 -34
  80. package/src/artifacts/policy.mjs +4 -7
  81. package/src/artifacts/safe-fs-backend-internal.mjs +69 -21
  82. package/src/commands/prepare.mjs +30 -1
  83. package/src/commands/publish.mjs +1 -0
  84. package/src/commands/reconcile.mjs +4 -1
  85. package/src/commands/setup.mjs +667 -28
  86. package/src/commands/verify.mjs +4 -1
  87. package/src/core/approval.mjs +4 -6
  88. package/src/core/config.mjs +8 -8
  89. package/src/core/pkg-root.mjs +22 -0
  90. package/src/core/plan.mjs +68 -5
  91. package/src/core/run.mjs +4 -4
  92. package/src/core/trusted-resource.mjs +96 -0
  93. package/src/docs/version-gate.mjs +164 -0
  94. package/src/producers/build-adapters.mjs +512 -55
  95. package/src/snapshot/frozen.mjs +133 -3
  96. package/src/snapshot/public-map.mjs +7 -4
  97. package/src/snapshot/scan.mjs +2 -1
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  import { readFile } from 'node:fs/promises';
13
- import { readFileSync } from 'node:fs';
14
13
  import { join } from 'node:path';
15
14
  import { createHash } from 'node:crypto';
16
15
  import YAML from 'yaml';
@@ -22,21 +21,19 @@ import {
22
21
  PATH_UNSAFE,
23
22
  } from '../core/errors.mjs';
24
23
  import { canonicalArtifactPath } from './path-key.mjs';
24
+ import { readTrustedPackageResourceSync } from '../core/trusted-resource.mjs';
25
25
 
26
26
  // ---------------------------------------------------------------------------
27
27
  // Schema validation
28
28
  // ---------------------------------------------------------------------------
29
29
 
30
- const POLICY_SCHEMA_PATH = new URL(
31
- '../../schemas/artifact-policy.schema.json',
32
- import.meta.url,
33
- );
34
-
35
30
  const ajv = new Ajv({ allErrors: true, strict: false });
36
31
  addFormats(ajv);
37
32
 
38
33
  // Compile schema once at module load (synchronous).
39
- const _validate = ajv.compile(JSON.parse(readFileSync(POLICY_SCHEMA_PATH, 'utf8')));
34
+ const _validate = ajv.compile(JSON.parse(readTrustedPackageResourceSync(
35
+ 'schemas/artifact-policy.schema.json',
36
+ ).toString('utf8')));
40
37
 
41
38
  /**
42
39
  * Validate a policy object against the artifact-policy JSON Schema.
@@ -10,10 +10,10 @@
10
10
 
11
11
  import { createRequire } from 'node:module';
12
12
  import { dirname, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
13
14
  import { realpath } from 'node:fs/promises';
14
15
  import { readFileSync, realpathSync, lstatSync } from 'node:fs';
15
16
  import { createHash } from 'node:crypto';
16
- import { fileURLToPath } from 'node:url';
17
17
 
18
18
  import {
19
19
  ReleaseError,
@@ -25,17 +25,71 @@ import {
25
25
  // Constants
26
26
  // ---------------------------------------------------------------------------
27
27
 
28
- const __filename = fileURLToPath(import.meta.url);
29
- const __dirname = dirname(__filename);
30
-
31
- const DEFAULT_ADDON_REL = '../../native/safe-write/build/Release/safe_write.node';
32
- const PREBUILDS_REL = '../../native/safe-write/prebuilds.json';
28
+ // Keep this internal loader self-contained. Besides making the native trust
29
+ // boundary auditable, this lets isolated consumers test/copy it together with
30
+ // errors.mjs only. In the bundle the banner supplies __bundlePkgRoot; in source
31
+ // mode the module's own URL deterministically identifies the package root.
32
+ const PKG_ROOT = typeof __bundlePkgRoot !== 'undefined'
33
+ ? __bundlePkgRoot
34
+ : resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
35
+ const NATIVE_BASE = resolve(PKG_ROOT, 'native/safe-write');
36
+ const PREBUILDS_PATH = resolve(NATIVE_BASE, 'prebuilds.json');
37
+ const PREBUILDS_RESOURCE = 'native/safe-write/prebuilds.json';
38
+ const DEFAULT_ADDON_PATH = resolve(NATIVE_BASE, 'build/Release/safe_write.node');
33
39
 
34
40
  const EXPECTED_EXPORTS = Object.freeze([
35
41
  'openRoot', 'openDir', 'readEntry', 'readFile',
36
42
  'createTemp', 'rename', 'abortTemp', 'mkdir', 'rmdir', 'unlink', 'chmod', 'fsync', 'close',
37
43
  ]);
38
44
 
45
+ function failPrebuildManifest(reason, message = 'prebuilds manifest not readable') {
46
+ throw new ReleaseError(
47
+ SAFE_WRITE_UNAVAILABLE,
48
+ message,
49
+ { resource: PREBUILDS_RESOURCE, reason },
50
+ );
51
+ }
52
+
53
+ function readTrustedPrebuildManifest() {
54
+ let lexicalStat;
55
+ try {
56
+ lexicalStat = lstatSync(PREBUILDS_PATH, { bigint: true });
57
+ } catch {
58
+ failPrebuildManifest('MISSING');
59
+ }
60
+
61
+ if (lexicalStat.isSymbolicLink()) {
62
+ failPrebuildManifest('SYMLINK', 'prebuilds manifest must not be a symlink');
63
+ }
64
+ if (!lexicalStat.isFile()) {
65
+ failPrebuildManifest('NOT_REGULAR_FILE', 'prebuilds manifest is not a regular file');
66
+ }
67
+ if (lexicalStat.nlink !== 1n) {
68
+ failPrebuildManifest('UNEXPECTED_HARDLINK_COUNT', 'prebuilds manifest has invalid nlink count');
69
+ }
70
+
71
+ let physicalBase;
72
+ let physicalManifest;
73
+ try {
74
+ physicalBase = realpathSync(NATIVE_BASE);
75
+ physicalManifest = realpathSync(PREBUILDS_PATH);
76
+ } catch {
77
+ failPrebuildManifest('REALPATH_FAILED');
78
+ }
79
+ if (!physicalManifest.startsWith(physicalBase + '/') && physicalManifest !== physicalBase) {
80
+ failPrebuildManifest(
81
+ 'PHYSICAL_PATH_ESCAPE',
82
+ 'prebuilds manifest physical path escapes base directory',
83
+ );
84
+ }
85
+
86
+ try {
87
+ return JSON.parse(readFileSync(physicalManifest, 'utf8'));
88
+ } catch {
89
+ failPrebuildManifest('READ_FAILED');
90
+ }
91
+ }
92
+
39
93
  // ---------------------------------------------------------------------------
40
94
  // Error sanitization
41
95
  // ---------------------------------------------------------------------------
@@ -58,7 +112,7 @@ function sanitizeError(err) {
58
112
  * Returns the verified physical path on success, or null on failure.
59
113
  */
60
114
  function isWithinPackageBuildDir(addonPath) {
61
- const buildDir = resolve(__dirname, '../../native/safe-write/build/');
115
+ const buildDir = resolve(NATIVE_BASE, 'build/');
62
116
  const resolved = resolve(addonPath);
63
117
  if (!resolved.startsWith(buildDir + '/') && resolved !== buildDir) {
64
118
  return null;
@@ -106,8 +160,8 @@ function validateManifestEntry(entry, key) {
106
160
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
107
161
  }
108
162
 
109
- const resolved = resolve(__dirname, '../../native/safe-write', entry.path);
110
- const baseDir = resolve(__dirname, '../../native/safe-write/');
163
+ const resolved = resolve(NATIVE_BASE, entry.path);
164
+ const baseDir = NATIVE_BASE;
111
165
  if (!resolved.startsWith(baseDir + '/') && resolved !== baseDir) {
112
166
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
113
167
  }
@@ -126,7 +180,8 @@ function validateManifestEntry(entry, key) {
126
180
 
127
181
  function requireAddon(addonPath) {
128
182
  try {
129
- const require = createRequire(import.meta.url);
183
+ // Use PKG_ROOT-relative resolution so bundled mode can find the addon.
184
+ const require = createRequire(resolve(PKG_ROOT, 'package.json'));
130
185
  return require(addonPath);
131
186
  } catch (err) {
132
187
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
@@ -149,14 +204,7 @@ function requireAddon(addonPath) {
149
204
 
150
205
  export function readStableAddon(options = {}) {
151
206
  const hooks = options._hooks || {};
152
-
153
- const prebuildsPath = resolve(__dirname, PREBUILDS_REL);
154
- let manifest;
155
- try {
156
- manifest = JSON.parse(readFileSync(prebuildsPath, 'utf8'));
157
- } catch {
158
- throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest not readable');
159
- }
207
+ const manifest = readTrustedPrebuildManifest();
160
208
 
161
209
  if (!manifest.prebuilds || typeof manifest.prebuilds !== 'object') {
162
210
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest has invalid structure');
@@ -174,7 +222,7 @@ export function readStableAddon(options = {}) {
174
222
  validateManifestEntry(entry, key);
175
223
 
176
224
  // Resolve lexical candidate path
177
- const addonFile = resolve(__dirname, '../../native/safe-write', entry.path);
225
+ const addonFile = resolve(NATIVE_BASE, entry.path);
178
226
 
179
227
  // Step 1: lstat LEXICAL path FIRST — reject symlink, non-regular, nlink!=1
180
228
  // Must lstat the original path to detect symlinks; lstat on a resolved path
@@ -207,7 +255,7 @@ export function readStableAddon(options = {}) {
207
255
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon not accessible');
208
256
  }
209
257
 
210
- const nativeBaseDir = realpathSync(resolve(__dirname, '../../native/safe-write/'));
258
+ const nativeBaseDir = realpathSync(NATIVE_BASE);
211
259
  if (!physicalAddonFile.startsWith(nativeBaseDir + '/') && physicalAddonFile !== nativeBaseDir) {
212
260
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon physical path escapes base directory');
213
261
  }
@@ -305,7 +353,7 @@ export function loadNativeAddon(addonPath) {
305
353
 
306
354
  // 2. Development mode
307
355
  if (process.env.RELEASE_SKILL_NATIVE_DEV === '1') {
308
- const devPath = resolve(__dirname, DEFAULT_ADDON_REL);
356
+ const devPath = DEFAULT_ADDON_PATH;
309
357
  const physicalPath = isWithinPackageBuildDir(devPath);
310
358
  if (!physicalPath) {
311
359
  throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'dev addon path is outside build directory');
@@ -37,6 +37,7 @@ import {
37
37
  buildFrozenGitRepository,
38
38
  buildFrozenNpmTarball,
39
39
  computeFrozenSnapshot,
40
+ normalizeGitTimestamp,
40
41
  sealFrozenSnapshot,
41
42
  } from '../snapshot/frozen.mjs';
42
43
  import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED } from '../core/errors.mjs';
@@ -514,7 +515,11 @@ async function buildProductionAssets(
514
515
  runDir,
515
516
  unitBaselineResults,
516
517
  buildGitRepository = buildFrozenGitRepository,
518
+ freezeTimestamp,
517
519
  ) {
520
+ // The plan freeze timestamp is sampled exactly once by prepareRelease.
521
+ // Every unit reuses this single value; the wall clock is never re-read.
522
+ const canonicalFreezeTimestamp = normalizeGitTimestamp(freezeTimestamp, 'plan freeze timestamp');
518
523
  for (const { unit } of unitResults) {
519
524
  const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
520
525
  if (npmDistribution && !['public', 'restricted'].includes(npmDistribution.access)) {
@@ -561,6 +566,7 @@ async function buildProductionAssets(
561
566
  version,
562
567
  expectedSnapshotDigest: sealed.digest,
563
568
  parent,
569
+ commitTimestamp: canonicalFreezeTimestamp,
564
570
  });
565
571
 
566
572
  let npm = null;
@@ -586,6 +592,7 @@ async function buildProductionAssets(
586
592
  gitObjectDir: relative(root, repositoryDir),
587
593
  commit: git.commit,
588
594
  tree: git.tree,
595
+ commitTimestamp: canonicalFreezeTimestamp,
589
596
  branchStrategy,
590
597
  ...(git.parentCommit ? { parentCommit: git.parentCommit } : {}),
591
598
  branch,
@@ -683,6 +690,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
683
690
  const claudeDist = (unit.distributions ?? []).find((d) => d.type === 'claude-plugin');
684
691
  if (claudeDist) {
685
692
  const identity = marketplaceIdentity(claudeDist);
693
+ const claudeTimeoutMs = Number.isInteger(claudeDist.timeoutMs) ? claudeDist.timeoutMs : 300000;
686
694
  actions.push({
687
695
  id: `claude-marketplace-install-${unit.id}`,
688
696
  type: 'claude-marketplace-install',
@@ -695,6 +703,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
695
703
  repo: unit.publicRepo,
696
704
  version,
697
705
  entrySkill: identity.entrySkill,
706
+ timeoutMs: claudeTimeoutMs,
698
707
  },
699
708
  expected: {
700
709
  installed: true,
@@ -709,6 +718,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
709
718
  const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
710
719
  if (codexDist) {
711
720
  const identity = marketplaceIdentity(codexDist);
721
+ const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
712
722
  actions.push({
713
723
  id: `codex-marketplace-install-${unit.id}`,
714
724
  type: 'codex-marketplace-install',
@@ -721,6 +731,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
721
731
  repo: unit.publicRepo,
722
732
  version,
723
733
  entrySkill: identity.entrySkill,
734
+ timeoutMs: codexTimeoutMs,
724
735
  },
725
736
  expected: {
726
737
  installed: true,
@@ -877,6 +888,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
877
888
  const claudeDist = (unit.distributions ?? []).find((d) => d.type === 'claude-plugin');
878
889
  if (claudeDist) {
879
890
  const identity = marketplaceIdentity(claudeDist);
891
+ const claudeTimeoutMs = Number.isInteger(claudeDist.timeoutMs) ? claudeDist.timeoutMs : 300000;
880
892
  actions.push({
881
893
  id: `claude-marketplace-install-${unit.id}`,
882
894
  type: 'claude-marketplace-install',
@@ -892,6 +904,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
892
904
  entrySkill: identity.entrySkill,
893
905
  snapshotPath: asset.snapshotPath,
894
906
  manifestDigest: asset.manifestDigest,
907
+ timeoutMs: claudeTimeoutMs,
895
908
  },
896
909
  expected: {
897
910
  installed: true,
@@ -911,6 +924,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
911
924
  const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
912
925
  if (codexDist) {
913
926
  const identity = marketplaceIdentity(codexDist);
927
+ const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
914
928
  actions.push({
915
929
  id: `codex-marketplace-install-${unit.id}`,
916
930
  type: 'codex-marketplace-install',
@@ -926,6 +940,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
926
940
  entrySkill: identity.entrySkill,
927
941
  snapshotPath: asset.snapshotPath,
928
942
  manifestDigest: asset.manifestDigest,
943
+ timeoutMs: codexTimeoutMs,
929
944
  },
930
945
  expected: {
931
946
  installed: true,
@@ -1443,6 +1458,18 @@ export async function prepareRelease(options) {
1443
1458
  // --- Step 7: Build plan object ---
1444
1459
  await evidence.append({ phase: 'plan-assembly', status: 'started' });
1445
1460
 
1461
+ // Production plans sample their freeze timestamp exactly once, before the
1462
+ // first frozen Git object exists. This single canonical value becomes
1463
+ // GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for every unit's frozen commit,
1464
+ // every unit's frozenSnapshot.commitTimestamp, and plan.createdAt. It is
1465
+ // thereby bound by the plan digest and the approval record; publish,
1466
+ // retry, and reconcile consume it from the frozen plan and never re-read
1467
+ // the wall clock. A missing or invalid injected value fails closed here,
1468
+ // before any Git write.
1469
+ const freezeTimestamp = production
1470
+ ? normalizeGitTimestamp(clock ? clock() : new Date().toISOString(), 'plan freeze timestamp')
1471
+ : null;
1472
+
1446
1473
  const productionAssets = production
1447
1474
  ? await buildProductionAssets(
1448
1475
  unitResults,
@@ -1451,6 +1478,7 @@ export async function prepareRelease(options) {
1451
1478
  runDir,
1452
1479
  unitBaselineResults,
1453
1480
  options.buildFrozenGitRepositoryFn ?? buildFrozenGitRepository,
1481
+ freezeTimestamp,
1454
1482
  )
1455
1483
  : null;
1456
1484
 
@@ -1474,6 +1502,7 @@ export async function prepareRelease(options) {
1474
1502
  branchStrategy: productionAssets[idx].branchStrategy,
1475
1503
  commit: productionAssets[idx].commit,
1476
1504
  tree: productionAssets[idx].tree,
1505
+ commitTimestamp: productionAssets[idx].commitTimestamp,
1477
1506
  ...(productionAssets[idx].parentCommit
1478
1507
  ? { parentCommit: productionAssets[idx].parentCommit }
1479
1508
  : {}),
@@ -1512,7 +1541,7 @@ export async function prepareRelease(options) {
1512
1541
  } : {}),
1513
1542
  units,
1514
1543
  externalActions,
1515
- createdAt: (clock ? clock() : new Date().toISOString()),
1544
+ createdAt: production ? freezeTimestamp : (clock ? clock() : new Date().toISOString()),
1516
1545
  };
1517
1546
 
1518
1547
  await evidence.append({
@@ -389,6 +389,7 @@ export async function publishRelease(options) {
389
389
  gitObjectDir: frozen.gitObjectDir,
390
390
  commit: frozen.commit,
391
391
  tree: frozen.tree,
392
+ commitTimestamp: frozen.commitTimestamp,
392
393
  });
393
394
  assertInsideAssetRoot(assetRoot, git.gitDir, 'frozen git object directory');
394
395
  if (frozen.npm) {
@@ -222,7 +222,10 @@ export async function reconcileRelease(options) {
222
222
  // =======================================================================
223
223
  await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'started' });
224
224
 
225
- const completenessResult = validatePlanActionCompleteness(plan);
225
+ // Use legacyCompatibility: old PARTIAL plans (pre-v0.1.5) lack
226
+ // parameters.timeoutMs. Reconcile must still pass these plans,
227
+ // while strict mode (prepare/approve/publish) rejects them.
228
+ const completenessResult = validatePlanActionCompleteness(plan, { legacyCompatibility: true });
226
229
  if (!completenessResult.passed) {
227
230
  await evidence.append({
228
231
  phase: 'safety-gate',