release-skill 0.6.2 → 0.7.0

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 (112) 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 +48 -0
  7. package/CONTRIBUTING.md +1 -1
  8. package/INSTALL.md +47 -2
  9. package/INSTALL.zh-CN.md +29 -2
  10. package/README.md +137 -12
  11. package/README.zh-CN.md +110 -9
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/error-codes.json +1 -1
  15. package/adapters/claude/bin/kernel-protocol.json +1 -1
  16. package/adapters/claude/bin/license-texts/Apache-2.0.txt +201 -0
  17. package/adapters/claude/bin/license-texts/MIT.txt +21 -0
  18. package/adapters/claude/bin/registry.json +64 -1
  19. package/adapters/claude/bin/release-skill.bundle.mjs +50570 -42208
  20. package/adapters/claude/bin/rules.json +1 -1
  21. package/adapters/claude/schemas/.render-manifest.json +10 -6
  22. package/adapters/claude/schemas/postpublish-approval-record.schema.json +47 -0
  23. package/adapters/claude/schemas/release-plan.schema.json +200 -4
  24. package/adapters/claude/schemas/release-project.schema.json +86 -5
  25. package/adapters/claude/schemas/release-run.schema.json +11 -6
  26. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  27. package/adapters/codex/bin/error-codes.json +1 -1
  28. package/adapters/codex/bin/kernel-protocol.json +1 -1
  29. package/adapters/codex/bin/license-texts/Apache-2.0.txt +201 -0
  30. package/adapters/codex/bin/license-texts/MIT.txt +21 -0
  31. package/adapters/codex/bin/registry.json +64 -1
  32. package/adapters/codex/bin/release-skill.bundle.mjs +50570 -42208
  33. package/adapters/codex/bin/rules.json +1 -1
  34. package/adapters/codex/schemas/.render-manifest.json +10 -6
  35. package/adapters/codex/schemas/postpublish-approval-record.schema.json +47 -0
  36. package/adapters/codex/schemas/release-plan.schema.json +200 -4
  37. package/adapters/codex/schemas/release-project.schema.json +86 -5
  38. package/adapters/codex/schemas/release-run.schema.json +11 -6
  39. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  40. package/adapters/kimi/bin/error-codes.json +1 -1
  41. package/adapters/kimi/bin/kernel-protocol.json +1 -1
  42. package/adapters/kimi/bin/license-texts/Apache-2.0.txt +201 -0
  43. package/adapters/kimi/bin/license-texts/MIT.txt +21 -0
  44. package/adapters/kimi/bin/registry.json +64 -1
  45. package/adapters/kimi/bin/release-skill.bundle.mjs +50570 -42208
  46. package/adapters/kimi/bin/rules.json +1 -1
  47. package/adapters/kimi/schemas/.render-manifest.json +10 -6
  48. package/adapters/kimi/schemas/postpublish-approval-record.schema.json +47 -0
  49. package/adapters/kimi/schemas/release-plan.schema.json +200 -4
  50. package/adapters/kimi/schemas/release-project.schema.json +86 -5
  51. package/adapters/kimi/schemas/release-run.schema.json +11 -6
  52. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  53. package/adapters/workbuddy/bin/error-codes.json +1 -1
  54. package/adapters/workbuddy/bin/kernel-protocol.json +1 -1
  55. package/adapters/workbuddy/bin/license-texts/Apache-2.0.txt +201 -0
  56. package/adapters/workbuddy/bin/license-texts/MIT.txt +21 -0
  57. package/adapters/workbuddy/bin/registry.json +64 -1
  58. package/adapters/workbuddy/bin/release-skill.bundle.mjs +50570 -42208
  59. package/adapters/workbuddy/bin/rules.json +1 -1
  60. package/adapters/workbuddy/schemas/.render-manifest.json +10 -6
  61. package/adapters/workbuddy/schemas/postpublish-approval-record.schema.json +47 -0
  62. package/adapters/workbuddy/schemas/release-plan.schema.json +200 -4
  63. package/adapters/workbuddy/schemas/release-project.schema.json +86 -5
  64. package/adapters/workbuddy/schemas/release-run.schema.json +11 -6
  65. package/bin/error-codes.json +1 -1
  66. package/bin/kernel-protocol.json +1 -1
  67. package/bin/license-texts/Apache-2.0.txt +201 -0
  68. package/bin/license-texts/MIT.txt +21 -0
  69. package/bin/registry.json +64 -1
  70. package/bin/release-skill-cli.mjs +187 -9
  71. package/bin/release-skill.bundle.mjs +50570 -42208
  72. package/bin/rules.json +1 -1
  73. package/package.json +6 -4
  74. package/platform-manifest.json +4 -4
  75. package/references/.render-manifest.json +5 -5
  76. package/references/01-state-machine.md +22 -2
  77. package/references/02-project-config.md +1 -1
  78. package/references/06-adapter-contract.md +6 -3
  79. package/schemas/.render-manifest.json +10 -6
  80. package/schemas/postpublish-approval-record.schema.json +47 -0
  81. package/schemas/release-plan.schema.json +200 -4
  82. package/schemas/release-project.schema.json +86 -5
  83. package/schemas/release-run.schema.json +11 -6
  84. package/src/adapters/distribute-git.mjs +17 -11
  85. package/src/commands/approve.mjs +163 -1
  86. package/src/commands/distribute.mjs +649 -89
  87. package/src/commands/postverify.mjs +808 -0
  88. package/src/commands/prepare.mjs +456 -45
  89. package/src/commands/setup.mjs +715 -0
  90. package/src/commands/ship.mjs +152 -5
  91. package/src/commands/verify.mjs +92 -15
  92. package/src/core/approval.mjs +93 -68
  93. package/src/core/bounded-output.mjs +46 -0
  94. package/src/core/derived-artifact-gates.mjs +258 -0
  95. package/src/core/docs-refresh-preset.mjs +171 -0
  96. package/src/core/errors.mjs +4 -0
  97. package/src/core/evidence.mjs +8 -1
  98. package/src/core/git-url-policy.mjs +214 -0
  99. package/src/core/hooks.mjs +28 -0
  100. package/src/core/marketplace-registry-entry.mjs +175 -0
  101. package/src/core/notify-handoff.mjs +76 -0
  102. package/src/core/postpublish-approval.mjs +241 -0
  103. package/src/core/postpublish-bundle.mjs +342 -0
  104. package/src/core/postpublish-projection.mjs +210 -0
  105. package/src/core/postpublish.mjs +449 -25
  106. package/src/core/preset-executor.mjs +175 -0
  107. package/src/core/preset-gitwrite.mjs +484 -0
  108. package/src/core/presets.mjs +754 -0
  109. package/src/core/proposal-inbox.mjs +646 -0
  110. package/src/core/redact.mjs +16 -3
  111. package/src/core/run.mjs +91 -6
  112. package/src/producers/build-adapters.mjs +28 -14
