release-skill 0.9.4 → 0.9.5

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 (58) 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 +29 -0
  7. package/INSTALL.md +2 -2
  8. package/INSTALL.zh-CN.md +2 -2
  9. package/README.md +20 -16
  10. package/README.zh-CN.md +19 -16
  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 +866 -166
  14. package/adapters/claude/schemas/release-plan.schema.json +36 -0
  15. package/adapters/claude/schemas/release-project.schema.json +31 -0
  16. package/adapters/claude/skills/release-finish/SKILL.md +5 -5
  17. package/adapters/claude/skills/release-help/SKILL.md +3 -3
  18. package/adapters/claude/skills/release-verify/SKILL.md +2 -1
  19. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  20. package/adapters/codex/bin/release-skill.bundle.mjs +866 -166
  21. package/adapters/codex/schemas/release-plan.schema.json +36 -0
  22. package/adapters/codex/schemas/release-project.schema.json +31 -0
  23. package/adapters/codex/skills/release-finish/SKILL.md +5 -5
  24. package/adapters/codex/skills/release-help/SKILL.md +3 -3
  25. package/adapters/codex/skills/release-verify/SKILL.md +2 -1
  26. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  27. package/adapters/kimi/bin/release-skill.bundle.mjs +866 -166
  28. package/adapters/kimi/schemas/release-plan.schema.json +36 -0
  29. package/adapters/kimi/schemas/release-project.schema.json +31 -0
  30. package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
  31. package/adapters/kimi/skills/release-help/SKILL.md +3 -3
  32. package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
  33. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  34. package/adapters/workbuddy/bin/release-skill.bundle.mjs +866 -166
  35. package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
  36. package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
  37. package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
  38. package/adapters/workbuddy/skills/release-help/SKILL.md +3 -3
  39. package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
  40. package/bin/release-skill-cli.mjs +87 -20
  41. package/bin/release-skill.bundle.mjs +866 -166
  42. package/package.json +1 -1
  43. package/platform-manifest.json +4 -4
  44. package/schemas/release-plan.schema.json +36 -0
  45. package/schemas/release-project.schema.json +31 -0
  46. package/skills/release-finish/SKILL.md +5 -5
  47. package/skills/release-help/SKILL.md +3 -3
  48. package/skills/release-verify/SKILL.md +2 -1
  49. package/skills-src/release-finish/SKILL.md +5 -5
  50. package/skills-src/release-help/SKILL.md +3 -3
  51. package/skills-src/release-verify/SKILL.md +2 -1
  52. package/src/commands/post-release-local.mjs +204 -10
  53. package/src/commands/prepare.mjs +178 -9
  54. package/src/commands/ship.mjs +26 -11
  55. package/src/commands/verify.mjs +175 -2
  56. package/src/core/plan.mjs +45 -1
  57. package/src/platforms/registry.mjs +97 -0
  58. package/src/readme/contract.mjs +23 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -2,11 +2,11 @@
2
2
  "schemaVersion": "1.0.0",
3
3
  "platformId": "release-skill",
4
4
  "platformName": "release-skill Plugin Platform",
5
- "version": "0.9.4",
5
+ "version": "0.9.5",
6
6
  "logicalRoot": "skills-src/",
7
- "logicalRootDigest": "673e640c5e6b017bacc31649886805c879ac244b1b79b9917d2515f88f09e8ca",
8
- "projectionDigest": "582e662d6f288725ffa09cd83034a339b8e05ec5c44a4989c16bf4e6a774e3c4",
9
- "projectionId": "release-skill:platform:0.9.4",
7
+ "logicalRootDigest": "3ead4305d76f86d100ca328e5051fceea9adc9fd308f2658b1bc24642b6af302",
8
+ "projectionDigest": "811660beafb951a5c2121128f3b142caf3dbe1fcf92a59183e6528983a5abfe8",
9
+ "projectionId": "release-skill:platform:0.9.5",
10
10
  "manifestRequired": true,
11
11
  "projectionSources": {
12
12
  "skills-src-root": {
@@ -539,6 +539,18 @@
539
539
  ],
540
540
  "description": "市场来源类型:bundled-family(技能族自带市场文件)或 standalone-index(独立市场仓库);每个平台每次发布只冻结一种来源"
541
541
  },
542
+ "firstReleaseBootstrap": {
543
+ "type": "string",
544
+ "enum": [
545
+ "manual-index-checkpoint"
546
+ ],
547
+ "description": "首次 standalone-index 发布的人工索引停点;仅显式 opt-in 时启用"
548
+ },
549
+ "marketplaceIndexSha": {
550
+ "type": ["string", "null"],
551
+ "pattern": "^[0-9a-f]{40}$",
552
+ "description": "最终市场索引提交 SHA;首次 bootstrap prepare 阶段必须保持 null,verify 仅记录实际观察值"
553
+ },
542
554
  "marketplaceCommitSha": {
543
555
  "type": "string",
544
556
  "pattern": "^[0-9a-f]{40}$",
@@ -697,6 +709,30 @@
697
709
  ]
