release-skill 0.9.3 → 0.9.4

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 (53) 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 +47 -15
  10. package/README.zh-CN.md +42 -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/consumer-contract-vectors.json +7 -7
  14. package/adapters/claude/bin/foundation-resource-binding.json +1 -1
  15. package/adapters/claude/bin/release-skill.bundle.mjs +651 -190
  16. package/adapters/claude/skills/release-help/SKILL.md +10 -3
  17. package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
  20. package/adapters/codex/bin/foundation-resource-binding.json +1 -1
  21. package/adapters/codex/bin/release-skill.bundle.mjs +651 -190
  22. package/adapters/codex/skills/release-help/SKILL.md +10 -3
  23. package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
  24. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  25. package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
  26. package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
  27. package/adapters/kimi/bin/release-skill.bundle.mjs +651 -190
  28. package/adapters/kimi/skills/release-help/SKILL.md +10 -3
  29. package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
  30. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  31. package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
  32. package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
  33. package/adapters/workbuddy/bin/release-skill.bundle.mjs +651 -190
  34. package/adapters/workbuddy/skills/release-help/SKILL.md +10 -3
  35. package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
  36. package/bin/consumer-contract-vectors.json +7 -7
  37. package/bin/foundation-resource-binding.json +1 -1
  38. package/bin/release-skill-cli.mjs +26 -2
  39. package/bin/release-skill.bundle.mjs +651 -190
  40. package/package.json +4 -4
  41. package/platform-manifest.json +4 -4
  42. package/references/.render-manifest.json +4 -4
  43. package/references/01-state-machine.md +18 -0
  44. package/references/02-project-config.md +20 -0
  45. package/skills/release-help/SKILL.md +10 -3
  46. package/skills/release-prepare/SKILL.md +11 -3
  47. package/skills-src/release-help/SKILL.md +10 -3
  48. package/skills-src/release-prepare/SKILL.md +11 -3
  49. package/src/commands/post-release-local.mjs +169 -16
  50. package/src/commands/prepare.mjs +183 -7
  51. package/src/commands/ship.mjs +164 -2
  52. package/src/core/foundation-plugin-verification.mjs +22 -2
  53. package/src/producers/foundation-resource-projection.mjs +3 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -79,9 +79,9 @@
79
79
  "ajv-formats": "3.0.1",
80
80
  "libnpmpublish": "11.2.0",
81
81
  "npm-registry-fetch": "18.0.2",
82
- "skill-family-contracts": "0.15.0",
83
- "skill-family-engineering-kit": "0.15.0",
84
- "skill-family-harness-node": "0.15.0",
82
+ "skill-family-contracts": "0.16.0",
83
+ "skill-family-engineering-kit": "0.16.0",
84
+ "skill-family-harness-node": "0.16.0",
85
85
  "yaml": "2.9.0"
86
86
  },
