release-skill 0.1.1

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 (125) hide show
  1. package/.agents/plugins/marketplace.json +23 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +26 -0
  5. package/CHANGELOG.md +68 -0
  6. package/CODE_OF_CONDUCT.md +76 -0
  7. package/CONTRIBUTING.md +49 -0
  8. package/INSTALL.md +182 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +25 -0
  11. package/README.md +501 -0
  12. package/README.zh-CN.md +463 -0
  13. package/SECURITY.md +48 -0
  14. package/adapters/claude/.claude-plugin/marketplace.json +16 -0
  15. package/adapters/claude/.claude-plugin/plugin.json +10 -0
  16. package/adapters/claude/skills/release-assess/SKILL.md +52 -0
  17. package/adapters/claude/skills/release-help/SKILL.md +60 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +71 -0
  19. package/adapters/claude/skills/release-publish/SKILL.md +55 -0
  20. package/adapters/claude/skills/release-reconcile/SKILL.md +73 -0
  21. package/adapters/claude/skills/release-verify/SKILL.md +70 -0
  22. package/adapters/codex/.codex-plugin/plugin.json +26 -0
  23. package/adapters/codex/skills/release-assess/SKILL.md +52 -0
  24. package/adapters/codex/skills/release-help/SKILL.md +60 -0
  25. package/adapters/codex/skills/release-prepare/SKILL.md +71 -0
  26. package/adapters/codex/skills/release-publish/SKILL.md +55 -0
  27. package/adapters/codex/skills/release-reconcile/SKILL.md +73 -0
  28. package/adapters/codex/skills/release-verify/SKILL.md +70 -0
  29. package/bin/release-skill.mjs +743 -0
  30. package/native/safe-write/binding.gyp +40 -0
  31. package/native/safe-write/prebuilds.json +4 -0
  32. package/native/safe-write/src/safe_write.cc +2023 -0
  33. package/package.json +75 -0
  34. package/references/.render-manifest.json +33 -0
  35. package/references/00-target-state.md +124 -0
  36. package/references/01-state-machine.md +155 -0
  37. package/references/02-project-config.md +217 -0
  38. package/references/03-readme-quality.md +136 -0
  39. package/references/04-supply-chain.md +147 -0
  40. package/references/05-evidence-and-errors.md +164 -0
  41. package/references/06-adapter-contract.md +178 -0
  42. package/schemas/.render-manifest.json +37 -0
  43. package/schemas/approval-record.schema.json +115 -0
  44. package/schemas/artifact-lock.schema.json +111 -0
  45. package/schemas/artifact-plan.schema.json +52 -0
  46. package/schemas/artifact-policy.schema.json +76 -0
  47. package/schemas/evidence-event.schema.json +89 -0
  48. package/schemas/release-plan.schema.json +369 -0
  49. package/schemas/release-project.schema.json +359 -0
  50. package/schemas/release-run.schema.json +195 -0
  51. package/skills/release-assess/SKILL.md +52 -0
  52. package/skills/release-help/SKILL.md +60 -0
  53. package/skills/release-prepare/SKILL.md +71 -0
  54. package/skills/release-publish/SKILL.md +55 -0
  55. package/skills/release-reconcile/SKILL.md +73 -0
  56. package/skills/release-verify/SKILL.md +70 -0
  57. package/skills-src/release-assess/SKILL.md +52 -0
  58. package/skills-src/release-help/SKILL.md +60 -0
  59. package/skills-src/release-prepare/SKILL.md +71 -0
  60. package/skills-src/release-publish/SKILL.md +55 -0
  61. package/skills-src/release-reconcile/SKILL.md +73 -0
  62. package/skills-src/release-verify/SKILL.md +70 -0
  63. package/src/adapters/contract.mjs +214 -0
  64. package/src/adapters/git-github.mjs +214 -0
  65. package/src/adapters/npm.mjs +947 -0
  66. package/src/adapters/plugin-marketplace.mjs +1365 -0
  67. package/src/adapters/push-snapshot.mjs +216 -0
  68. package/src/artifacts/adoption.mjs +743 -0
  69. package/src/artifacts/artifact-plan.mjs +162 -0
  70. package/src/artifacts/entry.mjs +240 -0
  71. package/src/artifacts/git-authority.mjs +637 -0
  72. package/src/artifacts/graph.mjs +189 -0
  73. package/src/artifacts/inspect.mjs +520 -0
  74. package/src/artifacts/inventory.mjs +192 -0
  75. package/src/artifacts/merge/binary.mjs +77 -0
  76. package/src/artifacts/merge/entry-merge.mjs +228 -0
  77. package/src/artifacts/merge/json.mjs +641 -0
  78. package/src/artifacts/merge/markdown.mjs +246 -0
  79. package/src/artifacts/merge/regions.mjs +156 -0
  80. package/src/artifacts/merge/text.mjs +432 -0
  81. package/src/artifacts/merge/tree.mjs +202 -0
  82. package/src/artifacts/merge/yaml.mjs +669 -0
  83. package/src/artifacts/path-key.mjs +94 -0
  84. package/src/artifacts/policy.mjs +319 -0
  85. package/src/artifacts/producer-registry.mjs +439 -0
  86. package/src/artifacts/project-lock.mjs +732 -0
  87. package/src/artifacts/resolution.mjs +658 -0
  88. package/src/artifacts/safe-fs-backend-internal.mjs +680 -0
  89. package/src/artifacts/safe-fs.mjs +72 -0
  90. package/src/artifacts/state.mjs +495 -0
  91. package/src/artifacts/transaction-journal.mjs +983 -0
  92. package/src/artifacts/transaction.mjs +1361 -0
  93. package/src/commands/approve.mjs +280 -0
  94. package/src/commands/artifacts.mjs +627 -0
  95. package/src/commands/assess.mjs +838 -0
  96. package/src/commands/prepare.mjs +1377 -0
  97. package/src/commands/publish.mjs +883 -0
  98. package/src/commands/reconcile.mjs +1255 -0
  99. package/src/commands/verify.mjs +915 -0
  100. package/src/core/approval.mjs +332 -0
  101. package/src/core/baseline.mjs +272 -0
  102. package/src/core/blackbox-hard-gates.mjs +142 -0
  103. package/src/core/config.mjs +448 -0
  104. package/src/core/digest.mjs +90 -0
  105. package/src/core/errors.mjs +113 -0
  106. package/src/core/evidence.mjs +167 -0
  107. package/src/core/hooks.mjs +241 -0
  108. package/src/core/node-version.mjs +64 -0
  109. package/src/core/plan.mjs +735 -0
  110. package/src/core/previous-public-baseline.mjs +204 -0
  111. package/src/core/run.mjs +681 -0
  112. package/src/core/state-machine.mjs +76 -0
  113. package/src/core/version-consistency.mjs +111 -0
  114. package/src/producers/build-adapters.mjs +231 -0
  115. package/src/producers/render-public-assets.mjs +152 -0
  116. package/src/producers/sync-skills.mjs +96 -0
  117. package/src/readme/contract.mjs +297 -0
  118. package/src/readme/examples.mjs +288 -0
  119. package/src/readme/parity.mjs +122 -0
  120. package/src/snapshot/export.mjs +99 -0
  121. package/src/snapshot/frozen.mjs +401 -0
  122. package/src/snapshot/manifest.mjs +207 -0
  123. package/src/snapshot/public-map.mjs +1459 -0
  124. package/src/snapshot/public-path.mjs +110 -0
  125. package/src/snapshot/scan.mjs +419 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Artifact plan assembly, digest computation, and atomic protocol write.
