release-skill 0.1.1 → 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 (108) 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 +89 -0
  5. package/INSTALL.md +216 -5
  6. package/INSTALL.zh-CN.md +358 -0
  7. package/README.md +411 -67
  8. package/README.zh-CN.md +377 -59
  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 +14 -18
  28. package/adapters/claude/skills/release-prepare/SKILL.md +16 -6
  29. package/adapters/claude/skills/release-publish/SKILL.md +7 -7
  30. package/adapters/claude/skills/release-reconcile/SKILL.md +6 -6
  31. package/adapters/claude/skills/release-setup/SKILL.md +95 -0
  32. package/adapters/claude/skills/release-verify/SKILL.md +7 -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 +21 -18
  51. package/adapters/codex/skills/release-prepare/SKILL.md +23 -6
  52. package/adapters/codex/skills/release-publish/SKILL.md +14 -7
  53. package/adapters/codex/skills/release-reconcile/SKILL.md +13 -6
  54. package/adapters/codex/skills/release-setup/SKILL.md +102 -0
  55. package/adapters/codex/skills/release-verify/SKILL.md +14 -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 -732
  59. package/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  60. package/native/safe-write/prebuilds.json +22 -2
  61. package/native/safe-write/src/safe_write.cc +11 -2
  62. package/package.json +8 -2
  63. package/references/02-project-config.md +55 -4
  64. package/references/05-evidence-and-errors.md +6 -2
  65. package/schemas/release-plan.schema.json +556 -65
  66. package/schemas/release-project.schema.json +406 -29
  67. package/schemas/release-run.schema.json +165 -18
  68. package/scripts/build-bundle.mjs +133 -0
  69. package/skills/release-assess/SKILL.md +5 -6
  70. package/skills/release-help/SKILL.md +14 -18
  71. package/skills/release-prepare/SKILL.md +16 -6
  72. package/skills/release-publish/SKILL.md +7 -7
  73. package/skills/release-reconcile/SKILL.md +6 -6
  74. package/skills/release-setup/SKILL.md +95 -0
  75. package/skills/release-verify/SKILL.md +7 -7
  76. package/skills-src/release-assess/SKILL.md +5 -6
  77. package/skills-src/release-help/SKILL.md +14 -18
  78. package/skills-src/release-prepare/SKILL.md +16 -6
  79. package/skills-src/release-publish/SKILL.md +7 -7
  80. package/skills-src/release-reconcile/SKILL.md +6 -6
  81. package/skills-src/release-setup/SKILL.md +95 -0
  82. package/skills-src/release-verify/SKILL.md +7 -7
  83. package/src/adapters/contract.mjs +3 -0
  84. package/src/adapters/git-github.mjs +84 -2
  85. package/src/adapters/npm.mjs +5 -13
  86. package/src/adapters/plugin-marketplace.mjs +132 -52
  87. package/src/adapters/push-snapshot.mjs +84 -17
  88. package/src/artifacts/policy.mjs +4 -7
  89. package/src/artifacts/safe-fs-backend-internal.mjs +69 -21
  90. package/src/commands/prepare.mjs +244 -20
  91. package/src/commands/publish.mjs +46 -0
  92. package/src/commands/reconcile.mjs +152 -0
  93. package/src/commands/setup.mjs +1525 -0
  94. package/src/commands/verify.mjs +122 -26
  95. package/src/core/approval.mjs +4 -6
  96. package/src/core/config.mjs +42 -8
  97. package/src/core/errors.mjs +4 -0
  98. package/src/core/pkg-root.mjs +22 -0
  99. package/src/core/plan.mjs +132 -4
  100. package/src/core/previous-public-baseline.mjs +21 -1
  101. package/src/core/run.mjs +4 -4
  102. package/src/core/trusted-resource.mjs +96 -0
  103. package/src/core/verification-gates.mjs +451 -0
  104. package/src/docs/version-gate.mjs +164 -0
  105. package/src/producers/build-adapters.mjs +512 -55
  106. package/src/snapshot/frozen.mjs +221 -7
  107. package/src/snapshot/public-map.mjs +7 -4
  108. package/src/snapshot/scan.mjs +2 -1