87
87
  "devDependencies": {
@@ -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.3",
5
+ "version": "0.9.4",
6
6
  "logicalRoot": "skills-src/",
7
- "logicalRootDigest": "be294a1ed865d99841b78b60039136ab28b13ceaedf28570b2fbd91febd1ef65",
8
- "projectionDigest": "fbd95e66fe7183c9cfd34eb83fa084e259a00806b3f129734cf866de632723cf",
9
- "projectionId": "release-skill:platform:0.9.3",
7
+ "logicalRootDigest": "673e640c5e6b017bacc31649886805c879ac244b1b79b9917d2515f88f09e8ca",
8
+ "projectionDigest": "582e662d6f288725ffa09cd83034a339b8e05ec5c44a4989c16bf4e6a774e3c4",
9
+ "projectionId": "release-skill:platform:0.9.4",
10
10
  "manifestRequired": true,
11
11
  "projectionSources": {
12
12
  "skills-src-root": {
@@ -6,12 +6,12 @@
6
6
  "bytes": 6335
7
7
  },
8
8
  "01-state-machine.md": {
9
- "digest": "5afc8e9c29c0d5fb0de0db8b248bbb812fc715af185ac594a172fe68d9a24fed",
10
- "bytes": 12617
9
+ "digest": "85e20f4ba60a833325516b9ced9351f9e324cd7b386460d3fb108b79b8875265",
10
+ "bytes": 14857
11
11
  },
12
12
  "02-project-config.md": {
13
- "digest": "09e3c470dea54fe9437528620cc169596e75b5162e137683923347c9cb3702a6",
14
- "bytes": 23167
13
+ "digest": "50f9b775a4864270642b752d040546f6c365f0b49bf1bfe8a3aacefe88eae702",
14
+ "bytes": 26033
15
15
  },
16
16
  "03-readme-quality.md": {
17
17
  "digest": "d74e41052592443ebbd450d0ace5f9b9b19c7a94db50f952b17c1c5a4c5dc230",
@@ -84,6 +84,24 @@ postPublish hooks 分两个阶段,各自对应独立的 distribute run:
84
84
  - **postVerify 阶段**(`phase: postVerify`):主 run VERIFIED 后由 ship/verify 路由触发,产出独立的 distribute run(复用现有 run 记录与 checkpoint 机制);失败即该 run PARTIAL、reconcile 可续。主 run 的 VERIFIED 不回退,但 postVerify run 的失败必须以证据显著记录,绝不静默。
85
85
  - **checkpoint 级批准**:`requiresApproval: true` 的 hook 执行前需要绑定 `(planDigest, hookId)` 的独立批准记录(24h 过期、5 分钟时钟偏移容忍)。批准未获时:尚无外部 checkpoint 成功 → checkpoint 标记 `AWAITING_APPROVAL`、run 进 NEEDS_INPUT;已有外部 checkpoint 成功 → run 保持 PARTIAL。
86
86
 
87
+ ### 2.3 发布范围与延期单元(v0.9.4)
88
+
89
+ 多发布单元项目可以在首次计划冻结前显式选择本轮发布范围。`prepare` 和新建的
90
+ `ship` 状态接受可重复的 `--unit <id>`;未传该参数时,范围仍是配置中的全部发布单元。
91
+
92
+ 显式选择不会增加生命周期状态。未选中的单元只显示为延期单元,不进入本轮
93
+ `plan.units`,也不获得 PREPARED、APPROVED、PUBLISHED 或 VERIFIED 含义。完整项目配置、
94
+ 生成物新鲜度和顶层 hooks 仍按项目级规则检查;按单元绑定的版本、文档、快照、分发和
95
+ 验证门只处理选中单元。
96
+
97
+ 计划冻结后,`plan.units` 是唯一发布范围权威。批准必须绑定计划内的全部动作,
98
+ `publish`、`reconcile`、`verify` 和 `distribute` 必须完整处理该计划,不接受单元选择参数。
99
+ `ship` 可以沿用首次保存的显式范围恢复,但不得在同一状态上改变范围。旧状态没有保存
100
+ 显式范围时按全范围处理,不能在原状态上追加 `--unit`。
101
+
102
+ 外部写入开始后不得缩小范围。任一检查点成功后出现失败,仍进入 PARTIAL,并由
103
+ `reconcile` 在原冻结计划内向前恢复;延期单元需要另行 prepare、审阅和批准。
104
+
87
105
  ---
88
106
 
89
107
  ## 3. 禁止转换
@@ -126,6 +126,26 @@ unit 恰好声明一个 npm distribution。prepare 等所有 subject tarball 冻
126
126
  重新下载远端字节并核对 SHA-256。离线消费者只需该文件和其中 subjects 对应的精确 tarball,便可
127
127
  核对包名、版本、文件名、tarball SHA-256、源码仓库与源码基线提交。
128
128
 
129
+ ### 2.1 显式发布范围(v0.9.4)
130
+
131
+ 发布范围不增加配置字段。`releaseUnits[]` 继续声明项目可发布的全部单元;操作者可以在
132
+ `prepare` 或新建 `ship` 状态时重复传入 `--unit <id>`,从中选择本轮范围。未传参数时
133
+ 选择全部单元。显式参数至少包含一个已声明的唯一 ID,计划顺序始终按
134
+ `releaseUnits[]` 的声明顺序确定,不受命令行顺序影响。
135
+
136
+ 选择只裁剪按单元绑定的工作,包括版本与发布文档检查、公开文件快照、分发合同、
137
+ `verificationGates[].scope.unit` 对应的验证门和发布后声明。配置 Schema、单元 ID
138
+ 唯一性、生成物新鲜度和顶层 `hooks` 仍覆盖完整项目。顶层 Hook 没有单元作用域,
139
+ 不得根据命令、路径或输出猜测归属。
140
+
141
+ `publicSourceAuthorityReceipt` 是现有的附加依赖闭包。显式范围只要包含 coordinator
142
+ 或任一 subject,就必须同时包含该收据声明涉及的全部单元。prepare 不会自动扩大范围;
143
+ 范围不完整时会在 Hook、快照和计划写入前停止,并返回缺少的单元和精确重试参数。
144
+
145
+ 冻结计划只包含选中单元及其派生门、动作和发布后声明。`plan.units` 是冻结后的唯一
146
+ 发布范围权威,完整配置摘要仍进入计划绑定。延期单元不写回项目配置,也不形成状态、
147
+ 豁免或待办记录。`publish`、`reconcile`、`verify` 和 `distribute` 不接受单元选择参数。
148
+
129
149
  `postPublish` 只能位于某个 `releaseUnits[]` 条目内。下面的完整结构样例会由 schema 契约测试直接读取并校验,避免文档层级再次漂移:
130
150
 
131
151
  ```yaml
@@ -17,11 +17,15 @@ description: "Discoverable entry point for release-skill: dependency and environ
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
19
19
 
20
- ## 0.9.3 候选边界
20
+ ## 0.9.4 候选边界
21
21
 
22
- 当前源码候选已接入四项工作流保护、Hook cache v2 和稳定隔离安装树记录。三项 Foundation 依赖精确使用 0.15.0 的公开包根 API。Hook cache 只复用绝对路径或经真实 cwd 校验的 cwd-relative executable identity;裸 PATH、PATHEXT、Windows 和观察不可用时,Hook 仍冷执行,缓存复用失败关闭且不写入 v2 cache。缓存没有 TTL。
22
+ 当前源码候选允许多发布单元项目在计划冻结前显式选择本轮范围。`prepare` 和新建的 `ship` 状态支持重复传入 `--unit <id>`;未传时继续选择全部配置单元。成功选择会列出选中与延期单元。延期单元不进入本轮计划,也不获得发布状态。
23
23
 
24
- 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.3 仍是源码候选,不能从本说明推断已批准、发布或验证。
24
+ 选择只影响按单元绑定的检查与动作。完整配置、生成物新鲜度和顶层 Hook 仍覆盖整个项目。`publicSourceAuthorityReceipt` 涉及的 coordinator 和 subjects 必须作为完整闭包共同选择,系统不会自动扩选。计划冻结后,`plan.units` 是唯一范围权威;publish、reconcile、verify 和 distribute 不接受 `--unit`。
25
+
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
+
28
+ 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.4 仍是源码候选,不能从本说明推断已批准、发布或验证。
25
29
 
26
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 无权把本地就绪解释为生产就绪。
27
31
 
@@ -44,6 +48,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
44
48
  # 日常发布快速路径:发布前确认可读计划摘要,状态文件可恢复。
45
49
  # 受限 postPublish hook 的 checkpoint 批准与计划批准分开。
46
50
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
51
+ # 多发布单元项目显式选择本轮范围;未传 --unit 时仍为全部单元
52
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> \
53
+ --target-version <version> --unit <unit-a> --unit <unit-b> --json
47
54
  # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
48
55
  # 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
49
56
  # 命令调用本身即授权执行配置中的 hooks
@@ -13,7 +13,9 @@ description: Freeze an immutable release plan with local configuration, document
13
13
 
14
14
  运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
15
15
 
16
- 0.9.3 候选的 Hook cache v2 只复用有完整身份和输入证据的成功 Hook 结果。裸 PATHPATHEXTWindows、TTL、损坏记录或 Foundation 观察不可用时,缓存保持失败关闭,Hook 仍按完整路径冷执行;该机制不改变 prepare 的计划、批准和发布权威。
16
+ 0.9.4 候选允许多发布单元项目在冻结前显式选择本轮范围。`--unit <id>` 可以重复传入;未传时仍选择全部单元。显式选择只跳过延期单元的单元级工作,完整配置校验、生成物新鲜度和顶层 Hook 仍全部执行。计划冻结后以 `plan.units` 为唯一范围权威,publishreconcileverify distribute 不接受单元选择。
17
+
18
+ 0.9.3 引入的 Hook cache v2 继续只复用有完整身份和输入证据的成功 Hook 结果。裸 PATH、PATHEXT、Windows、TTL、损坏记录或 Foundation 观察不可用时,缓存保持失败关闭,Hook 仍按完整路径冷执行;该机制不改变 prepare 的计划、批准和发布权威。
17
19
 
18
20
  **Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。旧参数 `--acknowledge-hook-side-effects` 和 `--acknowledge-gate-side-effects` 仍可解析,但只作为无效果的兼容输入,不能改变授权或执行语义。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
19
21
 
@@ -25,11 +27,13 @@ description: Freeze an immutable release plan with local configuration, document
25
27
 
26
28
  **发布文档新鲜度门**: 配置了 `releaseDocuments` 的单元在 hook 授权门前先执行同一只读规划器:`clean` 继续;`changes` 抛 `RELEASE_DOCS_STALE`,详情列出相对路径、语种、`refreshDigest` 和精确演练/写入参数数组。prepare 只检查、不写工作树。正式 prepare 前先运行只读演练;有变化时向用户展示文件/语种/版本/`refreshDigest`,只有在用户明确授权"本地发布文档写入"后,才执行带 `--write --confirm-refresh <refreshDigest> --ack-local-document-write` 三项绑定的写入,随后运行聚焦校验,要求维护者审阅并提交刷新结果,再重新 prepare。该授权不扩展为 hook、提交、push 或 publish 授权。
27
29
 
30
+ **显式发布范围**: 只有用户已经指出本轮要发布哪些单元时,才把这些 ID 逐个传给 `--unit`。release-skill 不根据失败自动排除单元。选择命中 `publicSourceAuthorityReceipt` 的 coordinator 或 subject 时,必须包含收据声明的完整单元闭包;缺少单元时按命令返回的精确 argv 重新选择,不自动扩选。成功后展示 `releaseScope.selectedUnitIds`、`releaseScope.deferredUnitIds` 和批准摘要。延期仅表示没有进入本轮计划,不表示通过或失败。
31
+
28
32
  ## 正向执行路径
29
33
 
30
34
  1. 使用插件根相对路径运行 CLI:`CLI="node ${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs"`
31
35
  2. 配置含 `releaseDocuments` 时,先运行只读演练 `${CLI} docs refresh --unit <id> --json`;`status: "changes"` 时展示逐文件路径/语种/版本/`refreshDigest`,取得"本地发布文档写入"明确授权后才执行 `nextCommand.argv` 写入,审阅并提交刷新结果后再继续;`status: "clean"` 时直接进入 prepare
32
- 3. 运行 `${CLI} prepare --root <path> --offline --json`
36
+ 3. 运行 `${CLI} prepare --root <path> --offline --json`;用户已明确选择范围时,为每个单元追加一个 `--unit <id>`
33
37
  4. 检查 exit code 0,读取 JSON 返回的 immutable `planPath=plans/<planDigest>.json`,再从该文件读取 `status`、`units`、`externalActions`
34
38
  5. 向用户展示可读的 `approvalSummary`:版本、公开仓库、分支策略、branch/tag、npm 与 GitHub Release 目标、全部外部动作、例外,以及需要独立 checkpoint 批准的 postPublish hook。`planDigest` 仅作为内部绑定字段,不要求用户复制或确认。后续 approve/publish 只能使用该 immutable planPath,等待确认后再 approve。计划批准不包含受限 postPublish hook 的 checkpoint 批准
35
39
 
@@ -64,6 +68,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --js
64
68
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> \
65
69
  --write --confirm-refresh <refreshDigest> --ack-local-document-write --json
66
70
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offline --json
71
+ # 显式选择发布范围;未传 --unit 时仍准备全部配置单元
72
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offline \
73
+ --unit <unit-a> --unit <unit-b> --json
67
74
  # 生产 happy end:bound 基线必须 online;远端目标唯一性仍由 publish 全局预检
68
75
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --online --production --json
69
76
  ```
@@ -81,11 +88,12 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --onlin
81
88
  | GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
82
89
  | GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
83
90
  | GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
91
+ | GATE_FAILED(发布范围依赖闭包不完整) | 按详情补齐 `publicSourceAuthorityReceipt` 声明涉及的 coordinator 和全部 subjects,再重新 prepare;不得自动扩选或忽略收据 |
84
92
  | GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
85
93
  | RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
86
94
  | RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
87
95
  | SECRET_DETECTED | 移除密钥并更新 allowlist |
88
- | CONFIG_INVALID | 检查 version.sourcepackage.json |
96
+ | CONFIG_INVALID | 检查 version.sourcepackage.json,以及 `--unit` 是否为空、重复或不在 `releaseUnits[]` 中 |
89
97
 
90
98
  重试时只保留最新结构化错误码和失败门,不沿用早期猜测;重跑确定性命令获得新证据。
91
99
 
@@ -17,11 +17,15 @@ description: "Discoverable entry point for release-skill: dependency and environ
17
17
  - 只读诊断:运行 dry-run 检查,不修改任何文件
18
18
  - 故障引导:根据错误码指向对应的修复 Skill
19
19
 
20
- ## 0.9.3 候选边界
20
+ ## 0.9.4 候选边界
21
21
 
22
- 当前源码候选已接入四项工作流保护、Hook cache v2 和稳定隔离安装树记录。三项 Foundation 依赖精确使用 0.15.0 的公开包根 API。Hook cache 只复用绝对路径或经真实 cwd 校验的 cwd-relative executable identity;裸 PATH、PATHEXT、Windows 和观察不可用时,Hook 仍冷执行,缓存复用失败关闭且不写入 v2 cache。缓存没有 TTL。
22
+ 当前源码候选允许多发布单元项目在计划冻结前显式选择本轮范围。`prepare` 和新建的 `ship` 状态支持重复传入 `--unit <id>`;未传时继续选择全部配置单元。成功选择会列出选中与延期单元。延期单元不进入本轮计划,也不获得发布状态。
23
23
 
24
- 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.3 仍是源码候选,不能从本说明推断已批准、发布或验证。
24
+ 选择只影响按单元绑定的检查与动作。完整配置、生成物新鲜度和顶层 Hook 仍覆盖整个项目。`publicSourceAuthorityReceipt` 涉及的 coordinator 和 subjects 必须作为完整闭包共同选择,系统不会自动扩选。计划冻结后,`plan.units` 是唯一范围权威;publish、reconcile、verify 和 distribute 不接受 `--unit`。
25
+
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
+
28
+ 稳定隔离安装树记录只在宿主命令退出、目录已隔离且扫描期间没有并发写入时执行;宿主附加链接只记录、不跟随,声明载荷中的 symlink 失败关闭,legacy 全树语义保持不变。0.9.4 仍是源码候选,不能从本说明推断已批准、发布或验证。
25
29
 
26
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 无权把本地就绪解释为生产就绪。
27
31
 
@@ -44,6 +48,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" assess --root <path> --offlin
44
48
  # 日常发布快速路径:发布前确认可读计划摘要,状态文件可恢复。
45
49
  # 受限 postPublish hook 的 checkpoint 批准与计划批准分开。
46
50
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-version <version> --json
51
+ # 多发布单元项目显式选择本轮范围;未传 --unit 时仍为全部单元
52
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> \
53
+ --target-version <version> --unit <unit-a> --unit <unit-b> --json
47
54
  # 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
48
55
  # 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
49
56
  # 命令调用本身即授权执行配置中的 hooks
@@ -13,7 +13,9 @@ description: Freeze an immutable release plan with local configuration, document
13
13
 
14
14
  运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
15
15
 
16
- 0.9.3 候选的 Hook cache v2 只复用有完整身份和输入证据的成功 Hook 结果。裸 PATHPATHEXTWindows、TTL、损坏记录或 Foundation 观察不可用时,缓存保持失败关闭,Hook 仍按完整路径冷执行;该机制不改变 prepare 的计划、批准和发布权威。
16
+ 0.9.4 候选允许多发布单元项目在冻结前显式选择本轮范围。`--unit <id>` 可以重复传入;未传时仍选择全部单元。显式选择只跳过延期单元的单元级工作,完整配置校验、生成物新鲜度和顶层 Hook 仍全部执行。计划冻结后以 `plan.units` 为唯一范围权威,publishreconcileverify distribute 不接受单元选择。
17
+
18
+ 0.9.3 引入的 Hook cache v2 继续只复用有完整身份和输入证据的成功 Hook 结果。裸 PATH、PATHEXT、Windows、TTL、损坏记录或 Foundation 观察不可用时,缓存保持失败关闭,Hook 仍按完整路径冷执行;该机制不改变 prepare 的计划、批准和发布权威。
17
19
 
18
20
  **Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。旧参数 `--acknowledge-hook-side-effects` 和 `--acknowledge-gate-side-effects` 仍可解析,但只作为无效果的兼容输入,不能改变授权或执行语义。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
19
21
 
@@ -25,11 +27,13 @@ description: Freeze an immutable release plan with local configuration, document
25
27
 
26
28
  **发布文档新鲜度门**: 配置了 `releaseDocuments` 的单元在 hook 授权门前先执行同一只读规划器:`clean` 继续;`changes` 抛 `RELEASE_DOCS_STALE`,详情列出相对路径、语种、`refreshDigest` 和精确演练/写入参数数组。prepare 只检查、不写工作树。正式 prepare 前先运行只读演练;有变化时向用户展示文件/语种/版本/`refreshDigest`,只有在用户明确授权"本地发布文档写入"后,才执行带 `--write --confirm-refresh <refreshDigest> --ack-local-document-write` 三项绑定的写入,随后运行聚焦校验,要求维护者审阅并提交刷新结果,再重新 prepare。该授权不扩展为 hook、提交、push 或 publish 授权。
27
29
 
30
+ **显式发布范围**: 只有用户已经指出本轮要发布哪些单元时,才把这些 ID 逐个传给 `--unit`。release-skill 不根据失败自动排除单元。选择命中 `publicSourceAuthorityReceipt` 的 coordinator 或 subject 时,必须包含收据声明的完整单元闭包;缺少单元时按命令返回的精确 argv 重新选择,不自动扩选。成功后展示 `releaseScope.selectedUnitIds`、`releaseScope.deferredUnitIds` 和批准摘要。延期仅表示没有进入本轮计划,不表示通过或失败。
31
+
28
32
  ## 正向执行路径
29
33
 
30
34
  1. 使用插件根相对路径运行 CLI:`CLI="node ${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs"`
31
35
  2. 配置含 `releaseDocuments` 时,先运行只读演练 `${CLI} docs refresh --unit <id> --json`;`status: "changes"` 时展示逐文件路径/语种/版本/`refreshDigest`,取得"本地发布文档写入"明确授权后才执行 `nextCommand.argv` 写入,审阅并提交刷新结果后再继续;`status: "clean"` 时直接进入 prepare
32
- 3. 运行 `${CLI} prepare --root <path> --offline --json`
36
+ 3. 运行 `${CLI} prepare --root <path> --offline --json`;用户已明确选择范围时,为每个单元追加一个 `--unit <id>`
33
37
  4. 检查 exit code 0,读取 JSON 返回的 immutable `planPath=plans/<planDigest>.json`,再从该文件读取 `status`、`units`、`externalActions`
34
38
  5. 向用户展示可读的 `approvalSummary`:版本、公开仓库、分支策略、branch/tag、npm 与 GitHub Release 目标、全部外部动作、例外,以及需要独立 checkpoint 批准的 postPublish hook。`planDigest` 仅作为内部绑定字段,不要求用户复制或确认。后续 approve/publish 只能使用该 immutable planPath,等待确认后再 approve。计划批准不包含受限 postPublish hook 的 checkpoint 批准
35
39
 
@@ -64,6 +68,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> --js
64
68
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" docs refresh --unit <id> \
65
69
  --write --confirm-refresh <refreshDigest> --ack-local-document-write --json
66
70
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offline --json
71
+ # 显式选择发布范围;未传 --unit 时仍准备全部配置单元
72
+ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offline \
73
+ --unit <unit-a> --unit <unit-b> --json
67
74
  # 生产 happy end:bound 基线必须 online;远端目标唯一性仍由 publish 全局预检
68
75
  node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --online --production --json
69
76
  ```
@@ -81,11 +88,12 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --onlin
81
88
  | GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
82
89
  | GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
83
90
  | GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
91
+ | GATE_FAILED(发布范围依赖闭包不完整) | 按详情补齐 `publicSourceAuthorityReceipt` 声明涉及的 coordinator 和全部 subjects,再重新 prepare;不得自动扩选或忽略收据 |
84
92
  | GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
85
93
  | RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
86
94
  | RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
87
95
  | SECRET_DETECTED | 移除密钥并更新 allowlist |
88
- | CONFIG_INVALID | 检查 version.sourcepackage.json |
96
+ | CONFIG_INVALID | 检查 version.sourcepackage.json,以及 `--unit` 是否为空、重复或不在 `releaseUnits[]` 中 |
89
97
 
90
98
  重试时只保留最新结构化错误码和失败门,不沿用早期猜测;重跑确定性命令获得新证据。
91
99
 
@@ -35,6 +35,16 @@ const SAFE_ENV_KEYS = Object.freeze([
35
35
  'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
36
36
  'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CONFIG_DIR',
37
37
  ]);
38
+ const CODEBUDDY_PLUGIN_LIST_ARGS = Object.freeze(['plugin', 'list', '--json']);
39
+ const CODEBUDDY_MARKETPLACE_LIST_ARGS = Object.freeze(['plugin', 'marketplace', 'list']);
40
+
41
+ function attachFoundationFailure(error, { envelope, stdout }) {
42
+ Object.defineProperties(error, {
43
+ foundationEnvelope: { value: envelope, enumerable: false },
44
+ foundationStdout: { value: stdout, enumerable: false },
45
+ });
46
+ return error;
47
+ }
38
48
 
39
49
  function actionTarget(plan, action, host) {
40
50
  const parameters = action.parameters ?? {};
@@ -259,7 +269,7 @@ async function defaultRun(command, args, options = {}) {
259
269
  watchdogReason: envelope.watchdogReason,
260
270
  ...(envelope.evidence?.spawnError ? { spawnError: envelope.evidence.spawnError } : {}),
261
271
  };
262
- throw error;
272
+ throw attachFoundationFailure(error, { envelope, stdout });
263
273
  }
264
274
  return { stdout, stderr };
265
275
  }, { prefix: 'release-skill-host-command-' });
@@ -303,6 +313,96 @@ function parseJson(stdout, label) {
303
313
  }
304
314
  }
305
315
 
316
+ function foundationEnvelopeFromError(error) {
317
+ return error?.foundationEnvelope ?? null;
318
+ }
319
+
320
+ function foundationStdoutFromError(error) {
321
+ return error?.foundationStdout ?? null;
322
+ }
323
+
324
+ function isCodeBuddyResidualEnvelope(error) {
325
+ const envelope = foundationEnvelopeFromError(error);
326
+ const evidence = envelope?.evidence;
327
+ const signals = evidence?.signalSequence;
328
+ const signal = signals?.[0];
329
+ return envelope?.ok === false
330
+ && envelope.exitStatus === 124
331
+ && envelope.processStatus === 'TERMINATED'
332
+ && envelope.terminationReason === 'child_exit'
333
+ && envelope.watchdogReason === 'residual_process_group'
334
+ && evidence?.childExitCode === 0
335
+ && (evidence.childSignal === null || evidence.childSignal === undefined)
336
+ && evidence.residualGroupCleanupCompleted === true
337
+ && evidence.forcedKill === false
338
+ && evidence.outputLimitExceeded === null
339
+ && Array.isArray(signals)
340
+ && signals.length === 1
341
+ && signal?.signal === 'SIGTERM'
342
+ && signal.requestedMode === 'process_group'
343
+ && signal.successfulMode === 'process_group';
344
+ }
345
+
346
+ function isCodeBuddyReadCommand(target, command, args) {
347
+ return target.host === 'codebuddy'
348
+ && (command === 'codebuddy' || command === 'cbc' || command === CODEBUDDY_MACOS_PATH)
349
+ && Array.isArray(args)
350
+ && (
351
+ args.every((value, index) => value === CODEBUDDY_PLUGIN_LIST_ARGS[index])
352
+ && args.length === CODEBUDDY_PLUGIN_LIST_ARGS.length
353
+ || args.every((value, index) => value === CODEBUDDY_MARKETPLACE_LIST_ARGS[index])
354
+ && args.length === CODEBUDDY_MARKETPLACE_LIST_ARGS.length
355
+ );
356
+ }
357
+
358
+ function isCodeBuddyWriteCommand(target, command, args, kind) {
359
+ if (target.host !== 'codebuddy' || !['codebuddy', 'cbc', CODEBUDDY_MACOS_PATH].includes(command)) return false;
360
+ const expected = kind === 'marketplace-update'
361
+ ? ['plugin', 'marketplace', 'update', target.marketplace]
362
+ : ['plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user'];
363
+ return Array.isArray(args) && args.length === expected.length && args.every((value, index) => value === expected[index]);
364
+ }
365
+
366
+ async function runCodeBuddyRead(target, command, args, label, env, run) {
367
+ try {
368
+ return { output: await run(command, [...args], { env }), recovered: false };
369
+ } catch (error) {
370
+ if (!isCodeBuddyReadCommand(target, command, args) || !isCodeBuddyResidualEnvelope(error)) throw error;
371
+ const residualStdout = foundationStdoutFromError(error);
372
+ if (typeof residualStdout !== 'string') throw error;
373
+ parseJson(residualStdout, label);
374
+ return {
375
+ output: { stdout: residualStdout, stderr: '' },
376
+ recovered: true,
377
+ };
378
+ }
379
+ }
380
+
381
+ function exactCodeBuddyMarketplaceObservation(target, stdout) {
382
+ const parsed = parseJson(stdout, `${target.host} marketplace list`);
383
+ if (!Array.isArray(parsed)) throw new Error(`${target.host} marketplace list did not return an array`);
384
+ const matches = parsed.filter((entry) => entry?.name === target.marketplace);
385
+ if (matches.length !== 1) {
386
+ throw new Error(`${target.host} marketplace list did not contain exactly one ${target.marketplace}`);
387
+ }
388
+ if (matches[0].type !== 'git') {
389
+ throw new Error(`${target.host} marketplace ${target.marketplace} is not a git marketplace`);
390
+ }
391
+ return { installed: true, exact: true, found: matches[0] };
392
+ }
393
+
394
+ async function observeCodeBuddyMarketplaceAfterResidual(target, command, env, run) {
395
+ const observed = await runCodeBuddyRead(
396
+ target,
397
+ command,
398
+ CODEBUDDY_MARKETPLACE_LIST_ARGS,
399
+ `${target.host} marketplace list`,
400
+ env,
401
+ run,
402
+ );
403
+ return exactCodeBuddyMarketplaceObservation(target, observed.output.stdout);
404
+ }
405
+
306
406
  function exactPluginObservation(target, stdout) {
307
407
  const selector = `${target.plugin}@${target.marketplace}`;
308
408
  const parsed = parseJson(stdout, `${target.host} plugin list`);
@@ -433,7 +533,10 @@ async function runStructuredUpdate(target, detected, run, {
433
533
  }) {
434
534
  if (target.host === 'codebuddy' || target.host === 'workbuddy') {
435
535
  const env = hostEnvironment(target.host);
436
- const listed = await run(detected.command, ['plugin', 'list', '--json'], { env });
536
+ const listedObservation = target.host === 'codebuddy'
537
+ ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
538
+ : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
539
+ const listed = listedObservation.output;
437
540
  const observed = exactPluginObservation(target, listed.stdout);
438
541
  if (
439
542
  target.sourceForm !== 'bundled-family'
@@ -445,7 +548,13 @@ async function runStructuredUpdate(target, detected, run, {
445
548
  reason: `${target.host} source identity is not eligible for a frozen bundled-family update`,
446
549
  };
447
550
  }
448
- if (observed.exact) return { status: 'ALREADY_CURRENT', version: target.version };
551
+ if (observed.exact) {
552
+ return {
553
+ status: 'ALREADY_CURRENT',
554
+ version: target.version,
555
+ ...(listedObservation.recovered ? { residualRecovery: true } : {}),
556
+ };
557
+ }
449
558
  if (!observed.installed) {
450
559
  return {
451
560
  status: 'MANUAL_REQUIRED',
@@ -471,14 +580,51 @@ async function runStructuredUpdate(target, detected, run, {
471
580
  reason: `${target.host} frozen tag and mutable ref do not both resolve to ${target.pluginCommit}`,
472
581
  };
473
582
  }
474
- await run(detected.command, ['plugin', 'marketplace', 'update', target.marketplace], { env });
475
- await run(detected.command, [
476
- 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
477
- ], { env });
478
- const afterList = await run(detected.command, ['plugin', 'list', '--json'], { env });
479
- const after = exactPluginObservation(target, afterList.stdout);
583
+ let marketplaceRecovered = false;
584
+ try {
585
+ await run(detected.command, ['plugin', 'marketplace', 'update', target.marketplace], { env });
586
+ } catch (error) {
587
+ if (!isCodeBuddyWriteCommand(target, detected.command, ['plugin', 'marketplace', 'update', target.marketplace], 'marketplace-update')
588
+ || !isCodeBuddyResidualEnvelope(error)) throw error;
589
+ await observeCodeBuddyMarketplaceAfterResidual(target, detected.command, env, run);
590
+ marketplaceRecovered = true;
591
+ }
592
+ let after;
593
+ let pluginRecovered = false;
594
+ try {
595
+ await run(detected.command, [
596
+ 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
597
+ ], { env });
598
+ } catch (error) {
599
+ if (!isCodeBuddyWriteCommand(target, detected.command, [
600
+ 'plugin', 'update', `${target.plugin}@${target.marketplace}`, '--scope', 'user',
601
+ ], 'plugin-update') || !isCodeBuddyResidualEnvelope(error)) throw error;
602
+ const afterList = await runCodeBuddyRead(
603
+ target,
604
+ detected.command,
605
+ CODEBUDDY_PLUGIN_LIST_ARGS,
606
+ `${target.host} plugin list`,
607
+ env,
608
+ run,
609
+ );
610
+ after = exactPluginObservation(target, afterList.output.stdout);
611
+ pluginRecovered = true;
612
+ }
613
+ if (!pluginRecovered) {
614
+ const afterList = target.host === 'codebuddy'
615
+ ? await runCodeBuddyRead(target, detected.command, CODEBUDDY_PLUGIN_LIST_ARGS, `${target.host} plugin list`, env, run)
616
+ : { output: await run(detected.command, [...CODEBUDDY_PLUGIN_LIST_ARGS], { env }), recovered: false };
617
+ after = exactPluginObservation(target, afterList.output.stdout);
618
+ }
480
619
  if (!after.exact) throw new Error(`${target.host} did not update to the frozen plugin identity`);
481
- return { status: 'UPDATED', version: target.version, restartRequired: true };
620
+ return {
621
+ status: 'UPDATED',
622
+ version: target.version,
623
+ restartRequired: true,
624
+ ...(marketplaceRecovered || listedObservation.recovered || pluginRecovered
625
+ ? { residualRecovery: true }
626
+ : {}),
627
+ };
482
628
  }
483
629
 
484
630
  const command = detected.command;
@@ -566,9 +712,9 @@ expect {
566
712
  timeout { exit 95 }
567
713
  eof { exit 96 }
568
714
  }
569
- expect -re {> $}
715
+ expect -re $promptPattern
570
716
  }
571
- -re {> $} {}
717
+ -re $promptPattern {}
572
718
  timeout { exit 93 }
573
719
  eof { exit 94 }
574
720
  }
@@ -581,8 +727,9 @@ foreach variable {RELEASE_SKILL_KIMI_COMMAND RELEASE_SKILL_KIMI_INSTALL_URL RELE
581
727
  set kimiCommand $env(RELEASE_SKILL_KIMI_COMMAND)
582
728
  set installUrl $env(RELEASE_SKILL_KIMI_INSTALL_URL)
583
729
  set removePlugin $env(RELEASE_SKILL_KIMI_REMOVE_PLUGIN)
730
+ set promptPattern {(?:(?:^|\\r|\\n)> |(?:^|\\r|\\n)(?:\\033\\[[0-9;?]*[ -/]*[@-~])*│[ \\t]*>[ \\t]*│(?:\\033\\[[0-9;?]*[ -/]*[@-~])*)(?![^\\n]|\\n)}
584
731
  spawn $kimiCommand
585
- expect -re {> $}
732
+ expect -re $promptPattern
586
733
  ${removeCommand}send -- "/plugins install $installUrl\\r"
587
734
  expect {
588
735
  -nocase -re {trust and install} {
@@ -595,9 +742,9 @@ expect {
595
742
  timeout { exit 91 }
596
743
  eof { exit 92 }
597
744
  }
598
- expect -re {> $}
745
+ expect -re $promptPattern
599
746
  send -- "/reload\\r"
600
- expect -re {> $}
747
+ expect -re $promptPattern
601
748
  send -- "/exit\\r"
602
749
  expect eof
603
750
  `;
@@ -730,6 +877,12 @@ function aggregateStatus(results) {
730
877
 
731
878
  function failedHostResult(target, error) {
732
879
  const message = error?.message ?? String(error);
880
+ const details = error?.details;
881
+ const publicDetails = details && typeof details === 'object' && !Array.isArray(details)
882
+ ? Object.fromEntries(Object.entries(details).filter(([key]) => (
883
+ key !== 'foundationEnvelope' && key !== 'foundationStdout' && key !== 'foundationStderr'
884
+ )))
885
+ : details;
733
886
  return {
734
887
  host: target.host,
735
888
  unitId: target.unitId,
@@ -737,7 +890,7 @@ function failedHostResult(target, error) {
737
890
  error: message,
738
891
  reason: typeof error?.reason === 'string' ? error.reason : message,
739
892
  ...(error?.code !== undefined ? { code: error.code } : {}),
740
- ...(error?.details !== undefined ? { details: error.details } : {}),
893
+ ...(publicDetails !== undefined ? { details: publicDetails } : {}),
741
894
  };
742
895
  }
743
896