release-skill 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -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 +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +19 -19
- package/README.zh-CN.md +17 -18
- 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 +249 -17
- package/adapters/claude/skills/release-help/SKILL.md +5 -1
- package/adapters/claude/skills/release-verify/SKILL.md +13 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +249 -17
- package/adapters/codex/skills/release-help/SKILL.md +5 -1
- package/adapters/codex/skills/release-verify/SKILL.md +13 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +249 -17
- package/adapters/kimi/skills/release-help/SKILL.md +5 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +13 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +249 -17
- package/adapters/workbuddy/skills/release-help/SKILL.md +5 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +13 -1
- package/bin/release-skill-cli.mjs +130 -6
- package/bin/release-skill.bundle.mjs +249 -17
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/skills/release-help/SKILL.md +5 -1
- package/skills/release-verify/SKILL.md +13 -1
- package/skills-src/release-help/SKILL.md +5 -1
- package/skills-src/release-verify/SKILL.md +13 -1
- package/src/adapters/plugin-marketplace.mjs +54 -5
- package/src/commands/prepare.mjs +5 -3
- package/src/commands/publish.mjs +30 -3
- package/src/commands/verify.mjs +14 -7
- package/src/core/skill-resource-closure.mjs +19 -1
- package/src/core/surface-host-bindings.mjs +35 -0
|
@@ -12,7 +12,7 @@ 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 批准。若计划声明 `postVerify` hook
|
|
15
|
+
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准。若计划声明 `postVerify` hook,可用 `postverify` 直接执行独立收尾 run,也可由 `ship` 编排该阶段;本机收尾必须等待完成的 postVerify run,不能把仅有 `VERIFIED` 的 verify run 当作本机收尾授权。核心发布流程跨平台,WorkBuddy 本机收尾仅支持 macOS
|
|
16
16
|
- 最小示例:展示从 release-help 到 release-assess 的最短路径
|
|
17
17
|
- 只读诊断:运行 dry-run 检查,不修改任何文件
|
|
18
18
|
- 故障引导:根据错误码指向对应的修复 Skill
|
|
@@ -51,6 +51,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-v
|
|
|
51
51
|
# 多发布单元项目显式选择本轮范围;未传 --unit 时仍为全部单元
|
|
52
52
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> \
|
|
53
53
|
--target-version <version> --unit <unit-a> --unit <unit-b> --json
|
|
54
|
+
# 对已经 VERIFIED 的计划独立执行 postVerify hook;不读取或写入 ship state
|
|
55
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" postverify --root <path> \
|
|
56
|
+
--plan <plan-path> --approval <approval-path> --run <verified-run-path> \
|
|
57
|
+
--hook-approval <immutable-hook-approval-path> --json
|
|
54
58
|
# 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
|
|
55
59
|
# 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
|
|
56
60
|
# 命令调用本身即授权执行配置中的 hooks
|
|
@@ -15,6 +15,11 @@ verify 是发布流程的最终验证阶段,是唯一能将状态提升到 `VE
|
|
|
15
15
|
它执行远端状态重检、精确 npm 安装烟雾测试和消费者插件安装验证。
|
|
16
16
|
verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再次派生运行。
|
|
17
17
|
|
|
18
|
+
计划声明 `postVerify` hook 时,`postverify` 命令负责执行独立的收尾阶段。它接收
|
|
19
|
+
`VERIFIED` verify run、同一计划的 approval,以及逐个传入的 checkpoint approval,
|
|
20
|
+
然后原样调用现有 `postVerifyRelease`。该命令产生独立的 `postverify` run,不读取或写入
|
|
21
|
+
ship state;hook approval 缺失、错误或过期时,在 hook 执行前失败关闭。
|
|
22
|
+
|
|
18
23
|
**注意**: distribute gate (W1) 已经实现并集成在标准 verify 流程中。verify 现在会检查 postPublish 分发状态(git mirror + marketplace index),只有当所有外部动作都完成并通过验证时才达到 VERIFIED。
|
|
19
24
|
|
|
20
25
|
**工作流兼容性**:
|
|
@@ -45,13 +50,17 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
45
50
|
2. 使用插件根相对路径运行 CLI;命令调用本身即授权执行已配置的 verification gate 和 smoke process
|
|
46
51
|
3. 检查 exit code 和结构化状态:`VERIFIED`(全部通过)/ 失败(具体错误)
|
|
47
52
|
4. 只有 `VERIFIED` 才是发布 happy end
|
|
48
|
-
5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook
|
|
53
|
+
5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook:直接收尾使用独立的 `postverify --plan --approval --run --hook-approval`,由该命令产生 `DISTRIBUTED` postVerify run;若仍由持久化 ship state 承担编排,则使用 `ship --hook-approval` 完成同一阶段,再把最终 run 路径交给 `release-finish`。没有 postVerify hook 时,把当前 verify run 路径交给 `release-finish`。随后按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
|
|
49
54
|
|
|
50
55
|
## 确定性脚本调用
|
|
51
56
|
|
|
52
57
|
```bash
|
|
53
58
|
# 从插件根运行
|
|
54
59
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <plan-path> --run <run-path> --json
|
|
60
|
+
# 独立执行已 VERIFIED 计划的 postVerify hook;每个 requiresApproval hook 单独传入批准记录
|
|
61
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" postverify --root <path> \
|
|
62
|
+
--plan <plan-path> --approval <approval-path> --run <verified-run-path> \
|
|
63
|
+
--hook-approval <immutable-hook-approval-path> --json
|
|
55
64
|
```
|
|
56
65
|
|
|
57
66
|
## 验证步骤
|
|
@@ -63,6 +72,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
63
72
|
5. 对 Claude/Codex marketplace distribution 执行全新隔离消费者安装验证;收集 Kimi/CodeBuddy 的非阻塞 `manualFollowUps`
|
|
64
73
|
6. 全部通过 → `VERIFIED`
|
|
65
74
|
|
|
75
|
+
`postverify` 不属于上述远端验证步骤。它只处理已经达到 `VERIFIED` 的计划,并把
|
|
76
|
+
`postVerify` 阶段的执行结果写入独立 run。
|
|
77
|
+
|
|
66
78
|
## 发布后收尾
|
|
67
79
|
|
|
68
80
|
`VERIFIED` 不代表要自动修改开发分支或本机宿主。进入 `release-finish` 后,只有用户明确同意,才执行对应的本地动作。本机插件更新失败不会改变发布终态。
|
|
@@ -12,7 +12,7 @@ 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 批准。若计划声明 `postVerify` hook
|
|
15
|
+
- 能力说明:缺少配置时走 `help → setup → assess`;已有配置的安全默认路径是 `help → assess → prepare --offline`;日常生产发布优先使用可恢复的 `ship`,兼容的分阶段闭环仍是 `prepare --online --production → approve → publish → verify`。冻结计划批准是正常发布级流程的唯一批准门;声明 `requiresApproval: true` 的 postPublish hook 仍须等待独立 checkpoint 批准。若计划声明 `postVerify` hook,可用 `postverify` 直接执行独立收尾 run,也可由 `ship` 编排该阶段;本机收尾必须等待完成的 postVerify run,不能把仅有 `VERIFIED` 的 verify run 当作本机收尾授权。核心发布流程跨平台,WorkBuddy 本机收尾仅支持 macOS
|
|
16
16
|
- 最小示例:展示从 release-help 到 release-assess 的最短路径
|
|
17
17
|
- 只读诊断:运行 dry-run 检查,不修改任何文件
|
|
18
18
|
- 故障引导:根据错误码指向对应的修复 Skill
|
|
@@ -51,6 +51,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> --target-v
|
|
|
51
51
|
# 多发布单元项目显式选择本轮范围;未传 --unit 时仍为全部单元
|
|
52
52
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" ship --root <path> \
|
|
53
53
|
--target-version <version> --unit <unit-a> --unit <unit-b> --json
|
|
54
|
+
# 对已经 VERIFIED 的计划独立执行 postVerify hook;不读取或写入 ship state
|
|
55
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" postverify --root <path> \
|
|
56
|
+
--plan <plan-path> --approval <approval-path> --run <verified-run-path> \
|
|
57
|
+
--hook-approval <immutable-hook-approval-path> --json
|
|
54
58
|
# 开发阶段执行声明 hooks 并生成 prepare 可复用的内容绑定收据
|
|
55
59
|
# 配置时刻即授权(FM-16 处置 A):hook 是任意本地进程、无隔离、触发前无确认点,
|
|
56
60
|
# 命令调用本身即授权执行配置中的 hooks
|
|
@@ -15,6 +15,11 @@ verify 是发布流程的最终验证阶段,是唯一能将状态提升到 `VE
|
|
|
15
15
|
它执行远端状态重检、精确 npm 安装烟雾测试和消费者插件安装验证。
|
|
16
16
|
verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再次派生运行。
|
|
17
17
|
|
|
18
|
+
计划声明 `postVerify` hook 时,`postverify` 命令负责执行独立的收尾阶段。它接收
|
|
19
|
+
`VERIFIED` verify run、同一计划的 approval,以及逐个传入的 checkpoint approval,
|
|
20
|
+
然后原样调用现有 `postVerifyRelease`。该命令产生独立的 `postverify` run,不读取或写入
|
|
21
|
+
ship state;hook approval 缺失、错误或过期时,在 hook 执行前失败关闭。
|
|
22
|
+
|
|
18
23
|
**注意**: distribute gate (W1) 已经实现并集成在标准 verify 流程中。verify 现在会检查 postPublish 分发状态(git mirror + marketplace index),只有当所有外部动作都完成并通过验证时才达到 VERIFIED。
|
|
19
24
|
|
|
20
25
|
**工作流兼容性**:
|
|
@@ -45,13 +50,17 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
45
50
|
2. 使用插件根相对路径运行 CLI;命令调用本身即授权执行已配置的 verification gate 和 smoke process
|
|
46
51
|
3. 检查 exit code 和结构化状态:`VERIFIED`(全部通过)/ 失败(具体错误)
|
|
47
52
|
4. 只有 `VERIFIED` 才是发布 happy end
|
|
48
|
-
5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook
|
|
53
|
+
5. 达到 `VERIFIED` 后先检查计划是否声明 `postVerify` hook:直接收尾使用独立的 `postverify --plan --approval --run --hook-approval`,由该命令产生 `DISTRIBUTED` postVerify run;若仍由持久化 ship state 承担编排,则使用 `ship --hook-approval` 完成同一阶段,再把最终 run 路径交给 `release-finish`。没有 postVerify hook 时,把当前 verify run 路径交给 `release-finish`。随后按清单主动询问分支合并和本机宿主插件更新;发布策略已包含分支动作时略过合并询问
|
|
49
54
|
|
|
50
55
|
## 确定性脚本调用
|
|
51
56
|
|
|
52
57
|
```bash
|
|
53
58
|
# 从插件根运行
|
|
54
59
|
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <plan-path> --run <run-path> --json
|
|
60
|
+
# 独立执行已 VERIFIED 计划的 postVerify hook;每个 requiresApproval hook 单独传入批准记录
|
|
61
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" postverify --root <path> \
|
|
62
|
+
--plan <plan-path> --approval <approval-path> --run <verified-run-path> \
|
|
63
|
+
--hook-approval <immutable-hook-approval-path> --json
|
|
55
64
|
```
|
|
56
65
|
|
|
57
66
|
## 验证步骤
|
|
@@ -63,6 +72,9 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
63
72
|
5. 对 Claude/Codex marketplace distribution 执行全新隔离消费者安装验证;收集 Kimi/CodeBuddy 的非阻塞 `manualFollowUps`
|
|
64
73
|
6. 全部通过 → `VERIFIED`
|
|
65
74
|
|
|
75
|
+
`postverify` 不属于上述远端验证步骤。它只处理已经达到 `VERIFIED` 的计划,并把
|
|
76
|
+
`postVerify` 阶段的执行结果写入独立 run。
|
|
77
|
+
|
|
66
78
|
## 发布后收尾
|
|
67
79
|
|
|
68
80
|
`VERIFIED` 不代表要自动修改开发分支或本机宿主。进入 `release-finish` 后,只有用户明确同意,才执行对应的本地动作。本机插件更新失败不会改变发布终态。
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from '../core/installation-contract.mjs';
|
|
36
36
|
|
|
37
37
|
import { createHash } from 'node:crypto';
|
|
38
|
+
import { createFilesystemRootBinding, observeFilesystemTree } from 'skill-family-harness-node';
|
|
38
39
|
import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
|
|
39
40
|
import { PLATFORMS, getPlatform, resolvePlatformRoute, resolveCapabilityConflicts } from '../platforms/registry.mjs';
|
|
40
41
|
import {
|
|
@@ -101,6 +102,41 @@ const EXTRA_INSTALLED_PATHS_CAP = 200;
|
|
|
101
102
|
/** Diagnostic cap: at most this many conflict paths are listed per error. */
|
|
102
103
|
const PAYLOAD_CONFLICT_REPORT_CAP = 10;
|
|
103
104
|
|
|
105
|
+
function foundationPayloadMembers(observation) {
|
|
106
|
+
return observation.members.map((member) => {
|
|
107
|
+
if (member.type === 'file') {
|
|
108
|
+
return {
|
|
109
|
+
path: member.path,
|
|
110
|
+
type: member.type,
|
|
111
|
+
mode: member.statMode & ~0o222,
|
|
112
|
+
size: member.bytes,
|
|
113
|
+
contentDigest: member.sha256,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (member.type === 'directory') {
|
|
117
|
+
return { path: member.path, type: member.type, mode: member.statMode };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
path: member.path,
|
|
121
|
+
type: member.type,
|
|
122
|
+
mode: member.statMode,
|
|
123
|
+
size: member.bytes,
|
|
124
|
+
targetBase64: member.targetBase64,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function observeMarketplaceInstallTree(installPath) {
|
|
130
|
+
const canonicalInstallPath = await realpath(installPath);
|
|
131
|
+
const rootBinding = await createFilesystemRootBinding(canonicalInstallPath);
|
|
132
|
+
const observation = await observeFilesystemTree({
|
|
133
|
+
root: canonicalInstallPath,
|
|
134
|
+
rootBinding,
|
|
135
|
+
symlinkPolicy: { mode: 'record' },
|
|
136
|
+
});
|
|
137
|
+
return foundationPayloadMembers(observation);
|
|
138
|
+
}
|
|
139
|
+
|
|
104
140
|
/** 消费端安装验证配方版本(与 prepare.mjs 一致)。 */
|
|
105
141
|
const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
|
|
106
142
|
|
|
@@ -534,10 +570,10 @@ export async function verifyInstalledMarketplacePayload(action, context, install
|
|
|
534
570
|
// containment semantics; they differ only in the authority subtree source
|
|
535
571
|
// (declared-manifest reads the entry's declared source subpath; external
|
|
536
572
|
// short-circuits to the whole tree '.').
|
|
537
|
-
const
|
|
573
|
+
const installedMembers = await observeMarketplaceInstallTree(installPath);
|
|
538
574
|
const authorityPayload = transportPayload(authorityEntries);
|
|
539
575
|
const installedByPath = new Map(
|
|
540
|
-
|
|
576
|
+
installedMembers.map((entry) => [entry.path, entry]),
|
|
541
577
|
);
|
|
542
578
|
const conflicts = [];
|
|
543
579
|
for (const authorityEntry of authorityPayload) {
|
|
@@ -567,15 +603,25 @@ export async function verifyInstalledMarketplacePayload(action, context, install
|
|
|
567
603
|
);
|
|
568
604
|
}
|
|
569
605
|
const authorityPaths = new Set(authorityPayload.map((entry) => entry.path));
|
|
570
|
-
const
|
|
571
|
-
.
|
|
572
|
-
|
|
606
|
+
const extraMembers = installedMembers
|
|
607
|
+
.filter((entry) => entry.type !== 'directory' && !authorityPaths.has(entry.path));
|
|
608
|
+
const extraPaths = extraMembers.map((entry) => entry.path);
|
|
573
609
|
const extraInstalledPaths = extraPaths.slice(0, EXTRA_INSTALLED_PATHS_CAP);
|
|
610
|
+
const extraInstalledLinks = extraMembers
|
|
611
|
+
.filter((entry) => entry.type === 'symlink')
|
|
612
|
+
.slice(0, EXTRA_INSTALLED_PATHS_CAP)
|
|
613
|
+
.map((entry) => ({
|
|
614
|
+
path: entry.path,
|
|
615
|
+
targetBase64: entry.targetBase64,
|
|
616
|
+
bytes: entry.size,
|
|
617
|
+
statMode: entry.mode,
|
|
618
|
+
}));
|
|
574
619
|
// This is not an expected-value backfill: the sealed authority digest was
|
|
575
620
|
// revalidated above and every declared file was independently compared.
|
|
576
621
|
return {
|
|
577
622
|
manifestDigest: action.manifestDigest,
|
|
578
623
|
extraInstalledPaths,
|
|
624
|
+
...(extraInstalledLinks.length > 0 ? { extraInstalledLinks } : {}),
|
|
579
625
|
...(extraPaths.length > EXTRA_INSTALLED_PATHS_CAP
|
|
580
626
|
? { extraInstalledPathsTotal: extraPaths.length }
|
|
581
627
|
: {}),
|
|
@@ -605,6 +651,9 @@ function extraInstalledPathsAudit(binding) {
|
|
|
605
651
|
if (!binding || !Array.isArray(binding.extraInstalledPaths)) return {};
|
|
606
652
|
return {
|
|
607
653
|
extraInstalledPaths: binding.extraInstalledPaths,
|
|
654
|
+
...(Array.isArray(binding.extraInstalledLinks) && binding.extraInstalledLinks.length > 0
|
|
655
|
+
? { extraInstalledLinks: binding.extraInstalledLinks }
|
|
656
|
+
: {}),
|
|
608
657
|
...(binding.extraInstalledPathsTotal !== undefined
|
|
609
658
|
? { extraInstalledPathsTotal: binding.extraInstalledPathsTotal }
|
|
610
659
|
: {}),
|
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/verify.mjs
CHANGED
|
@@ -2052,11 +2052,12 @@ export async function verifyRelease(options) {
|
|
|
2052
2052
|
await evidence.append({ phase: 'verify', step: 'skill-resource-closure', status: 'started' });
|
|
2053
2053
|
|
|
2054
2054
|
// Collect install paths from marketplace adapter checks.
|
|
2055
|
-
// R-13 P2 (ruling 6):
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2055
|
+
// R-13 P2 (ruling 6): prepare and publish derive surface bindings in
|
|
2056
|
+
// frozen snapshot-root coordinates. An adapter observation is already
|
|
2057
|
+
// rooted at the installed plugin, so verify derives the same manifest
|
|
2058
|
+
// skills fact relative to '.' at that consumer coordinate. Plans
|
|
2059
|
+
// without a frozen installation contract (legacy) fall back to the
|
|
2060
|
+
// plugin-root surface ('.') with the legacy permissive checks.
|
|
2060
2061
|
const installSurfaces = [];
|
|
2061
2062
|
for (const check of adapterChecks) {
|
|
2062
2063
|
const platform = PLATFORMS.find((p) => p.actionType === check.actionType);
|
|
@@ -2065,11 +2066,16 @@ export async function verifyRelease(options) {
|
|
|
2065
2066
|
const unit = (plan.units ?? []).find((u) => u.id === unitId);
|
|
2066
2067
|
const dist = unit?.distributions?.find((d) => d.type === platform.distributionType);
|
|
2067
2068
|
let binding = null;
|
|
2069
|
+
let frozenPluginRoot = null;
|
|
2068
2070
|
const contract = dist?.installationContract;
|
|
2069
2071
|
if (contract?.normalizedManifest) {
|
|
2072
|
+
frozenPluginRoot = pluginRootFromManifestRelativePath(contract.manifestRelativePath);
|
|
2070
2073
|
binding = await deriveSurfaceHostBinding({
|
|
2071
2074
|
manifest: contract.normalizedManifest,
|
|
2072
|
-
|
|
2075
|
+
// Adapter observations are already rooted at the installed
|
|
2076
|
+
// plugin. The frozen manifest's skills path is therefore
|
|
2077
|
+
// relative to '.', not to the snapshot-root manifest path.
|
|
2078
|
+
pluginRoot: '.',
|
|
2073
2079
|
platform,
|
|
2074
2080
|
snapshotDir: check.observation.installPath,
|
|
2075
2081
|
});
|
|
@@ -2082,6 +2088,7 @@ export async function verifyRelease(options) {
|
|
|
2082
2088
|
unitId,
|
|
2083
2089
|
surfaceId: binding?.surfaceId ?? '.',
|
|
2084
2090
|
binding,
|
|
2091
|
+
frozenPluginRoot,
|
|
2085
2092
|
unit,
|
|
2086
2093
|
});
|
|
2087
2094
|
}
|
|
@@ -2192,7 +2199,7 @@ export async function verifyRelease(options) {
|
|
|
2192
2199
|
// publish's recheck uses.
|
|
2193
2200
|
const pluginDistCount = (surface.unit?.distributions ?? [])
|
|
2194
2201
|
.filter((d) => d.type !== 'npm').length;
|
|
2195
|
-
if (pluginDistCount === 1) {
|
|
2202
|
+
if (pluginDistCount === 1 && surface.frozenPluginRoot === '.') {
|
|
2196
2203
|
const observedReceipt = createSkillResourceClosureReceipt(closureResult, {
|
|
2197
2204
|
unitId: surface.unitId,
|
|
2198
2205
|
preparedAt: expectedUnitReceipt.preparedAt ?? null,
|
|
@@ -742,9 +742,27 @@ export async function checkSkillResourceClosure({
|
|
|
742
742
|
* @param {Array<{ id: string, host: string, skillCount: number }>} surfaces
|
|
743
743
|
* @returns {{ passed: boolean, missing: Array<{ host: string, skillCount: number }> }}
|
|
744
744
|
*/
|
|
745
|
-
export function evaluateDeclaredHostSurfaceCoverage(expectedHosts, surfaces) {
|
|
745
|
+
export function evaluateDeclaredHostSurfaceCoverage(expectedHosts, surfaces, coverageClaims = []) {
|
|
746
746
|
const missing = [];
|
|
747
|
+
const observedById = new Map((surfaces ?? []).map((surface) => [surface.id, surface]));
|
|
748
|
+
const claimsByHost = new Map();
|
|
749
|
+
for (const claim of coverageClaims ?? []) {
|
|
750
|
+
const hostClaims = claimsByHost.get(claim.host) ?? [];
|
|
751
|
+
hostClaims.push(claim);
|
|
752
|
+
claimsByHost.set(claim.host, hostClaims);
|
|
753
|
+
}
|
|
747
754
|
for (const expectedHost of [...new Set(expectedHosts ?? [])].sort((a, b) => a.localeCompare(b))) {
|
|
755
|
+
const declaredClaims = claimsByHost.get(expectedHost) ?? [];
|
|
756
|
+
if (declaredClaims.length > 0) {
|
|
757
|
+
const missingClaim = declaredClaims.find((claim) => {
|
|
758
|
+
const surface = observedById.get(claim.surfaceId);
|
|
759
|
+
return !surface || !(surface.skillCount >= 1);
|
|
760
|
+
});
|
|
761
|
+
if (!missingClaim) continue;
|
|
762
|
+
const surface = observedById.get(missingClaim.surfaceId);
|
|
763
|
+
missing.push({ host: expectedHost, skillCount: surface?.skillCount ?? 0 });
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
748
766
|
const surface = (surfaces ?? []).find((item) => item.host === expectedHost);
|
|
749
767
|
if (!surface || !(surface.skillCount >= 1)) {
|
|
750
768
|
missing.push({ host: expectedHost, skillCount: surface?.skillCount ?? 0 });
|
|
@@ -186,3 +186,38 @@ export async function deriveSurfaceHostBinding({ manifest, pluginRoot, platform,
|
|
|
186
186
|
const host = await normalizeHostId(platform.buildAdapter.name);
|
|
187
187
|
return { surfaceId, host };
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Group manifest-backed claims before invoking the closure checker.
|
|
192
|
+
*
|
|
193
|
+
* A shared canonical source surface is scanned once. Claims remain complete
|
|
194
|
+
* for host coverage, while checker overrides are passed only for an
|
|
195
|
+
* unshared surface; this keeps the checker fail-closed when called directly
|
|
196
|
+
* with conflicting host claims.
|
|
197
|
+
*
|
|
198
|
+
* @param {Array<{surfaceId: string, host: string}>} claims
|
|
199
|
+
* @returns {{ coverageClaims: Array<{surfaceId: string, host: string}>, checkerBindings: Array<{surfaceId: string, host: string}> }}
|
|
200
|
+
*/
|
|
201
|
+
export function groupSurfaceHostBindings(claims = []) {
|
|
202
|
+
if (!Array.isArray(claims)) {
|
|
203
|
+
throw new TypeError('surfaceHostBindings must be an array');
|
|
204
|
+
}
|
|
205
|
+
const bySurface = new Map();
|
|
206
|
+
for (const claim of claims) {
|
|
207
|
+
if (!claim || typeof claim !== 'object'
|
|
208
|
+
|| typeof claim.surfaceId !== 'string' || typeof claim.host !== 'string') {
|
|
209
|
+
throw new TypeError('surfaceHostBindings entries must be objects with string surfaceId and host');
|
|
210
|
+
}
|
|
211
|
+
const existing = bySurface.get(claim.surfaceId) ?? [];
|
|
212
|
+
existing.push({ surfaceId: claim.surfaceId, host: claim.host });
|
|
213
|
+
bySurface.set(claim.surfaceId, existing);
|
|
214
|
+
}
|
|
215
|
+
const coverageClaims = [...bySurface.values()]
|
|
216
|
+
.flat()
|
|
217
|
+
.sort((left, right) => left.surfaceId.localeCompare(right.surfaceId) || left.host.localeCompare(right.host));
|
|
218
|
+
const checkerBindings = [...bySurface.values()]
|
|
219
|
+
.filter((surfaceClaims) => surfaceClaims.length === 1)
|
|
220
|
+
.map(([claim]) => claim)
|
|
221
|
+
.sort((left, right) => left.surfaceId.localeCompare(right.surfaceId) || left.host.localeCompare(right.host));
|
|
222
|
+
return { coverageClaims, checkerBindings };
|
|
223
|
+
}
|