package/src/core/run.mjs CHANGED
@@ -12,7 +12,7 @@
12
12
  * @module core/run
13
13
  */
14
14
 
15
- import { lstat, mkdir, readFile } from 'node:fs/promises';
15
+ import { lstat, mkdir, readFile, stat } from 'node:fs/promises';
16
16
  import { realpathSync } from 'node:fs';
17
17
  import { dirname, join, resolve, basename, relative, isAbsolute } from 'node:path';
18
18
  import Ajv from 'ajv';
@@ -120,22 +120,107 @@ export function validateRun(run, options = {}) {
120
120
  }
121
121
  }
122
122
 
123
+ /**
124
+ * Resolve a run path that may point at a run DIRECTORY (O6, 2026-08-18
125
+ * release-cycle investigation §3.2): verify `--run` ergonomically accepts a
126
+ * run directory and binds its `release-run.json`; a regular file input is
127
+ * returned unchanged so every existing file-path caller keeps its exact
128
+ * behaviour (compat).
129
+ *
130
+ * Fail-closed rules for the directory form:
131
+ * - a symlinked directory input is rejected (never silently dereferenced);
132
+ * - a directory without `release-run.json` names the expected file.
133
+ * A missing path is returned unchanged so the legacy readFile error surfaces.
134
+ *
135
+ * @param {string} runPath - Run file or run directory path.
136
+ * @returns {Promise<string>} The resolved run FILE path.
137
+ * @throws {ReleaseError} GATE_FAILED for symlinked or release-run.json-less
138
+ * directory inputs.
139
+ */
140
+ export async function resolveRunPath(runPath) {
141
+ let stats;
142
+ try {
143
+ stats = await lstat(runPath);
144
+ } catch (err) {
145
+ if (err?.code === 'ENOENT') return runPath; // legacy readFile error path
146
+ throw new ReleaseError(
147
+ GATE_FAILED,
148
+ `cannot inspect release run path: ${err.message}`,
149
+ { runPath, cause: err.code },
150
+ );
151
+ }
152
+
153
+ if (stats.isFile()) return runPath; // unchanged file behaviour (compat)
154
+
155
+ if (stats.isSymbolicLink()) {
156
+ // A symlink that ultimately points at a directory would otherwise surface
157
+ // a raw EISDIR; resolve the target type and fail closed for directories.
158
+ let targetStats;
159
+ try {
160
+ targetStats = await stat(runPath);
161
+ } catch (err) {
162
+ if (err?.code === 'ENOENT') return runPath; // dangling: legacy error
163
+ throw new ReleaseError(
164
+ GATE_FAILED,
165
+ `cannot inspect release run path: ${err.message}`,
166
+ { runPath, cause: err.code },
167
+ );
168
+ }
169
+ if (targetStats.isDirectory()) {
170
+ throw new ReleaseError(
171
+ GATE_FAILED,
172
+ 'release run path is a symlinked directory; pass the run file (release-run.json) or the real run directory',
173
+ { runPath },
174
+ );
175
+ }
176
+ return runPath; // symlinked FILE keeps the legacy readFile behaviour
177
+ }
178
+
179
+ if (stats.isDirectory()) {
180
+ const candidate = join(runPath, 'release-run.json');
181
+ try {
182
+ const candidateStats = await lstat(candidate);
183
+ if (!candidateStats.isFile()) {
184
+ throw new ReleaseError(
185
+ GATE_FAILED,
186
+ `release run directory does not contain a regular release-run.json: ${runPath}`,
187
+ { runPath, expected: 'release-run.json' },
188
+ );
189
+ }
190
+ } catch (err) {
191
+ if (err instanceof ReleaseError) throw err;
192
+ throw new ReleaseError(
193
+ GATE_FAILED,
194
+ `release run directory must contain release-run.json: ${err.message}`,
195
+ { runPath, expected: 'release-run.json', cause: err.code },
196
+ );
197
+ }
198
+ return candidate;
199
+ }
200
+
201
+ return runPath; // anything else keeps the legacy readFile error path
202
+ }
203
+
123
204
  /**
124
205
  * Load, parse, and validate a run file from disk.
125
206
  *
126
- * @param {string} runPath - Absolute path to the run file.
207
+ * Accepts either the run file itself or its containing run directory (O6);
208
+ * a directory input resolves to `<dir>/release-run.json` via resolveRunPath.
209
+ *
210
+ * @param {string} runPath - Absolute path to the run file or run directory.
127
211
  * @returns {Promise<Object>} The validated run object.
128
212
  * @throws {ReleaseError} GATE_FAILED if the file cannot be read, parsed, or validated.
129
213
  */