3
+ *
4
+ * The plan is a content-addressed document that binds every immutable input
5
+ * (repository identity, policy digest, base/current/producer manifest digests)
6
+ * to a single `nextAction` recommendation.
7
+ *
8
+ * Plans are written only to:
9
+ * - `.release-skill/runs/<run-id>/artifact-plan.json` (protocol run directory)
10
+ * - User-specified `--output` path
11
+ *
12
+ * Writing uses a temporary file + fsync + rename to ensure atomicity.
13
+ * No inventory target is ever written by this module.
14
+ *
15
+ * @module artifacts/artifact-plan
16
+ */
17
+
18
+ import { writeFile, mkdir, readFile, rename, open } from 'node:fs/promises';
19
+ import { dirname } from 'node:path';
20
+
21
+ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Public API
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Assemble a canonical artifact plan from classified inputs.
29
+ *
30
+ * The plan covers:
31
+ * - `apiVersion`: fixed contract version
32
+ * - `operation`: 'inspect' | 'init' | 'status'
33
+ * - `bindings`: all immutable input digests
34
+ * - `artifacts`: per-artifact decision array
35
+ * - `safeToWrite`: true only when every artifact is safe
36
+ * - `targetUnchanged`: always true for read-only operations
37
+ * - `nextAction`: a single recommended command
38
+ * - `planDigest`: `sha256:<hex>` of the canonical plan
39
+ *
40
+ * @param {object} options
41
+ * @param {'inspect'|'init'|'status'} options.operation - Operation type.
42
+ * @param {object} options.bindings - Immutable input bindings.
43
+ * @param {string} options.bindings.repositoryIdentity - Repository identity hash.
44
+ * @param {string} options.bindings.policyDigest - Policy digest.
45
+ * @param {string} options.bindings.baseManifestDigest - Base manifest digest.
46
+ * @param {string} options.bindings.currentManifestDigest - Current manifest digest.
47
+ * @param {string} options.bindings.producerClosureDigest - Producer closure digest.
48
+ * @param {Array<object>} options.artifacts - Artifact decisions.
49
+ * @param {boolean} options.safeToWrite - Whether all artifacts are safe to write.
50
+ * @param {boolean} options.targetUnchanged - Whether targets are unchanged.
51
+ * @returns {object} Frozen artifact plan with planDigest.
52
+ */
53
+ export function assemblePlan({
54
+ operation,
55
+ bindings,
56
+ artifacts = [],
57
+ safeToWrite = false,
58
+ targetUnchanged = true,
59
+ } = {}) {
60
+ const nextAction = chooseNextAction(operation, artifacts, safeToWrite);
61
+
62
+ const plan = {
63
+ apiVersion: 'release-skill.dev/artifact-plan/v1',
64
+ operation,
65
+ bindings: Object.freeze({ ...bindings }),
66
+ artifacts: Object.freeze(artifacts.map((a) => Object.freeze({ ...a }))),
67
+ safeToWrite,
68
+ targetUnchanged,
69
+ nextAction,
70
+ };
71
+
72
+ // Compute planDigest over the canonical form (without the digest field itself)
73
+ const canonical = canonicalJson(plan);
74
+ plan.planDigest = `sha256:${sha256Hex(canonical)}`;
75
+
76
+ return Object.freeze(plan);
77
+ }
78
+
79
+ /**
80
+ * Write a plan atomically to the specified path.
81
+ *
82
+ * Uses temporary file + fsync + rename for atomicity.
83
+ * Creates parent directories if needed.
84
+ *
85
+ * @param {object} plan - Frozen artifact plan.
86
+ * @param {string} outputPath - Absolute path to write the plan.
87
+ */
88
+ export async function writePlan(plan, outputPath) {
89
+ const dir = dirname(outputPath);
90
+ await mkdir(dir, { recursive: true });
91
+
92
+ const tmpPath = `${outputPath}.tmp`;
93
+ const content = JSON.stringify(plan, null, 2);
94
+
95
+ // Write to temp file with fsync
96
+ const fh = await open(tmpPath, 'w');
97
+ try {
98
+ await writeFile(fh, content, 'utf8');
99
+ await fh.sync();
100
+ } finally {
101
+ await fh.close();
102
+ }
103
+
104
+ // Atomic rename
105
+ await rename(tmpPath, outputPath);
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Internal: nextAction selection
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Choose a single nextAction for the plan.
114
+ *
115
+ * The nextAction is always a single object with a `command` field.
116
+ * Priority:
117
+ * 1. If any artifact has status that blocks writes → `artifacts adopt`
118
+ * 2. If any artifact needs acceptance → `artifacts accept`
119
+ * 3. Otherwise → `artifacts apply` (all clean/mergeable)
120
+ *
121
+ * @param {string} operation - The plan operation type.
122
+ * @param {Array<object>} artifacts - Artifact decisions.
123
+ * @param {boolean} safeToWrite - Whether all artifacts are safe to write.
124
+ * @returns {{ command: string }}
125
+ */
126
+ function chooseNextAction(operation, artifacts, safeToWrite) {
127
+ // Statuses that require adoption
128
+ const BLOCKING = new Set([
129
+ 'ADOPTION_REQUIRED', 'CONFLICT', 'BASE_UNAVAILABLE',
130
+ 'POLICY_INVALID', 'POLICY_CHANGE_PENDING',
131
+ ]);
132
+
133
+ // Statuses that need acceptance
134
+ const NEEDS_ACCEPT = new Set(['GENERATOR_CHANGED', 'MERGEABLE']);
135
+
136
+ if (operation === 'init') {
137
+ // Init always recommends adopt with bootstrap-plan
138
+ return Object.freeze({
139
+ command: 'artifacts adopt --bootstrap-plan',
140
+ });
141
+ }
142
+
143
+ if (!safeToWrite) {
144
+ // Find the highest-priority blocking status
145
+ const blocking = artifacts.filter((a) => BLOCKING.has(a.status));
146
+ if (blocking.length > 0) {
147
+ return Object.freeze({
148
+ command: 'artifacts adopt --plan',
149
+ });
150
+ }
151
+ }
152
+
153
+ if (artifacts.some((a) => NEEDS_ACCEPT.has(a.status))) {
154
+ return Object.freeze({
155
+ command: 'artifacts accept --plan',
156
+ });
157
+ }
158
+
159
+ return Object.freeze({
160
+ command: 'artifacts apply --plan',
161
+ });
162
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Artifact entry reader and manifest digest.
3
+ *
4
+ * Reads a single artifact entry from the working tree or git object store.
5
+ * Entry kinds: 'absent' | 'regular' | 'tree'.
6
+ *
7
+ * Tree entries carry a manifestDigest computed from a canonical subset of
8
+ * entry fields (path, type, mode, size, sha256) to enable deterministic
9
+ * comparison without materialising full content.
10
+ *
11
+ * @module artifacts/entry
12
+ */
13
+
14
+ import { lstat, readdir, readFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
16
+ import { promisify } from 'node:util';
17
+ import { execFile } from 'node:child_process';
18
+ import { canonicalJson, sha256Hex } from '../core/digest.mjs';
19
+ import { ReleaseError, PATH_UNSAFE } from '../core/errors.mjs';
20
+
21
+ const execFileAsync = promisify(execFile);
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Constants
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Paths to skip during recursive directory enumeration. */
28
+ const SKIP_DIRS = new Set(['.git']);
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Internal helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Convert a Node.js `stat.mode` (decimal) to a git-style octal mode string.
36
+ *
37
+ * Git uses a restricted subset of POSIX modes:
38
+ * - '100644' — regular file
39
+ * - '100755' — executable file
40
+ * - '040000' — tree (directory)
41
+ * - '120000' — symlink
42
+ * - '160000' — gitlink (submodule)
43
+ *
44
+ * For files, group-write is stripped to produce '100644' unless the
45
+ * executable bit is set, in which case '100755' is returned.
46
+ *
47
+ * @param {import('node:fs').Stats} stat
48
+ * @returns {string} Git octal mode string.
49
+ */
50
+ function statToGitMode(stat) {
51
+ if (stat.isDirectory()) return '040000';
52
+ if (stat.isSymbolicLink()) return '120000';
53
+ // Regular file: check executable bit (owner + group + other)
54
+ const mode = stat.mode & 0o777;
55
+ const executable = (mode & 0o111) !== 0;
56
+ return executable ? '100755' : '100644';
57
+ }
58
+
59
+ /**
60
+ * Compute the git blob object ID (SHA-1) for a file's content.
61
+ *
62
+ * Uses `git hash-object` without `-w` so no objects are written to the
63
+ * repository's object store.
64
+ *
65
+ * @param {string} root - Repository root (cwd for git).
66
+ * @param {string} absPath - Absolute path to the file.
67
+ * @returns {Promise<string>} 40-character hex SHA-1.
68
+ */
69
+ async function gitHashObject(root, absPath) {
70
+ const { stdout } = await execFileAsync(
71
+ 'git',
72
+ ['hash-object', absPath],
73
+ { cwd: root, shell: false },
74
+ );
75
+ return stdout.trim();
76
+ }
77
+
78
+ /**
79
+ * Recursively enumerate files in a directory, returning entry metadata.
80
+ *
81
+ * Directories and files under `.git/` are skipped. Symbolic links and
82
+ * hardlinks (nlink > 1) cause an immediate PATH_UNSAFE error (v1 policy).
83
+ *
84
+ * @param {string} root - Repository root (for git hash-object).
85
+ * @param {string} dirPath - Absolute path to the directory being enumerated.
86
+ * @param {string} relBase - Relative path prefix for entries within this tree.
87
+ * @returns {Promise<Array<{path:string, type:string, mode:string, gitOid:string, sha256:string, size:number}>>}
88
+ */
89
+ async function enumerateTreeEntries(root, dirPath, relBase) {
90
+ const entries = [];
91
+ const items = await readdir(dirPath, { withFileTypes: true });
92
+
93
+ for (const item of items) {
94
+ if (SKIP_DIRS.has(item.name)) continue;
95
+
96
+ const absPath = join(dirPath, item.name);
97
+ const relPath = relBase ? `${relBase}/${item.name}` : item.name;
98
+ const st = await lstat(absPath);
99
+
100
+ if (st.isSymbolicLink()) {
101
+ throw new ReleaseError(
102
+ PATH_UNSAFE,
103
+ `symlink encountered in tree: ${relPath}`,
104
+ { path: relPath },
105
+ );
106
+ }
107
+
108
+ if (!st.isDirectory() && st.nlink > 1) {
109
+ throw new ReleaseError(
110
+ PATH_UNSAFE,
111
+ `hardlink detected (nlink=${st.nlink}): ${relPath}`,
112
+ { path: relPath, nlink: st.nlink },
113
+ );
114
+ }
115
+
116
+ if (st.isDirectory()) {
117
+ const subEntries = await enumerateTreeEntries(root, absPath, relPath);
118
+ entries.push(...subEntries);
119
+ } else {
120
+ const content = await readFile(absPath);
121
+ entries.push(
122
+ Object.freeze({
123
+ path: relPath,
124
+ type: 'blob',
125
+ mode: statToGitMode(st),
126
+ gitOid: await gitHashObject(root, absPath),
127
+ sha256: sha256Hex(content),
128
+ size: st.size,
129
+ }),
130
+ );
131
+ }
132
+ }
133
+
134
+ // Sort by path for deterministic ordering
135
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
136
+ return entries;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Manifest digest
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Compute a deterministic manifest digest for a set of tree entries.
145
+ *
146
+ * The digest covers path, type, mode, size, and sha256 of each entry
147
+ * (gitOid is excluded because it depends on the git hash algorithm, which
148
+ * is SHA-1 in current git and not guaranteed stable across implementations).
149
+ *
150
+ * @param {Array<{path:string, type:string, mode:string, sha256:string, size:number}>} entries
151
+ * @returns {string} `sha256:<hex>` digest.
152
+ */
153
+ export function digestEntryManifest(entries) {
154
+ const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
155
+ path, type, mode, size, sha256,
156
+ }));
157
+ return `sha256:${sha256Hex(canonicalJson(canonical))}`;
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Public API
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /**
165
+ * Read a single artifact entry from the working tree.
166
+ *
167
+ * Entry kinds:
168
+ * - `{ kind: 'absent' }` — path does not exist on disk.
169
+ * - `{ kind: 'regular', path, type, mode, gitOid, sha256, size }` — a single file.
170
+ * - `{ kind: 'tree', entries, manifestDigest }` — a directory; `entries` is
171
+ * the recursive enumeration of all files within it.
172
+ *
173
+ * Symbolic links and hardlinks (nlink > 1) are rejected with PATH_UNSAFE
174
+ * (v1 policy: writable/publish entries only allow absent, regular file, tree).
175
+ *
176
+ * @param {object} options
177
+ * @param {string} options.root - Repository root (absolute).
178
+ * @param {string} options.path - Relative POSIX path to the entry.
179
+ * @param {'worktree'} [options.source='worktree'] - Read source.
180
+ * @returns {Promise<ArtifactEntry>}
181
+ * @throws {ReleaseError} PATH_UNSAFE on symlink or hardlink.
182
+ */
183
+ export async function readEntry({ root, path, source = 'worktree' } = {}) {
184
+ if (source !== 'worktree') {
185
+ throw new ReleaseError(PATH_UNSAFE, `unsupported readEntry source: ${source}`, { source });
186
+ }
187
+
188
+ const absPath = join(root, path);
189
+
190
+ let st;
191
+ try {
192
+ st = await lstat(absPath);
193
+ } catch (err) {
194
+ if (err.code === 'ENOENT' || err.code === 'ENOTDIR') {
195
+ return Object.freeze({ kind: 'absent' });
196
+ }
197
+ throw err;
198
+ }
199
+
200
+ // Symlinks are always unsafe (v1)
201
+ if (st.isSymbolicLink()) {
202
+ throw new ReleaseError(
203
+ PATH_UNSAFE,
204
+ `symlink encountered: ${path}`,
205
+ { path },
206
+ );
207
+ }
208
+
209
+ // Directory → tree entry with recursive manifest
210
+ if (st.isDirectory()) {
211
+ const entries = await enumerateTreeEntries(root, absPath, path);
212
+ const manifestDigest = digestEntryManifest(entries);
213
+ return Object.freeze({
214
+ kind: 'tree',
215
+ entries: Object.freeze(entries),
216
+ manifestDigest,
217
+ });
218
+ }
219
+
220
+ // Hardlink detection (v1: reject nlink > 1)
221
+ if (st.nlink > 1) {
222
+ throw new ReleaseError(
223
+ PATH_UNSAFE,
224
+ `hardlink detected (nlink=${st.nlink}): ${path}`,
225
+ { path, nlink: st.nlink },
226
+ );
227
+ }
228
+
229
+ // Regular file
230
+ const content = await readFile(absPath);
231
+ return Object.freeze({
232
+ kind: 'regular',
233
+ path,
234
+ type: 'blob',
235
+ mode: statToGitMode(st),
236
+ gitOid: await gitHashObject(root, absPath),
237
+ sha256: sha256Hex(content),
238
+ size: st.size,
239
+ });
240
+ }