@@ -1,12 +1,14 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { execFile as execFileCb, spawn } from 'node:child_process';
3
3
  import { gunzipSync } from 'node:zlib';
4
- import { createRequire } from 'node:module';
5
4
  import { promisify } from 'node:util';
6
5
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
7
6
  import { constants as fsConstants } from 'node:fs';
8
7
  import { chmod, lstat, mkdtemp, open, readFile, realpath, rm } from 'node:fs/promises';
9
8
 
9
+ import libnpmpublish from 'libnpmpublish';
10
+ import npmRegistryFetch from 'npm-registry-fetch';
11
+
10
12
  import {
11
13
  ActionStatus,
12
14
  ActionType,
@@ -67,20 +69,14 @@ async function run(command, args, options = {}) {
67
69
  * Authentication and registry options must be supplied explicitly by the
68
70
  * caller; errors are sanitized before they cross the adapter boundary.
69
71
  */
70
- let _libnpmpublish;
71
- let _npmRegistryFetch;
72
72
  async function defaultPublishTarballBuffer({ buffer, manifest, opts: publishOpts }) {
73
- if (!_libnpmpublish) {
74
- const require = createRequire(import.meta.url);
75
- _libnpmpublish = require('libnpmpublish');
76
- }
77
73
  const libOpts = {};
78
74
  if (publishOpts.registry) libOpts.registry = publishOpts.registry;
79
75
  if (publishOpts.token) libOpts.forceAuth = { token: publishOpts.token };
80
76
  if (publishOpts.access) libOpts.access = publishOpts.access;
81
77
  if (publishOpts.tag) libOpts.defaultTag = publishOpts.tag;
82
78
  if (publishOpts.provenance) libOpts.provenance = publishOpts.provenance;
83
- return _libnpmpublish.publish(manifest, buffer, libOpts);
79
+ return libnpmpublish.publish(manifest, buffer, libOpts);
84
80
  }
85
81
 
86
82
  export function registryTokenKey(registry) {
@@ -144,11 +140,7 @@ export async function resolveNpmRegistryAuthToken(options) {
144
140
 
145
141
  async function defaultWhoamiWithToken({ registry, token, cwd, exec }) {
146
142
  try {
147
- if (!_npmRegistryFetch) {
148
- const require = createRequire(import.meta.url);
149
- _npmRegistryFetch = require('npm-registry-fetch');
150
- }
151
- const result = await _npmRegistryFetch.json('/-/whoami', {
143
+ const result = await npmRegistryFetch.json('/-/whoami', {
152
144
  registry: `${normalizeRegistry(registry)}/`,
153
145
  forceAuth: { token },
154
146
  preferOnline: true,
@@ -33,6 +33,43 @@ const execFile = promisify(execFileCb);
33
33
 
34
34
  const NAME = 'plugin-marketplace';
35
35
 
36
+ function transportPayload(entries) {
37
+ return entries.map(({ path, type, mode, size, contentDigest }) => ({
38
+ path,
39
+ type,
40
+ // The local authority removes write bits when sealing. Git checkout and
41
+ // plugin installation restore owner-write permission, while preserving
42
+ // executable intent. Ignore only write bits; retain every other mode bit.
43
+ mode: mode & ~0o222,
44
+ size,
45
+ contentDigest,
46
+ }));
47
+ }
48
+
49
+ async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
50
+ const sourcePath = await resolveFrozenPath(
51
+ context.root,
52
+ action.snapshotPath,
53
+ 'frozen marketplace snapshot',
54
+ );
55
+ const sourceSnapshot = await computeFrozenSnapshot(sourcePath);
56
+ if (sourceSnapshot.digest !== action.manifestDigest) {
57
+ throw new Error('frozen marketplace snapshot digest no longer matches the plan');
58
+ }
59
+ const installedSnapshot = await computeFrozenSnapshot(installPath, {
60
+ excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
61
+ });
62
+ if (
63
+ JSON.stringify(transportPayload(sourceSnapshot.entries))
64
+ !== JSON.stringify(transportPayload(installedSnapshot.entries))
65
+ ) {
66
+ throw new Error('installed marketplace payload differs in path, bytes, size, or non-write mode bits');
67
+ }
68
+ // This is not an expected-value backfill: the sealed authority digest was
69
+ // revalidated above and the installed payload was independently compared.
70
+ return action.manifestDigest;
71
+ }
72
+
36
73
  async function writeEvidenceAtomic(filePath, value) {
37
74
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
38
75
  try {
@@ -386,26 +423,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
386
423
  });
387
424
  }
388
425
 
389
- // Verify marketplace files exist
390
- const manifestRelative = consumer === 'claude'
391
- ? '.claude-plugin/plugin.json'
392
- : '.codex-plugin/plugin.json';
426
+ // Verify marketplace files exist.
427
+ // marketplace.json is at the snapshot root; plugin manifest is
428
+ // resolved relative to the entry's declared source path.
393
429
  const marketplaceRelative = consumer === 'claude'
394
430
  ? '.claude-plugin/marketplace.json'
395
431
  : '.agents/plugins/marketplace.json';
396
432
 
397
- const manifestPath = resolve(snapshotDirReal, manifestRelative);
398
433
  const marketplacePath = resolve(snapshotDirReal, marketplaceRelative);
399
434
 
400
- const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
401
- if (!manifestResult.valid) {
402
- return createResult({
403
- actionType,
404
- status: ActionStatus.PREFLIGHT_FAILED,
405
- error: `frozen snapshot ${manifestRelative} invalid: ${manifestResult.error}`,
406
- });
407
- }
408
-
409
435
  // marketplace.json must exist and have root name (no root version required)
410
436
  const marketplaceResult = await validateManifestFile(marketplacePath, ['name']);
411
437
  if (!marketplaceResult.valid) {
@@ -444,23 +470,70 @@ export function createPluginMarketplaceAdapter(deps = {}) {
444
470
  }
445
471
  const entry = pluginEntry[0];
446
472
 
447
- // Entry source must be "./" (Claude: string, Codex: object with path "./")
448
- if (consumer === 'claude') {
449
- if (entry.source !== './') {
450
- return createResult({
451
- actionType,
452
- status: ActionStatus.PREFLIGHT_FAILED,
453
- error: `Claude marketplace plugin entry source must be "./", got "${entry.source}"`,
454
- });
455
- }
456
- } else {
457
- if (entry.source?.source !== 'local' || entry.source?.path !== './') {
458
- return createResult({
459
- actionType,
460
- status: ActionStatus.PREFLIGHT_FAILED,
461
- error: `Codex marketplace plugin entry source must be {source:"local",path:"./"}, got ${JSON.stringify(entry.source)}`,
462
- });
463
- }
473
+ // Entry source must be a safe relative path within the snapshot.
474
+ // Accepts "./" (root-level), "./adapters/claude" (subdirectory),
475
+ // etc. Rejects absolute paths, ".." traversal, remote URLs, and
476
+ // empty strings.
477
+ const sourcePath = consumer === 'claude'
478
+ ? entry.source
479
+ : entry.source?.source === 'local' ? entry.source?.path : null;
480
+ if (typeof sourcePath !== 'string' || sourcePath.length === 0) {
481
+ return createResult({
482
+ actionType,
483
+ status: ActionStatus.PREFLIGHT_FAILED,
484
+ error: `marketplace plugin entry source must be a non-empty relative path${consumer === 'codex' ? ' (object with source:"local")' : ''}, got ${JSON.stringify(entry.source)}`,
485
+ });
486
+ }
487
+ if (
488
+ sourcePath.startsWith('/') ||
489
+ sourcePath.includes('..') ||
490
+ sourcePath.includes('\\') ||
491
+ /^https?:\/\//i.test(sourcePath)
492
+ ) {
493
+ return createResult({
494
+ actionType,
495
+ status: ActionStatus.PREFLIGHT_FAILED,
496
+ error: `marketplace plugin entry source "${sourcePath}" is not a safe relative path`,
497
+ });
498
+ }
499
+ // Verify the declared source directory exists and contains the
500
+ // expected plugin manifest inside the frozen snapshot.
501
+ const sourceDirAbs = resolve(snapshotDirReal, sourcePath);
502
+ const sourceDirReal = await realpath(sourceDirAbs).catch(() => null);
503
+ if (!sourceDirReal) {
504
+ return createResult({
505
+ actionType,
506
+ status: ActionStatus.PREFLIGHT_FAILED,
507
+ error: `marketplace plugin entry source directory does not exist: ${sourcePath}`,
508
+ });
509
+ }
510
+ // Containment check: source must stay inside the snapshot
511
+ const sourceRelCheck = relative(snapshotDirReal, sourceDirReal);
512
+ if (sourceRelCheck.startsWith('..') || isAbsolute(sourceRelCheck)) {
513
+ return createResult({
514
+ actionType,
515
+ status: ActionStatus.PREFLIGHT_FAILED,
516
+ error: `marketplace plugin entry source "${sourcePath}" escapes the frozen snapshot`,
517
+ });
518
+ }
519
+
520
+ // Resolve plugin manifest relative to the declared source path.
521
+ // For root layouts (source: "./"), this resolves to
522
+ // snapshot/.claude-plugin/plugin.json
523
+ // For subdirectory layouts (source: "./adapters/claude"), this resolves to
524
+ // snapshot/adapters/claude/.claude-plugin/plugin.json
525
+ const manifestRelative = consumer === 'claude'
526
+ ? join(sourcePath, '.claude-plugin', 'plugin.json')
527
+ : join(sourcePath, '.codex-plugin', 'plugin.json');
528
+ const manifestPath = resolve(snapshotDirReal, manifestRelative);
529
+
530
+ const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
531
+ if (!manifestResult.valid) {
532
+ return createResult({
533
+ actionType,
534
+ status: ActionStatus.PREFLIGHT_FAILED,
535
+ error: `frozen snapshot ${manifestRelative} invalid: ${manifestResult.error}`,
536
+ });
464
537
  }
465
538
 
466
539
  // Claude carries the version in the marketplace entry. Codex keeps
@@ -862,10 +935,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
862
935
  let executeManifestDigest = null;
863
936
  if (installPath) {
864
937
  try {
865
- const { digest } = await computeFrozenSnapshot(installPath, {
866
- excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
867
- });
868
- executeManifestDigest = digest;
938
+ executeManifestDigest = await verifyInstalledMarketplacePayload(
939
+ action,
940
+ context,
941
+ installPath,
942
+ consumer,
943
+ );
869
944
  } catch {
870
945
  // Digest computation failure is caught at verify time
871
946
  }
@@ -1176,26 +1251,30 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1176
1251
  });
1177
1252
  }
1178
1253
 
1179
- // Compute real manifestDigest from installed content using frozen algorithm
1254
+ // Bind the installed payload back to the sealed authority while
1255
+ // normalizing only transport-restored write permission bits.
1180
1256
  let manifestDigest;
1257
+ let manifestError = null;
1181
1258
  try {
1182
- // Codex retains a root .git checkout as consumer-owned transport
1183
- // metadata. It is not part of the published plugin payload.
1184
- const { digest } = await computeFrozenSnapshot(installPath, {
1185
- excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
1186
- });
1187
- manifestDigest = digest;
1259
+ manifestDigest = await verifyInstalledMarketplacePayload(
1260
+ action,
1261
+ context,
1262
+ installPath,
1263
+ consumer,
1264
+ );
1188
1265
  } catch (digestErr) {
1189
- return createResult({
1190
- actionType,
1191
- status: ActionStatus.OBSERVED,
1192
- observation: {
1193
- installed: true,
1194
- installPath,
1195
- entrySkillFound: true,
1196
- error: `failed to compute manifestDigest: ${digestErr.message}`,
1197
- },
1198
- });
1266
+ // Preserve independently observed fields for diagnostics. This
1267
+ // raw digest is not accepted as plan authority because the error
1268
+ // is returned and verify therefore fails closed.
1269
+ try {
1270
+ const installedSnapshot = await computeFrozenSnapshot(installPath, {
1271
+ excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
1272
+ });
1273
+ manifestDigest = installedSnapshot.digest;
1274
+ } catch {
1275
+ manifestDigest = undefined;
1276
+ }
1277
+ manifestError = `failed to bind manifestDigest to frozen authority: ${digestErr.message}`;
1199
1278
  }
1200
1279
 
1201
1280
  // Build observation with CLI-proven fields only (no action backfill)
@@ -1318,6 +1397,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1318
1397
  actionType,
1319
1398
  status: ActionStatus.OBSERVED,
1320
1399
  observation,
1400
+ error: manifestError,
1321
1401
  });
1322
1402
  }
1323
1403
 
@@ -54,6 +54,37 @@ function validateAction(action) {
54
54
  validateOid(action.commit, 'commit');
55
55
  validateOid(action.tree, 'tree');
56
56
  githubRepositoryUrl(action.repo, action.githubHost);
57
+
58
+ const validStrategies = ['create-release-branch', 'advance-existing-branch', 'initialize-default-branch'];
59
+ const strategy = action.branchStrategy ?? 'create-release-branch';
60
+ if (typeof strategy !== 'string' || !validStrategies.includes(strategy)) {
61
+ throw new Error(`branchStrategy must be one of: ${validStrategies.join(', ')}`);
62
+ }
63
+ if (strategy === 'advance-existing-branch' || strategy === 'initialize-default-branch') {
64
+ if (!action.parentCommit || typeof action.parentCommit !== 'string') {
65
+ throw new Error(`${strategy} requires parentCommit`);
66
+ }
67
+ validateOid(action.parentCommit, 'parentCommit');
68
+ }
69
+ if (strategy === 'advance-existing-branch') {
70
+ if (!action.expectedBaselineCommit || typeof action.expectedBaselineCommit !== 'string') {
71
+ throw new Error('advance-existing-branch requires expectedBaselineCommit');
72
+ }
73
+ validateOid(action.expectedBaselineCommit, 'expectedBaselineCommit');
74
+ if (action.expectedBaselineCommit !== action.parentCommit) {
75
+ throw new Error('advance-existing-branch expectedBaselineCommit must equal parentCommit');
76
+ }
77
+ }
78
+ if (strategy === 'create-release-branch' && (action.parentCommit || action.expectedBaselineCommit)) {
79
+ throw new Error('create-release-branch must not declare parentCommit or expectedBaselineCommit');
80
+ }
81
+
82
+ if (action.parentCommit != null && typeof action.parentCommit === 'string') {
83
+ validateOid(action.parentCommit, 'parentCommit');
84
+ }
85
+ if (action.expectedBaselineCommit != null && typeof action.expectedBaselineCommit === 'string') {
86
+ validateOid(action.expectedBaselineCommit, 'expectedBaselineCommit');
87
+ }
57
88
  }
58
89
 
59
90
  async function inspectLocalObjects(action, context, exec) {
@@ -62,6 +93,7 @@ async function inspectLocalObjects(action, context, exec) {
62
93
  gitObjectDir: action.gitObjectDir,
63
94
  commit: action.commit,
64
95
  tree: action.tree,
96
+ parentCommit: action.parentCommit,
65
97
  exec,
66
98
  });
67
99
  await exec('git', ['check-ref-format', `refs/heads/${action.branch}`], { shell: false });
@@ -91,12 +123,32 @@ export function createPushSnapshotAdapter(deps = {}) {
91
123
  ['ls-remote', '--heads', remoteUrl, `refs/heads/${action.branch}`],
92
124
  { shell: false },
93
125
  );
94
- if (stdout.trim()) {
95
- return createResult({
96
- actionType: action.actionType,
97
- status: ActionStatus.PREFLIGHT_FAILED,
98
- error: `remote branch already exists: ${action.branch}; human intervention required`,
99
- });
126
+ const remoteTip = stdout.trim().split(/\s+/)[0] ?? '';
127
+ const strategy = action.branchStrategy ?? 'create-release-branch';
128
+
129
+ if (strategy === 'advance-existing-branch') {
130
+ if (!remoteTip) {
131
+ return createResult({
132
+ actionType: action.actionType,
133
+ status: ActionStatus.PREFLIGHT_FAILED,
134
+ error: `advance-existing-branch requires remote branch to exist: ${action.branch}`,
135
+ });
136
+ }
137
+ if (remoteTip !== action.expectedBaselineCommit) {
138
+ return createResult({
139
+ actionType: action.actionType,
140
+ status: ActionStatus.PREFLIGHT_FAILED,
141
+ error: `remote branch tip mismatch: expected ${action.expectedBaselineCommit} but found ${remoteTip}`,
142
+ });
143
+ }
144
+ } else if (strategy === 'initialize-default-branch' || strategy === 'create-release-branch') {
145
+ if (remoteTip) {
146
+ return createResult({
147
+ actionType: action.actionType,
148
+ status: ActionStatus.PREFLIGHT_FAILED,
149
+ error: `remote branch already exists: ${action.branch}; human intervention required`,
150
+ });
151
+ }
100
152
  }
101
153
  return createResult({ actionType: action.actionType, status: ActionStatus.PREFLIGHT_PASSED });
102
154
  } catch (err) {
@@ -119,17 +171,32 @@ export function createPushSnapshotAdapter(deps = {}) {
119
171
  });
120
172
  const gitDir = await inspectLocalObjects(action, context, exec);
121
173
  const remoteUrl = githubRepositoryUrl(action.repo, action.githubHost);
122
- await exec(
123
- 'git',
124
- [
125
- '--git-dir', gitDir,
126
- 'push',
127
- `--force-with-lease=refs/heads/${action.branch}:`,
128
- remoteUrl,
129
- `${action.commit}:refs/heads/${action.branch}`,
130
- ],
131
- { shell: false },
132
- );
174
+ const strategy = action.branchStrategy ?? 'create-release-branch';
175
+
176
+ if (strategy === 'advance-existing-branch') {
177
+ await exec(
178
+ 'git',
179
+ [
180
+ '--git-dir', gitDir,
181
+ 'push',
182
+ remoteUrl,
183
+ `${action.commit}:refs/heads/${action.branch}`,
184
+ ],
185
+ { shell: false },
186
+ );
187
+ } else {
188
+ await exec(
189
+ 'git',
190
+ [
191
+ '--git-dir', gitDir,
192
+ 'push',
193
+ `--force-with-lease=refs/heads/${action.branch}:`,
194
+ remoteUrl,
195
+ `${action.commit}:refs/heads/${action.branch}`,
196
+ ],
197
+ { shell: false },
198
+ );
199
+ }
133
200
  return createResult({ actionType: action.actionType, status: ActionStatus.EXECUTED });
134
201
  } catch (err) {
135
202
  return createResult({
@@ -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');