release-skill 0.6.1 → 0.6.3

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 (77) 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 +40 -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 +129 -9
  11. package/README.zh-CN.md +111 -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/release-skill.bundle.mjs +6596 -1586
  15. package/adapters/claude/schemas/.render-manifest.json +10 -6
  16. package/adapters/claude/schemas/postpublish-approval-record.schema.json +47 -0
  17. package/adapters/claude/schemas/release-plan.schema.json +74 -1
  18. package/adapters/claude/schemas/release-project.schema.json +73 -1
  19. package/adapters/claude/schemas/release-run.schema.json +11 -6
  20. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  21. package/adapters/codex/bin/release-skill.bundle.mjs +6596 -1586
  22. package/adapters/codex/schemas/.render-manifest.json +10 -6
  23. package/adapters/codex/schemas/postpublish-approval-record.schema.json +47 -0
  24. package/adapters/codex/schemas/release-plan.schema.json +74 -1
  25. package/adapters/codex/schemas/release-project.schema.json +73 -1
  26. package/adapters/codex/schemas/release-run.schema.json +11 -6
  27. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  28. package/adapters/kimi/bin/release-skill.bundle.mjs +6596 -1586
  29. package/adapters/kimi/schemas/.render-manifest.json +10 -6
  30. package/adapters/kimi/schemas/postpublish-approval-record.schema.json +47 -0
  31. package/adapters/kimi/schemas/release-plan.schema.json +74 -1
  32. package/adapters/kimi/schemas/release-project.schema.json +73 -1
  33. package/adapters/kimi/schemas/release-run.schema.json +11 -6
  34. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  35. package/adapters/workbuddy/bin/release-skill.bundle.mjs +6596 -1586
  36. package/adapters/workbuddy/schemas/.render-manifest.json +10 -6
  37. package/adapters/workbuddy/schemas/postpublish-approval-record.schema.json +47 -0
  38. package/adapters/workbuddy/schemas/release-plan.schema.json +74 -1
  39. package/adapters/workbuddy/schemas/release-project.schema.json +73 -1
  40. package/adapters/workbuddy/schemas/release-run.schema.json +11 -6
  41. package/bin/release-skill-cli.mjs +181 -3
  42. package/bin/release-skill.bundle.mjs +6596 -1586
  43. package/package.json +2 -1
  44. package/platform-manifest.json +4 -4
  45. package/references/.render-manifest.json +5 -5
  46. package/references/01-state-machine.md +22 -2
  47. package/schemas/.render-manifest.json +10 -6
  48. package/schemas/postpublish-approval-record.schema.json +47 -0
  49. package/schemas/release-plan.schema.json +74 -1
  50. package/schemas/release-project.schema.json +73 -1
  51. package/schemas/release-run.schema.json +11 -6
  52. package/src/commands/approve.mjs +167 -1
  53. package/src/commands/distribute.mjs +411 -33
  54. package/src/commands/lineage.mjs +101 -32
  55. package/src/commands/postverify.mjs +734 -0
  56. package/src/commands/prepare.mjs +339 -43
  57. package/src/commands/publish.mjs +10 -1
  58. package/src/commands/setup.mjs +715 -0
  59. package/src/commands/ship.mjs +152 -5
  60. package/src/commands/verify.mjs +92 -15
  61. package/src/core/approval.mjs +93 -68
  62. package/src/core/bounded-output.mjs +46 -0
  63. package/src/core/derived-artifact-gates.mjs +258 -0
  64. package/src/core/docs-refresh-preset.mjs +167 -0
  65. package/src/core/errors.mjs +4 -0
  66. package/src/core/hooks.mjs +28 -0
  67. package/src/core/marketplace-registry-entry.mjs +174 -0
  68. package/src/core/notify-handoff.mjs +76 -0
  69. package/src/core/postpublish-approval.mjs +110 -0
  70. package/src/core/postpublish.mjs +424 -7
  71. package/src/core/preset-executor.mjs +156 -0
  72. package/src/core/preset-gitwrite.mjs +463 -0
  73. package/src/core/presets.mjs +706 -0
  74. package/src/core/proposal-inbox.mjs +630 -0
  75. package/src/core/run.mjs +91 -6
  76. package/src/core/skill-resource-closure.mjs +240 -10
  77. package/src/platforms/registry.mjs +12 -0
@@ -27,7 +27,7 @@
27
27
  * @module commands/lineage
28
28
  */
29
29
 
30
- import { execFile as execFileCb } from 'node:child_process';
30
+ import { execFile as execFileCb, spawn } from 'node:child_process';
31
31
  import { promisify } from 'node:util';
