release-skill 0.2.5 → 0.2.7

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 (76) 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 +42 -0
  7. package/INSTALL.md +40 -16
  8. package/INSTALL.zh-CN.md +34 -12
  9. package/README.md +42 -15
  10. package/README.zh-CN.md +36 -15
  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/release-skill.bundle.mjs +2671 -1552
  14. package/adapters/claude/schemas/.render-manifest.json +6 -6
  15. package/adapters/claude/schemas/release-plan.schema.json +67 -0
  16. package/adapters/claude/schemas/release-project.schema.json +6 -0
  17. package/adapters/claude/schemas/release-run.schema.json +65 -0
  18. package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
  19. package/adapters/claude/skills/release-setup/SKILL.md +10 -1
  20. package/adapters/claude/skills/release-verify/SKILL.md +4 -2
  21. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  22. package/adapters/codex/bin/release-skill.bundle.mjs +2671 -1552
  23. package/adapters/codex/schemas/.render-manifest.json +6 -6
  24. package/adapters/codex/schemas/release-plan.schema.json +67 -0
  25. package/adapters/codex/schemas/release-project.schema.json +6 -0
  26. package/adapters/codex/schemas/release-run.schema.json +65 -0
  27. package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
  28. package/adapters/codex/skills/release-setup/SKILL.md +10 -1
  29. package/adapters/codex/skills/release-verify/SKILL.md +4 -2
  30. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  31. package/adapters/kimi/bin/release-skill.bundle.mjs +2671 -1552
  32. package/adapters/kimi/schemas/.render-manifest.json +6 -6
  33. package/adapters/kimi/schemas/release-plan.schema.json +67 -0
  34. package/adapters/kimi/schemas/release-project.schema.json +6 -0
  35. package/adapters/kimi/schemas/release-run.schema.json +65 -0
  36. package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
  37. package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
  38. package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
  39. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  40. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2671 -1552
  41. package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
  42. package/adapters/workbuddy/schemas/release-plan.schema.json +67 -0
  43. package/adapters/workbuddy/schemas/release-project.schema.json +6 -0
  44. package/adapters/workbuddy/schemas/release-run.schema.json +65 -0
  45. package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
  46. package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
  47. package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
  48. package/bin/release-skill.bundle.mjs +2671 -1552
  49. package/package.json +1 -1
  50. package/references/.render-manifest.json +4 -4
  51. package/references/02-project-config.md +28 -2
  52. package/references/05-evidence-and-errors.md +6 -0
  53. package/schemas/.render-manifest.json +6 -6
  54. package/schemas/release-plan.schema.json +67 -0
  55. package/schemas/release-project.schema.json +6 -0
  56. package/schemas/release-run.schema.json +65 -0
  57. package/skills/release-prepare/SKILL.md +5 -0
  58. package/skills/release-setup/SKILL.md +10 -1
  59. package/skills/release-verify/SKILL.md +4 -2
  60. package/skills-src/release-prepare/SKILL.md +5 -0
  61. package/skills-src/release-setup/SKILL.md +10 -1
  62. package/skills-src/release-verify/SKILL.md +4 -2
  63. package/src/adapters/npm.mjs +54 -2
  64. package/src/adapters/plugin-marketplace.mjs +4 -29
  65. package/src/commands/prepare.mjs +161 -4
  66. package/src/commands/publish.mjs +74 -2
  67. package/src/commands/reconcile.mjs +58 -0
  68. package/src/commands/setup.mjs +194 -0
  69. package/src/commands/verify.mjs +60 -1
  70. package/src/core/errors.mjs +12 -0
  71. package/src/core/source-authority.mjs +547 -0
  72. package/src/npm/npm-entry-closure.mjs +195 -0
  73. package/src/platforms/codebuddy.mjs +19 -11
  74. package/src/platforms/codex.mjs +18 -10
  75. package/src/platforms/kimi.mjs +26 -39
  76. package/src/snapshot/frozen.mjs +51 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -10,8 +10,8 @@
