release-skill 0.6.3 → 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 (89) 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 +25 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +25 -17
  10. package/README.zh-CN.md +16 -14
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/error-codes.json +1 -1
  14. package/adapters/claude/bin/kernel-protocol.json +1 -1
  15. package/adapters/claude/bin/license-texts/Apache-2.0.txt +201 -0
  16. package/adapters/claude/bin/license-texts/MIT.txt +21 -0
  17. package/adapters/claude/bin/registry.json +64 -1
  18. package/adapters/claude/bin/release-skill.bundle.mjs +34860 -31252
  19. package/adapters/claude/bin/rules.json +1 -1
  20. package/adapters/claude/schemas/.render-manifest.json +6 -6
  21. package/adapters/claude/schemas/release-plan.schema.json +136 -4
  22. package/adapters/claude/schemas/release-project.schema.json +13 -4
  23. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  24. package/adapters/codex/bin/error-codes.json +1 -1
  25. package/adapters/codex/bin/kernel-protocol.json +1 -1
  26. package/adapters/codex/bin/license-texts/Apache-2.0.txt +201 -0
  27. package/adapters/codex/bin/license-texts/MIT.txt +21 -0
  28. package/adapters/codex/bin/registry.json +64 -1
  29. package/adapters/codex/bin/release-skill.bundle.mjs +34860 -31252
  30. package/adapters/codex/bin/rules.json +1 -1
  31. package/adapters/codex/schemas/.render-manifest.json +6 -6
  32. package/adapters/codex/schemas/release-plan.schema.json +136 -4
  33. package/adapters/codex/schemas/release-project.schema.json +13 -4
  34. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  35. package/adapters/kimi/bin/error-codes.json +1 -1
  36. package/adapters/kimi/bin/kernel-protocol.json +1 -1
  37. package/adapters/kimi/bin/license-texts/Apache-2.0.txt +201 -0
  38. package/adapters/kimi/bin/license-texts/MIT.txt +21 -0
  39. package/adapters/kimi/bin/registry.json +64 -1
  40. package/adapters/kimi/bin/release-skill.bundle.mjs +34860 -31252
  41. package/adapters/kimi/bin/rules.json +1 -1
  42. package/adapters/kimi/schemas/.render-manifest.json +6 -6
  43. package/adapters/kimi/schemas/release-plan.schema.json +136 -4
  44. package/adapters/kimi/schemas/release-project.schema.json +13 -4
  45. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  46. package/adapters/workbuddy/bin/error-codes.json +1 -1
  47. package/adapters/workbuddy/bin/kernel-protocol.json +1 -1
  48. package/adapters/workbuddy/bin/license-texts/Apache-2.0.txt +201 -0
  49. package/adapters/workbuddy/bin/license-texts/MIT.txt +21 -0
  50. package/adapters/workbuddy/bin/registry.json +64 -1
  51. package/adapters/workbuddy/bin/release-skill.bundle.mjs +34860 -31252
  52. package/adapters/workbuddy/bin/rules.json +1 -1
  53. package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
  54. package/adapters/workbuddy/schemas/release-plan.schema.json +136 -4
  55. package/adapters/workbuddy/schemas/release-project.schema.json +13 -4
  56. package/bin/error-codes.json +1 -1
  57. package/bin/kernel-protocol.json +1 -1
  58. package/bin/license-texts/Apache-2.0.txt +201 -0
  59. package/bin/license-texts/MIT.txt +21 -0
  60. package/bin/registry.json +64 -1
  61. package/bin/release-skill-cli.mjs +6 -6
  62. package/bin/release-skill.bundle.mjs +34860 -31252
  63. package/bin/rules.json +1 -1
  64. package/package.json +5 -4
  65. package/platform-manifest.json +2 -2
  66. package/references/02-project-config.md +1 -1
  67. package/references/06-adapter-contract.md +6 -3
  68. package/schemas/.render-manifest.json +6 -6
  69. package/schemas/release-plan.schema.json +136 -4
  70. package/schemas/release-project.schema.json +13 -4
  71. package/src/adapters/distribute-git.mjs +17 -11
  72. package/src/commands/approve.mjs +6 -10
  73. package/src/commands/distribute.mjs +251 -69
  74. package/src/commands/postverify.mjs +81 -7
  75. package/src/commands/prepare.mjs +176 -3
  76. package/src/core/docs-refresh-preset.mjs +8 -4
  77. package/src/core/evidence.mjs +8 -1
  78. package/src/core/git-url-policy.mjs +214 -0
  79. package/src/core/marketplace-registry-entry.mjs +4 -3
  80. package/src/core/postpublish-approval.mjs +132 -1
  81. package/src/core/postpublish-bundle.mjs +342 -0
  82. package/src/core/postpublish-projection.mjs +210 -0
  83. package/src/core/postpublish.mjs +26 -19
  84. package/src/core/preset-executor.mjs +25 -6
  85. package/src/core/preset-gitwrite.mjs +37 -16
  86. package/src/core/presets.mjs +67 -19
  87. package/src/core/proposal-inbox.mjs +36 -20
  88. package/src/core/redact.mjs +16 -3
  89. package/src/producers/build-adapters.mjs +28 -14
