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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +29 -0
- package/INSTALL.md +69 -32
- package/INSTALL.zh-CN.md +64 -26
- package/README.md +101 -37
- package/README.zh-CN.md +87 -28
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +79284 -0
- package/adapters/claude/bin/release-skill.mjs +34 -0
- package/adapters/claude/native/safe-write/binding.gyp +40 -0
- package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/claude/native/safe-write/prebuilds.json +24 -0
- package/adapters/claude/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/claude/schemas/.render-manifest.json +37 -0
- package/adapters/claude/schemas/approval-record.schema.json +115 -0
- package/adapters/claude/schemas/artifact-lock.schema.json +111 -0
- package/adapters/claude/schemas/artifact-plan.schema.json +52 -0
- package/adapters/claude/schemas/artifact-policy.schema.json +76 -0
- package/adapters/claude/schemas/evidence-event.schema.json +89 -0
- package/adapters/claude/schemas/release-plan.schema.json +860 -0
- package/adapters/claude/schemas/release-project.schema.json +736 -0
- package/adapters/claude/schemas/release-run.schema.json +342 -0
- package/adapters/claude/skills/release-assess/SKILL.md +5 -6
- package/adapters/claude/skills/release-help/SKILL.md +11 -18
- package/adapters/claude/skills/release-prepare/SKILL.md +6 -6
- package/adapters/claude/skills/release-publish/SKILL.md +1 -4
- package/adapters/claude/skills/release-reconcile/SKILL.md +5 -5
- package/adapters/claude/skills/release-setup/SKILL.md +64 -80
- package/adapters/claude/skills/release-verify/SKILL.md +4 -7
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +79284 -0
- package/adapters/codex/bin/release-skill.mjs +34 -0
- package/adapters/codex/native/safe-write/binding.gyp +40 -0
- package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/codex/native/safe-write/prebuilds.json +24 -0
- package/adapters/codex/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/codex/schemas/.render-manifest.json +37 -0
- package/adapters/codex/schemas/approval-record.schema.json +115 -0
- package/adapters/codex/schemas/artifact-lock.schema.json +111 -0
- package/adapters/codex/schemas/artifact-plan.schema.json +52 -0
- package/adapters/codex/schemas/artifact-policy.schema.json +76 -0
- package/adapters/codex/schemas/evidence-event.schema.json +89 -0
- package/adapters/codex/schemas/release-plan.schema.json +860 -0
- package/adapters/codex/schemas/release-project.schema.json +736 -0
- package/adapters/codex/schemas/release-run.schema.json +342 -0
- package/adapters/codex/skills/release-assess/SKILL.md +12 -6
- package/adapters/codex/skills/release-help/SKILL.md +18 -18
- package/adapters/codex/skills/release-prepare/SKILL.md +13 -6
- package/adapters/codex/skills/release-publish/SKILL.md +8 -4
- package/adapters/codex/skills/release-reconcile/SKILL.md +12 -5
- package/adapters/codex/skills/release-setup/SKILL.md +71 -80
- package/adapters/codex/skills/release-verify/SKILL.md +11 -7
- package/bin/release-skill-cli.mjs +807 -0
- package/bin/release-skill.bundle.mjs +79284 -0
- package/bin/release-skill.mjs +23 -788
- package/package.json +6 -2
- package/references/02-project-config.md +2 -2
- package/schemas/release-plan.schema.json +7 -1
- package/schemas/release-project.schema.json +8 -0
- package/scripts/build-bundle.mjs +133 -0
- package/skills/release-assess/SKILL.md +5 -6
- package/skills/release-help/SKILL.md +11 -18
- package/skills/release-prepare/SKILL.md +6 -6
- package/skills/release-publish/SKILL.md +1 -4
- package/skills/release-reconcile/SKILL.md +5 -5
- package/skills/release-setup/SKILL.md +64 -80
- package/skills/release-verify/SKILL.md +4 -7
- package/skills-src/release-assess/SKILL.md +5 -6
- package/skills-src/release-help/SKILL.md +11 -18
- package/skills-src/release-prepare/SKILL.md +6 -6
- package/skills-src/release-publish/SKILL.md +1 -4
- package/skills-src/release-reconcile/SKILL.md +5 -5
- package/skills-src/release-setup/SKILL.md +64 -80
- package/skills-src/release-verify/SKILL.md +4 -7
- package/src/adapters/npm.mjs +5 -13
- package/src/adapters/plugin-marketplace.mjs +67 -31
- package/src/artifacts/policy.mjs +4 -7
- package/src/artifacts/safe-fs-backend-internal.mjs +69 -21
- package/src/commands/prepare.mjs +22 -1
- package/src/commands/publish.mjs +1 -0
- package/src/commands/setup.mjs +667 -28
- package/src/core/approval.mjs +4 -6
- package/src/core/config.mjs +8 -8
- package/src/core/pkg-root.mjs +22 -0
- package/src/core/plan.mjs +9 -4
- package/src/core/run.mjs +4 -4
- package/src/core/trusted-resource.mjs +96 -0
- package/src/docs/version-gate.mjs +164 -0
- package/src/producers/build-adapters.mjs +512 -55
- package/src/snapshot/frozen.mjs +133 -3
- package/src/snapshot/public-map.mjs +7 -4
- 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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
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(
|
|
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(
|
|
110
|
-
const baseDir =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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');
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -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({
|
package/src/commands/publish.mjs
CHANGED
|
@@ -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) {
|