release-skill 0.4.1 → 0.5.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 (100) 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 +41 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +27 -9
  10. package/README.zh-CN.md +23 -8
  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 +154 -0
  14. package/adapters/claude/bin/kernel-protocol.json +60 -0
  15. package/adapters/claude/bin/registry.json +165 -0
  16. package/adapters/claude/bin/release-skill.bundle.mjs +3918 -1462
  17. package/adapters/claude/bin/rules.json +104 -0
  18. package/adapters/claude/schemas/release-plan.schema.json +5 -0
  19. package/adapters/claude/schemas/release-project.schema.json +5 -0
  20. package/adapters/claude/skills/release-assess/SKILL.md +2 -0
  21. package/adapters/claude/skills/release-help/SKILL.md +3 -1
  22. package/adapters/claude/skills/release-prepare/SKILL.md +2 -2
  23. package/adapters/claude/skills/release-verify/SKILL.md +2 -0
  24. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  25. package/adapters/codex/bin/error-codes.json +154 -0
  26. package/adapters/codex/bin/kernel-protocol.json +60 -0
  27. package/adapters/codex/bin/registry.json +165 -0
  28. package/adapters/codex/bin/release-skill.bundle.mjs +3918 -1462
  29. package/adapters/codex/bin/rules.json +104 -0
  30. package/adapters/codex/schemas/release-plan.schema.json +5 -0
  31. package/adapters/codex/schemas/release-project.schema.json +5 -0
  32. package/adapters/codex/skills/release-assess/SKILL.md +2 -0
  33. package/adapters/codex/skills/release-help/SKILL.md +3 -1
  34. package/adapters/codex/skills/release-prepare/SKILL.md +2 -2
  35. package/adapters/codex/skills/release-verify/SKILL.md +2 -0
  36. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  37. package/adapters/kimi/bin/error-codes.json +154 -0
  38. package/adapters/kimi/bin/kernel-protocol.json +60 -0
  39. package/adapters/kimi/bin/registry.json +165 -0
  40. package/adapters/kimi/bin/release-skill.bundle.mjs +3918 -1462
  41. package/adapters/kimi/bin/rules.json +104 -0
  42. package/adapters/kimi/schemas/release-plan.schema.json +5 -0
  43. package/adapters/kimi/schemas/release-project.schema.json +5 -0
  44. package/adapters/kimi/skills/release-assess/SKILL.md +2 -0
  45. package/adapters/kimi/skills/release-help/SKILL.md +3 -1
  46. package/adapters/kimi/skills/release-prepare/SKILL.md +2 -2
  47. package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
  48. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  49. package/adapters/workbuddy/bin/error-codes.json +154 -0
  50. package/adapters/workbuddy/bin/kernel-protocol.json +60 -0
  51. package/adapters/workbuddy/bin/registry.json +165 -0
  52. package/adapters/workbuddy/bin/release-skill.bundle.mjs +3918 -1462
  53. package/adapters/workbuddy/bin/rules.json +104 -0
  54. package/adapters/workbuddy/schemas/release-plan.schema.json +5 -0
  55. package/adapters/workbuddy/schemas/release-project.schema.json +5 -0
  56. package/adapters/workbuddy/skills/release-assess/SKILL.md +2 -0
  57. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -1
  58. package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -2
  59. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
  60. package/bin/error-codes.json +154 -0
  61. package/bin/kernel-protocol.json +60 -0
  62. package/bin/registry.json +165 -0
  63. package/bin/release-skill.bundle.mjs +3918 -1462
  64. package/bin/rules.json +104 -0
  65. package/package.json +3 -1
  66. package/schemas/release-plan.schema.json +5 -0
  67. package/schemas/release-project.schema.json +5 -0
  68. package/scripts/build-bundle.mjs +159 -5
  69. package/scripts/sync-public-files.mjs +5 -0
  70. package/skills/release-assess/SKILL.md +2 -0
  71. package/skills/release-help/SKILL.md +3 -1
  72. package/skills/release-prepare/SKILL.md +2 -2
  73. package/skills/release-verify/SKILL.md +2 -0
  74. package/skills-src/release-assess/SKILL.md +2 -0
  75. package/skills-src/release-help/SKILL.md +3 -1
  76. package/skills-src/release-prepare/SKILL.md +2 -2
  77. package/skills-src/release-verify/SKILL.md +2 -0
  78. package/src/adapters/contract.mjs +10 -10
  79. package/src/adapters/plugin-marketplace.mjs +21 -5
  80. package/src/artifacts/entry.mjs +4 -1
  81. package/src/artifacts/producer-registry.mjs +9 -6
  82. package/src/artifacts/project-lock.mjs +324 -214
  83. package/src/commands/assess.mjs +60 -2
  84. package/src/commands/attest.mjs +11 -0
  85. package/src/commands/prepare.mjs +19 -13
  86. package/src/core/approval.mjs +5 -2
  87. package/src/core/digest.mjs +47 -12
  88. package/src/core/foundation-inflight.mjs +39 -0
  89. package/src/core/hook-cache.mjs +3 -0
  90. package/src/core/installation-contract.mjs +4 -2
  91. package/src/core/plan.mjs +34 -30
  92. package/src/core/run.mjs +22 -36
  93. package/src/core/skill-resource-closure.mjs +6 -4
  94. package/src/core/trusted-resource.mjs +64 -15
  95. package/src/platforms/codebuddy.mjs +92 -10
  96. package/src/platforms/registry.mjs +4 -3
  97. package/src/producers/build-adapters.mjs +27 -9
  98. package/src/producers/render-public-assets.mjs +12 -7
  99. package/src/producers/sync-skills.mjs +12 -7
  100. package/src/snapshot/public-map.mjs +10 -2
