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
@@ -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 "${CODEBUDDY_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 "${CODEBUDDY_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
45
45
  node "${CODEBUDDY_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 "${CODEBUDDY_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 "${CODEBUDDY_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --pla
66
66
  ## 发布后收尾
67
67
 
68
68
  `VERIFIED` 不代表要自动修改开发分支或本机宿主。进入 `release-finish` 后,只有用户明确同意,才执行对应的本地动作。本机插件更新失败不会改变发布终态。
69
+ 核心 prepare、publish、verify 流程跨平台;WorkBuddy 的本机收尾探测和更新仅支持 macOS。
69
70
 
70
71
  ## 烟雾测试
71
72
 
@@ -91,6 +91,39 @@ async function exitAfterFlush(exitCode) {
91
91
  process.exit(exitCode);
92
92
  }
93
93
 
94
+ /**
95
+ * Keep command-line error JSON useful without exposing internal error objects,
96
+ * process envelopes, or filesystem details. Local-finish evidence errors
97
+ * intentionally expose only their stable cause and executable next steps.
98
+ *
99
+ * @param {unknown} error
100
+ * @returns {Record<string, unknown>}
101
+ */
102
+ function publicErrorDetails(error) {
103
+ const details = error?.details;
104
+ if (!details || typeof details !== 'object' || Array.isArray(details)) return {};
105
+ const output = {};
106
+ if (details.cause && typeof details.cause === 'object' && !Array.isArray(details.cause)) {
107
+ const cause = {};
108
+ if (typeof details.cause.code === 'string') cause.code = details.cause.code;
109
+ if (typeof details.cause.message === 'string') cause.message = details.cause.message;
110
+ if (Object.keys(cause).length > 0) output.cause = cause;
111
+ }
112
+ if (Array.isArray(details.nextSteps)) {
113
+ const nextSteps = details.nextSteps
114
+ .filter((step) => step && typeof step === 'object' && !Array.isArray(step))
115
+ .map((step) => ({
116
+ ...(typeof step.code === 'string' ? { code: step.code } : {}),
117
+ ...(typeof step.message === 'string' ? { message: step.message } : {}),
118
+ ...(Array.isArray(step.argv) && step.argv.every((arg) => typeof arg === 'string')
119
+ ? { argv: [...step.argv] } : {}),
120
+ }))
121
+ .filter((step) => Object.keys(step).length > 0);
122
+ if (nextSteps.length > 0) output.nextSteps = nextSteps;
123
+ }
124
+ return output;
125
+ }
126
+
94
127
  /**
95
128
  * Check if a command is available and get its version.
96
129
  *
@@ -903,9 +936,15 @@ if (command === 'ship') {
903
936
  } else {
904
937
  console.log('Post-release: branch advancement was already included; skip the merge question.');
905
938
  }
906
- if (result.postRelease.localHostUpdate?.promptRequired) {
939
+ const localHostUpdate = result.postRelease.localHostUpdate;
940
+ if (localHostUpdate?.available === true && typeof localHostUpdate.runPath === 'string') {
907
941
  console.log(`Post-release: ask whether to update local host plugins (${result.postRelease.localHostUpdate.hosts.join(', ')}).`);
908
- console.log(`Post-release command: release-skill post-release --plan ${result.planPath} --run ${result.verifyRunPath}`);
942
+ console.log(`Post-release command: release-skill post-release --plan ${result.planPath} --run ${localHostUpdate.runPath}`);
943
+ console.log('Choose --hosts before adding --update-local-hosts to perform a local update.');
944
+ } else {
945
+ for (const step of localHostUpdate?.nextSteps ?? []) {
946
+ console.log(`Next [${step.code}] ${step.message} (${step.argv.join(' ')})`);
947
+ }
909
948
  }
910
949
  }
911
950
  for (const followUp of result.manualFollowUps ?? []) {
@@ -1293,14 +1332,13 @@ if (command === 'post-release') {
1293
1332
  const planPath = value('--plan') ? resolve(value('--plan')) : undefined;
1294
1333
  const runPath = value('--run') ? resolve(value('--run')) : undefined;
1295
1334
  if (!planPath || !runPath) {
1296
- const message = 'post-release requires --plan <path> and --run <verified-run-path>';
1335
+ const message = 'post-release requires --plan <path> and --run <verify-or-postverify-run-path>';
1297
1336
  if (hasJson) console.log(JSON.stringify({ error: 'MISSING_PARAMETERS', message, exitCode: 1 }));
1298
1337
  else console.error(`Error: ${message}`);
1299
1338
  await exitAfterFlush(1);
1300
1339
  }
1301
1340
  try {
1302
1341
  const {
1303
- assertVerifiedReleaseRun,
1304
1342
  derivePostReleaseChecklist,
1305
1343
  updateLocalHostPlugins,
1306
1344
  } = await import('../src/commands/post-release-local.mjs');
@@ -1308,24 +1346,26 @@ if (command === 'post-release') {
1308
1346
  const {
1309
1347
  loadRun,
1310
1348
  resolveRunPath,
1311
- validateRunLineage,
1312
1349
  } = await import('../src/core/run.mjs');
1313
1350
  const plan = JSON.parse(await readFile(planPath, 'utf8'));
1314
1351
  validatePlan(plan);
1352
+ const updateRequested = args.includes('--update-local-hosts');
1315
1353
  const resolvedRunPath = await resolveRunPath(runPath);
1316
1354
  const runRecord = await loadRun(resolvedRunPath, {
1317
1355
  requireDigest: true,
1318
1356
  authorityPlanPath: planPath,
1319
1357
  });
1320
- await validateRunLineage(runRecord, {
1321
- plan,
1322
- planPath,
1323
- runPath: resolvedRunPath,
1324
- production: Boolean(plan.production),
1325
- });
1326
- assertVerifiedReleaseRun(plan, runRecord);
1327
-
1328
- const updateRequested = args.includes('--update-local-hosts');
1358
+ if (!updateRequested) {
1359
+ const { assertLocalFinishRun } = await import('../src/commands/post-release-local.mjs');
1360
+ await assertLocalFinishRun({
1361
+ plan,
1362
+ planPath,
1363
+ runPath: resolvedRunPath,
1364
+ runRecord,
1365
+ production: Boolean(plan.production),
1366
+ root: resolve(value('--root') ?? process.cwd()),
1367
+ });
1368
+ }
1329
1369
  const hostsIndex = args.indexOf('--hosts');
1330
1370
  const rawHosts = hostsIndex !== -1
1331
1371
  && args[hostsIndex + 1]
@@ -1338,12 +1378,19 @@ if (command === 'post-release') {
1338
1378
  .filter(Boolean);
1339
1379
  const result = updateRequested
1340
1380
  ? await updateLocalHostPlugins({
1341
- plan,
1381
+ planPath,
1382
+ runPath: resolvedRunPath,
1342
1383
  root: resolve(value('--root') ?? process.cwd()),
1343
1384
  confirmPlanDigest: value('--confirm-plan'),
1344
1385
  selectedHosts,
1345
1386
  })
1346
- : derivePostReleaseChecklist(plan);
1387
+ : derivePostReleaseChecklist(plan, {
1388
+ runPath: runRecord.command === 'postverify' && runRecord.status === 'DISTRIBUTED'
1389
+ ? resolvedRunPath
1390
+ : runRecord.command === 'verify' ? resolvedRunPath : undefined,
1391
+ root: resolve(value('--root') ?? process.cwd()),
1392
+ postVerifyComplete: runRecord.command === 'postverify' && runRecord.status === 'DISTRIBUTED',
1393
+ });
1347
1394
 
1348
1395
  if (hasJson) {
1349
1396
  console.log(JSON.stringify(result, null, 2));
@@ -1351,8 +1398,14 @@ if (command === 'post-release') {
1351
1398
  console.log(`Post-release status: ${result.status}`);
1352
1399
  if (result.merge.promptRequired) console.log('Ask whether the user wants to merge the remaining branch.');
1353
1400
  else console.log('Branch advancement was already included in the release workflow; skip the merge question.');
1354
- if (result.localHostUpdate.promptRequired) {
1401
+ if (result.localHostUpdate.available === true && typeof result.localHostUpdate.runPath === 'string') {
1355
1402
  console.log(`Ask whether to update local host plugins: ${result.localHostUpdate.hosts.join(', ')}`);
1403
+ console.log(`Post-release command: release-skill post-release --plan ${planPath} --run ${result.localHostUpdate.runPath}`);
1404
+ console.log('Choose --hosts before adding --update-local-hosts to perform a local update.');
1405
+ } else {
1406
+ for (const step of result.localHostUpdate.nextSteps ?? []) {
1407
+ console.log(`Next [${step.code}] ${step.message} (${step.argv.join(' ')})`);
1408
+ }
1356
1409
  }
1357
1410
  } else {
1358
1411
  console.log(`Local host update: ${result.status}`);
@@ -1368,9 +1421,15 @@ if (command === 'post-release') {
1368
1421
  console.log(JSON.stringify({
1369
1422
  error: err.code ?? 'POST_RELEASE_FAILED',
1370
1423
  message: err.message,
1424
+ details: publicErrorDetails(err),
1371
1425
  exitCode: err.exitCode ?? 1,
1372
1426
  }));
1373
- } else console.error(`Error: ${err.message}`);
1427
+ } else {
1428
+ console.error(`Error: ${err.message}`);
1429
+ for (const step of err.details?.nextSteps ?? []) {
1430
+ console.error(`Next [${step.code}] ${step.message} (${step.argv.join(' ')})`);
1431
+ }
1432
+ }
1374
1433
  await exitAfterFlush(err.exitCode ?? 1);
1375
1434
  }
1376
1435
  }
@@ -1425,7 +1484,7 @@ if (command === 'verify') {
1425
1484
  } = await import('../src/commands/post-release-local.mjs');
1426
1485
  const plan = JSON.parse(await readFile(planPath, 'utf8'));
1427
1486
  try {
1428
- result.postRelease = derivePostReleaseChecklist(plan);
1487
+ result.postRelease = derivePostReleaseChecklist(plan, { root, runPath: result.runPath });
1429
1488
  } catch (error) {
1430
1489
  result.postRelease = unavailablePostReleaseChecklist(plan, error);
1431
1490
  }
@@ -1451,8 +1510,15 @@ if (command === 'verify') {
1451
1510
  } else if (result.postRelease?.merge.promptRequired) {
1452
1511
  console.log('Post-release: ask whether to merge the remaining branch.');
1453
1512
  }
1454
- if (result.postRelease?.localHostUpdate.promptRequired) {
1513
+ if (result.postRelease?.localHostUpdate?.available === true
1514
+ && typeof result.postRelease.localHostUpdate.runPath === 'string') {
1455
1515
  console.log(`Post-release: ask whether to update local host plugins (${result.postRelease.localHostUpdate.hosts.join(', ')}).`);
1516
+ console.log(`Post-release command: release-skill post-release --plan ${planPath} --run ${result.postRelease.localHostUpdate.runPath}`);
1517
+ console.log('Choose --hosts before adding --update-local-hosts to perform a local update.');
1518
+ } else {
1519
+ for (const step of result.postRelease?.localHostUpdate?.nextSteps ?? []) {
1520
+ console.log(`Next [${step.code}] ${step.message} (${step.argv.join(' ')})`);
1521
+ }
1456
1522
  }
1457
1523
  }
1458
1524
 
@@ -1462,6 +1528,7 @@ if (command === 'verify') {
1462
1528
  const errOutput = {
1463
1529
  error: err.code ?? 'UNKNOWN_ERROR',
1464
1530
  message: err.message,
1531
+ details: err.details ?? {},
1465
1532
  exitCode: err.exitCode ?? 1,
1466
1533
  };
1467
1534
  console.log(JSON.stringify(errOutput));