@@ -48,6 +48,11 @@ import {
48
48
  POST_PUBLISH_VERIFY_FAILED,
49
49
  } from './errors.mjs';
50
50
  import { preflightPresetWorkspace, assertPresetWorkspaceExecution } from './presets.mjs';
51
+ import {
52
+ checkGitRemoteUrl,
53
+ describeGitRemoteUrlFailure,
54
+ redactUrlCredentialsIfPresent,
55
+ } from './git-url-policy.mjs';
51
56
 
52
57
  const execFileAsync = promisify(execFileCb);
53
58
 
@@ -60,7 +65,6 @@ const GIT_TIMEOUT_MS = 120_000;
60
65
  const TRANSFER_TIMEOUT_MS = 300_000;
61
66
  const TMP_PREFIX = 'release-skill-proposal-';
62
67
 
63
- const SAFE_REMOTE_URL_RE = /^(?:https?|file):\/\/.+\.git$/;
64
68
  const SAFE_BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
65
69
  const SHA_RE = /^[a-f0-9]{40}$/;
66
70
 
@@ -105,11 +109,12 @@ function hasControlChars(value) {
105
109
  }
106
110
 
107
111
  function assertSafeRemoteUrl(remoteUrl) {
108
- if (typeof remoteUrl !== 'string' || !SAFE_REMOTE_URL_RE.test(remoteUrl) || hasControlChars(remoteUrl)) {
112
+ const verdict = checkGitRemoteUrl(remoteUrl);
113
+ if (!verdict.ok) {
109
114
  throw new ReleaseError(
110
115
  GATE_FAILED,
111
- 'proposal-inbox remoteUrl must be an http(s)/file URL ending in .git',
112
- { remoteUrl },
116
+ `proposal-inbox remoteUrl ${describeGitRemoteUrlFailure(verdict.reason)}`,
117
+ { reason: verdict.reason },
113
118
  );
114
119
  }
115
120
  }
@@ -240,7 +245,8 @@ export function buildProposalDocument(context) {
240
245
  * @returns {string}
241
246
  */
242
247
  export function buildManualSyncPrompt({ remoteUrl, proposalPath, branch }) {
243
- return `manual sync required: review proposal ${proposalPath} in downstream repository ${remoteUrl} (branch ${branch}); suggested action: open a pull request or apply the proposal through the downstream governance workflow`;
248
+ const safeRemoteUrl = redactUrlCredentialsIfPresent(remoteUrl);
249
+ return `manual sync required: review proposal ${proposalPath} in downstream repository ${safeRemoteUrl} (branch ${branch}); suggested action: open a pull request or apply the proposal through the downstream governance workflow`;
244
250
  }
245
251
 
246
252
  // ---------------------------------------------------------------------------
@@ -275,7 +281,7 @@ export async function crossCheckPushedCommit(exec, remoteUrl, branch, pushedComm
275
281
  throw new ReleaseError(
276
282
  POST_PUBLISH_VERIFY_FAILED,
277
283
  `post-push cross-check failed: remote branch ${branch} is at ${observed ?? '<missing>'}, which disagrees with the pushed commit ${pushedCommit}`,
278
- { remoteUrl, branch, pushedCommit, observed },
284
+ { remoteUrl: redactUrlCredentialsIfPresent(remoteUrl), branch, pushedCommit, observed },
279
285
  );
280
286
  }
281
287
  return observed;
@@ -333,12 +339,13 @@ export async function deliverProposalGitPush(params) {
333
339
  });
334
340
  } catch (error) {
335
341
  const classification = classifyNetFailure(stderrText(error));
342
+ const safeRemoteUrl = redactUrlCredentialsIfPresent(remoteUrl);
336
343
  throw new ReleaseError(
337
344
  classification === 'auth' ? REMOTE_CONFLICT : REMOTE_UNAVAILABLE,
338
345
  classification === 'auth'
339
- ? `proposal-inbox remote refused authentication for ${remoteUrl}; the host keychain credential is reused and never prompted, read, or retried`
340
- : `cannot reach proposal-inbox remote ${remoteUrl} — start VPN / check network; delivery fails closed and never retries with credentials`,
341
- { remoteUrl, stderrTail: stderrTail(error) },
346
+ ? `proposal-inbox remote refused authentication for ${safeRemoteUrl}; the host keychain credential is reused and never prompted, read, or retried`
347
+ : `cannot reach proposal-inbox remote ${safeRemoteUrl} — start VPN / check network; delivery fails closed and never retries with credentials`,
348
+ { remoteUrl: safeRemoteUrl, stderrTail: stderrTail(error) },
342
349
  );
343
350
  }