package/bin/rules.json ADDED
@@ -0,0 +1,104 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "kind": "skill-family.contracts.rules",
4
+ "contractsVersion": "1.4.0",
5
+ "budget": {
6
+ "firstVersionMax": 20,
7
+ "absoluteMax": 30,
8
+ "note": "Mechanical gate: runChecks fails with SFC1008 when the mandatory rule count exceeds firstVersionMax, and fails harder when it exceeds absoluteMax. The count includes every rule with severity mandatory, including budget rules themselves."
9
+ },
10
+ "checkTypes": [
11
+ {
12
+ "type": "schema.compile",
13
+ "description": "Each registered schema must compile under its declared dialect; an uncompilable schema fails with SFC1012."
14
+ },
15
+ {
16
+ "type": "schema.unique-id",
17
+ "description": "No two registered schemas may share a $id; duplicates fail with SFC1003."
18
+ },
19
+ {
20
+ "type": "protocol.unique-name",
21
+ "description": "No two registered protocols may share the same name and version; duplicates fail with SFC1004."
22
+ },
23
+ {
24
+ "type": "schema.ref-resolves",
25
+ "description": "Every $ref in registered schemas must resolve within the registered schema set; unresolved targets fail with SFC1005."
26
+ },
27
+ {
28
+ "type": "schema.dialect-declared",
29
+ "description": "Every registered schema must declare a dialect from the supported set, consistent with its $schema URI; violations fail with SFC1006."
30
+ },
31
+ {
32
+ "type": "fixture.positive-passes",
33
+ "description": "Every fixture expecting validity must validate against its target schema under its declared dialect and policy; mismatches fail with SFC1010."
34
+ },
35
+ {
36
+ "type": "fixture.negative-coded",
37
+ "description": "Every fixture expecting invalidity must fail with exactly its declared stable error code; mismatches fail with SFC1010."
38
+ },
39
+ {
40
+ "type": "error-code.registered",
41
+ "description": "Every error code declared in fixture expectations must exist in the frozen error registry; unknown codes fail with SFC1009."
42
+ },
43
+ {
44
+ "type": "rules.budget",
45
+ "description": "The mandatory rule count must stay within the first-version budget (and never exceed the absolute cap); excess fails with SFC1008."
46
+ }
47
+ ],
48
+ "rules": [
49
+ {
50
+ "ruleId": "CR-001",
51
+ "severity": "mandatory",
52
+ "checkType": "schema.compile",
53
+ "target": { "registry": "schemas" }
54
+ },
55
+ {
56
+ "ruleId": "CR-006",
57
+ "severity": "mandatory",
58
+ "checkType": "schema.unique-id",
59
+ "target": { "registry": "schemas" }
60
+ },
61
+ {
62
+ "ruleId": "CR-007",
63
+ "severity": "mandatory",
64
+ "checkType": "protocol.unique-name",
65
+ "target": { "registry": "protocols" }
66
+ },
67
+ {
68
+ "ruleId": "CR-008",
69
+ "severity": "mandatory",
70
+ "checkType": "schema.ref-resolves",
71
+ "target": { "registry": "schemas" }
72
+ },
73
+ {
74
+ "ruleId": "CR-009",
75
+ "severity": "mandatory",
76
+ "checkType": "schema.dialect-declared",
77
+ "target": { "registry": "schemas" }
78
+ },
79
+ {
80
+ "ruleId": "CR-010",
81
+ "severity": "mandatory",
82
+ "checkType": "fixture.positive-passes",
83
+ "target": { "fixtures": "expect-valid" }
84
+ },
85
+ {
86
+ "ruleId": "CR-011",
87
+ "severity": "mandatory",
88
+ "checkType": "fixture.negative-coded",
89
+ "target": { "fixtures": "expect-invalid" }
90
+ },
91
+ {
92
+ "ruleId": "CR-012",
93
+ "severity": "mandatory",
94
+ "checkType": "error-code.registered",
95
+ "target": { "references": "fixture-expectations" }
96
+ },
97
+ {
98
+ "ruleId": "CR-013",
99
+ "severity": "mandatory",
100
+ "checkType": "rules.budget",
101
+ "target": { "budget": "firstVersionMax" }
102
+ }
103
+ ]
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -76,6 +76,8 @@
76
76
  "ajv-formats": "3.0.1",
77
77
  "libnpmpublish": "11.1.0",