10
10
  "bytes": 8840
11
11
  },
12
12
  "02-project-config.md": {
13
- "digest": "1e3c10fb4598aa6406b9d7cb4fdab2f663c3dad017dce88098b9e79868796806",
14
- "bytes": 16514
13
+ "digest": "0cff13ebb270f3515290e0ce60b91303143777d1a4948db28cf2c69612ceff8e",
14
+ "bytes": 19029
15
15
  },
16
16
  "03-readme-quality.md": {
17
17
  "digest": "d74e41052592443ebbd450d0ace5f9b9b19c7a94db50f952b17c1c5a4c5dc230",
@@ -22,8 +22,8 @@
22
22
  "bytes": 4682
23
23
  },
24
24
  "05-evidence-and-errors.md": {
25
- "digest": "00039b9bd3724f40ec084b366103f1b84a2b599c5e884f23904d337f4c193661",
26
- "bytes": 8484
25
+ "digest": "ebcb4940ff2a25ce6945c3b11cfcafe751f4a1f515276b7e021717ea5844eebf",
26
+ "bytes": 9688
27
27
  },
28
28
  "06-adapter-contract.md": {
29
29
  "digest": "39f0271bc06eae38dade518ece8f7e16a18f53099a433e3ae07e9d20ff7a142e",
@@ -23,8 +23,9 @@ apiVersion: release-skill/v1
23
23
  kind: ReleaseProject
24
24
 
25
25
  project:
26
- name: <string> # 项目标识,必填
27
- defaultBranch: <string> # 默认分支,必填
26
+ name: <string> # 项目标识,必填
27
+ defaultBranch: <string> # workspace 远端默认分支,必填;不限定为 main
28
+ sourceRepository: <owner/repo> # workspace 源仓库;生产发布必填
28
29
 
29
30
  releaseUnits: # 发布单元数组,至少 1 个
30
31
  - id: <string> # 唯一标识
@@ -125,6 +126,31 @@ human-owned 权威源,然后重新 prepare、审阅并 approve;后续环节
125
126
 
126
127
  旧的 `policy.requiredPublicFiles` 已不支持。加载器必须返回 `CONFIG_INVALID`,并引导迁移到各自的 `releaseUnits[].requiredPublicFiles`。
127
128
 
129
+ ### 3.1.1 Workspace 源码权威内容闭包
130
+
131
+ `project.sourceRepository` 标识承载当前 workspace 人工源文件的 GitHub
132
+ `owner/repo`,它与各发布单元的 `publicRepo` 不是同一概念;多个发布单元可以投影到不同
133
+ 公开仓库,但它们仍可共享一个 workspace 源仓库。`project.defaultBranch` 表示该源仓库的
134
+ 真实远端默认分支,可以是 `main`、`master`、`develop`、`trunk` 或其他明确名称。
135
+ setup 只在 workspace Git remotes 唯一一致时提出候选;缺失或冲突时要求人工裁决,不保存
136
+ `auto`。
137
+
138
+ 生产 prepare 在 hooks 完成后,从所有 `publicFiles.from`(目录递归展开)和每个 unit 的
139
+ `version.source` 计算源码输入闭包,冻结 workspace 相对路径、内容 SHA-256 和 Git mode。
140
+ 只有闭包内 staged、unstaged 或 untracked 的文件会阻断 prepare;无关 dirty 文件不阻断。
141
+ 离线 prepare 明确记录 `unobserved-offline`,远端内容的强制比较留在 publish。
142
+
143
+ production publish 在任何 adapter `execute` 前,从
144
+ `sourceRepository/defaultBranch` 读取冻结路径集合,先确认配置分支仍是远端实际默认分支,
145
+ 再比较内容和 mode。判定只依赖内容闭包,不依赖 commit ancestry,因此普通 merge、
146
+ squash、rebase 和默认分支上的无关后续提交可以通过;冲突解决或 revert 丢失 README、
147
+ 版本源或其他公开输入会按具体路径失败关闭。系统不会自动 merge、rebase、切分支、push
148
+ 或创建 PR。
149
+
150
+ 通过后,publish 写入与 plan digest 绑定的源码权威收据;PARTIAL 经 reconcile 恢复时继承
151
+ 并重新校验该收据。verify 只消费这份冻结收据,不在发布完成后用可变默认分支重新判定历史
152
+ 发布。
153
+
128
154
  ### 3.2 公开分支策略
129
155
 
130
156
  - `create-release-branch`:目标分支必须不存在;创建独立 release 分支,冻结提交没有父提交。
@@ -26,6 +26,12 @@
26
26
  | `RELEASE_DOCS_CONFLICT` | 目标文档存在人工冲突 | 非受管同版本 CHANGELOG 条目、受管标记缺失/重复/损坏、版本标记非唯一 | 人工修复目标并保留人工修改后重新演练 |
27
27
  | `RELEASE_DOCS_REFRESH_STALE` | 写入确认绑定的候选已变化 | 演练后说明源或目标发生变化,仍用旧 `refreshDigest` 写入 | 重新演练取得新的 `refreshDigest` 并重新确认 |
28
28
  | `RELEASE_DOCS_STALE` | prepare 检测到发布文档未刷新 | 说明源已更新但 README/CHANGELOG 受管内容未同步 | `docs refresh` 演练 → 确认写入 → 审阅提交 → 重新 prepare |
29
+ | `CONFIG_MISSING` | 生产源码权威配置缺失 | 缺少明确的 `project.sourceRepository` 或 `project.defaultBranch` | 补充经人工确认的 workspace 源仓库与默认分支后重新 prepare |
30
+ | `REMOTE_UNAVAILABLE` | 无法可靠读取 workspace 源仓库 | 网络、认证或远端协议错误导致默认分支内容不可观察 | 修复网络或认证后基于同一冻结计划重试;不得猜测成功 |
31
+ | `REF_MISSING` | 配置的远端默认分支不存在 | `refs/heads/<defaultBranch>` 不存在 | 核对真实分支名并重新 prepare |
32
+ | `NOT_DEFAULT` | 配置分支不是远端实际默认分支 | 默认分支在 prepare 后改变,或配置错误 | 人工确认远端默认分支,更新配置并重新 prepare |
33
+ | `CONTENT_MISMATCH` | 远端默认分支缺少冻结源码内容 | README、版本源、公开映射输入的内容或 mode 不一致 | 人工 merge/adopt/reject;接受的内容进入默认分支后重试 publish |
34
+ | `DIRTY_SOURCE_INPUT` | 源码输入闭包存在未提交变化 | `publicFiles.from` 或 `version.source` 有 staged、unstaged、untracked 变化 | 提交或撤销这些具体输入的变化后重新 prepare;无关 dirty 不受影响 |
29
35
 
30
36
  ---
31
37
 
@@ -22,16 +22,16 @@
22
22
  "bytes": 2390
23
23
  },