32
32
 
33
33
  const execFile = promisify(execFileCb);
@@ -132,6 +132,50 @@ async function git(root, args, options = {}) {
132
132
  return stdout.trim();
133
133
  }
134
134
 
135
+ /**
136
+ * Run a git command and return RAW stdout (no trimming). Trailing bytes are
137
+ * significant for `cat-file commit` output: the message tail (trailing
138
+ * whitespace / blank lines) must survive untouched.
139
+ */
140
+ async function gitRaw(root, args, options = {}) {
141
+ const { stdout } = await execFile('git', args, {
142
+ cwd: root,
143
+ encoding: 'utf8',
144
+ maxBuffer: 64 * 1024 * 1024,
145
+ ...options,
146
+ });
147
+ return stdout;
148
+ }
149
+
150
+ /**
151
+ * Parse a commit ident line — the value of an `author`/`committer` header in
152
+ * the raw commit object — of the canonical form `Name <email> ts tz`.
153
+ *
154
+ * The anchored regex uses a greedy `.*` so the LAST ` <email> ts tz` group at
155
+ * the end of the line wins: names containing spaces (or even '<'/'>' — legal
156
+ * in stored objects) still resolve correctly. This replaces the broken
157
+ * `split(/\s+>/)` parsing, which never matched a legal ident line (there is
158
+ * no whitespace before '>') and silently produced name=<entire line>,
159
+ * email='' — corrupting identities written by `commit-tree` during rebuild
160
+ * (SFA field report 2026-08-18).
161
+ *
162
+ * @param {string} line - e.g. `乌龙 <2505468+mzdbxqh@users.noreply.github.com> 1785824474 +0000`
163
+ * @returns {{ name: string, email: string, ts: string, tz: string }}
164
+ * @throws {Error} when the line is not a valid ident line. Fail-closed:
165
+ * rebuild must never fabricate or silently write an empty identity.
166
+ */
167
+ export function parseCommitIdent(line) {
168
+ if (typeof line !== 'string') {
169
+ throw new Error(`commit ident line must be a string, got ${typeof line}`);
170
+ }
171
+ const match = line.match(/^(.*) <([^>]*)> (\d+) ([+-]\d{4})$/);
172
+ if (!match) {
173
+ throw new Error(`unparseable commit ident line (fail-closed): "${line}"`);
174
+ }
175
+ const [, name, email, ts, tz] = match;
176
+ return { name, email, ts, tz };
177
+ }
178
+
135
179
  /**
136
180
  * List all tags with their commit SHAs.
137
181
  *
@@ -201,45 +245,44 @@ export async function objectExists(root, sha) {
201
245
  /**
202
246
  * Read the raw commit object and extract identity/date fields.
203
247
  *
248
+ * The cat-file output is NOT trimmed: the header block and the message are
249
+ * split at the first blank line and the message tail bytes (trailing
250
+ * whitespace / blank lines) are preserved exactly, so a rebuilt commit can
251
+ * carry byte-identical message bytes.
252
+ *
204
253
  * @param {string} root - repository root
205
254
  * @param {string} commitSha - commit SHA
206
255
  * @returns {Promise<{ message: string, author: { name: string, email: string, date: string }, committer: { name: string, email: string, date: string } }>}
256
+ * @throws {Error} when the object is malformed or an ident line does not
257
+ * parse (fail-closed — never fabricate or silently write empty identity).
207
258
  */
