release-skill 0.1.3 → 0.1.4

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/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -3
  4. package/CHANGELOG.md +29 -0
  5. package/INSTALL.md +69 -32
  6. package/INSTALL.zh-CN.md +64 -26
  7. package/README.md +101 -37
  8. package/README.zh-CN.md +87 -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 +79284 -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 +736 -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 +79284 -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 +736 -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 +79284 -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 +8 -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 +67 -31
  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 +22 -1
  83. package/src/commands/publish.mjs +1 -0
  84. package/src/commands/setup.mjs +667 -28
  85. package/src/core/approval.mjs +4 -6
  86. package/src/core/config.mjs +8 -8
  87. package/src/core/pkg-root.mjs +22 -0
  88. package/src/core/plan.mjs +9 -4
  89. package/src/core/run.mjs +4 -4
  90. package/src/core/trusted-resource.mjs +96 -0
  91. package/src/docs/version-gate.mjs +164 -0
  92. package/src/producers/build-adapters.mjs +512 -55
  93. package/src/snapshot/frozen.mjs +133 -3
  94. package/src/snapshot/public-map.mjs +7 -4
  95. package/src/snapshot/scan.mjs +2 -1
@@ -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,
@@ -1443,6 +1450,18 @@ export async function prepareRelease(options) {
1443
1450
  // --- Step 7: Build plan object ---
1444
1451
  await evidence.append({ phase: 'plan-assembly', status: 'started' });
1445
1452
 
1453
+ // Production plans sample their freeze timestamp exactly once, before the
1454
+ // first frozen Git object exists. This single canonical value becomes
1455
+ // GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for every unit's frozen commit,
1456
+ // every unit's frozenSnapshot.commitTimestamp, and plan.createdAt. It is
1457
+ // thereby bound by the plan digest and the approval record; publish,
1458
+ // retry, and reconcile consume it from the frozen plan and never re-read
1459
+ // the wall clock. A missing or invalid injected value fails closed here,
1460
+ // before any Git write.
1461
+ const freezeTimestamp = production
1462
+ ? normalizeGitTimestamp(clock ? clock() : new Date().toISOString(), 'plan freeze timestamp')
1463
+ : null;
1464
+
1446
1465
  const productionAssets = production
1447
1466
  ? await buildProductionAssets(
1448
1467
  unitResults,
@@ -1451,6 +1470,7 @@ export async function prepareRelease(options) {
1451
1470
  runDir,
1452
1471
  unitBaselineResults,
1453
1472
  options.buildFrozenGitRepositoryFn ?? buildFrozenGitRepository,
1473
+ freezeTimestamp,
1454
1474
  )
1455
1475
  : null;
1456
1476
 
@@ -1474,6 +1494,7 @@ export async function prepareRelease(options) {
1474
1494
  branchStrategy: productionAssets[idx].branchStrategy,
1475
1495
  commit: productionAssets[idx].commit,
1476
1496
  tree: productionAssets[idx].tree,
1497
+ commitTimestamp: productionAssets[idx].commitTimestamp,
1477
1498
  ...(productionAssets[idx].parentCommit
1478
1499
  ? { parentCommit: productionAssets[idx].parentCommit }
1479
1500
  : {}),
@@ -1512,7 +1533,7 @@ export async function prepareRelease(options) {
1512
1533
  } : {}),
1513
1534
  units,
1514
1535
  externalActions,
1515
- createdAt: (clock ? clock() : new Date().toISOString()),
1536
+ createdAt: production ? freezeTimestamp : (clock ? clock() : new Date().toISOString()),
1516
1537
  };
1517
1538
 
1518
1539
  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) {