24
24
  "release-plan.schema.json": {
25
- "digest": "d7869cdb335618754eb787fe352c4c1775c799900f3bb926197c51359699a648",
26
- "bytes": 24088
25
+ "digest": "01c96659223918b5ce0e2d0d01d1ce5095baeb056de401babb8928add4d2a221",
26
+ "bytes": 35256
27
27
  },
28
28
  "release-project.schema.json": {
29
- "digest": "53efe202fc5e2bf63c8d846cf4d50648991163b270536c1450c4be44e45d4b93",
30
- "bytes": 25761
29
+ "digest": "a3d46be1e1e8105103a505c6d58d14f3733c8da91ab8289e097ffd259bf1cd93",
30
+ "bytes": 30982
31
31
  },
32
32
  "release-run.schema.json": {
33
- "digest": "f453845fd2aab02c7b0812b1f62fd1ab7fa37104b8e752914906551c0e8c2637",
34
- "bytes": 8959
33
+ "digest": "bf3c8fb4ca2fc078272a45a6ee6bf663fd2420a722b2f995dbaabf2635e2179e",
34
+ "bytes": 14824
35
35
  }
36
36
  }
37
37
  }
@@ -992,6 +992,73 @@
992
992
  "type": "string",
993
993
  "minLength": 1
994
994
  },