78
78
  "npm-registry-fetch": "18.0.2",
79
+ "skill-family-contracts": "0.4.0",
80
+ "skill-family-harness-node": "0.4.0",
79
81
  "yaml": "2.9.0"
80
82
  },
81
83
  "devDependencies": {
@@ -398,6 +398,11 @@
398
398
  "minLength": 1,
399
399
  "pattern": "^[a-z0-9][a-z0-9._-]*$"
400
400
  },
401
+ "marketplaceSource": {
402
+ "type": "string",
403
+ "minLength": 1,
404
+ "description": "消费者添加统一市场所用的来源 URL(如 codebuddy plugin marketplace add);可选,仅在可知时声明"
405
+ },
401
406
  "marketplaceRepo": {
402
407
  "type": "string",
403
408
  "pattern": "^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$",
@@ -191,6 +191,11 @@
191
191
  "minLength": 1,
192
192
  "pattern": "^[a-z0-9][a-z0-9._-]*$"
193
193
  },
194
+ "marketplaceSource": {
195
+ "type": "string",
196
+ "minLength": 1,
197
+ "description": "消费者添加统一市场所用的来源 URL(如 codebuddy plugin marketplace add);可选,仅在可知时声明"
198
+ },
194
199
  "marketplaceRepo": {
195
200
  "type": "string",
196
201
  "pattern": "^[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*$",
@@ -7,7 +7,10 @@
7
7
  * ESM file that runs without node_modules.
8
8
  *
9
9
  * Schemas and native addons remain external (loaded at runtime from
10
- * the plugin root via PKG_ROOT).
10
+ * the plugin root via PKG_ROOT). Runtime JSON resources read by inlined
11
+ * dependency modules (new URL(<rel>, import.meta.url)) are emitted as
12
+ * deterministic sidecars next to the bundle (see "Runtime sidecar
13
+ * resources" section below).
11
14
  *
12
15
  * Usage:
13
16
  * node scripts/build-bundle.mjs # build mode
@@ -15,7 +18,8 @@
15
18
  */
16
19
 
17
20
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
18
- import { dirname, join } from 'node:path';
21
+ import { existsSync, readFileSync } from 'node:fs';
22
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
19
23
  import { createHash } from 'node:crypto';
20
24
 
21
25
  const CHECK_MODE = process.argv.includes('--check');
@@ -55,6 +59,100 @@ const REQUIRE_SHIM_PATTERN = /var __require = \/\* @__PURE__ \*\/ \(\(x\) => typ
55
59
 
56
60
  const REQUIRE_SHIM_REPLACEMENT = `var __require = __bundleRealRequire;`;
57
61
 
62
+ // ─── Runtime sidecar resources for inlined dependencies ────────────────
63
+ //
64
+ // Dependencies bundled into the CLI (e.g. skill-family-contracts) can load
65
+ // JSON resources at module init via new URL(<rel>, import.meta.url). Once
66
+ // inlined into the bundle, import.meta.url is the bundle's own URL, so such
67
+ // reads resolve relative to the bundle directory (bin/). To keep the bundle
68
+ // self-contained in copied/installed closures (no node_modules), the build
69
+ // emits each referenced resource that is not already provided at that
70
+ // target as a deterministic sidecar next to the bundle. Reads whose target
71
+ // already exists (e.g. ../package.json resolving to the package's own
72
+ // package.json) are host-provided by design and are not emitted. --check
73
+ // verifies bundle bytes, every expected sidecar, and that every runtime
74
+ // read in the output resolves next to the bundle (fail-closed).
75
+
76
+ // Module markers esbuild leaves in the output: `// <path>` comments and
77
+ // `<path>() {` __esm keys, with <path> relative to the package root.
78
+ const MODULE_MARK_RE = /(?:\/\/\s*|")((?:\.\.\/)+[^\s"()]+\.(?:mjs|js|cjs))/g;
79
+ const RUNTIME_URL_READ_RE = /new URL\(\s*"([^"]+)"\s*,\s*import\.meta\.url\s*\)/g;
80
+
81
+ function isAbsoluteOrScheme(rel) {
82
+ return rel.startsWith('/') || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(rel);
83
+ }
84
+
85
+ /** Collect every relative runtime read (module source → rel) present in the output. */
86
+ function collectRuntimeReads(content, pkgRoot) {
87
+ const reads = [];
88
+ const seenModules = new Set();
89
+ for (const mark of content.matchAll(MODULE_MARK_RE)) {
90
+ const modulePath = mark[1];
91
+ if (seenModules.has(modulePath)) continue;
92
+ seenModules.add(modulePath);
93
+ const sourceFile = join(pkgRoot, modulePath);
94
+ if (!existsSync(sourceFile)) continue;
95
+ const sourceText = readFileSync(sourceFile, 'utf-8');
96
+ for (const read of sourceText.matchAll(RUNTIME_URL_READ_RE)) {
97
+ const rel = read[1];
98
+ if (isAbsoluteOrScheme(rel)) continue;
99
+ reads.push({ modulePath, rel });
100
+ }
101
+ }
102
+ return reads;
103
+ }
104
+
105
+ /** Sidecars to emit: reads whose target next to the bundle is not already provided. */
106
+ function computeSidecars(reads, pkgRoot, outDir) {
107
+ const sidecars = [];
108
+ for (const { modulePath, rel } of reads) {
109
+ if (rel.endsWith('/')) continue; // directory URL composition; not a file load
110
+ const target = resolve(outDir, rel);
111
+ const source = resolve(join(pkgRoot, modulePath), '..', rel);
112
+ if (!existsSync(source)) {
113
+ throw new Error(
114
+ `build-bundle: runtime read "${rel}" in ${modulePath} has no source resource (${source})`,
115
+ );
116
+ }
117
+ // Host-provided targets (e.g. ../package.json resolving to the package's
118
+ // own package.json) lie outside the bundle directory and must not be
119
+ // emitted or freshened. Targets inside the bundle directory are
120
+ // sidecar-managed: an existing target is NOT host-provided — a stale
121
+ // sidecar from a previous dependency generation must be freshened in
122
+ // write mode and must fail the --check byte comparison.
123
+ const relativeToOutDir = relative(outDir, target);
124
+ if (relativeToOutDir.startsWith('..') || isAbsolute(relativeToOutDir)) {
125
+ continue;
126
+ }
127
+ if (!sidecars.some((s) => s.target === target)) {
128
+ sidecars.push({ target, source });
129
+ }
130
+ }
131
+ return sidecars;
132
+ }
133
+
134
+ /** Fail-closed check: every relative runtime read in the output must resolve next to the bundle. */
135
+ function verifyRuntimeReadsResolve(content, outDir) {
136
+ const missing = new Set();
137
+ for (const read of content.matchAll(RUNTIME_URL_READ_RE)) {
138
+ const rel = read[1];
139
+ if (isAbsoluteOrScheme(rel) || rel.endsWith('/')) continue;
140
+ if (!existsSync(resolve(outDir, rel))) missing.add(rel);
141
+ }
142
+ return [...missing].sort();
143
+ }
144
+
145
+ // Inlined Foundation modules must not perform bundle-relative runtime reads
146
+ // of files that adapter closures do not ship. skill-family-harness-node's
147
+ // report.mjs reads its own package.json at module init
148
+ // (readFileSync(new URL("../package.json", import.meta.url))); once inlined
149
+ // into the bundle that read resolves next to the bundle and breaks in
150
+ // adapter closures (which ship no package.json by design — the banner
151
+ // injects __bundlePkg instead). Neutralize the read at build time with the
152
+ // same injected package identity, fail-closed if Foundation changes shape.
153
+ const REPORT_PACKAGE_META_READ_RE =
154
+ /JSON\.parse\(readFileSync\(new URL\("\.\.\/package\.json", import\.meta\.url\), "utf8"\)\)/;
155
+
58
156
  async function buildBundle() {
59
157
  // Dynamic import so the script works even if esbuild is not yet installed.
60
158
  let esbuild;
@@ -68,6 +166,25 @@ async function buildBundle() {
68
166
  const pkgJson = JSON.parse(await readFile(join(PKG_ROOT, 'package.json'), 'utf-8'));
69
167
  const banner = buildBanner({ name: pkgJson.name, version: pkgJson.version });
70
168
 
169
+ const identity = Object.freeze({ name: pkgJson.name, version: pkgJson.version });
170
+
171
+ const identityPlugins = [{
172
+ name: 'inline-foundation-package-identity',
173
+ setup(build) {
174
+ build.onLoad({ filter: /skill-family-harness-node[\\/]src[\\/]report\.mjs$/ }, async (args) => {
175
+ const source = await readFile(args.path, 'utf-8');
176
+ const injected = source.replace(REPORT_PACKAGE_META_READ_RE, () =>
177
+ `Object.freeze(${JSON.stringify(identity)})`);
178
+ if (injected === source) {
179
+ throw new Error(
180
+ 'build-bundle: could not neutralize Foundation report.mjs package.json read',
181
+ );
182
+ }
183
+ return { contents: injected, loader: 'js' };
184
+ });
185
+ },
186
+ }];
187
+
71
188
  const result = await esbuild.build({
72
189
  entryPoints: [ENTRY],
73
190
  absWorkingDir: PKG_ROOT,
@@ -77,6 +194,7 @@ async function buildBundle() {
77
194
  target: 'node22',
78
195
  outfile: OUTFILE,
79
196
  banner: { js: banner },
197
+ plugins: identityPlugins,
80
198
  // External: Node.js builtins, native addon loader, and the native addon itself.
81
199
  external: [
82
200
  'node:*',
@@ -106,11 +224,20 @@ async function buildBundle() {
106
224
  // Normalize them so committed bundles pass repository whitespace checks.
107
225
  content = content.replace(/[ \t]+$/gm, '');
108
226
 
109
- return content;
227
+ // Compute runtime sidecars (resources read by inlined dependency modules
228
+ // via new URL(<rel>, import.meta.url), resolved next to the bundle).
229
+ const sidecars = computeSidecars(
230
+ collectRuntimeReads(content, PKG_ROOT),
231
+ PKG_ROOT,
232
+ dirname(OUTFILE),
233
+ );
234
+
235
+ return { content, sidecars };
110
236
  }
111
237
 
112
238
  async function main() {
113
- const content = await buildBundle();
239
+ const { content, sidecars } = await buildBundle();
240
+ const outDir = dirname(OUTFILE);
114
241
 
115
242
  if (CHECK_MODE) {
116
243
  let existing;
@@ -128,11 +255,38 @@ async function main() {
128
255
  console.error(` expected: sha256:${expectedHash}`);
129
256
  process.exit(1);
130
257
  }
258
+ for (const { target, source } of sidecars) {
259
+ let existingSidecar;
260
+ try {
261
+ existingSidecar = await readFile(target);
262
+ } catch {
263
+ console.error(`Bundle drift: runtime sidecar missing: ${relative(PKG_ROOT, target)}`);
264
+ process.exit(1);
265
+ }
266
+ const expectedSidecar = await readFile(source);
267
+ if (!existingSidecar.equals(expectedSidecar)) {
268
+ console.error(`Bundle drift: runtime sidecar differs: ${relative(PKG_ROOT, target)}`);
269
+ process.exit(1);
270
+ }
271
+ }
272
+ const unresolved = verifyRuntimeReadsResolve(content, outDir);
273
+ if (unresolved.length > 0) {
274
+ console.error(`Bundle drift: runtime resource reads unresolved next to bundle: ${unresolved.join(', ')}`);
275
+ process.exit(1);
276
+ }
131
277
  console.log('Bundle in sync.');
132
278
  process.exit(0);
133
279
  }
134
280
 
135
- await mkdir(dirname(OUTFILE), { recursive: true });
281
+ for (const { target, source } of sidecars) {
282
+ const bytes = await readFile(source);
283
+ await mkdir(dirname(target), { recursive: true });
284
+ await writeFile(target, bytes);
285
+ const hash = createHash('sha256').update(bytes).digest('hex');
286
+ console.log(`Sidecar written: ${relative(PKG_ROOT, target)}`);
287
+ console.log(` sha256:${hash}`);
288
+ }
289
+ await mkdir(outDir, { recursive: true });
136
290
  await writeFile(OUTFILE, content, 'utf-8');
137
291
  const hash = createHash('sha256').update(content).digest('hex');
138
292
  console.log(`Bundle written: ${OUTFILE}`);
@@ -228,6 +228,10 @@ async function collectEntries(packageRoot) {
228
228
  // - release-notes/ (consumed by docs-refresh, not a public artifact)
229
229
  // - native/*/build/ (native build artifacts, only prebuilds are shipped)
230
230
  // - scripts/* (mostly dev-only; explicit whitelist below)
231
+ // - vendor/ (G4 foundation-pin: check-time drift evidence, not
232
+ // a distribution artifact; the shipped bundle
233
+ // inlines the adopted capability — matches the npm
234
+ // pack closure contract)
231
235
  const baseFiles = allFiles.filter((f) => {
232
236
  if (f.startsWith('adapters/')) return false;
233
237
  if (f.startsWith('test/')) return false;
@@ -235,6 +239,7 @@ async function collectEntries(packageRoot) {
235
239
  if (f.startsWith('release-notes/')) return false;
236
240
  if (f.startsWith('native/') && f.includes('/build/')) return false;
237
241
  if (f.startsWith('scripts/')) return false;
242
+ if (f.startsWith('vendor/')) return false;
238
243
  return true;
239
244
  });
240
245
 
@@ -44,6 +44,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
44
44
 
45
45
  offline assess 不访问 GitHub/npm 认证,因此不会以顶层 `AUTH_MISSING` 作为正常诊断结果;生产认证缺口由 help 的 `readiness.productionPublish` 和发布前在线门禁报告。
46
46
 
47
+ 在线模式下 npm 版本检查区分两种失败:registry 明确返回 E404/ETARGET 才算「版本不存在,无 gap」;网络、认证、超时等检查失败会以 `NPM_VERSION_CHECK_FAILED`(warning 级 gap)显式报告,不得把检查失败当作「版本未发布」。
48
+
47
49
  重试时只保留最新结构化错误码和失败门,不沿用早期猜测。
48
50
 
49
51
  ## 后续引导
@@ -38,9 +38,11 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
38
38
  # 日常发布快速路径:仅在发布前确认一次可读计划摘要,状态文件可恢复
39
39
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
40
40
  # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
41
+ # 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
41
42
  # 命令调用本身即授权执行配置中的 hooks
42
43
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> --json
43
44
  # 仅旧冻结计划兼容:记录历史 Kimi/CodeBuddy 人工证明
45
+ # (本地自声明收据,证明力弱:--actor 仅非空字符串校验、无外部签名核验;新计划不适用)
44
46
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
45
47
  --platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
46
48
  # 发布文档刷新:默认只读演练
@@ -59,7 +61,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> \
59
61
  - **只读演练**:`docs refresh --unit <id> --json` 输出逐文件相对路径、locale、新旧摘要、`version`、`locales`、`inputDigest`、`refreshDigest` 和 `nextCommand.argv`;候选无变化时 `status: "clean"`。
60
62
  - **确认写入**:必须同时提供 `--write`、精确 `--confirm-refresh <refreshDigest>` 和 `--ack-local-document-write`,全部目标作为一个事务提交;成功后立即复演必须为 `clean`。
61
63
 
62
- **授权边界**:本地发布文档写入授权只覆盖声明的本地文档目标,不是 Git 提交、push、publish 或安装的授权。hook/gate 由对应命令调用直接授权。写入后必须审阅、提交,再重新 prepare。
64
+ **授权边界**:本地发布文档写入授权只覆盖声明的本地文档目标,不是 Git 提交、push、publish 或安装的授权。hook/gate 由对应命令调用直接授权——在 project.yaml 配置 hook 即完成授权(配置时刻即授权契约,FM-16 处置 A):hook 是任意本地进程、无文件系统/网络隔离、触发前无确认点,配置者须对其内容负责;恢复触发前强制确认门为后续加固项。写入后必须审阅、提交,再重新 prepare。
63
65
 
64
66
  ## 故障路由
65
67
 
@@ -11,9 +11,9 @@ description: Freeze an immutable release plan with local configuration, document
11
11
 
12
12
  ## 职责与边界
13
13
 
14
- 运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。hook 是任意本地进程,不受文件系统/网络隔离,可能产生项目目录外的副作用或远端写入。
14
+ 运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
15
15
 
16
- **Hook Gate 执行**: 命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节。
16
+ **Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
17
17
 
18
18
  **阶段通过规则**: 本阶段的通过只能由 CLI exit code 0 和结构化状态码 `PREPARED` 确认。Agent 无权自行宣布计划冻结成功。
19
19
 
@@ -19,6 +19,8 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
19
19
 
20
20
  验证远端所有自动化 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对 Claude/Codex marketplace distribution 执行全新隔离消费者安装验证。新计划中的 Kimi/CodeBuddy 安装只返回 `manualFollowUps`,明确标记 `verifiedBySystem: false`,不阻塞 `VERIFIED`;缺少该策略字段的旧冻结计划继续走历史 attestation 兼容路径。
21
21
 
22
+ **attestation 证明力边界**: `attest` 命令记录的旧计划兼容人工证明(`humanConfirmed: true` → `PASSED_MANUAL`)是本地自声明收据,不是签名见证:`--actor` 仅做非空字符串校验,无外部签名或身份核验,任何能运行 CLI 的进程都能自称任意 actor;伪造收据需写 `.release-skill` authority 目录,与直接改写收据文件属同一信任边界。因此旧兼容路径的人工证明只用于旧冻结计划收尾,不构成独立可审计的人工见证。新计划的 Kimi/CodeBuddy 安装一律走 `manualFollowUps`(`verifiedBySystem: false`),不参与 `VERIFIED` 终态判定。
23
+
22
24
  **阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
23
25
 
24
26
  **源 run 要求**: `--run` 必需;源 run 的所有 checkpoint 必须为 succeeded 或 skipped。
@@ -44,6 +44,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
44
44
 
45
45
  offline assess 不访问 GitHub/npm 认证,因此不会以顶层 `AUTH_MISSING` 作为正常诊断结果;生产认证缺口由 help 的 `readiness.productionPublish` 和发布前在线门禁报告。
46
46
 
47
+ 在线模式下 npm 版本检查区分两种失败:registry 明确返回 E404/ETARGET 才算「版本不存在,无 gap」;网络、认证、超时等检查失败会以 `NPM_VERSION_CHECK_FAILED`(warning 级 gap)显式报告,不得把检查失败当作「版本未发布」。
48
+
47
49
  重试时只保留最新结构化错误码和失败门,不沿用早期猜测。
48
50
 
49
51
  ## 后续引导
@@ -38,9 +38,11 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
38
38
  # 日常发布快速路径:仅在发布前确认一次可读计划摘要,状态文件可恢复
39
39
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
40
40
  # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
41
+ # 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
41
42
  # 命令调用本身即授权执行配置中的 hooks
42
43
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" hooks validate --root <path> --json
43
44
  # 仅旧冻结计划兼容:记录历史 Kimi/CodeBuddy 人工证明
45
+ # (本地自声明收据,证明力弱:--actor 仅非空字符串校验、无外部签名核验;新计划不适用)
44
46
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" attest --root <path> \
45
47
  --platform <kimi|codebuddy> --plugin <id> --result <passed|failed> --actor <person> --json
46
48
  # 发布文档刷新:默认只读演练
@@ -59,7 +61,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> \
59
61
  - **只读演练**:`docs refresh --unit <id> --json` 输出逐文件相对路径、locale、新旧摘要、`version`、`locales`、`inputDigest`、`refreshDigest` 和 `nextCommand.argv`;候选无变化时 `status: "clean"`。
60
62
  - **确认写入**:必须同时提供 `--write`、精确 `--confirm-refresh <refreshDigest>` 和 `--ack-local-document-write`,全部目标作为一个事务提交;成功后立即复演必须为 `clean`。
61
63
 
62
- **授权边界**:本地发布文档写入授权只覆盖声明的本地文档目标,不是 Git 提交、push、publish 或安装的授权。hook/gate 由对应命令调用直接授权。写入后必须审阅、提交,再重新 prepare。
64
+ **授权边界**:本地发布文档写入授权只覆盖声明的本地文档目标,不是 Git 提交、push、publish 或安装的授权。hook/gate 由对应命令调用直接授权——在 project.yaml 配置 hook 即完成授权(配置时刻即授权契约,FM-16 处置 A):hook 是任意本地进程、无文件系统/网络隔离、触发前无确认点,配置者须对其内容负责;恢复触发前强制确认门为后续加固项。写入后必须审阅、提交,再重新 prepare。
63
65
 
64
66
  ## 故障路由
65
67
 
@@ -11,9 +11,9 @@ description: Freeze an immutable release plan with local configuration, document
11
11
 
12
12
  ## 职责与边界
13
13
 
14
- 运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。hook 是任意本地进程,不受文件系统/网络隔离,可能产生项目目录外的副作用或远端写入。
14
+ 运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
15
15
 
16
- **Hook Gate 执行**: 命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节。
16
+ **Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
17
17
 
18
18
  **阶段通过规则**: 本阶段的通过只能由 CLI exit code 0 和结构化状态码 `PREPARED` 确认。Agent 无权自行宣布计划冻结成功。
19
19
 
@@ -19,6 +19,8 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
19
19
 
20
20
  验证远端所有自动化 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对 Claude/Codex marketplace distribution 执行全新隔离消费者安装验证。新计划中的 Kimi/CodeBuddy 安装只返回 `manualFollowUps`,明确标记 `verifiedBySystem: false`,不阻塞 `VERIFIED`;缺少该策略字段的旧冻结计划继续走历史 attestation 兼容路径。
21
21
 
22
+ **attestation 证明力边界**: `attest` 命令记录的旧计划兼容人工证明(`humanConfirmed: true` → `PASSED_MANUAL`)是本地自声明收据,不是签名见证:`--actor` 仅做非空字符串校验,无外部签名或身份核验,任何能运行 CLI 的进程都能自称任意 actor;伪造收据需写 `.release-skill` authority 目录,与直接改写收据文件属同一信任边界。因此旧兼容路径的人工证明只用于旧冻结计划收尾,不构成独立可审计的人工见证。新计划的 Kimi/CodeBuddy 安装一律走 `manualFollowUps`(`verifiedBySystem: false`),不参与 `VERIFIED` 终态判定。
23
+
22
24
  **阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
23
25
 
24
26
  **源 run 要求**: `--run` 必需;源 run 的所有 checkpoint 必须为 succeeded 或 skipped。
@@ -13,7 +13,9 @@
13
13
  * @module adapters/contract
14
14
  */
15
15
 
16
- import { writeFile, rename, rm } from 'node:fs/promises';
16
+ import { dirname, basename } from 'node:path';
17
+
18
+ import { writeFileAtomic } from 'skill-family-harness-node';
17
19
 
18
20
  import { ReleaseError, AUTH_MISSING } from '../core/errors.mjs';
19
21
 
@@ -193,18 +195,16 @@ export function resolveTimeoutMs(action) {
193
195
  * create, rename into place). Crash-safe: a partial write never replaces an
194
196
  * existing file.
195
197
  *
196
- * @param {string} filePath
198
+ * 机制已委托 Foundation writeFileAtomic(resolveContained 路径包含三层校验 +
199
+ * 独占临时文件 + fsync + rename + 目录 fsync);文件内容与 mode 保持本地语义。
200
+ *
201
+ * @param {string} filePath - Absolute path to write.
197
202
  * @param {object} value
198
203
  */
199
204
  export async function writeEvidenceAtomic(filePath, value) {
200
- const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
201
- try {
202
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
203
- await rename(tempPath, filePath);
204
- } catch (err) {
205
- await rm(tempPath, { force: true }).catch(() => {});
206
- throw err;
207
- }
205
+ const root = dirname(filePath);
206
+ const relPath = basename(filePath);
207
+ await writeFileAtomic(root, relPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
208
208
  }
209
209
 
210
210
  /**
@@ -48,8 +48,8 @@ import {
48
48
  import {
49
49
  CODEBUDDY_REQUIREMENT_FILE,
50
50
  CODEBUDDY_ATTESTATION_FILE,
51
- CODEBUDDY_MARKETPLACE_NAME,
52
- CODEBUDDY_MARKETPLACE_SOURCE,
51
+ resolveCodeBuddyMarketplace,
52
+ resolveCodeBuddyMarketplaceSource,
53
53
  resolveCodeBuddyBoundPlanDigest,
54
54
  codebuddyAuthorityDir,
55
55
  validateCodeBuddyAttestation,
@@ -3095,15 +3095,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3095
3095
  try {
3096
3096
  attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
3097
3097
  } catch {
3098
+ // The marketplace source hint follows the frozen action's
3099
+ // resolved marketplace; unknown configured marketplaces cite no
3100
+ // fabricated URL.
3101
+ const marketplaceSource = resolveCodeBuddyMarketplaceSource(action);
3098
3102
  return createResult({
3099
3103
  actionType,
3100
3104
  status: ActionStatus.OBSERVED,
3101
3105
  observation: {
3102
3106
  installed: false,
3103
3107
  manualInstallRequired: true,
3104
- marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
3108
+ ...(marketplaceSource ? { marketplaceSource } : {}),
3105
3109
  attestationDir,
3106
- error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${CODEBUDDY_MARKETPLACE_SOURCE})`,
3110
+ error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install${marketplaceSource ? ` (marketplace ${marketplaceSource})` : ''}`,
3107
3111
  },
3108
3112
  });
3109
3113
  }
@@ -3162,7 +3166,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3162
3166
  });
3163
3167
  }
3164
3168
  const normalizedPath = normalizedAttestation.installPath.replaceAll('\\', '/');
3165
- const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
3169
+ // The expected suffix follows the frozen action's resolved
3170
+ // marketplace (default constant for legacy plans).
3171
+ let resolvedMarketplace;
3172
+ try {
3173
+ resolvedMarketplace = resolveCodeBuddyMarketplace(action);
3174
+ } catch (resolveErr) {
3175
+ return createResult({
3176
+ actionType,
3177
+ status: ActionStatus.OBSERVED,
3178
+ observation: { installed: false, error: resolveErr.message },
3179
+ });
3180
+ }
3181
+ const expectedSuffix = `.workbuddy/plugins/marketplaces/${resolvedMarketplace}/plugins/${action.plugin}`;
3166
3182
  if (!normalizedPath.endsWith(expectedSuffix)) {
3167
3183
  return createResult({
3168
3184
  actionType,
@@ -16,6 +16,7 @@ import { join } from 'node:path';
16
16
  import { promisify } from 'node:util';
17
17
  import { execFile } from 'node:child_process';
18
18
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
19
+ import { digestDocument } from 'skill-family-contracts';
19
20
  import { ReleaseError, PATH_UNSAFE } from '../core/errors.mjs';
20
21
 
21
22
  const execFileAsync = promisify(execFile);
@@ -151,10 +152,12 @@ async function enumerateTreeEntries(root, dirPath, relBase) {
151
152
  * @returns {string} `sha256:<hex>` digest.
152
153
  */
153
154
  export function digestEntryManifest(entries) {
155
+ // 纯 JSON 投影(path/type/mode/size/sha256 定字段),直接委托 Foundation
156
+ // digestDocument(= sha256(contracts canonicalJson),与迁移前字节一致)。
154
157
  const canonical = entries.map(({ path, type, mode, sha256, size }) => ({
155
158
  path, type, mode, size, sha256,
156
159
  }));
157
- return `sha256:${sha256Hex(canonicalJson(canonical))}`;
160
+ return `sha256:${digestDocument(canonical)}`;
158
161
  }
159
162
 
160
163
  // ---------------------------------------------------------------------------
@@ -14,10 +14,10 @@ import { readFile, readdir, stat, mkdir, writeFile, rm } from 'node:fs/promises'
14
14
  import { join } from 'node:path';
15
15
  import { mkdtemp } from 'node:fs/promises';
16
16
  import { tmpdir } from 'node:os';
17
- import { createHash } from 'node:crypto';
18
17
  import { readFileSync } from 'node:fs';
19
18
 
20
19
  import { canonicalJson, sha256Hex } from '../core/digest.mjs';
20
+ import { digestBytes } from 'skill-family-harness-node';
21
21
  import {
22
22
  ReleaseError,
23
23
  ARTIFACT_POLICY_INVALID,
@@ -155,11 +155,14 @@ export async function createBuiltInProducerRegistry() {
155
155
  * @returns {string}
156
156
  */
157
157
  export function computeImplDigest(sourceBytes) {
158
- const h = createHash('sha256');
159
- for (const buf of sourceBytes) h.update(buf);
160
- h.update(`node:${process.version}`);
161
- h.update('locale:en-US timezone:UTC');
162
- return `sha256:${h.digest('hex')}`;
158
+ // 多段哈希(源字节 + node 版本 + 固定 locale/timezone)等价于对拼接字节
159
+ // sha256,委托 Foundation digestBytes 实现,字节逐位一致。
160
+ const bytes = Buffer.concat([
161
+ ...sourceBytes,
162
+ Buffer.from(`node:${process.version}`),
163
+ Buffer.from('locale:en-US timezone:UTC'),
164
+ ]);
165
+ return `sha256:${digestBytes(bytes)}`;
163
166
  }
164
167
 
165
168
  // ---------------------------------------------------------------------------