208
259
  export async function readCommitMeta(root, commitSha) {
209
- const raw = await git(root, ['cat-file', 'commit', commitSha]);
210
- const lines = raw.split('\n');
260
+ const raw = await gitRaw(root, ['cat-file', 'commit', commitSha]);
261
+ const separator = raw.indexOf('\n\n');
262
+ if (separator === -1) {
263
+ throw new Error(`malformed commit object ${commitSha}: missing header/message separator`);
264
+ }
265
+ const message = raw.slice(separator + 2);
211
266
  const headers = {};
212
- const messageLines = [];
213
- let inBody = false;
214
- for (const line of lines) {
215
- if (inBody) {
216
- messageLines.push(line);
217
- continue;
218
- }
219
- if (line === '') {
220
- inBody = true;
221
- continue;
222
- }
223
- const colon = line.indexOf(' ');
224
- if (colon === -1) continue;
225
- const key = line.slice(0, colon);
226
- const value = line.slice(colon + 1);
227
- headers[key] = value;
267
+ for (const line of raw.slice(0, separator).split('\n')) {
268
+ if (line.startsWith(' ')) continue; // continuation line (e.g. gpgsig)
269
+ const space = line.indexOf(' ');
270
+ if (space === -1) continue;
271
+ headers[line.slice(0, space)] = line.slice(space + 1);
228
272
  }
229
- const message = messageLines.join('\n');
230
- const authorParts = (headers.author ?? '').split(/\s+>/);
231
- const committerParts = (headers.committer ?? '').split(/\s+>/);
273
+ const author = parseCommitIdent(headers.author ?? '');
274
+ const committer = parseCommitIdent(headers.committer ?? '');
232
275
  return {
233
276
  message,
234
277
  author: {
235
- name: authorParts[0] ?? '',
236
- email: (authorParts[1] ?? '').replace(/^</, '').replace(/\d+ [+-]\d{4}$/, '').trim(),
237
- date: (headers.author ?? '').match(/(\d+ [+-]\d{4})$/)?.[1] ?? '',
278
+ name: author.name,
279
+ email: author.email,
280
+ date: `${author.ts} ${author.tz}`,
238
281
  },
239
282
  committer: {
240
- name: committerParts[0] ?? '',
241
- email: (committerParts[1] ?? '').replace(/^</, '').replace(/\d+ [+-]\d{4}$/, '').trim(),
242
- date: (headers.committer ?? '').match(/(\d+ [+-]\d{4})$/)?.[1] ?? '',
283
+ name: committer.name,
284
+ email: committer.email,
285
+ date: `${committer.ts} ${committer.tz}`,
243
286
  },
244
287
  };
245
288
  }
@@ -384,20 +427,25 @@ export async function analyzeLineage(root) {
384
427
  * Create a rebuilt commit object with `commit-tree`, preserving the original
385
428
  * author/committer identity and dates via environment variables.
386
429
  *
430
+ * The message is delivered via stdin (`-F -`) rather than `-m`: `commit-tree
431
+ * -m` strips trailing blank lines, while stdin preserves the original message
432
+ * bytes exactly (including trailing whitespace), so a rebuilt node can be
433
+ * byte-identical to the original commit.
434
+ *
387
435
  * Writes ONLY a commit object into the local object database. Never writes a
388
436
  * ref and never pushes.
389
437
  *
390
438
  * @param {string} root - repository root
391
439
  * @param {string} treeSha - tree hash for the rebuilt commit
392
440
  * @param {string|null} parentSha - parent commit (null → chain root)
393
- * @param {string} message - commit message
441
+ * @param {string} message - commit message (exact bytes)
394
442
  * @param {{ author: {name: string, email: string, date: string}, committer: {name: string, email: string, date: string} }} identity
395
443
  * @returns {Promise<string>} rebuilt commit SHA
396
444
  */
397
445
  export async function createRebuiltCommit(root, treeSha, parentSha, message, identity) {
398
446
  const args = ['commit-tree', treeSha];
399
447
  if (parentSha) args.push('-p', parentSha);
400
- args.push('-m', message);
448
+ args.push('-F', '-');
401
449
  const env = {
402
450
  ...process.env,
403
451
  GIT_AUTHOR_NAME: identity.author.name,
@@ -407,8 +455,29 @@ export async function createRebuiltCommit(root, treeSha, parentSha, message, ide
407
455
  GIT_COMMITTER_EMAIL: identity.committer.email,
408
456
  GIT_COMMITTER_DATE: identity.committer.date,
409
457
  };
410
- const { stdout } = await execFile('git', args, { cwd: root, encoding: 'utf8', env });
411
- return stdout.trim();
458
+ // Async execFile has no stdin-input support (options.input would be ignored
459
+ // and the child would hang waiting on an open stdin), so spawn is used to
460
+ // deliver the exact message bytes on stdin.
461
+ return new Promise((resolvePromise, rejectPromise) => {
462
+ const child = spawn('git', args, { cwd: root, env });
463
+ let stdout = '';
464
+ let stderr = '';
465
+ child.stdout.setEncoding('utf8');
466
+ child.stderr.setEncoding('utf8');
467
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
468
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
469
+ child.on('error', rejectPromise);
470
+ child.on('close', (code) => {
471
+ if (code !== 0) {
472
+ rejectPromise(new Error(`git commit-tree failed (exit ${code}): ${stderr.trim()}`));
473
+ return;
474
+ }
475
+ resolvePromise(stdout.trim());
476
+ });
477
+ child.stdin.on('error', () => {}); // EPIPE after early exit is reported via close
478
+ child.stdin.write(message);
479
+ child.stdin.end();
480
+ });
412
481
  }
413
482
 
414
483
  /**