995
+ "sourceAuthority": {
996
+ "type": "object",
997
+ "description": "Source authority content closure gate binding: proves frozen source inputs exist in the workspace remote default branch",
998
+ "required": [
999
+ "sourceRepository",
1000
+ "defaultBranch",
1001
+ "entries",
1002
+ "inputDigest",
1003
+ "algorithmVersion"
1004
+ ],
1005
+ "additionalProperties": false,
1006
+ "properties": {
1007
+ "sourceRepository": {
1008
+ "type": "string",
1009
+ "minLength": 1,
1010
+ "pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$",
1011
+ "description": "GitHub owner/repo of the workspace source repository"
1012
+ },
1013
+ "defaultBranch": {
1014
+ "type": "string",
1015
+ "minLength": 1,
1016
+ "description": "Workspace remote default branch name"
1017
+ },
1018
+ "entries": {
1019
+ "type": "array",
1020
+ "minItems": 1,
1021
+ "description": "Frozen workspace-relative source inputs compared with the remote default branch",
1022
+ "items": {
1023
+ "type": "object",
1024
+ "required": [
1025
+ "path",
1026
+ "digest",
1027
+ "mode"
1028
+ ],
1029
+ "additionalProperties": false,
1030
+ "properties": {
1031
+ "path": {
1032
+ "type": "string",
1033
+ "minLength": 1,
1034
+ "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
1035
+ },
1036
+ "digest": {
1037
+ "type": "string",
1038
+ "pattern": "^[a-f0-9]{64}$"
1039
+ },
1040
+ "mode": {
1041
+ "type": "string",
1042
+ "enum": [
1043
+ "100644",
1044
+ "100755"
1045
+ ]
1046
+ }
1047
+ }
1048
+ }
1049
+ },
1050
+ "inputDigest": {
1051
+ "type": "string",
1052
+ "pattern": "^[a-f0-9]{64}$",
1053
+ "description": "SHA-256 digest of the deterministic source-input closure"
1054
+ },
1055
+ "algorithmVersion": {
1056
+ "type": "integer",
1057
+ "minimum": 1,
1058
+ "description": "Source-input closure algorithm version"
1059
+ }
1060
+ }
1061
+ },
995
1062
  "waivers": {
996
1063
  "type": "array",
997
1064
  "items": {
@@ -35,6 +35,12 @@
35
35
  "defaultBranch": {
36
36
  "type": "string",
37
37
  "minLength": 1
38
+ },
39
+ "sourceRepository": {
40
+ "type": "string",
41
+ "minLength": 1,
42
+ "pattern": "^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$",
43
+ "description": "GitHub owner/repo of the workspace source repository for source authority content gate"
38
44
  }
39
45
  }
40
46
  },
@@ -227,6 +227,71 @@
227
227
  }
228
228
  }
229
229
  },