130
214
  export async function loadRun(runPath, options = {}) {
215
+ const resolvedRunPath = await resolveRunPath(runPath);
131
216
  let raw;
132
217
  try {
133
- raw = await readFile(runPath, 'utf8');
218
+ raw = await readFile(resolvedRunPath, 'utf8');
134
219
  } catch (err) {
135
220
  throw new ReleaseError(
136
221
  GATE_FAILED,
137
222
  `cannot read release run: ${err.message}`,
138
- { runPath, cause: err.code },
223
+ { runPath: resolvedRunPath, cause: err.code },
139
224
  );
140
225
  }
141
226
 
@@ -146,13 +231,13 @@ export async function loadRun(runPath, options = {}) {
146
231
  throw new ReleaseError(
147
232
  GATE_FAILED,
148
233
  `release run is not valid JSON: ${err.message}`,
149
- { runPath },
234
+ { runPath: resolvedRunPath },
150
235
  );
151
236
  }
152
237
 
153
238
  validateRun(run, options);
154
239
  if (options.authorityPlanPath) {
155
- assertImmutableRunAuthority(runPath, options.authorityPlanPath, run);
240
+ assertImmutableRunAuthority(resolvedRunPath, options.authorityPlanPath, run);
156
241
  }
157
242
  return run;
158
243
  }