698
710
  }
699
711
  },
712
+ {
713
+ "if": {
714
+ "required": [
715
+ "firstReleaseBootstrap"
716
+ ]
717
+ },
718
+ "then": {
719
+ "required": [
720
+ "marketplaceSourceType",
721
+ "marketplaceRepo"
722
+ ],
723
+ "properties": {
724
+ "type": {
725
+ "enum": [
726
+ "claude-plugin",
727
+ "codex-plugin"
728
+ ]
729
+ },
730
+ "marketplaceSourceType": {
731
+ "const": "standalone-index"
732
+ }
733
+ }
734
+ }
735
+ },
700
736
  {
701
737
  "if": {
702
738
  "required": [
@@ -257,6 +257,13 @@
257
257
  ],
258
258
  "description": "市场来源类型:bundled-family(技能族自带市场文件)或 standalone-index(独立市场仓库);每个平台每次发布只冻结一种来源"
259
259
  },
260
+ "firstReleaseBootstrap": {
261
+ "type": "string",
262
+ "enum": [
263
+ "manual-index-checkpoint"
264
+ ],
265
+ "description": "首次 standalone-index 发布的人工索引停点;仅显式 opt-in 时启用"
266
+ },
260
267
  "marketplaceIndexPath": {
261
268
  "type": "string",
262
269
  "minLength": 1,
@@ -447,6 +454,30 @@
447
454
  ]
448
455
  }
449
456
  },