230
+ "sourceAuthorityReceipts": {
231
+ "type": "array",
232
+ "description": "Source authority content closure receipts: one per publish run proving remote content matches frozen inputs",
233
+ "items": {
234
+ "type": "object",
235
+ "required": [
236
+ "sourceRepository",
237
+ "defaultBranch",
238
+ "inputDigest",
239
+ "algorithmVersion",
240
+ "entryCount",
241
+ "planDigest",
242
+ "verifiedAt",
243
+ "result"
244
+ ],
245
+ "additionalProperties": false,
246
+ "properties": {
247
+ "sourceRepository": {
248
+ "type": "string",
249
+ "minLength": 1
250
+ },
251
+ "defaultBranch": {
252
+ "type": "string",
253
+ "minLength": 1
254
+ },
255
+ "inputDigest": {
256
+ "type": "string",
257
+ "pattern": "^[a-f0-9]{64}$"
258
+ },
259
+ "algorithmVersion": {
260
+ "type": "integer",
261
+ "minimum": 1
262
+ },
263
+ "entryCount": {
264
+ "type": "integer",
265
+ "minimum": 1
266
+ },
267
+ "planDigest": {
268
+ "type": "string",
269
+ "minLength": 1
270
+ },
271
+ "verifiedAt": {
272
+ "type": "string",
273
+ "format": "date-time"
274
+ },
275
+ "result": {
276
+ "type": "string",
277
+ "enum": [
278
+ "CONSISTENT",
279
+ "MISMATCH"
280
+ ]
281
+ },
282
+ "observedCommit": {
283
+ "type": "string",
284
+ "pattern": "^[a-f0-9]{40,64}$"
285
+ },
286
+ "mismatchedPaths": {
287
+ "type": "array",
288
+ "items": {
289
+ "type": "string"
290
+ }
291
+ }
292
+ }
293
+ }
294
+ },
230
295
  "consumerVerificationReceipts": {
231
296
  "type": "array",
232
297
  "description": "消费端验证收据,每个 marketplace action 一条记录",
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
36
36
 
37
37
  若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
38
38
  Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
39
+ 每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
40
+ `bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
41
+ 该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
42
+ 猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
39
43
  每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
40
44
  版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
41
45
  `--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
79
83
  | GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
80
84
  | GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
81
85
  | GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
86
+ | GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
82
87
  | GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
83
88
  | RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
84
89
  | RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
14
14
  - 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
15
15
  - 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
16
16
  - 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
17
+ - npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
18
+ tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
19
+ 不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
17
20
  - 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
18
21
  - Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
19
22
 
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
43
46
  node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
44
47
  ```
45
48
 
46
- 若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实,删除本会话目录后从第 2 步重跑,不得猜测选边。无冲突时可人工增量编辑 `answers.json`:hook 写入 `recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入 `verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持 `mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
49
+ 若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
50
+ 删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
51
+ 人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
52
+ 配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
53
+ `recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
54
+ `verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
55
+ `mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
47
56
 
48
57
  3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
49
58
 
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
17
17
 
18
18
  ## 职责与边界
19
19
 
20
- 验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
20
+ 验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
21
21
 
22
22
  **阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
23
23
 
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
55
55
  - 在 `os.tmpdir()` 创建隔离目录
56
56
  - 执行 `npm install <package>@<version>` 带安全标志
57
57
  - 验证安装的 `package.json` name 和 version 精确匹配
58
+ - 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
59
+ 目标是普通文件;失败时不得写入 `VERIFIED`
58
60
  - 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
59
- - 若未配置 `smokeBin`:仅安装 + name/version 检查即通过
61
+ - 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
60
62
 
61
63
  ## 常见错误
62
64
 
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
36
36
 
37
37
  若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
38
38
  Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
39
+ 每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
40
+ `bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
41
+ 该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
42
+ 猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
39
43
  每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
40
44
  版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
41
45
  `--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
79
83
  | GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
80
84
  | GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
81
85
  | GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
86
+ | GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
82
87
  | GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
83
88
  | RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
84
89
  | RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
14
14
  - 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
15
15
  - 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
16
16
  - 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
17
+ - npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
18
+ tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
19
+ 不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
17
20
  - 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
18
21
  - Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
19
22
 
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
43
46
  node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
44
47
  ```
45
48
 
46
- 若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实,删除本会话目录后从第 2 步重跑,不得猜测选边。无冲突时可人工增量编辑 `answers.json`:hook 写入 `recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入 `verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持 `mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
49
+ 若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
50
+ 删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
51
+ 人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
52
+ 配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
53
+ `recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
54
+ `verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
55
+ `mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
47
56
 
48
57
  3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
49
58
 
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
17
17
 
18
18
  ## 职责与边界
19
19
 
20
- 验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
20
+ 验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
21
21
 
22
22
  **阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
23
23
 
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
55
55
  - 在 `os.tmpdir()` 创建隔离目录
56
56
  - 执行 `npm install <package>@<version>` 带安全标志
57
57
  - 验证安装的 `package.json` name 和 version 精确匹配
58
+ - 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
59
+ 目标是普通文件;失败时不得写入 `VERIFIED`
58
60
  - 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
59
- - 若未配置 `smokeBin`:仅安装 + name/version 检查即通过
61
+ - 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
60
62
 
61
63
  ## 常见错误
62
64
 
@@ -16,6 +16,11 @@ import {
16
16
  createResult,
17
17
  matchObservation,
18
18
  } from './contract.mjs';
19
+ import {
20
+ checkNpmEntryClosure,
21
+ buildTarballFileIndex,
22
+ } from '../npm/npm-entry-closure.mjs';
23
+ import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
19
24
 
20
25
  const execFile = promisify(execFileCb);
21
26
  const NAME = 'npm';
@@ -349,6 +354,53 @@ export async function verifyFrozenNpmTarballIdentity(action, root) {
349
354
  });