@@ -394,8 +394,16 @@ function renderSkillForPlatform(content, platformName) {
394
394
  /**
395
395
  * Collect binary file paths from a source directory recursively.
396
396
  * Returns array of relative paths.
397
+ *
398
+ * `excludedTopDirs` names top-level directories that are skipped entirely.
399
+ * It only exists for subtrees that are already excluded from the shipped
400
+ * output (see the native/safe-write build/ exclusion below): their contents
401
+ * are never read and never shipped, so descending into them would only let
402
+ * transient, regeneratable artifacts (node-gyp creates a
403
+ * build/node_gyp_bins/python3 symlink while a rebuild is running) fail the
404
+ * collection without protecting any shipped byte.
397
405
  */
398
- async function collectBinaryFiles(baseDir, physicalRoot, relPrefix = '') {
406
+ async function collectBinaryFiles(baseDir, physicalRoot, relPrefix = '', excludedTopDirs) {
399
407
  const results = [];
400
408
  const physicalDir = await assertTrustedDirectory(
401
409
  baseDir,
@@ -409,6 +417,7 @@ async function collectBinaryFiles(baseDir, physicalRoot, relPrefix = '') {
409
417
  throw new Error(`SECURITY: symlink in resource tree: ${relPath}`);
410
418
  }
411
419
  if (entry.isDirectory()) {
420
+ if (relPrefix === '' && excludedTopDirs?.has(entry.name)) continue;
412
421
  results.push(...await collectBinaryFiles(join(physicalDir, entry.name), physicalRoot, relPath));
413
422
  } else if (entry.isFile()) {
414
423
  results.push({ relPath, sourcePath: join(physicalDir, entry.name) });
@@ -495,23 +504,24 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
495
504
  // Copy bin/ entry point and bundle (text files). The self-contained
496
505
  // bundle ships runtime sidecars next to it (JSON resources read via
497
506
  // new URL(<rel>, import.meta.url) by inlined Foundation modules, e.g.
498
- // error-codes.json); enumerate the directory so every sidecar is
499
- // replicated deterministically. The source CLI (release-skill-cli.mjs)
500
- // is never shipped into adapters. The wrapper and the bundle are
501
- // required entries: a missing one fails closed before any write.
502
- const binFiles = (await readdir(join(root, 'bin')))
503
- .filter((name) => name !== 'release-skill-cli.mjs')
504
- .sort();
507
+ // error-codes.json; since F-06/T6 the inlined Engineering Kit also
508
+ // carries license-text sidecars under bin/license-texts/); enumerate
509
+ // the directory recursively so every sidecar is replicated
510
+ // deterministically. The source CLI (release-skill-cli.mjs) is never
511
+ // shipped into adapters. The wrapper and the bundle are required
512
+ // entries: a missing one fails closed before any write.
513
+ const binFileEntries = await collectBinaryFiles(join(root, 'bin'), root);
514
+ const binRelPaths = binFileEntries.map(({ relPath }) => relPath);
505
515
  for (const required of ['release-skill.mjs', 'release-skill.bundle.mjs']) {
506
- if (!binFiles.includes(required)) {
516
+ if (!binRelPaths.includes(required)) {
507
517
  throw new Error(`SECURITY: required bundle entry is missing: bin/${required}`);
508
518
  }
509
519
  }
510
- for (const name of binFiles) {
511
- const srcPath = join(root, 'bin', name);
512
- const content = await readTrustedFile(srcPath, root, `bin/${name}`);
520
+ for (const { relPath: rel, sourcePath } of binFileEntries) {
521
+ if (rel === 'release-skill-cli.mjs') continue;
522
+ const content = await readTrustedFile(sourcePath, root, `bin/${rel}`);
513
523
  files.push({
514
- relPath: join('bin', name),
524
+ relPath: join('bin', rel),
515
525
  content,
516
526
  isBinary: false,
517
527
  });
@@ -537,8 +547,12 @@ async function generateAdapterFiles(platform, skills, templateJson, root, market
537
547
  // Copy native/safe-write/ directory (prebuilds.json, prebuilds/, src/).
538
548
  // Exclude build/ — it contains platform-specific build artifacts that are
539
549
  // regenerated by node-gyp and should not be in the published adapter.
550
+ // Collection never descends into build/: it is output-excluded anyway, and
551
+ // a concurrent node-gyp rebuild transiently places symlinked helper bins
552
+ // there (node_gyp_bins/python3) that must not fail the derived-artifact
553
+ // gates.
540
554
  const nativeBase = join(root, 'native', 'safe-write');
541
- const nativeFiles = await collectBinaryFiles(nativeBase, root);
555
+ const nativeFiles = await collectBinaryFiles(nativeBase, root, '', new Set(['build']));
542
556
  if (!nativeFiles.some(({ relPath }) => relPath === 'prebuilds.json')) {
543
557
  throw new Error('SECURITY: required native resource is missing: native/safe-write/prebuilds.json');
544
558
  }