344
351
 
@@ -375,8 +382,8 @@ export async function deliverProposalGitPush(params) {
375
382
  }
376
383
  throw new ReleaseError(
377
384
  REMOTE_CONFLICT,
378
- `proposal ${proposalPath} already exists in ${remoteUrl} with different content; overwriting a downstream proposal requires a human decision`,
379
- { remoteUrl, proposalPath },
385
+ `proposal ${proposalPath} already exists in ${redactUrlCredentialsIfPresent(remoteUrl)} with different content; overwriting a downstream proposal requires a human decision`,
386
+ { remoteUrl: redactUrlCredentialsIfPresent(remoteUrl), proposalPath },
380
387
  );
381
388
  }
382
389
 
@@ -401,12 +408,13 @@ export async function deliverProposalGitPush(params) {
401
408
  });
402
409
  } catch (error) {
403
410
  const classification = classifyNetFailure(stderrText(error));
411
+ const safeRemoteUrl = redactUrlCredentialsIfPresent(remoteUrl);
404
412
  throw new ReleaseError(
405
413
  classification === 'auth' ? REMOTE_CONFLICT : REMOTE_UNAVAILABLE,
406
414
  classification === 'auth'
407
- ? `proposal-inbox push refused authentication for ${remoteUrl}; the host keychain credential is reused and never prompted, read, or retried`
408
- : `proposal-inbox push to ${remoteUrl} failed — start VPN / check network; delivery fails closed and never retries with credentials`,
409
- { remoteUrl, branch, stderrTail: stderrTail(error) },
415
+ ? `proposal-inbox push refused authentication for ${safeRemoteUrl}; the host keychain credential is reused and never prompted, read, or retried`
416
+ : `proposal-inbox push to ${safeRemoteUrl} failed — start VPN / check network; delivery fails closed and never retries with credentials`,
417
+ { remoteUrl: safeRemoteUrl, branch, stderrTail: stderrTail(error) },
410
418
  );
411
419
  }
412
420
 