350
355
  }
351
356
 
357
+ async function verifyNpmTarballBufferContract(buffer, action, tarballDir) {
358
+ const manifest = extractManifestFromTarball(buffer, {
359
+ name: action.package,
360
+ version: action.version,
361
+ });
362
+ const fileIndex = await buildTarballFileIndex(buffer, tarballDir);
363
+ const closureResult = checkNpmEntryClosure(manifest, fileIndex);
364
+ if (closureResult.errors.length > 0) {
365
+ throw new ReleaseError(
366
+ GATE_FAILED,
367
+ `npm entry closure check failed for ${action.package}@${action.version}: ${closureResult.errors.length} error(s)`,
368
+ {
369
+ gate: 'npm-entry-closure',
370
+ package: action.package,
371
+ version: action.version,
372
+ entries: closureResult.entries,
373
+ errors: closureResult.errors,
374
+ diagnostics: closureResult.diagnostics,
375
+ },
376
+ );
377
+ }
378
+ return { manifest, closureResult };
379
+ }
380
+
381
+ /**
382
+ * Shared frozen tarball contract helper: reads the verified tarball bytes ONCE
383
+ * and performs both identity verification (name/version from manifest) and
384
+ * entry closure validation (bin/main/module/types/typings/exports targets
385
+ * exist as regular files inside the tarball).
386
+ *
387
+ * The same Buffer that passes `readVerifiedTarballBytes` is reused for both
388
+ * the existing identity reader and the controlled extraction/file-index
389
+ * boundary. This adds no new tar parser and performs no second source read.
390
+ *
391
+ * @param {object} action - The frozen action with tarballPath, tarballSha256,
392
+ * integrity, package, and version.
393
+ * @param {string} root - Absolute project root.
394
+ * @param {string} [tarballDir] - Temp directory for tar extraction (defaults to os.tmpdir()).
395
+ * @returns {Promise<{ manifest: object, closureResult: object }>}
396
+ * @throws {ReleaseError} GATE_FAILED with details.gate='npm-entry-closure'
397
+ * when any declared entry is missing or invalid.
398
+ */
399
+ export async function verifyFrozenNpmTarballContract(action, root, tarballDir) {
400
+ const buffer = await readVerifiedTarballBytes(action, root);
401
+ return verifyNpmTarballBufferContract(buffer, action, tarballDir);
402
+ }
403
+
352
404
  /**
353
405
  * Read and verify the frozen tarball, then write it to a controlled named
354
406
  * temp file under a temp directory adjacent to the source. Returns the temp
@@ -700,7 +752,7 @@ export function createNpmAdapter(deps = {}) {
700
752
  }
701
753
  const cwd = resolvePackageCwd(action.cwd, context.root);
702
754
  if (action.tarballPath) {
703
- await verifyFrozenNpmTarballIdentity(action, context.root);
755
+ await verifyFrozenNpmTarballContract(action, context.root);
704
756
  }
705
757
 
706
758
  let whoamiUser;
@@ -775,7 +827,7 @@ export function createNpmAdapter(deps = {}) {
775
827
  throw new Error('frozen npm tarball requires a stable Buffer publish capability');
776
828
  }
777
829
  const buffer = await readVerifiedTarballBytes(action, context.root);
778
- const manifest = extractManifestFromTarball(buffer, { name: action.package, version: action.version });
830
+ const { manifest } = await verifyNpmTarballBufferContract(buffer, action);
779
831
  let token;
780
832
  try {
781
833
  token = await resolveAuthToken({ registry: normalizedRegistry, cwd, exec, env: authEnv });
@@ -2393,7 +2393,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2393
2393
 
2394
2394
  // A non-automatable platform (kimi) has NO scriptable install CLI.
2395
2395
  // It is handled entirely by its manual-requirement strategy, which
2396
- // uses a stable plan-digest-keyed home and deliberately SKIPS the
2396
+ // uses a stable plugin-level authority directory and deliberately SKIPS the
2397
2397
  // per-run isolated consumer dir and its runDir containment check
2398
2398
  // (that model only fits structured-cli platforms, which exec a real
2399
2399
  // CLI into a per-run HOME).
@@ -2913,25 +2913,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2913
2913
  });
2914
2914
  }
2915
2915
 
2916
- // installPath 验证:必须在 managed root 内,不得逃逸,不得是符号链接
2916
+ // installPath 验证:必须是真实目录,不得是符号链接。
2917
+ // 安装路径不要求位于 attestationDir 内(用户使用实际全局/用户安装目录)。
2917
2918
  let verifiedInstallPath = null;
2918
2919
  if (normalizedAttestation.installPath) {
2919
- const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
2920
2920
  const installPathAbs = resolve(normalizedAttestation.installPath);
2921
2921
 
2922
- // 先验证 managed root 存在(KIMI_CODE_HOME 检查)
2923
- const managedRootReal = await realpath(managedRoot).catch(() => null);
2924
- if (!managedRootReal) {
2925
- return createResult({
2926
- actionType,
2927
- status: ActionStatus.OBSERVED,
2928
- observation: {
2929
- installed: false,
2930
- error: 'KIMI_CODE_HOME does not exist',
2931
- },
2932
- });
2933
- }
2934
-
2935
2922
  // 检查 installPath 词法路径是否存在(lstat 在 realpath 之前)
2936
2923
  let lexicalStat;
2937
2924
  try {
@@ -2968,18 +2955,6 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2968
2955
  },
2969
2956
  });
2970
2957
  }
2971
- const rel = relative(managedRootReal, installPathReal);
2972
- const sep = process.platform === 'win32' ? '\\' : '/';
2973
- if (rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)) {
2974
- return createResult({
2975
- actionType,
2976
- status: ActionStatus.OBSERVED,
2977
- observation: {
2978
- installed: false,
2979
- error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`,
2980
- },
2981
- });
2982
- }
2983
2958
  verifiedInstallPath = installPathReal;
2984
2959
  }
2985
2960
 
@@ -3283,7 +3258,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
3283
3258
  try {
3284
3259
  codexRequirement = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_REQUIREMENT_FILE), 'utf8'));
3285
3260
  } catch {
3286
- // No requirement file — must not read isolated attestation
3261
+ // No requirement file — must not read the stable attestation
3287
3262
  }
3288
3263
 
3289
3264
  // Validate requirement binds to this action and plan (完整绑定验证)