release-skill 0.9.6 → 0.9.8
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.
- package/.agents/plugins/marketplace.json +9 -0
- package/.claude-plugin/marketplace.json +11 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +55 -0
- package/INSTALL.md +17 -9
- package/INSTALL.zh-CN.md +15 -9
- package/README.md +33 -28
- package/README.zh-CN.md +28 -26
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +357 -52
- package/adapters/claude/skills/release-finish/SKILL.md +12 -4
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +357 -52
- package/adapters/codex/skills/release-finish/SKILL.md +12 -4
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +357 -52
- package/adapters/kimi/skills/release-finish/SKILL.md +12 -4
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +357 -52
- package/adapters/workbuddy/skills/release-finish/SKILL.md +12 -4
- package/bin/release-skill.bundle.mjs +357 -52
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/skills/release-finish/SKILL.md +12 -4
- package/skills-src/release-finish/SKILL.md +12 -4
- package/src/commands/post-release-local.mjs +274 -39
- package/src/commands/prepare.mjs +5 -3
- package/src/commands/publish.mjs +30 -3
- package/src/commands/ship.mjs +4 -6
- package/src/commands/verify.mjs +22 -15
- package/src/core/postpublish.mjs +24 -0
- package/src/core/recovery.mjs +8 -6
- package/src/core/skill-resource-closure.mjs +19 -1
- package/src/core/surface-host-bindings.mjs +35 -0
package/package.json
CHANGED
package/platform-manifest.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.9.8",
|
|
6
6
|
"logicalRoot": "skills-src/",
|
|
7
|
-
"logicalRootDigest": "
|
|
8
|
-
"projectionDigest": "
|
|
9
|
-
"projectionId": "release-skill:platform:0.9.
|
|
7
|
+
"logicalRootDigest": "aec96745b30289ed48d576fbfc98ecd3aacb834f83394be76d4734d7d0fc0ad4",
|
|
8
|
+
"projectionDigest": "e5117d89c9f965e15a1464cc84f542cb1e75a685f842183d0cfc7a105ace5ceb",
|
|
9
|
+
"projectionId": "release-skill:platform:0.9.8",
|
|
10
10
|
"manifestRequired": true,
|
|
11
11
|
"projectionSources": {
|
|
12
12
|
"skills-src-root": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: release-finish
|
|
3
|
-
description: 发布达到 VERIFIED
|
|
3
|
+
description: 发布达到 VERIFIED 后处理发布收尾:确认 postVerify 提案送达边界,按发布分支策略决定是否询问合并,并在用户确认后更新本机 Claude、Codex、Kimi、CodeBuddy/WorkBuddy 插件
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# release-finish
|
|
@@ -13,7 +13,7 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
|
|
|
13
13
|
|
|
14
14
|
这是发布后的独立收尾,不属于 `prepare → approve → publish → verify` 状态机。它不能把本机更新结果写成新的发布状态,也不能因为本机更新失败而降低 `VERIFIED`。
|
|
15
15
|
|
|
16
|
-
默认只读取冻结计划和 verify 或 postVerify run
|
|
16
|
+
默认只读取冻结计划和 verify 或 postVerify run。没有用户明确同意,不合并分支,不更新插件。Kimi 只在插件信任界面中的仓库和标签都与冻结目标一致时确认安装;目录信任不自动确认。
|
|
17
17
|
|
|
18
18
|
## 先生成收尾清单
|
|
19
19
|
|
|
@@ -37,6 +37,14 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
|
|
|
37
37
|
2. `merge.promptRequired=true` 时,向用户说明尚未覆盖的发布分支,并询问是否需要合并。用户同意后,先只读核对源分支、目标分支、工作区状态和项目既有合并方式,再用明确的分支名执行;本脚本不猜分支,也不自动推送。
|
|
38
38
|
3. `localHostUpdate.promptRequired=true` 时,列出计划覆盖的宿主,询问是否更新本机插件。两个问题可以一次问完。
|
|
39
39
|
|
|
40
|
+
## postVerify 提案送达边界
|
|
41
|
+
|
|
42
|
+
`proposal-inbox` postVerify hook 只负责把冻结提案送到配置的接收端,并在 hook checkpoint 中记录送达结果。送达成功不表示提案已经应用,也不表示接收端完成了渲染或公开同步。
|
|
43
|
+
|
|
44
|
+
接收端按照自己的 runbook 和治理要求审阅、应用、渲染并公开同步。release-finish 不内置某个接收端的仓库、命令或推送步骤,也不增加另一套收据、账本、Schema、状态机或 hook。
|
|
45
|
+
|
|
46
|
+
本机宿主是否依赖某个市场,只根据冻结计划记录的真实安装来源判断。提案送达或某个接收端的处理结果不是所有宿主更新的统一前置条件;只有宿主的冻结安装来源确实指向该接收端产物时,才按接收端自己的 runbook 完成必要处理。
|
|
47
|
+
|
|
40
48
|
## 用户同意更新本机宿主
|
|
41
49
|
|
|
42
50
|
把用户选择的宿主和清单返回的精确 `planDigest` 传回脚本:
|
|
@@ -54,8 +62,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
|
|
|
54
62
|
|
|
55
63
|
只传用户选择且计划声明的宿主。宿主 CLI 不存在时返回 `SKIPPED_NOT_INSTALLED`。各宿主按以下规则处理:
|
|
56
64
|
|
|
57
|
-
- Claude
|
|
58
|
-
- Kimi 的精确当前安装会在返回 `ALREADY_CURRENT`
|
|
65
|
+
- Claude 或 Codex 只有在市场需要重绑时,才会在该宿主第一条写命令之前只读查询冻结的市场仓库、引用和提交。远端不可达、引用缺失或提交不一致时返回 `MANUAL_REQUIRED`,该宿主不执行市场或插件写入;其他已选宿主继续处理。精确当前安装仍核对真实载荷,但不强制联网。
|
|
66
|
+
- Kimi 的精确当前安装会在返回 `ALREADY_CURRENT` 前核对真实载荷;发生安装或迁移时,只在操作完成后核对结果。配置根依次取显式 `kimiHome`、`KIMI_CODE_HOME`、用户主目录下的 `.kimi-code`,TUI 与安装后观察使用同一根。release-finish 当前只采用并验证 Kimi Code 的受控终端交互界面(TUI)路径:清理 ANSI/OSC 控制序列和软换行后,在插件信任对话框内分别核对冻结仓库与标签,确认选中 `Trust and install` 后才提交,再重新加载。出现 `Trust this folder?`、未知界面、超时、提前退出,或无法确认身份和选中项时返回 `MANUAL_REQUIRED` 或失败结果,不确认目录信任,也不继续安装。包名、版本、发布标签、已安装修订号和受管安装根必须与冻结计划一致;`.git` 只提供附加诊断,不是通过条件。旧的本地路径安装会在同一 TUI 会话中先移除,再按发布标签安装。
|
|
59
67
|
- 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
68
|
|
|
61
69
|
任何宿主失败都保留其他宿主的实际结果,不回滚,也不把失败冒充成功。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: release-finish
|
|
3
|
-
description: 发布达到 VERIFIED
|
|
3
|
+
description: 发布达到 VERIFIED 后处理发布收尾:确认 postVerify 提案送达边界,按发布分支策略决定是否询问合并,并在用户确认后更新本机 Claude、Codex、Kimi、CodeBuddy/WorkBuddy 插件
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# release-finish
|
|
@@ -13,7 +13,7 @@ description: 发布达到 VERIFIED 后处理可选的本地收尾:按发布分
|
|
|
13
13
|
|
|
14
14
|
这是发布后的独立收尾,不属于 `prepare → approve → publish → verify` 状态机。它不能把本机更新结果写成新的发布状态,也不能因为本机更新失败而降低 `VERIFIED`。
|
|
15
15
|
|
|
16
|
-
默认只读取冻结计划和 verify 或 postVerify run
|
|
16
|
+
默认只读取冻结计划和 verify 或 postVerify run。没有用户明确同意,不合并分支,不更新插件。Kimi 只在插件信任界面中的仓库和标签都与冻结目标一致时确认安装;目录信任不自动确认。
|
|
17
17
|
|
|
18
18
|
## 先生成收尾清单
|
|
19
19
|
|
|
@@ -37,6 +37,14 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
|
|
|
37
37
|
2. `merge.promptRequired=true` 时,向用户说明尚未覆盖的发布分支,并询问是否需要合并。用户同意后,先只读核对源分支、目标分支、工作区状态和项目既有合并方式,再用明确的分支名执行;本脚本不猜分支,也不自动推送。
|
|
38
38
|
3. `localHostUpdate.promptRequired=true` 时,列出计划覆盖的宿主,询问是否更新本机插件。两个问题可以一次问完。
|
|
39
39
|
|
|
40
|
+
## postVerify 提案送达边界
|
|
41
|
+
|
|
42
|
+
`proposal-inbox` postVerify hook 只负责把冻结提案送到配置的接收端,并在 hook checkpoint 中记录送达结果。送达成功不表示提案已经应用,也不表示接收端完成了渲染或公开同步。
|
|
43
|
+
|
|
44
|
+
接收端按照自己的 runbook 和治理要求审阅、应用、渲染并公开同步。release-finish 不内置某个接收端的仓库、命令或推送步骤,也不增加另一套收据、账本、Schema、状态机或 hook。
|
|
45
|
+
|
|
46
|
+
本机宿主是否依赖某个市场,只根据冻结计划记录的真实安装来源判断。提案送达或某个接收端的处理结果不是所有宿主更新的统一前置条件;只有宿主的冻结安装来源确实指向该接收端产物时,才按接收端自己的 runbook 完成必要处理。
|
|
47
|
+
|
|
40
48
|
## 用户同意更新本机宿主
|
|
41
49
|
|
|
42
50
|
把用户选择的宿主和清单返回的精确 `planDigest` 传回脚本:
|
|
@@ -54,8 +62,8 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill-local-finish.mjs" \
|
|
|
54
62
|
|
|
55
63
|
只传用户选择且计划声明的宿主。宿主 CLI 不存在时返回 `SKIPPED_NOT_INSTALLED`。各宿主按以下规则处理:
|
|
56
64
|
|
|
57
|
-
- Claude
|
|
58
|
-
- Kimi 的精确当前安装会在返回 `ALREADY_CURRENT`
|
|
65
|
+
- Claude 或 Codex 只有在市场需要重绑时,才会在该宿主第一条写命令之前只读查询冻结的市场仓库、引用和提交。远端不可达、引用缺失或提交不一致时返回 `MANUAL_REQUIRED`,该宿主不执行市场或插件写入;其他已选宿主继续处理。精确当前安装仍核对真实载荷,但不强制联网。
|
|
66
|
+
- Kimi 的精确当前安装会在返回 `ALREADY_CURRENT` 前核对真实载荷;发生安装或迁移时,只在操作完成后核对结果。配置根依次取显式 `kimiHome`、`KIMI_CODE_HOME`、用户主目录下的 `.kimi-code`,TUI 与安装后观察使用同一根。release-finish 当前只采用并验证 Kimi Code 的受控终端交互界面(TUI)路径:清理 ANSI/OSC 控制序列和软换行后,在插件信任对话框内分别核对冻结仓库与标签,确认选中 `Trust and install` 后才提交,再重新加载。出现 `Trust this folder?`、未知界面、超时、提前退出,或无法确认身份和选中项时返回 `MANUAL_REQUIRED` 或失败结果,不确认目录信任,也不继续安装。包名、版本、发布标签、已安装修订号和受管安装根必须与冻结计划一致;`.git` 只提供附加诊断,不是通过条件。旧的本地路径安装会在同一 TUI 会话中先移除,再按发布标签安装。
|
|
59
67
|
- 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
68
|
|
|
61
69
|
任何宿主失败都保留其他宿主的实际结果,不回滚,也不把失败冒充成功。
|
|
@@ -35,7 +35,7 @@ const SAFE_ENV_KEYS = Object.freeze([
|
|
|
35
35
|
'PATH', 'HOME', 'USER', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM', 'TMPDIR',
|
|
36
36
|
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
37
37
|
'SSL_CERT_FILE', 'SSL_CERT_DIR', 'NODE_EXTRA_CA_CERTS',
|
|
38
|
-
'CLAUDE_CONFIG_DIR', 'CODEX_HOME', '
|
|
38
|
+
'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'KIMI_CODE_HOME',
|
|
39
39
|
'CODEBUDDY_CONFIG_DIR', 'WORKBUDDY_CONFIG_DIR',
|
|
40
40
|
]);
|
|
41
41
|
const CODEBUDDY_PLUGIN_LIST_ARGS = Object.freeze(['plugin', 'list', '--json']);
|
|
@@ -422,7 +422,7 @@ function hostEnvironment(host, { kimiHome } = {}) {
|
|
|
422
422
|
env.CODEBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
|
|
423
423
|
env.WORKBUDDY_CONFIG_DIR = join(env.HOME, '.workbuddy');
|
|
424
424
|
}
|
|
425
|
-
if (host === 'kimi' && kimiHome) env.
|
|
425
|
+
if (host === 'kimi' && kimiHome) env.KIMI_CODE_HOME = kimiHome;
|
|
426
426
|
return env;
|
|
427
427
|
}
|
|
428
428
|
|
|
@@ -647,6 +647,67 @@ function normalizeGitSource(source) {
|
|
|
647
647
|
.replace(/\/$/u, '');
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
function parseFrozenRemoteRef(target, stdout) {
|
|
651
|
+
const directRefs = new Map();
|
|
652
|
+
const peeledRefs = new Map();
|
|
653
|
+
const expected = target.marketplaceRef;
|
|
654
|
+
const allowedDirect = expected.startsWith('refs/')
|
|
655
|
+
? new Set([expected])
|
|
656
|
+
: new Set([`refs/heads/${expected}`, `refs/tags/${expected}`]);
|
|
657
|
+
for (const line of String(stdout ?? '').split(/\r?\n/u)) {
|
|
658
|
+
if (line.length === 0) continue;
|
|
659
|
+
const match = /^([a-f0-9]{40})\t([^\s]+)$/u.exec(line);
|
|
660
|
+
if (!match) throw new Error('git ls-remote returned an invalid line');
|
|
661
|
+
const [, commit, remoteRef] = match;
|
|
662
|
+
const peeled = remoteRef.endsWith('^{}');
|
|
663
|
+
const direct = peeled ? remoteRef.slice(0, -3) : remoteRef;
|
|
664
|
+
if (!allowedDirect.has(direct)) {
|
|
665
|
+
throw new Error(`git ls-remote returned an unexpected ref ${remoteRef}`);
|
|
666
|
+
}
|
|
667
|
+
const destination = peeled ? peeledRefs : directRefs;
|
|
668
|
+
const previous = destination.get(direct);
|
|
669
|
+
if (previous && previous !== commit) {
|
|
670
|
+
throw new Error(`git ls-remote returned conflicting values for ${remoteRef}`);
|
|
671
|
+
}
|
|
672
|
+
destination.set(direct, commit);
|
|
673
|
+
}
|
|
674
|
+
const resolved = [...allowedDirect]
|
|
675
|
+
.filter((remoteRef) => directRefs.has(remoteRef) || peeledRefs.has(remoteRef))
|
|
676
|
+
.map((remoteRef) => peeledRefs.get(remoteRef) ?? directRefs.get(remoteRef));
|
|
677
|
+
return {
|
|
678
|
+
found: resolved.length > 0,
|
|
679
|
+
exact: resolved.length > 0 && resolved.every((commit) => commit === target.marketplaceCommit),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function preflightStructuredMarketplace(target, env, run) {
|
|
684
|
+
try {
|
|
685
|
+
const observed = await run('git', [
|
|
686
|
+
'ls-remote', '--exit-code', `https://${target.githubHost}/${target.marketplaceRepo}.git`,
|
|
687
|
+
target.marketplaceRef, `${target.marketplaceRef}^{}`,
|
|
688
|
+
], { env, timeout: 30_000 });
|
|
689
|
+
const remote = parseFrozenRemoteRef(target, observed.stdout);
|
|
690
|
+
if (!remote.found) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
reason: `${target.host} frozen marketplace ref ${target.marketplaceRef} is missing`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
if (!remote.exact) {
|
|
697
|
+
return {
|
|
698
|
+
ok: false,
|
|
699
|
+
reason: `${target.host} frozen marketplace ref ${target.marketplaceRef} does not resolve to ${target.marketplaceCommit}`,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return { ok: true };
|
|
703
|
+
} catch (error) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
reason: `${target.host} could not prove the frozen marketplace ref: ${error?.message ?? String(error)}`,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
650
711
|
async function observeMarketplace(target, command, env, run) {
|
|
651
712
|
const listed = await run(command, ['plugin', 'marketplace', 'list', '--json'], { env });
|
|
652
713
|
const parsed = parseJson(listed.stdout, `${target.host} marketplace list`);
|
|
@@ -835,6 +896,11 @@ async function runStructuredUpdate(target, detected, run, {
|
|
|
835
896
|
return { status: 'ALREADY_CURRENT', version: target.version };
|
|
836
897
|
}
|
|
837
898
|
|
|
899
|
+
if (!before.marketplace.exact) {
|
|
900
|
+
const preflight = await preflightStructuredMarketplace(target, env, run);
|
|
901
|
+
if (!preflight.ok) return { status: 'MANUAL_REQUIRED', reason: preflight.reason };
|
|
902
|
+
}
|
|
903
|
+
|
|
838
904
|
const marketplaceRebound = await bindStructuredMarketplace(target, command, env, run, before);
|
|
839
905
|
const current = target.host === 'claude' && marketplaceRebound
|
|
840
906
|
? await observeStructuredTarget(target, command, env, run)
|
|
@@ -898,49 +964,205 @@ function parseCodeBuddyRemoteObservation(target, stdout) {
|
|
|
898
964
|
|
|
899
965
|
function kimiExpectProgram({ removePlugin } = {}) {
|
|
900
966
|
const removeCommand = removePlugin
|
|
901
|
-
? `
|
|
967
|
+
? `submitCommand "/plugins remove $removePlugin"
|
|
902
968
|
expect {
|
|
903
969
|
-nocase -re {(remove|delete|uninstall).*(confirm|sure)|(confirm|sure).*(remove|delete|uninstall)} {
|
|
904
970
|
expect {
|
|
905
|
-
-ex $removePlugin {
|
|
906
|
-
|
|
907
|
-
|
|
971
|
+
-ex $removePlugin {
|
|
972
|
+
send -- "\\033\\[B"
|
|
973
|
+
send -- "\\033\\[13u"
|
|
974
|
+
}
|
|
975
|
+
timeout { failTimeout remove-confirmation 125 127 }
|
|
976
|
+
eof { failEof remove-confirmation 126 }
|
|
977
|
+
}
|
|
978
|
+
expect {
|
|
979
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
980
|
+
-re $promptPattern {}
|
|
981
|
+
timeout { failTimeout remove-prompt 128 130 }
|
|
982
|
+
eof { failEof remove-prompt 129 }
|
|
908
983
|
}
|
|
909
|
-
expect -re $promptPattern
|
|
910
984
|
}
|
|
911
985
|
-re $promptPattern {}
|
|
912
|
-
|
|
913
|
-
|
|
986
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
987
|
+
timeout { failTimeout remove-dialog 122 124 }
|
|
988
|
+
eof { failEof remove-dialog 123 }
|
|
914
989
|
}
|
|
915
990
|
`
|
|
916
991
|
: '';
|
|
917
|
-
return `set timeout
|
|
918
|
-
foreach variable {RELEASE_SKILL_KIMI_COMMAND RELEASE_SKILL_KIMI_INSTALL_URL RELEASE_SKILL_KIMI_REMOVE_PLUGIN} {
|
|
992
|
+
return `set timeout 240
|
|
993
|
+
foreach variable {RELEASE_SKILL_KIMI_COMMAND RELEASE_SKILL_KIMI_INSTALL_URL RELEASE_SKILL_KIMI_REMOVE_PLUGIN RELEASE_SKILL_KIMI_EXPECTED_REPO RELEASE_SKILL_KIMI_EXPECTED_TAG} {
|
|
919
994
|
if {![info exists env($variable)]} { exit 90 }
|
|
920
995
|
}
|
|
921
996
|
set kimiCommand $env(RELEASE_SKILL_KIMI_COMMAND)
|
|
922
997
|
set installUrl $env(RELEASE_SKILL_KIMI_INSTALL_URL)
|
|
923
998
|
set removePlugin $env(RELEASE_SKILL_KIMI_REMOVE_PLUGIN)
|
|
924
|
-
set
|
|
999
|
+
set expectedRepo $env(RELEASE_SKILL_KIMI_EXPECTED_REPO)
|
|
1000
|
+
set expectedTag $env(RELEASE_SKILL_KIMI_EXPECTED_TAG)
|
|
1001
|
+
set promptPattern {(?:(?:^|\\r|\\n)> (?:\\r*\\n|$)|(?:^|\\r|\\n)(?:(?:\\033\\[[0-9;?]*[ -/]*[@-~])|\\033\\][^\\x07]*\\x07|[ \\t])*│[^\\r\\n]*>[^\\r\\n]*│(?:(?:\\033\\[[0-9;?]*[ -/]*[@-~])|\\033\\][^\\x07]*\\x07|[ \\t])*(?:\\r*\\n|$))}
|
|
1002
|
+
|
|
1003
|
+
proc cleanScreen {value} {
|
|
1004
|
+
regsub -all {\\033\\[[0-9;?]*[ -/]*[@-~]} $value {} value
|
|
1005
|
+
regsub -all {\\033\\][^\\x07]*\\x07} $value {} value
|
|
1006
|
+
regsub -all {\\r} $value {} value
|
|
1007
|
+
return $value
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
proc compactScreen {value} {
|
|
1011
|
+
set value [cleanScreen $value]
|
|
1012
|
+
regsub -all {[[:space:]]+} $value {} value
|
|
1013
|
+
return [string tolower $value]
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
proc extractPluginTrustDialog {value} {
|
|
1017
|
+
set cleaned [cleanScreen $value]
|
|
1018
|
+
set lowered [string tolower $cleaned]
|
|
1019
|
+
set dialogStart -1
|
|
1020
|
+
foreach marker {"install third-party plugin " "trust and install from "} {
|
|
1021
|
+
set markerStart [string last $marker $lowered]
|
|
1022
|
+
if {$markerStart > $dialogStart} { set dialogStart $markerStart }
|
|
1023
|
+
}
|
|
1024
|
+
if {$dialogStart < 0} { return "" }
|
|
1025
|
+
return [string range $cleaned $dialogStart end]
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
proc failTimeout {state timeoutCode unknownCode} {
|
|
1029
|
+
global expect_out
|
|
1030
|
+
set buffer ""
|
|
1031
|
+
if {[info exists expect_out(buffer)]} { set buffer [string trim [cleanScreen $expect_out(buffer)]] }
|
|
1032
|
+
if {$buffer ne ""} {
|
|
1033
|
+
puts stderr "KIMI_TUI_STATE:$state:unknown"
|
|
1034
|
+
exit $unknownCode
|
|
1035
|
+
}
|
|
1036
|
+
puts stderr "KIMI_TUI_STATE:$state:timeout"
|
|
1037
|
+
exit $timeoutCode
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
proc failEof {state code} {
|
|
1041
|
+
puts stderr "KIMI_TUI_STATE:$state:eof"
|
|
1042
|
+
exit $code
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
proc directoryTrust {} {
|
|
1046
|
+
puts stderr "KIMI_TUI_STATE:directory-trust:manual-required"
|
|
1047
|
+
exit 80
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
proc submitCommand {command} {
|
|
1051
|
+
send -- "\\033\\[200~"
|
|
1052
|
+
send -- $command
|
|
1053
|
+
send -- "\\033\\[201~"
|
|
1054
|
+
send -- "\\033\\[13u"
|
|
1055
|
+
}
|
|
1056
|
+
|
|
925
1057
|
spawn $kimiCommand
|
|
926
|
-
|
|
927
|
-
|
|
1058
|
+
if {[catch {exec stty columns 240 rows 60 < $spawn_out(slave,name)} resizeError]} {
|
|
1059
|
+
puts stderr "KIMI_TUI_STATE:terminal-size:failed"
|
|
1060
|
+
exit 131
|
|
1061
|
+
}
|
|
928
1062
|
expect {
|
|
929
|
-
-nocase -re {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1063
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1064
|
+
-re $promptPattern {}
|
|
1065
|
+
timeout { failTimeout initial-prompt 101 103 }
|
|
1066
|
+
eof { failEof initial-prompt 102 }
|
|
1067
|
+
}
|
|
1068
|
+
${removeCommand}submitCommand "/plugins install $installUrl"
|
|
1069
|
+
set dialogBuffer ""
|
|
1070
|
+
expect {
|
|
1071
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1072
|
+
-nocase -re {(?:Install third-party plugin|Trust and install from)[ \\t]} {
|
|
1073
|
+
append dialogBuffer $expect_out(buffer)
|
|
1074
|
+
}
|
|
1075
|
+
timeout { failTimeout plugin-trust-anchor 104 106 }
|
|
1076
|
+
eof { failEof plugin-trust-anchor 105 }
|
|
1077
|
+
}
|
|
1078
|
+
expect {
|
|
1079
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1080
|
+
-nocase -re {❯[^\\r\\n]*(?:Exit|Cancel|Trust and install)} {
|
|
1081
|
+
append dialogBuffer $expect_out(buffer)
|
|
1082
|
+
}
|
|
1083
|
+
-re {❯[^\\r\\n]*\\r*\\n} {
|
|
1084
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust-selected-row:unknown"
|
|
1085
|
+
exit 134
|
|
1086
|
+
}
|
|
1087
|
+
timeout {
|
|
1088
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust-selected-row:timeout"
|
|
1089
|
+
exit 132
|
|
1090
|
+
}
|
|
1091
|
+
eof { failEof plugin-trust-selected-row 133 }
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
set dialog [extractPluginTrustDialog $dialogBuffer]
|
|
1095
|
+
if {$dialog eq ""} {
|
|
1096
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust:dialog-unknown"
|
|
1097
|
+
exit 113
|
|
1098
|
+
}
|
|
1099
|
+
set compactDialog [compactScreen $dialog]
|
|
1100
|
+
if {[string first [string tolower [compactScreen $expectedRepo]] $compactDialog] < 0} {
|
|
1101
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust:repo-mismatch"
|
|
1102
|
+
exit 111
|
|
1103
|
+
}
|
|
1104
|
+
if {[string first [string tolower [compactScreen $expectedTag]] $compactDialog] < 0} {
|
|
1105
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust:tag-mismatch"
|
|
1106
|
+
exit 112
|
|
1107
|
+
}
|
|
1108
|
+
if {[regexp -nocase {(^|\\n)[^\\n]*❯[^\\n]*trust and install} $dialog]} {
|
|
1109
|
+
send -- "\\033\\[13u"
|
|
1110
|
+
} elseif {[regexp -nocase {(^|\\n)[^\\n]*❯[^\\n]*(cancel|exit)} $dialog]} {
|
|
1111
|
+
send -- "\\033\\[B"
|
|
1112
|
+
expect {
|
|
1113
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1114
|
+
-nocase -re {❯[^\\r\\n]*Trust and install} {}
|
|
1115
|
+
-re {❯[^\\r\\n]*\\r*\\n} {
|
|
1116
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust-confirm-selection:unknown"
|
|
1117
|
+
exit 109
|
|
934
1118
|
}
|
|
1119
|
+
timeout { failTimeout plugin-trust-confirm-selection 107 109 }
|
|
1120
|
+
eof { failEof plugin-trust-confirm-selection 108 }
|
|
935
1121
|
}
|
|
936
|
-
|
|
937
|
-
|
|
1122
|
+
send -- "\\033\\[13u"
|
|
1123
|
+
} else {
|
|
1124
|
+
puts stderr "KIMI_TUI_STATE:plugin-trust:selection-unknown"
|
|
1125
|
+
exit 113
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
expect {
|
|
1129
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1130
|
+
-nocase -re {Install finished[^\\r\\n]*see details below\\.} {}
|
|
1131
|
+
-nocase -re {Installing plugin from[^\\r\\n]*(?:\\r|\\n)} {
|
|
1132
|
+
exp_continue -continue_timer
|
|
1133
|
+
}
|
|
1134
|
+
-nocase -re {Install failed:[^\\r\\n]*} {
|
|
1135
|
+
puts stderr "KIMI_TUI_STATE:install-result:failed"
|
|
1136
|
+
exit 135
|
|
1137
|
+
}
|
|
1138
|
+
-nocase -re {(^|\\r|\\n)Install[^\\r\\n]*\\r*\\n} {
|
|
1139
|
+
puts stderr "KIMI_TUI_STATE:install-result:unknown"
|
|
1140
|
+
exit 138
|
|
1141
|
+
}
|
|
1142
|
+
timeout {
|
|
1143
|
+
puts stderr "KIMI_TUI_STATE:install-result:timeout"
|
|
1144
|
+
exit 136
|
|
1145
|
+
}
|
|
1146
|
+
eof { failEof install-result 137 }
|
|
1147
|
+
}
|
|
1148
|
+
expect {
|
|
1149
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1150
|
+
-re $promptPattern {}
|
|
1151
|
+
timeout { failTimeout post-install-prompt 114 116 }
|
|
1152
|
+
eof { failEof post-install-prompt 115 }
|
|
1153
|
+
}
|
|
1154
|
+
submitCommand "/reload"
|
|
1155
|
+
expect {
|
|
1156
|
+
-nocase -re {Trust this folder\\?} { directoryTrust }
|
|
1157
|
+
-re $promptPattern {}
|
|
1158
|
+
timeout { failTimeout reload-prompt 117 119 }
|
|
1159
|
+
eof { failEof reload-prompt 118 }
|
|
1160
|
+
}
|
|
1161
|
+
submitCommand "/exit"
|
|
1162
|
+
expect {
|
|
1163
|
+
eof { puts stderr "KIMI_TUI_STATE:exit-eof:eof" }
|
|
1164
|
+
timeout { failTimeout exit-eof 120 121 }
|
|
938
1165
|
}
|
|
939
|
-
expect -re $promptPattern
|
|
940
|
-
send -- "/reload\\r"
|
|
941
|
-
expect -re $promptPattern
|
|
942
|
-
send -- "/exit\\r"
|
|
943
|
-
expect eof
|
|
944
1166
|
`;
|
|
945
1167
|
}
|
|
946
1168
|
|
|
@@ -1018,7 +1240,7 @@ async function runKimiUpdate(target, detected, run, kimiHome, {
|
|
|
1018
1240
|
});
|
|
1019
1241
|
return { status: 'ALREADY_CURRENT', version: target.version };
|
|
1020
1242
|
}
|
|
1021
|
-
await withTemporaryWorkspace(async (workspace) => {
|
|
1243
|
+
const tuiOutcome = await withTemporaryWorkspace(async (workspace) => {
|
|
1022
1244
|
const checkout = join(workspace.root, 'plugin');
|
|
1023
1245
|
await run('git', [
|
|
1024
1246
|
'clone', '--depth', '1', '--branch', target.pluginTag, '--single-branch',
|
|
@@ -1033,18 +1255,31 @@ async function runKimiUpdate(target, detected, run, kimiHome, {
|
|
|
1033
1255
|
}
|
|
1034
1256
|
const installUrl = `https://github.com/${target.pluginRepo}/releases/tag/${target.pluginTag}`;
|
|
1035
1257
|
const removePlugin = before.source === 'legacy' ? target.plugin : '';
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1258
|
+
try {
|
|
1259
|
+
await run(detected.expectCommand, ['-c', kimiExpectProgram({
|
|
1260
|
+
...(removePlugin ? { removePlugin } : {}),
|
|
1261
|
+
})], {
|
|
1262
|
+
timeout: Math.max(300_000, target.timeoutMs),
|
|
1263
|
+
env: {
|
|
1264
|
+
...env,
|
|
1265
|
+
RELEASE_SKILL_KIMI_COMMAND: detected.command,
|
|
1266
|
+
RELEASE_SKILL_KIMI_INSTALL_URL: installUrl,
|
|
1267
|
+
RELEASE_SKILL_KIMI_REMOVE_PLUGIN: removePlugin,
|
|
1268
|
+
RELEASE_SKILL_KIMI_EXPECTED_REPO: `https://github.com/${target.pluginRepo}`,
|
|
1269
|
+
RELEASE_SKILL_KIMI_EXPECTED_TAG: target.pluginTag,
|
|
1270
|
+
},
|
|
1271
|
+
});
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
if (error?.exitStatus === 80) {
|
|
1274
|
+
return {
|
|
1275
|
+
status: 'MANUAL_REQUIRED',
|
|
1276
|
+
reason: 'Kimi requires folder trust; release-finish did not confirm the folder or continue installation',
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
throw error;
|
|
1280
|
+
}
|
|
1047
1281
|
}, { prefix: 'release-skill-kimi-update-' });
|
|
1282
|
+
if (tuiOutcome) return tuiOutcome;
|
|
1048
1283
|
const after = await observeKimiTarget(target, kimiHome, run);
|
|
1049
1284
|
if (!after.exact) throw new Error('Kimi did not report the frozen plugin identity after TUI installation');
|
|
1050
1285
|
await verifyStructuredInstalledPayload({
|
|
@@ -1098,7 +1333,7 @@ async function updateLocalHostPluginsInternal({
|
|
|
1098
1333
|
kimiHome,
|
|
1099
1334
|
verifyInstalledPayload = verifyInstalledMarketplacePayload,
|
|
1100
1335
|
} = {}) {
|
|
1101
|
-
const effectiveKimiHome = kimiHome ?? process.env.
|
|
1336
|
+
const effectiveKimiHome = kimiHome ?? process.env.KIMI_CODE_HOME ?? join(homedir(), '.kimi-code');
|
|
1102
1337
|
const checklist = derivePostReleaseChecklist(plan, { postVerifyComplete: true });
|
|
1103
1338
|
if (confirmPlanDigest !== plan.digest) {
|
|
1104
1339
|
throw new Error('plan digest confirmation does not match the frozen release plan');
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
normalizeHostId,
|
|
83
83
|
buildExpectedStandaloneIndexInstallIdentity,
|
|
84
84
|
} from '../platforms/registry.mjs';
|
|
85
|
-
import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
85
|
+
import { deriveSurfaceHostBinding, groupSurfaceHostBindings, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
86
86
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
87
87
|
import { buildInstallationContract, computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from '../core/installation-contract.mjs';
|
|
88
88
|
import {
|
|
@@ -2818,10 +2818,11 @@ async function runPrepareSkillResourceClosureGate({
|
|
|
2818
2818
|
if (binding) surfaceHostBindings.push(binding);
|
|
2819
2819
|
}
|
|
2820
2820
|
|
|
2821
|
+
const { checkerBindings, coverageClaims } = groupSurfaceHostBindings(surfaceHostBindings);
|
|
2821
2822
|
const closureResult = await checkSkillResourceClosure({
|
|
2822
2823
|
snapshotDir: manifest.outputDir,
|
|
2823
2824
|
host: 'root',
|
|
2824
|
-
surfaceHostBindings,
|
|
2825
|
+
surfaceHostBindings: checkerBindings,
|
|
2825
2826
|
});
|
|
2826
2827
|
|
|
2827
2828
|
// G5: bind execution time + exit code into the frozen receipt.
|
|
@@ -2880,6 +2881,7 @@ async function runPrepareSkillResourceClosureGate({
|
|
|
2880
2881
|
const hostCoverage = evaluateDeclaredHostSurfaceCoverage(
|
|
2881
2882
|
expectedHosts,
|
|
2882
2883
|
closureResult.surfaces,
|
|
2884
|
+
coverageClaims,
|
|
2883
2885
|
);
|
|
2884
2886
|
if (!hostCoverage.passed) {
|
|
2885
2887
|
await evidence.append({
|
|
@@ -2908,7 +2910,7 @@ async function runPrepareSkillResourceClosureGate({
|
|
|
2908
2910
|
// 含一个技能(binding-surface existence)。G4 按宿主名覆盖,这里按
|
|
2909
2911
|
// 绑定表面路径覆盖 —— 例如 manifest 指向 './adapters/claude/skills/'
|
|
2910
2912
|
// 但 publicFiles 丢弃该目录时,绑定表面缺失必须失败关闭。
|
|
2911
|
-
for (const binding of
|
|
2913
|
+
for (const binding of coverageClaims) {
|
|
2912
2914
|
const boundSurface = closureResult.surfaces.find(
|
|
2913
2915
|
(surface) => surface.id === binding.surfaceId,
|
|
2914
2916
|
);
|
package/src/commands/publish.mjs
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import { readFile, mkdir } from 'node:fs/promises';
|
|
29
29
|
import { isAbsolute, join, relative } from 'node:path';
|
|
30
|
-
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
31
|
-
import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
30
|
+
import { PLATFORMS, normalizeHostId } from '../platforms/registry.mjs';
|
|
31
|
+
import { deriveSurfaceHostBinding, groupSurfaceHostBindings, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
32
32
|
|
|
33
33
|
import { assertImmutablePlanAuthority, computePlanDigest, validatePlan, validatePlanActionCompleteness } from '../core/plan.mjs';
|
|
34
34
|
import {
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
assertSkillResourceClosureReceipt,
|
|
49
49
|
checkSkillResourceClosure,
|
|
50
50
|
createSkillResourceClosureReceipt,
|
|
51
|
+
evaluateDeclaredHostSurfaceCoverage,
|
|
51
52
|
} from '../core/skill-resource-closure.mjs';
|
|
52
53
|
import {
|
|
53
54
|
ADAPTER_ACTION_TYPE_MAP,
|
|
@@ -672,10 +673,11 @@ export async function publishRelease(options) {
|
|
|
672
673
|
});
|
|
673
674
|
if (binding) surfaceHostBindings.push(binding);
|
|
674
675
|
}
|
|
676
|
+
const { checkerBindings, coverageClaims } = groupSurfaceHostBindings(surfaceHostBindings);
|
|
675
677
|
const closureResult = await checkSkillResourceClosure({
|
|
676
678
|
snapshotDir,
|
|
677
679
|
host: 'root',
|
|
678
|
-
surfaceHostBindings,
|
|
680
|
+
surfaceHostBindings: checkerBindings,
|
|
679
681
|
});
|
|
680
682
|
if (closureResult.findings.length > 0) {
|
|
681
683
|
await evidence.append({
|
|
@@ -691,6 +693,31 @@ export async function publishRelease(options) {
|
|
|
691
693
|
{ unitId, findings: closureResult.findings },
|
|
692
694
|
);
|
|
693
695
|
}
|
|
696
|
+
const expectedHosts = [];
|
|
697
|
+
for (const distribution of frozenUnit?.distributions ?? []) {
|
|
698
|
+
const platform = PLATFORMS.find((item) => item.distributionType === distribution.type);
|
|
699
|
+
if (platform) expectedHosts.push(await normalizeHostId(platform.buildAdapter.name));
|
|
700
|
+
}
|
|
701
|
+
const hostCoverage = evaluateDeclaredHostSurfaceCoverage(
|
|
702
|
+
expectedHosts,
|
|
703
|
+
closureResult.surfaces,
|
|
704
|
+
coverageClaims,
|
|
705
|
+
);
|
|
706
|
+
if (!hostCoverage.passed) {
|
|
707
|
+
await evidence.append({
|
|
708
|
+
phase: 'safety-gate',
|
|
709
|
+
gate: 'skill-resource-closure',
|
|
710
|
+
status: 'failed',
|
|
711
|
+
unitId,
|
|
712
|
+
reason: 'declared-host-surface-missing',
|
|
713
|
+
missingHosts: hostCoverage.missing,
|
|
714
|
+
});
|
|
715
|
+
throw new ReleaseError(
|
|
716
|
+
GATE_FAILED,
|
|
717
|
+
`skill resource closure recheck failed for unit "${unitId}": declared host surface(s) missing or empty: ${hostCoverage.missing.map((item) => item.host).join(', ')}`,
|
|
718
|
+
{ unitId, missingHosts: hostCoverage.missing },
|
|
719
|
+
);
|
|
720
|
+
}
|
|
694
721
|
// G5: preparedAt/exitCode are record-layer fields frozen by prepare
|
|
695
722
|
// (bound by the plan digest); they cannot be recomputed from the
|
|
696
723
|
// snapshot, so the recheck carries them forward from the expected
|
package/src/commands/ship.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
effectiveHookRequiresApproval,
|
|
9
9
|
normalizePostPublishView,
|
|
10
10
|
postPublishActionId,
|
|
11
|
+
requiresPostPublishDistribution,
|
|
11
12
|
} from '../core/postpublish.mjs';
|
|
12
13
|
import {
|
|
13
14
|
derivePostReleaseChecklist,
|
|
@@ -652,13 +653,10 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
652
653
|
|
|
653
654
|
if (state.status === 'PUBLISHED' || state.status === 'NEEDS_MANUAL_ATTESTATIONS') {
|
|
654
655
|
// Step 1: Check if postPublish requires distribution.
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
// legacy absent postPublish resolves to the same empty view.
|
|
656
|
+
// Only targets and distribute-phase hooks route through distribute;
|
|
657
|
+
// phase:postVerify hooks belong to their independent post-VERIFIED run.
|
|
658
658
|
const plan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
659
|
-
const
|
|
660
|
-
(declaration.targets?.length ?? 0) > 0 || (declaration.hooks?.length ?? 0) > 0);
|
|
661
|
-
const needsDistribution = hasDistributeWork;
|
|
659
|
+
const needsDistribution = requiresPostPublishDistribution(plan);
|
|
662
660
|
if (needsDistribution && deps.distributeRelease) {
|
|
663
661
|
state.status = 'DISTRIBUTING';
|
|
664
662
|
await writeJsonAtomic(statePath, state);
|