@@ -581,29 +589,37 @@ export async function deliverProposalLocalFile(params) {
581
589
  * deliver it into the local checkout (never pushed). Shared by the
582
590
  * distribute saga and the postVerify run.
583
591
  *
592
+ * F-04: `releaseWorkspaceRoot` is the real project root — the ONLY
593
+ * resolution basis for `config.target.workspace` and the release-workspace
594
+ * write exclusion. The detached execution worktree is never passed here.
595
+ *
584
596
  * @param {object} params
585
597
  * @param {object} params.hook - Declared hook entry (config.target bound).
586
598
  * @param {object} params.contextProjection - The §2.3 context projection.
587
599
  * @param {object} params.commitIdentity - Frozen commitIdentity.
588
- * @param {string} params.root - Release workspace root (workspace resolution).
600
+ * @param {string} params.releaseWorkspaceRoot - Release workspace root
601
+ * (workspace resolution + release-workspace write exclusion).
589
602
  * @param {Function} [params.exec] - Injectable git exec (tests).
590
603
  * @returns {Promise<{ status: string, observation: object, proposalPath: string, manualSyncPrompt: string }>}
591
604
  */
592
605
  export async function executeProposalInboxLocalFileHook(params) {
593
- const { hook, contextProjection, commitIdentity, root, exec } = params ?? {};
606
+ const { hook, contextProjection, commitIdentity, releaseWorkspaceRoot, exec } = params ?? {};
594
607
  const target = hook?.config?.target;
595
608
  if (!target || typeof target.workspace !== 'string' || typeof target.branch !== 'string') {
596
609
  throw new ReleaseError(GATE_FAILED, 'proposal-inbox local-file requires config.target.workspace and config.target.branch');
597
610
  }
598
- if (typeof root !== 'string' || root.length === 0) {
599
- throw new ReleaseError(GATE_FAILED, 'proposal-inbox local-file requires the release workspace root');
611
+ if (typeof releaseWorkspaceRoot !== 'string' || releaseWorkspaceRoot.length === 0) {
612
+ throw new ReleaseError(
613
+ GATE_FAILED,
614
+ 'proposal-inbox local-file requires the release workspace root (releaseWorkspaceRoot); the detached execution worktree is never the release workspace',
615
+ );
600
616
  }
601
617
 
602
618
  // §2.6 workspace execution checks: preflight realpath, TOCTOU re-check,
603
619
  // release-workspace/runtime-directory exclusion — all before any write.
604
- const preflight = await preflightPresetWorkspace(target.workspace, { root });
620
+ const preflight = await preflightPresetWorkspace(target.workspace, { releaseWorkspaceRoot });
605
621
  const execution = await assertPresetWorkspaceExecution(target.workspace, {
606
- root,
622
+ releaseWorkspaceRoot,
607
623
  preflightRealpath: preflight.realpath,
608
624
  });
609
625
 
@@ -23,16 +23,26 @@
23
23
  * sha256 digests, stable field names, and fragment-anchored JSON pointers
24
24
  * (#/required) are likewise preserved verbatim. Any other two-or-more
25
25
  * segment '/'-led token stays redacted as the fail-closed default.
26
+ * - URL userinfo (F-05): strings additionally pass through the Git URL
27
+ * policy boundary (core/git-url-policy.mjs), which strips embedded
28
+ * username/password from any credential-bearing URL span and delegates the
29
+ * actual redaction to the Foundation redactor. Credential-free URLs and
30
+ * all non-URL text pass through unchanged, so error prose keeps its
31
+ * diagnostic value.
26
32
  * - Arrays and plain objects: recursed; every string value is redacted.
27
33
  * - Everything else (numbers, booleans, null, undefined, and non-plain
28
34
  * objects such as Buffers/Dates/Maps) is returned untouched.
29
35
  *
30
- * Pure and zero-dependency: no imports from src/artifacts/* (no cycles, no
31
- * cross-layer coupling); node built-ins only (none required).
36
+ * Pure; no imports from src/artifacts/* (no cycles, no cross-layer
37
+ * coupling). The only local dependency is the release-domain Git URL policy
38
+ * boundary, which itself only consumes the exact released Foundation
39
+ * package (no top-level await anywhere in this module graph).
32
40
  *
33
41
  * @module core/redact
34
42
  */
35
43
 
44
+ import { redactEmbeddedUrlCredentials } from './git-url-policy.mjs';
45
+
36
46
  /** Stable placeholder substituted for every redacted absolute path. */
37
47
  export const REDACTED_PATH_PLACEHOLDER = '<redacted-path>';
38
48
 
@@ -160,7 +170,7 @@ function isUncPath(token) {
160
170
  * @returns {string}
161
171
  */
162
172
  function redactString(input) {
163
- return input.replace(PATH_TOKEN_RE, (raw) => {
173
+ const pathRedacted = input.replace(PATH_TOKEN_RE, (raw) => {
164
174
  const match = TRAILING_PUNCT_RE.exec(raw);
165
175
  const core = match[1];
166
176
  const tail = match[2];
@@ -169,6 +179,9 @@ function redactString(input) {
169
179
  }
170
180
  return raw;
171
181
  });
182
+ // F-05: strip embedded URL credentials (delegates to the Foundation
183
+ // redactor through the Git URL policy boundary).
184
+ return redactEmbeddedUrlCredentials(pathRedacted);
172
185
  }
173
186
 
174
187
  /**
@@ -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
  }