457
+ {
458
+ "if": {
459
+ "required": [
460
+ "firstReleaseBootstrap"
461
+ ]
462
+ },
463
+ "then": {
464
+ "required": [
465
+ "marketplaceSourceType",
466
+ "marketplaceRepo"
467
+ ],
468
+ "properties": {
469
+ "type": {
470
+ "enum": [
471
+ "claude-plugin",
472
+ "codex-plugin"
473
+ ]
474
+ },
475
+ "marketplaceSourceType": {
476
+ "const": "standalone-index"
477
+ }
478
+ }
479
+ }
480
+ },
450
481
  {
451
482
  "if": {
452
483
  "required": [
@@ -13,7 +13,7 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
13
13
 
14
14
  这是发布后的独立收尾,不属于 `prepare → approve → publish → verify` 状态机。它不能把本机更新结果写成新的发布状态,也不能因为本机更新失败而降低 `VERIFIED`。
15
15
 
16
- 默认只读取冻结计划和 verify run。没有用户明确同意,不合并分支,不更新插件,不接受 Kimi 的安装信任提示。
16
+ 默认只读取冻结计划和 verify 或 postVerify run。没有用户明确同意,不合并分支,不更新插件,不接受 Kimi 的安装信任提示。
17
17
 
18
18
  ## 先生成收尾清单
19
19
 
@@ -23,11 +23,11 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
23
23
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
24
24
  --root <project-root> \
25
25
  --plan <plan-path> \
26
- --run <verified-run-path> \
26
+ --run <verify-or-postverify-run-path> \
27
27
  --json
28
28
  ```
29
29
 
30
- 脚本只接受状态为 `VERIFIED`、且 `planDigest` 与冻结计划一致的 verify run。
30
+ 脚本只接受与冻结计划一致的显式运行证据:计划未声明 `postVerify` hook 时,传入同计划的 `VERIFIED` verify run;计划声明了 `postVerify` hook 时,必须传入同计划、沿同一 `VERIFIED` verify run 继承谱系且所有 hook checkpoint 均为 `succeeded` 或 `NO_CHANGE` 的 `DISTRIBUTED` postVerify runpostVerify 尚未完成时,不能直接运行本机收尾;应先完成所需 checkpoint approval,再通过 `ship` 完成 postVerify,并使用结果中的 postVerify run 路径。
31
31
 
32
32
  ## 主动询问
33
33
 
@@ -45,7 +45,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
45
45
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
46
46
  --root <project-root> \
47
47
  --plan <plan-path> \
48
- --run <verified-run-path> \
48
+ --run <verify-or-postverify-run-path> \
49
49
  --update-local-hosts \
50
50
  --hosts claude,codex,kimi,codebuddy,workbuddy \
51
51
  --confirm-plan <planDigest> \
@@ -56,7 +56,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
56
56
 
57
57
  - Claude 先重新绑定冻结市场,再重新观察安装状态;旧插件若仍存在,才调用正式更新命令。Codex 继续按正式市场协议移除并安装冻结的 Git 引用。两者都在安装后核对插件、市场、版本和市场检出提交。
58
58
  - Kimi 的精确当前安装会在返回 `ALREADY_CURRENT` 前核对真实载荷;发生安装或迁移时,只在操作完成后核对结果。包名、版本、发布标签、已安装修订号和受管安装根必须与冻结计划一致;`.git` 只提供附加诊断,不是通过条件。旧的本地路径安装会在同一个受控终端交互界面(TUI)会话中先移除,再按发布标签安装、确认信任并重新加载。
59
- - CodeBuddy/WorkBuddy 只处理同一 bundled-family 插件和市场。只有冻结标签与计划声明的可变分支都从同一远端解析到冻结提交时,才调用正式市场更新和插件更新命令;完成后重新读取安装列表,并精确核对唯一条目的市场、版本和修订号。目标未安装、来源不符、远端不可访问或身份不一致时返回 `MANUAL_REQUIRED`,不修改宿主。
59
+ - CodeBuddy/WorkBuddy 只处理同一 bundled-family 插件和市场。CodeBuddy 仅探测全局 `codebuddy`/`cbc`,由收尾脚本把 `CODEBUDDY_CONFIG_DIR` 固定为有效 `HOME`(环境未提供时取操作系统用户主目录)下名为 `.codebuddy` 的目录;WorkBuddy 仅在 macOS 探测 WorkBuddy 应用内嵌 CLI,由收尾脚本把 `CODEBUDDY_CONFIG_DIR` 与 `WORKBUDDY_CONFIG_DIR` 同时固定为有效 `HOME` 下名为 `.workbuddy` 的目录,绝不把两者互作回退。只有冻结标签与计划声明的可变分支都从同一远端解析到冻结提交时,才调用正式市场更新和插件更新命令;完成后重新读取安装列表,并精确核对唯一条目的市场、版本和修订号。目标未安装、来源不符、远端不可访问或身份不一致时返回 `MANUAL_REQUIRED`,不修改宿主。非 macOS 的 WorkBuddy 返回 `SKIPPED_UNSUPPORTED_PLATFORM`。
60
60
 
61
61
  任何宿主失败都保留其他宿主的实际结果,不回滚,也不把失败冒充成功。
62
62
 
@@ -12,12 +12,12 @@ description: "Discoverable entry point for release-skill: dependency and environ
12
12
  ## 职责
13
13
 
14
14
  - 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
15
- - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准
15
+ - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准。若计划声明 `postVerify` hook,本机收尾必须等待 `ship` 产出的完成 postVerify run,不能把仅有 `VERIFIED` 的 verify run 当作本机收尾授权。核心发布流程跨平台,WorkBuddy 本机收尾仅支持 macOS
16
16
  - 最小示例:展示从 release-help 到 release-assess 的最短路径
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
19
19
 
20
- ## 0.9.4 候选边界
20
+ ## 0.9.5 候选边界
21
21
 
22
22
  当前源码候选允许多发布单元项目在计划冻结前显式选择本轮范围。`prepare` 和新建的 `ship` 状态支持重复传入 `--unit <id>`;未传时继续选择全部配置单元。成功选择会列出选中与延期单元。延期单元不进入本轮计划,也不获得发布状态。
23
23
 
@@ -25,7 +25,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
25
25
 
26
26
  0.9.3 引入的四项工作流保护、Hook cache v2 和稳定隔离安装树记录在 0.9.4 继续保留。0.9.4 精确消费 Foundation 0.16.0 的公开包根 API。Hook cache 只复用绝对路径或经真实 cwd 校验的 cwd-relative executable identity;裸 PATH、PATHEXT、Windows 和观察不可用时,Hook 仍冷执行,缓存复用失败关闭且不写入 v2 cache。缓存没有 TTL。
27
27
 
28
- 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.4 仍是源码候选,不能从本说明推断已批准、发布或验证。
28
+ 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.5 仍是源码候选,不能从本说明推断已批准、发布或验证。
29
29
 
30
30
  **阶段通过规则**: `status` 与 `readiness.localPreparation.status` 只判断本地 help/assess/prepare;其充要条件是 `READY` 且 exit code 为 0。`missingRequired` 列出缺失的 Node/Git。生产发布必须另外读取 `readiness.productionPublish`:缺少 npm/gh 时为 `NOT_READY`,依赖存在时仍是 `AUTH_CHECK_REQUIRED`,因为 help 不访问网络、不验证认证。Agent 无权把本地就绪解释为生产就绪。
31
31
 
@@ -45,7 +45,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
45
45
  2. 使用插件根相对路径运行 CLI;命令调用本身即授权执行已配置的 verification gate 和 smoke process
46
46
  3. 检查 exit code 和结构化状态:`VERIFIED`(全部通过)/ 失败(具体错误)
47
47
  4. 只有 `VERIFIED` 才是发布 happy end
48
- 5. 达到 `VERIFIED` 后立即路由 `release-finish`:先生成只读收尾清单,再按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
48
+ 5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook:有未完成 hook 时,先按 approval 合同批准并通过 `ship` 完成 postVerify,再把最终 `DISTRIBUTED` postVerify run 路径交给 `release-finish`;没有 postVerify hook 时,才把当前 verify run 路径交给 `release-finish`。随后按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
49
49
 
50
50
  ## 确定性脚本调用
51
51
 
@@ -66,6 +66,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
66
66
  ## 发布后收尾
67
67
 
68
68
  `VERIFIED` 不代表要自动修改开发分支或本机宿主。进入 `release-finish` 后,只有用户明确同意,才执行对应的本地动作。本机插件更新失败不会改变发布终态。
69
+ 核心 prepare、publish、verify 流程跨平台;WorkBuddy 的本机收尾探测和更新仅支持 macOS。
69
70
 
70
71
  ## 烟雾测试
71
72
 
@@ -13,7 +13,7 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
13
13
 
14
14
  这是发布后的独立收尾,不属于 `prepare → approve → publish → verify` 状态机。它不能把本机更新结果写成新的发布状态,也不能因为本机更新失败而降低 `VERIFIED`。
15
15
 
16
- 默认只读取冻结计划和 verify run。没有用户明确同意,不合并分支,不更新插件,不接受 Kimi 的安装信任提示。
16
+ 默认只读取冻结计划和 verify 或 postVerify run。没有用户明确同意,不合并分支,不更新插件,不接受 Kimi 的安装信任提示。
17
17
 
18
18
  ## 先生成收尾清单
19
19
 
@@ -23,11 +23,11 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
23
23
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
24
24
  --root <project-root> \
25
25
  --plan <plan-path> \
26
- --run <verified-run-path> \
26
+ --run <verify-or-postverify-run-path> \
27
27
  --json
28
28
  ```
29
29
 
30
- 脚本只接受状态为 `VERIFIED`、且 `planDigest` 与冻结计划一致的 verify run。
30
+ 脚本只接受与冻结计划一致的显式运行证据:计划未声明 `postVerify` hook 时,传入同计划的 `VERIFIED` verify run;计划声明了 `postVerify` hook 时,必须传入同计划、沿同一 `VERIFIED` verify run 继承谱系且所有 hook checkpoint 均为 `succeeded` 或 `NO_CHANGE` 的 `DISTRIBUTED` postVerify runpostVerify 尚未完成时,不能直接运行本机收尾;应先完成所需 checkpoint approval,再通过 `ship` 完成 postVerify,并使用结果中的 postVerify run 路径。
31
31
 
32
32
  ## 主动询问
33
33
 
@@ -45,7 +45,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
45
45
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
46
46
  --root <project-root> \
47
47
  --plan <plan-path> \
48
- --run <verified-run-path> \
48
+ --run <verify-or-postverify-run-path> \
49
49
  --update-local-hosts \
50
50
  --hosts claude,codex,kimi,codebuddy,workbuddy \
51
51
  --confirm-plan <planDigest> \
@@ -56,7 +56,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
56
56
 
57
57
  - Claude 先重新绑定冻结市场,再重新观察安装状态;旧插件若仍存在,才调用正式更新命令。Codex 继续按正式市场协议移除并安装冻结的 Git 引用。两者都在安装后核对插件、市场、版本和市场检出提交。
58
58
  - Kimi 的精确当前安装会在返回 `ALREADY_CURRENT` 前核对真实载荷;发生安装或迁移时,只在操作完成后核对结果。包名、版本、发布标签、已安装修订号和受管安装根必须与冻结计划一致;`.git` 只提供附加诊断,不是通过条件。旧的本地路径安装会在同一个受控终端交互界面(TUI)会话中先移除,再按发布标签安装、确认信任并重新加载。
59
- - CodeBuddy/WorkBuddy 只处理同一 bundled-family 插件和市场。只有冻结标签与计划声明的可变分支都从同一远端解析到冻结提交时,才调用正式市场更新和插件更新命令;完成后重新读取安装列表,并精确核对唯一条目的市场、版本和修订号。目标未安装、来源不符、远端不可访问或身份不一致时返回 `MANUAL_REQUIRED`,不修改宿主。
59
+ - CodeBuddy/WorkBuddy 只处理同一 bundled-family 插件和市场。CodeBuddy 仅探测全局 `codebuddy`/`cbc`,由收尾脚本把 `CODEBUDDY_CONFIG_DIR` 固定为有效 `HOME`(环境未提供时取操作系统用户主目录)下名为 `.codebuddy` 的目录;WorkBuddy 仅在 macOS 探测 WorkBuddy 应用内嵌 CLI,由收尾脚本把 `CODEBUDDY_CONFIG_DIR` 与 `WORKBUDDY_CONFIG_DIR` 同时固定为有效 `HOME` 下名为 `.workbuddy` 的目录,绝不把两者互作回退。只有冻结标签与计划声明的可变分支都从同一远端解析到冻结提交时,才调用正式市场更新和插件更新命令;完成后重新读取安装列表,并精确核对唯一条目的市场、版本和修订号。目标未安装、来源不符、远端不可访问或身份不一致时返回 `MANUAL_REQUIRED`,不修改宿主。非 macOS 的 WorkBuddy 返回 `SKIPPED_UNSUPPORTED_PLATFORM`。
60
60
 
61
61
  任何宿主失败都保留其他宿主的实际结果,不回滚,也不把失败冒充成功。
62
62
 
@@ -12,12 +12,12 @@ description: "Discoverable entry point for release-skill: dependency and environ
12
12
  ## 职责
13
13
 
14
14
  - 依赖和环境检查:Node.js >= 22、Git 决定本地准备就绪度;npm/gh 另行决定生产依赖就绪度
15
- - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准
15
+ - 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准。若计划声明 `postVerify` hook,本机收尾必须等待 `ship` 产出的完成 postVerify run,不能把仅有 `VERIFIED` 的 verify run 当作本机收尾授权。核心发布流程跨平台,WorkBuddy 本机收尾仅支持 macOS
16
16
  - 最小示例:展示从 release-help 到 release-assess 的最短路径
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
19
19
 
20
- ## 0.9.4 候选边界
20
+ ## 0.9.5 候选边界
21
21
 
22
22
  当前源码候选允许多发布单元项目在计划冻结前显式选择本轮范围。`prepare` 和新建的 `ship` 状态支持重复传入 `--unit <id>`;未传时继续选择全部配置单元。成功选择会列出选中与延期单元。延期单元不进入本轮计划,也不获得发布状态。
23
23
 
@@ -25,7 +25,7 @@ description: "Discoverable entry point for release-skill: dependency and environ
25
25
 
26
26
  0.9.3 引入的四项工作流保护、Hook cache v2 和稳定隔离安装树记录在 0.9.4 继续保留。0.9.4 精确消费 Foundation 0.16.0 的公开包根 API。Hook cache 只复用绝对路径或经真实 cwd 校验的 cwd-relative executable identity;裸 PATH、PATHEXT、Windows 和观察不可用时,Hook 仍冷执行,缓存复用失败关闭且不写入 v2 cache。缓存没有 TTL。
27
27
 
28
- 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.4 仍是源码候选,不能从本说明推断已批准、发布或验证。
28
+ 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.5 仍是源码候选,不能从本说明推断已批准、发布或验证。
29
29
 
30
30
  **阶段通过规则**: `status` 与 `readiness.localPreparation.status` 只判断本地 help/assess/prepare;其充要条件是 `READY` 且 exit code 为 0。`missingRequired` 列出缺失的 Node/Git。生产发布必须另外读取 `readiness.productionPublish`:缺少 npm/gh 时为 `NOT_READY`,依赖存在时仍是 `AUTH_CHECK_REQUIRED`,因为 help 不访问网络、不验证认证。Agent 无权把本地就绪解释为生产就绪。
31
31
 
@@ -45,7 +45,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
45
45
  2. 使用插件根相对路径运行 CLI;命令调用本身即授权执行已配置的 verification gate 和 smoke process
46
46
  3. 检查 exit code 和结构化状态:`VERIFIED`(全部通过)/ 失败(具体错误)
47
47
  4. 只有 `VERIFIED` 才是发布 happy end
48
- 5. 达到 `VERIFIED` 后立即路由 `release-finish`:先生成只读收尾清单,再按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
48
+ 5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook:有未完成 hook 时,先按 approval 合同批准并通过 `ship` 完成 postVerify,再把最终 `DISTRIBUTED` postVerify run 路径交给 `release-finish`;没有 postVerify hook 时,才把当前 verify run 路径交给 `release-finish`。随后按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
49
49
 
50
50
  ## 确定性脚本调用
51
51
 
@@ -66,6 +66,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
66
66
  ## 发布后收尾
67
67
 
68
68
  `VERIFIED` 不代表要自动修改开发分支或本机宿主。进入 `release-finish` 后,只有用户明确同意,才执行对应的本地动作。本机插件更新失败不会改变发布终态。
69
+ 核心 prepare、publish、verify 流程跨平台;WorkBuddy 的本机收尾探测和更新仅支持 macOS。
69
70
 
70
71
  ## 烟雾测试
71
72
 
@@ -1,4 +1,4 @@
1
- import { access } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, relative } from 'node:path';
4
4
 
@@ -12,6 +12,8 @@ import {
12
12
  import { getPlatform } from '../platforms/registry.mjs';
13
13
  import { resolveCodeBuddyMarketplace } from '../platforms/codebuddy.mjs';
14
14
  import { verifyInstalledMarketplacePayload } from '../adapters/plugin-marketplace.mjs';
15
+ import { normalizePostPublishView, postPublishActionId } from '../core/postpublish.mjs';
16
+ import { loadRun, validateRunLineage } from '../core/run.mjs';
15
17
 
16
18
  const HOSTS_BY_ACTION = Object.freeze({
17
19
  'claude-marketplace-install': ['claude'],
@@ -34,6 +36,7 @@ const SAFE_ENV_KEYS = Object.freeze([
34
36
  'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
35
37
  'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
36
38
  'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
39
+ 'CODEBUDDY_CONFIG_DIR', 'WORKBUDDY_CONFIG_DIR',
37
40
  ]);
38
41
  const CODEBUDDY_PLUGIN_LIST_ARGS = Object.freeze(['plugin', 'list', '--json']);
39
42
  const CODEBUDDY_MARKETPLACE_LIST_ARGS = Object.freeze(['plugin', 'marketplace', 'list']);
@@ -169,7 +172,27 @@ function assertExecutableTarget(target) {
169
172
  }
170
173
  }
171
174
 
172
- export function derivePostReleaseChecklist(plan) {
175
+ function buildShipNextStep({ root, statePath, unitIds }) {
176
+ const argv = ['release-skill', 'ship'];
177
+ if (typeof root === 'string' && root.length > 0) argv.push('--root', root);
178
+ if (typeof statePath === 'string' && statePath.length > 0) argv.push('--state', statePath);
179
+ if (Array.isArray(unitIds) && unitIds.length > 0) {
180
+ for (const unitId of unitIds) argv.push('--unit', unitId);
181
+ }
182
+ return {
183
+ code: 'COMPLETE_POST_VERIFY',
184
+ message: 'Complete the postVerify phase with ship before running post-release.',
185
+ argv,
186
+ };
187
+ }
188
+
189
+ export function derivePostReleaseChecklist(plan, {
190
+ runPath,
191
+ root,
192
+ statePath,
193
+ unitIds,
194
+ postVerifyComplete = false,
195
+ } = {}) {
173
196
  if (!plan || typeof plan !== 'object' || typeof plan.digest !== 'string') {
174
197
  throw new Error('a frozen release plan with digest is required');
175
198
  }
@@ -178,6 +201,9 @@ export function derivePostReleaseChecklist(plan) {
178
201
  !BRANCH_ACTION_INCLUDED.has(unit.productionConfig?.branchStrategy)
179
202
  ));
180
203
  const targets = pluginTargets(plan);
204
+ const hasPendingPostVerify = postVerifyHooks(plan).length > 0 && !postVerifyComplete && targets.length > 0;
205
+ const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
206
+ const selectedUnitIds = Array.isArray(unitIds) ? unitIds : undefined;
181
207
  return {
182
208
  command: 'post-release',
183
209
  status: 'AWAITING_USER_DECISION',
@@ -193,14 +219,173 @@ export function derivePostReleaseChecklist(plan) {
193
219
  })),
194
220
  },
195
221
  localHostUpdate: {
196
- promptRequired: targets.length > 0,
197
- available: targets.length > 0,
222
+ promptRequired: targets.length > 0 && !hasPendingPostVerify,
223
+ available: targets.length > 0 && !hasPendingPostVerify,
224
+ ...(!hasPendingPostVerify && runPath ? { runPath } : {}),
225
+ ...(hasPendingPostVerify ? {
226
+ ...(hasStatePath ? { nextSteps: [buildShipNextStep({ root, statePath, unitIds: selectedUnitIds })] } : {}),
227
+ } : {}),
198
228
  hosts: [...new Set(targets.map((target) => target.host))].sort(),
199
229
  targets,
200
230
  },
201
231
  };
202
232
  }
203
233
 
234
+ function postVerifyHooks(plan) {
235
+ if (plan?.planVersion === undefined) return [];
236
+ return normalizePostPublishView(plan).flatMap((declaration) => (
237
+ (declaration.hooks ?? [])
238
+ .filter((hook) => hook.phase === 'postVerify')
239
+ .map((hook) => ({
240
+ actionId: postPublishActionId({ planVersion: plan.planVersion, unitId: declaration.unitId, localId: hook.id }),
241
+ hook,
242
+ unitId: declaration.unitId,
243
+ }))
244
+ ));
245
+ }
246
+
247
+ function localFinishEvidenceError(message, { cause, root, statePath, unitIds } = {}) {
248
+ const error = new Error(`local host update evidence is not ready: ${message}; next step: obtain approval if required, then complete the postVerify phase with ship before rerunning post-release`);
249
+ error.code = 'LOCAL_FINISH_EVIDENCE_NOT_READY';
250
+ const hasStatePath = typeof statePath === 'string' && statePath.length > 0;
251
+ error.details = {
252
+ cause: {
253
+ code: cause?.code ?? 'LOCAL_FINISH_EVIDENCE_NOT_READY',
254
+ message: cause?.message ?? message,
255
+ },
256
+ ...(hasStatePath ? {
257
+ nextSteps: [buildShipNextStep({ root, statePath, unitIds })],
258
+ } : {}),
259
+ };
260
+ return error;
261
+ }
262
+
263
+ /**
264
+ * Validate the frozen run authority before allowing any local host command.
265
+ * This is the only exported entry that can perform local host writes.
266
+ */
267
+ export async function updateLocalHostPlugins({
268
+ plan: _suppliedPlan,
269
+ planPath,
270
+ runPath,
271
+ runRecord: _suppliedRunRecord,
272
+ production: _suppliedProduction,
273
+ root = process.cwd(),
274
+ statePath,
275
+ unitIds,
276
+ ...options
277
+ } = {}) {
278
+ if (!planPath || !runPath) {
279
+ throw localFinishEvidenceError(
280
+ 'planPath and runPath are required before local host updates',
281
+ { root, statePath, unitIds },
282
+ );
283
+ }
284
+ let plan;
285
+ let runRecord;
286
+ try {
287
+ plan = JSON.parse(await readFile(planPath, 'utf8'));
288
+ runRecord = await loadRun(runPath, {
289
+ requireDigest: true,
290
+ authorityPlanPath: planPath,
291
+ });
292
+ } catch (cause) {
293
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds });
294
+ }
295
+ await assertLocalFinishRun({
296
+ plan,
297
+ planPath,
298
+ runPath,
299
+ runRecord,
300
+ production: Boolean(plan.production),
301
+ root,
302
+ statePath,
303
+ unitIds,
304
+ });
305
+ return updateLocalHostPluginsInternal({
306
+ plan,
307
+ root,
308
+ ...options,
309
+ });
310
+ }
311
+
312
+ /** Validate the explicit run supplied to local-finish before any host probe. */
313
+ export async function assertLocalFinishRun({
314
+ plan,
315
+ planPath,
316
+ runPath,
317
+ runRecord,
318
+ production = false,
319
+ root,
320
+ statePath,
321
+ unitIds,
322
+ } = {}) {
323
+ const hooks = postVerifyHooks(plan);
324
+ if (hooks.length === 0) {
325
+ try {
326
+ await validateRunLineage(runRecord, { plan, planPath, runPath, production });
327
+ assertVerifiedReleaseRun(plan, runRecord);
328
+ } catch (cause) {
329
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
330
+ }
331
+ return { runPath, phase: 'verify' };
332
+ }
333
+ if (runRecord?.command !== 'postverify' || runRecord?.status !== 'DISTRIBUTED') {
334
+ throw localFinishEvidenceError('the plan declares postVerify hooks, so the supplied verify run cannot authorize local-finish', { root, statePath, unitIds, plan });
335
+ }
336
+ if (runRecord.planDigest !== plan?.digest) {
337
+ throw localFinishEvidenceError('postVerify run is not bound to the frozen plan', { root, statePath, unitIds, plan });
338
+ }
339
+ if (typeof runRecord.sourceRunPath !== 'string' || typeof runRecord.sourceRunId !== 'string' || typeof runRecord.sourceRunDigest !== 'string') {
340
+ throw localFinishEvidenceError('completed postVerify run has incomplete verify-run lineage', { root, statePath, unitIds, plan });
341
+ }
342
+ const checkpoints = Array.isArray(runRecord.checkpoints) ? runRecord.checkpoints : null;
343
+ const expectedIds = hooks.map(({ actionId }) => actionId);
344
+ const actualIds = checkpoints?.map((checkpoint) => checkpoint?.actionId) ?? [];
345
+ if (
346
+ !checkpoints
347
+ || actualIds.length !== expectedIds.length
348
+ || new Set(actualIds).size !== actualIds.length
349
+ || actualIds.some((id) => !expectedIds.includes(id))
350
+ || expectedIds.some((id) => !actualIds.includes(id))
351
+ || checkpoints.some((checkpoint) => (
352
+ checkpoint.actionType !== 'postpublish-hook'
353
+ || !['succeeded', 'NO_CHANGE'].includes(checkpoint.status)
354
+ ))
355
+ ) {
356
+ throw localFinishEvidenceError('postVerify checkpoints must match each declared hook exactly and be succeeded or NO_CHANGE', { root, statePath, unitIds, plan });
357
+ }
358
+ let sourceRun;
359
+ try {
360
+ sourceRun = await loadRun(runRecord.sourceRunPath, {
361
+ requireDigest: true,
362
+ authorityPlanPath: planPath,
363
+ });
364
+ } catch (cause) {
365
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
366
+ }
367
+ if (
368
+ sourceRun.command !== 'verify'
369
+ || sourceRun.status !== 'VERIFIED'
370
+ || sourceRun.runId !== runRecord.sourceRunId
371
+ || sourceRun.runDigest !== runRecord.sourceRunDigest
372
+ || sourceRun.planDigest !== plan.digest
373
+ ) {
374
+ throw localFinishEvidenceError('postVerify lineage does not point to the same-plan VERIFIED run', { root, statePath, unitIds, plan });
375
+ }
376
+ try {
377
+ await validateRunLineage(sourceRun, {
378
+ plan,
379
+ planPath,
380
+ runPath: runRecord.sourceRunPath,
381
+ production,
382
+ });
383
+ } catch (cause) {
384
+ throw localFinishEvidenceError(cause.message, { cause, root, statePath, unitIds, plan });
385
+ }
386
+ return { runPath, sourceRunPath: runRecord.sourceRunPath, phase: 'postverify' };
387
+ }
388
+
204
389
  export function unavailablePostReleaseChecklist(plan, error) {
205
390
  return {
206
391
  command: 'post-release',
@@ -232,7 +417,11 @@ function hostEnvironment(host, { kimiHome } = {}) {
232
417
  env.HOME ??= homedir();
233
418
  env.PATH ??= '/usr/bin:/bin';
234
419
  env.GIT_TERMINAL_PROMPT = '0';
235
- if (host === 'workbuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
420
+ if (host === 'codebuddy') env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.codebuddy');
421
+ if (host === 'workbuddy') {
422
+ env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
423
+ env.WORKBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
424
+ }
236
425
  if (host === 'kimi' && kimiHome) env.KIMI_CONFIG_DIR = kimiHome;
237
426
  return env;
238
427
  }
@@ -286,12 +475,17 @@ async function commandAvailable(command, host, run) {
286
475
  }
287
476
 
288
477
  async function defaultDetect(host, run = defaultRun) {
289
- if (host === 'codebuddy' || host === 'workbuddy') {
290
- for (const command of ['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH]) {
478
+ if (host === 'codebuddy') {
479
+ for (const command of ['codebuddy', 'cbc']) {
291
480
  if (await commandAvailable(command, host, run)) return { available: true, command };
292
481
  }
293
482
  return { available: false, reason: 'CodeBuddy/WorkBuddy CLI not found' };
294
483
  }
484
+ if (host === 'workbuddy') {
485
+ if (process.platform !== 'darwin') return { available: false, status: 'SKIPPED_UNSUPPORTED_PLATFORM', reason: 'WorkBuddy local update is supported only on macOS' };
486
+ if (await commandAvailable(CODEBUDDY_MACOS_PATH, host, run)) return { available: true, command: CODEBUDDY_MACOS_PATH };
487
+ return { available: false, reason: 'WorkBuddy embedded CLI not found' };
488
+ }
295
489
  if (host === 'kimi') {
296
490
  if (!await commandAvailable('kimi', host, run)) return { available: false, reason: 'kimi CLI not found' };
297
491
  try {
@@ -894,7 +1088,7 @@ function failedHostResult(target, error) {
894
1088
  };
895
1089
  }
896
1090
 
897
- export async function updateLocalHostPlugins({
1091
+ async function updateLocalHostPluginsInternal({
898
1092
  plan,
899
1093
  root = process.cwd(),
900
1094
  confirmPlanDigest,
@@ -905,7 +1099,7 @@ export async function updateLocalHostPlugins({
905
1099
  verifyInstalledPayload = verifyInstalledMarketplacePayload,
906
1100
  } = {}) {
907
1101
  const effectiveKimiHome = kimiHome ?? process.env.KIMI_CONFIG_DIR ?? join(homedir(), '.kimi-code');
908
- const checklist = derivePostReleaseChecklist(plan);
1102
+ const checklist = derivePostReleaseChecklist(plan, { postVerifyComplete: true });
909
1103
  if (confirmPlanDigest !== plan.digest) {
910
1104
  throw new Error('plan digest confirmation does not match the frozen release plan');
911
1105
  }
@@ -923,7 +1117,7 @@ export async function updateLocalHostPlugins({
923
1117
  results.push({
924
1118
  host: target.host,
925
1119
  unitId: target.unitId,
926
- status: 'SKIPPED_NOT_INSTALLED',
1120
+ status: detected?.status ?? 'SKIPPED_NOT_INSTALLED',
927
1121
  reason: detected?.reason ?? 'host unavailable',
928
1122
  });
929
1123
  continue;