@agile-team/wl-skills-ui 1.15.0 → 1.17.0

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/CHANGELOG.md CHANGED
@@ -4,8 +4,24 @@ All notable changes to **@agile-team/wl-skills-ui** will be documented in this f
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.17.0] - 2026-10-09
8
+
9
+ - 统一可见 notice(包版本/判定/技能或基础约束/规则/目标/runId/待检查项)、开始与收尾提示、规范与执行器版本漂移诊断;保持单包使用与宿主证据未验证口径。
10
+ - 按本包规则与实际 Profile 映射 Skill,不重复前端注释规则;保留旧入口哈希证明迁移与外来内容保护。
11
+
7
12
  ## [Unreleased]
8
13
 
14
+ ## [1.16.0] - 2026-10-08
15
+
16
+ ### Added
17
+
18
+ - 新增公开集成协议 describe/request 统一信封:能力清单与路由语料经真实打包产物验证,供外部宿主与适配器按公开契约接入。
19
+
20
+ ### Fixed
21
+
22
+ - 输入校验补齐:context 为 null、targets 为空等边界输入返回可解释拒绝,不再产生歧义结果。
23
+ - MCP 目录对齐真实 `tools/list` 的 18 项,修复目录声明与实际暴露不一致。
24
+
9
25
  ## [1.15.0] - 2026-10-07
10
26
 
11
27
  ### Added
package/README.md CHANGED
@@ -1,12 +1,20 @@
1
1
  # @agile-team/wl-skills-ui
2
2
 
3
- **企业级 UI 一致性与 AI 精确治理工具包 v1.15.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
3
+ **企业级 UI 一致性与 AI 精确治理工具包 v1.17.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
4
4
 
5
5
  它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@agile-team/wl-skills-ui.svg)](https://www.npmjs.com/package/@agile-team/wl-skills-ui)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D22-green.svg)]()
9
9
 
10
+ ## 如何确认本包正在起作用
11
+
12
+ 每次适用任务先运行项目本地 `wl-ui task "修改目标文件" --target src/Foo.vue --json`。编辑前展示真实 `notice`:包名与版本、判定、选中 Skill 或基础约束、具体规则、目标、runId 和尚未执行的检查。普通修改也需要基础约束提醒;相关但未覆盖的意图显示 gap 与建议;没有目标依据显示 needs-context,职责外显示 not-applicable,不强行匹配。
13
+
14
+ 执行实际检查时复用 `--run-id`,结束读取 `wl-ui status --run-id <id> --json`,分别报告执行和验证、实际检查文件、过期证据与未执行项。`notice.displayEvidence=unverified` 表示工具回执不能证明聊天界面展示;安装、路由或模型自报不能证明宿主加载/模型读取。
15
+
16
+ `wl-ui doctor-host --json` 对比已分发规范、本地执行器和正在运行的版本,漂移会显式报告。规范更新不会替代依赖升级:同步本包依赖、锁文件和受管入口;未使用的兄弟包无需安装。重开/刷新宿主加载后仍需观察真实任务调用,不能宣称所有 AI 自动触发。
17
+
10
18
  ## 你能获得什么
11
19
 
12
20
  | 目标 | 包提供的能力 | 带来的效果 |
@@ -94,6 +102,23 @@ CLI、安装器和 MCP 无需安装其他 wl-skills 包或统一客户端即可
94
102
 
95
103
  `init` 新插入的 HTML tokens link 使用独立边界标记并登记贡献,`clean` 只移除未修改的本包链接,保留项目其他 HTML。历史未标记的链接缺少归属证据,升级时仍作为项目配置保留;SCSS 与 runtime 接入只给出建议,不自动修改或认领项目已有 import。
96
104
 
105
+ ## 公开集成协议 v1
106
+
107
+ 供 harness、客户端或其他编排层以统一 JSON 入口调用本包判定与状态能力(业务执行仍走本包 CLI/MCP,不因协议新增兄弟包依赖)。
108
+
109
+ ```bash
110
+ wl-ui protocol describe --json # 能力目录、五操作映射、请求/响应 Schema、错误码
111
+ wl-ui protocol request --input-file request.json --json
112
+ ```
113
+
114
+ - 操作:`route` / `explain`(只读判定)、`task`(判定并持久化计划,尚未执行)、`status`(回查本包执行回执)、`doctor-host`(宿主入口静态诊断)。
115
+ - 请求字段:`protocolVersion`(1)、`operation`、`requestId`、`runId`、`projectRoot`、`task`、`targets`、`skill`、`host`+ `profile`。
116
+ - 统一信封:`{ ok, result | error, diagnostics }`;机器结果与诊断分离;非法输入在触达执行器前判 `invalid-input`,不写入任何记录。
117
+ - 错误码:`unsupported-protocol` / `unknown-operation` / `missing-input` / `invalid-input` / `internal-error`;请求失败(exit 2)时 stdout 仍为可解析 JSON 信封。
118
+ - **`ok=true` 只表示协议调用成功,不代表业务验证通过**;业务状态以 `result` 内 `validationStatus` / `executionStatus` 为准(仅计划时为 `unverified` / `not-executed`)。
119
+ - 副作用:仅 `task` 写入本包自己的 runs 目录(见 describe 中各操作 `sideEffects`);其余操作只读。
120
+ - 协议实现为本包内置的同源快照 `integration-protocol.cjs`,五包一致;跨包一致性由 conformance `integration-protocol.check.cjs` 验收。
121
+
97
122
  ## 能力地图
98
123
 
99
124
  | 层级 | 能力 | 典型内容 | 是否需要业务源码 |
@@ -483,7 +508,7 @@ npx wl-ui doctor --project . --print-overrides
483
508
 
484
509
  ## 版本与进一步阅读
485
510
 
486
- 当前 v1.15.0:任务判定、真实执行/验证回执、实际检查范围和过期检测;Codex 原生薄入口按需加载 canonical Skill。确定性路由覆盖已发布目录与显式缺口,不保证识别所有自然语言。v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
511
+ 当前 v1.17.0:公开集成协议 describe/request 统一信封(能力清单与路由语料经真实打包产物验证)、输入校验收口与 MCP 目录对齐真实 tools/list。v1.15.0:任务判定、真实执行/验证回执、实际检查范围和过期检测;Codex 原生薄入口按需加载 canonical Skill。确定性路由覆盖已发布目录与显式缺口,不保证识别所有自然语言。v1.14.0:明确独立能力边界,安装、更新、清理按本包贡献保护用户及其他包内容;共享文件采用单文件原子替换,支持历史路由与 `.clinerules` 目录迁移。v1.13.2:R044 审查多列表单超宽标签,R040 识别自定义 `stepper`,均供人工复核。v1.13.0:新增 `native-jh-ag` Profile、显式 Profile 兼容承诺与联邦 AG 依赖识别。
487
512
 
488
513
  - [版本摘要](docs/version-history.md)
489
514
  - [完整 Changelog](CHANGELOG.md)
@@ -0,0 +1,122 @@
1
+ /**
2
+ * protocol-cli.test.mjs — ui 公开集成协议统一信封与接线测试
3
+ */
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+ import { spawnSync } from "node:child_process";
10
+ import { fileURLToPath } from "node:url";
11
+ import { createRequire } from "node:module";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
16
+ const pkg = require("../../package.json");
17
+ const BIN = path.join(here, "..", "wl-ui.js");
18
+
19
+ function tempRoot() {
20
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-protocol-"));
21
+ }
22
+
23
+ test("describe 返回协议版本、能力目录与五个统一操作", () => {
24
+ const described = protocol.describe();
25
+ assert.equal(described.protocolVersion, 1);
26
+ assert.equal(described.package, pkg.name);
27
+ assert.ok(Array.isArray(described.capabilities) && described.capabilities.length > 0);
28
+ assert.deepEqual(described.operations.map((operation) => operation.id), ["route", "explain", "task", "status", "doctor-host"]);
29
+ assert.ok(described.errorCodes.includes("unsupported-protocol"));
30
+ });
31
+
32
+ test("route 只读判定返回六类状态之一", () => {
33
+ const envelope = protocol.request({ operation: "route", projectRoot: tempRoot(), task: "统一这个页面的表格视觉" }, runOperation);
34
+ assert.equal(envelope.ok, true);
35
+ const status = envelope.result.decision ? envelope.result.decision.status : envelope.result.status;
36
+ assert.ok(["matched", "baseline", "ambiguous", "gap", "not-applicable", "needs-context"].includes(status));
37
+ });
38
+
39
+ test("task 持久化返回 runId 且 status 可回查", () => {
40
+ const root = tempRoot();
41
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "扫描 src/views 视觉问题" }, runOperation);
42
+ assert.equal(planned.ok, true);
43
+ assert.ok(planned.result.runId);
44
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: planned.result.runId }, runOperation);
45
+ assert.equal(status.ok, true);
46
+ assert.equal(status.result.runId, planned.result.runId);
47
+ assert.equal(status.result.executionStatus, "not-executed");
48
+ });
49
+
50
+ test("doctor-host 按指定 host 诊断", () => {
51
+ const envelope = protocol.request({ operation: "doctor-host", projectRoot: tempRoot(), host: "claude" }, runOperation);
52
+ assert.equal(envelope.ok, true);
53
+ assert.equal(envelope.result.host, "claude");
54
+ });
55
+
56
+ test("协议错误码:missing-input / unknown-operation / invalid-input / unsupported-protocol", () => {
57
+ assert.equal(protocol.request({ operation: "route" }, runOperation).error.code, "missing-input");
58
+ assert.equal(protocol.request({ operation: "skin" }, runOperation).error.code, "unknown-operation");
59
+ assert.equal(protocol.request(42, runOperation).error.code, "invalid-input");
60
+ assert.equal(protocol.request({ protocolVersion: 3, operation: "route", task: "x" }, runOperation).error.code, "unsupported-protocol");
61
+ });
62
+
63
+ test("边界输入校验:非法类型在触达执行器前判 invalid-input(独立复验缺陷回归)", () => {
64
+ const invalidPayloads = [
65
+ { operation: "task", task: true },
66
+ { operation: "task", task: { text: "bad" } },
67
+ { operation: "task", task: "检查目标", targets: [null, 42, {}] },
68
+ { operation: "route", task: "检查目标", projectRoot: 42 },
69
+ { operation: "task", task: "检查目标", runId: {} },
70
+ ];
71
+ for (const payload of invalidPayloads) {
72
+ const envelope = protocol.request(payload, () => { throw new Error("不应触达执行器"); });
73
+ assert.equal(envelope.ok, false);
74
+ assert.equal(envelope.error.code, "invalid-input");
75
+ assert.ok(envelope.error.field);
76
+ }
77
+ });
78
+
79
+ test("CLI 缺 --input-file 时 stdout 输出 missing-input JSON 信封", () => {
80
+ const run = spawnSync(process.execPath, [BIN, "protocol", "request"], { encoding: "utf8" });
81
+ assert.equal(run.status, 2);
82
+ const envelope = JSON.parse(run.stdout);
83
+ assert.equal(envelope.ok, false);
84
+ assert.equal(envelope.error.code, "missing-input");
85
+ assert.equal(envelope.error.field, "input-file");
86
+ });
87
+
88
+ test("CLI protocol describe / request 全链路", () => {
89
+ const describe = spawnSync(process.execPath, [BIN, "protocol", "describe"], { encoding: "utf8" });
90
+ assert.equal(describe.status, 0);
91
+ assert.equal(JSON.parse(describe.stdout).package, "@agile-team/wl-skills-ui");
92
+ const root = tempRoot();
93
+ const file = path.join(root, "req.json");
94
+ const { writeFileSync } = require("node:fs");
95
+ writeFileSync(file, JSON.stringify({ operation: "doctor-host", projectRoot: root, host: "copilot" }));
96
+ const request = spawnSync(process.execPath, [BIN, "protocol", "request", "--input-file", file], { encoding: "utf8" });
97
+ assert.equal(request.status, 0);
98
+ assert.equal(JSON.parse(request.stdout).result.host, "copilot");
99
+ });
100
+
101
+
102
+ test("context null 判 invalid-input 且零写入(CLI 回归)", () => {
103
+ const root = tempRoot();
104
+ const file = path.join(root, "req.json");
105
+ const { writeFileSync, readdirSync } = require("node:fs");
106
+ writeFileSync(file, JSON.stringify({ operation: "task", projectRoot: root, task: "扫描 src/views 视觉问题", runId: "ctx-null", context: null }));
107
+ const run = spawnSync(process.execPath, [BIN, "protocol", "request", "--input-file", file], { encoding: "utf8" });
108
+ assert.equal(run.status, 2);
109
+ const envelope = JSON.parse(run.stdout);
110
+ assert.equal(envelope.ok, false);
111
+ assert.equal(envelope.error.code, "invalid-input");
112
+ assert.equal(envelope.error.field, "context");
113
+ assert.equal(readdirSync(root).filter((name) => name.startsWith(".")).length, 0, "不得写入任何任务记录");
114
+ });
115
+
116
+ test("targets 空数组与 Schema 一致(运行时接受空范围)", () => {
117
+ const root = tempRoot();
118
+ const envelope = protocol.request({ operation: "route", projectRoot: root, task: "扫描 src/views 视觉问题", targets: [] }, runOperation);
119
+ assert.equal(envelope.ok, true);
120
+ const described = protocol.describe();
121
+ assert.equal(described.schemas.request.properties.targets.minItems, undefined, "Schema 不得要求非空 targets");
122
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * protocol-evidence.test.mjs — ui 证据闭环严格断言(不得兜底通过)
3
+ */
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { mkdtempSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import path from "node:path";
9
+
10
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
11
+
12
+ function tempRoot() {
13
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-evidence-"));
14
+ }
15
+
16
+ test("status 严格回查同一 runId:字段精确、不兜底", () => {
17
+ const root = tempRoot();
18
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "统一表格视觉", runId: "evidence-run-a" }, runOperation);
19
+ assert.equal(planned.ok, true);
20
+ assert.equal(planned.result.runId, "evidence-run-a");
21
+ assert.equal(planned.result.executionStatus, "not-executed");
22
+ assert.equal(planned.result.validationStatus, "unverified");
23
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: "evidence-run-a" }, runOperation);
24
+ assert.equal(status.result.runId, "evidence-run-a");
25
+ assert.equal(status.result.executionStatus, "not-executed");
26
+ });
27
+
28
+ test("混用 runId 不得串记录", () => {
29
+ const root = tempRoot();
30
+ protocol.request({ operation: "task", projectRoot: root, task: "任务A统一表格视觉", runId: "evidence-run-a" }, runOperation);
31
+ protocol.request({ operation: "task", projectRoot: root, task: "任务B统一表格视觉", runId: "evidence-run-b" }, runOperation);
32
+ const statusB = protocol.request({ operation: "status", projectRoot: root, runId: "evidence-run-b" }, runOperation);
33
+ assert.equal(statusB.result.runId, "evidence-run-b");
34
+ assert.equal(JSON.stringify(statusB.result).includes("任务A"), false);
35
+ });
36
+
37
+ test("不存在的 runId 不得伪造成功记录", () => {
38
+ const root = tempRoot();
39
+ const status = protocol.request({ operation: "status", projectRoot: root, runId: "no-such-run" }, runOperation);
40
+ const serialized = JSON.stringify(status.result);
41
+ assert.equal(serialized.includes('"validationStatus":"passed"'), false);
42
+ assert.equal(serialized.includes('"executionStatus":"completed"'), false);
43
+ });
44
+
45
+ test("空检查集不得判定为通过", () => {
46
+ const root = tempRoot();
47
+ const planned = protocol.request({ operation: "task", projectRoot: root, task: "统一表格视觉", runId: "empty-run" }, runOperation);
48
+ assert.notEqual(planned.result.validationStatus, "passed");
49
+ assert.equal(planned.result.executionStatus, "not-executed");
50
+ });
@@ -0,0 +1,105 @@
1
+ [
2
+ {
3
+ "task": "统一表格视觉",
4
+ "skill": "element/el-table"
5
+ },
6
+ {
7
+ "task": "查询表单密集审计",
8
+ "skill": "element/el-form"
9
+ },
10
+ {
11
+ "task": "弹窗样式对齐",
12
+ "skill": "element/el-dialog"
13
+ },
14
+ {
15
+ "task": "状态标签颜色统一",
16
+ "skill": "element/el-tag"
17
+ },
18
+ {
19
+ "task": "列表布局骨架",
20
+ "skill": "layouts/list-page"
21
+ },
22
+ {
23
+ "task": "AG Grid 适配",
24
+ "skill": "vendors/ag-grid"
25
+ },
26
+ {
27
+ "task": "UI扫描",
28
+ "skill": "ops/scan"
29
+ },
30
+ {
31
+ "task": "UI修复统一视觉",
32
+ "skill": "ops/fix"
33
+ },
34
+ {
35
+ "task": "UI迁移迁移tokens",
36
+ "skill": "ops/migrate"
37
+ },
38
+ {
39
+ "task": "设计令牌接入消除颜色硬编码",
40
+ "skill": "runtime/design-tokens"
41
+ },
42
+ {
43
+ "task": "写诗",
44
+ "skill": null,
45
+ "status": "not-applicable"
46
+ },
47
+ {
48
+ "task": "生成测试用例",
49
+ "skill": null,
50
+ "status": "needs-context"
51
+ },
52
+ {
53
+ "task": "检查",
54
+ "skill": null,
55
+ "status": "needs-context"
56
+ },
57
+ {
58
+ "task": "整理页签和卡片组件",
59
+ "skill": "element/component-family"
60
+ },
61
+ {
62
+ "task": "详情布局整改",
63
+ "skill": "layouts/detail-page"
64
+ },
65
+ {
66
+ "task": "调整表单弹窗布局",
67
+ "skill": "layouts/form-dialog"
68
+ },
69
+ {
70
+ "task": "树列表布局",
71
+ "skill": "layouts/tree-list"
72
+ },
73
+ {
74
+ "task": "做一次UI审计",
75
+ "skill": "ops/audit"
76
+ },
77
+ {
78
+ "task": "runtime迁移",
79
+ "skill": "runtime/migration"
80
+ },
81
+ {
82
+ "task": "样式统一风格对齐",
83
+ "skill": "runtime/style-align"
84
+ },
85
+ {
86
+ "task": "BaseTable 适配",
87
+ "skill": "vendors/base-table"
88
+ },
89
+ {
90
+ "task": "C组件用法审计",
91
+ "skill": "vendors/c-components"
92
+ },
93
+ {
94
+ "task": "自定义封装组件治理",
95
+ "skill": "vendors/custom-wrappers"
96
+ },
97
+ {
98
+ "task": "jh组件接入",
99
+ "skill": "vendors/jh-components"
100
+ },
101
+ {
102
+ "task": "未知封装组件排查",
103
+ "skill": "vendors/unknown-wrapper"
104
+ }
105
+ ]
@@ -0,0 +1,63 @@
1
+ /**
2
+ * protocol-routing.test.mjs — ui 路由准确性逐技能语料(预期经人工复核冻结)
3
+ * ui 是依赖型包:安装态通过真实打包产物 npm 安装到隔离项目验证。
4
+ */
5
+ import { test, before } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import path from "node:path";
10
+ import { spawnSync } from "node:child_process";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const here = path.dirname(fileURLToPath(import.meta.url));
14
+ const { protocol, runOperation } = await import("../protocol-cli.mjs");
15
+ const cases = JSON.parse((await import("node:fs")).readFileSync(path.join(here, "protocol-routing-cases.json"), "utf8"));
16
+ const PKG_ROOT = path.join(here, "..", "..");
17
+
18
+ const state = { installedRoot: null, tarball: null };
19
+
20
+ function tempRoot() {
21
+ return mkdtempSync(path.join(tmpdir(), "wl-ui-routing-"));
22
+ }
23
+
24
+ function routeAt(projectRoot, task) {
25
+ return protocol.request({ operation: "route", projectRoot, task }, runOperation);
26
+ }
27
+
28
+ before(() => {
29
+ const packDir = tempRoot();
30
+ const pack = spawnSync("npm", ["pack", "--pack-destination", packDir, "--ignore-scripts"], { cwd: PKG_ROOT, encoding: "utf8", timeout: 180000 });
31
+ assert.equal(pack.status, 0, pack.stderr);
32
+ state.tarball = path.join(packDir, pack.stdout.trim().split("\n").pop());
33
+ state.installedRoot = tempRoot();
34
+ assert.equal(spawnSync("npm", ["init", "-y"], { cwd: state.installedRoot, encoding: "utf8" }).status, 0);
35
+ const install = spawnSync("npm", ["i", state.tarball], { cwd: state.installedRoot, encoding: "utf8", timeout: 180000 });
36
+ assert.equal(install.status, 0, install.stderr);
37
+ }, 600000);
38
+
39
+ for (const item of cases) {
40
+ const expectedBare = item.skill ? "gap" : item.status;
41
+ const expectedInstalled = item.installedStatus || (item.skill ? "matched" : item.status);
42
+
43
+ test(`未安装:「${item.task}」→ ${expectedBare}${item.skill ? ` + ${item.skill}` : ""}`, () => {
44
+ const envelope = routeAt(tempRoot(), item.task);
45
+ assert.equal(envelope.ok, true);
46
+ const decision = envelope.result.decision || envelope.result;
47
+ assert.equal(decision.status, expectedBare);
48
+ if (item.skill) assert.ok(decision.selectedSkills.includes(item.skill), `应选中 ${item.skill},实际 ${decision.selectedSkills}`);
49
+ else assert.equal((decision.selectedSkills || []).length, 0);
50
+ });
51
+
52
+ test(`已安装(真实打包产物):「${item.task}」→ ${expectedInstalled}${item.skill ? ` + ${item.skill}` : ""}`, () => {
53
+ const envelope = routeAt(state.installedRoot, item.task);
54
+ assert.equal(envelope.ok, true);
55
+ const decision = envelope.result.decision || envelope.result;
56
+ assert.equal(decision.status, expectedInstalled);
57
+ if (item.skill) assert.ok(decision.selectedSkills.includes(item.skill));
58
+ });
59
+ }
60
+
61
+ test.after(() => {
62
+ if (state.tarball) rmSync(path.dirname(state.tarball), { recursive: true, force: true });
63
+ });
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * integration-protocol.cjs — 五包统一公开集成协议 v1(单源快照)
5
+ *
6
+ * 由 kit-internal/conformance/scripts/sync-support.cjs 分发到各包,各包不得修改本文件;
7
+ * 包特定能力目录、操作映射与执行器在各包的 protocol 接线文件中提供。
8
+ *
9
+ * 协议约定:
10
+ * - describe:返回能力目录、协议版本、操作清单、输入输出约束与错误码;
11
+ * request/envelope Schema 按各包操作定义动态生成,与运行时校验一致。
12
+ * - request:所有字段在执行前完成类型校验;非法输入返回 invalid-input,不触发执行器、
13
+ * 不写入任何记录。非法的 operation/requestId 不回显到信封(保持 string|null)。
14
+ * - 信封结构互斥:ok=true 必有 result 且无 error;ok=false 必有 error 且无 result。
15
+ * - 本协议只统一判定、解释、任务记录、状态查询与宿主诊断;业务执行仍走各包原有 CLI/MCP。
16
+ */
17
+
18
+ const PROTOCOL_VERSION = 1;
19
+ const SUPPORTED_VERSIONS = [1];
20
+ const ERROR_CODES = ["unsupported-protocol", "unknown-operation", "missing-input", "invalid-input", "internal-error"];
21
+ const OPTIONAL_STRING_FIELDS = ["requestId", "runId", "projectRoot", "skill", "host", "type", "domain", "profile"];
22
+
23
+ const STRING_FIELD_SCHEMA = { type: "string", minLength: 1, pattern: "\\S" };
24
+ const ERROR_SCHEMA = {
25
+ type: "object",
26
+ required: ["code", "message"],
27
+ properties: {
28
+ code: { type: "string", enum: ERROR_CODES },
29
+ message: { type: "string", minLength: 1 },
30
+ field: { oneOf: [STRING_FIELD_SCHEMA, { type: "array", items: STRING_FIELD_SCHEMA, minItems: 1 }] },
31
+ supportedProtocolVersions: { type: "array", items: { type: "integer" }, minItems: 1 },
32
+ availableOperations: { type: "array", items: STRING_FIELD_SCHEMA, minItems: 1 },
33
+ },
34
+ additionalProperties: true,
35
+ };
36
+ const CONTEXT_FIELD_SCHEMA = {
37
+ type: "object",
38
+ properties: {
39
+ signals: { type: "array", items: STRING_FIELD_SCHEMA },
40
+ domainRelevant: { type: "boolean" },
41
+ },
42
+ additionalProperties: true,
43
+ };
44
+
45
+ function envelopeSchema(packageName) {
46
+ const stringOrNull = { type: ["string", "null"] };
47
+ return {
48
+ $schema: "https://json-schema.org/draft/2020-12/schema",
49
+ title: `wl-skills protocol envelope v1 (${packageName})`,
50
+ type: "object",
51
+ required: ["protocolVersion", "package", "packageVersion", "operation", "requestId", "ok", "diagnostics"],
52
+ properties: {
53
+ protocolVersion: { type: "integer", const: 1 },
54
+ package: { type: "string" },
55
+ packageVersion: stringOrNull,
56
+ operation: stringOrNull,
57
+ requestId: stringOrNull,
58
+ ok: { type: "boolean" },
59
+ result: { type: "object", description: "各包原执行器结果;业务状态以其中 executionStatus/validationStatus 等字段为准" },
60
+ error: ERROR_SCHEMA,
61
+ diagnostics: { type: "array", items: { type: "string" } },
62
+ },
63
+ oneOf: [
64
+ { properties: { ok: { const: true }, result: { type: "object" } }, required: ["result"], not: { required: ["error"] } },
65
+ { properties: { ok: { const: false }, error: ERROR_SCHEMA }, required: ["error"], not: { required: ["result"] } },
66
+ ],
67
+ };
68
+ }
69
+
70
+ function operationCondition(operation) {
71
+ const clauses = [];
72
+ if (operation.required.length > 0) clauses.push({ required: [...operation.required] });
73
+ if (operation.requireAny) clauses.push(...operation.requireAny.map((field) => ({ required: [field] })));
74
+ if (clauses.length === 0) return null;
75
+ return {
76
+ if: { properties: { operation: { const: operation.id } }, required: ["operation"] },
77
+ then: clauses.length === 1 ? clauses[0] : { anyOf: clauses },
78
+ };
79
+ }
80
+
81
+ function requestSchema(config) {
82
+ const {operations} = config;
83
+ const conditional = operations.map(operationCondition).filter(Boolean);
84
+ return {
85
+ $schema: "https://json-schema.org/draft/2020-12/schema",
86
+ title: `wl-skills protocol request v1 (${config.packageName})`,
87
+ type: "object",
88
+ required: ["operation"],
89
+ properties: {
90
+ protocolVersion: { type: "integer", const: 1 },
91
+ operation: { type: "string", enum: operations.map((operation) => operation.id) },
92
+ requestId: STRING_FIELD_SCHEMA,
93
+ runId: STRING_FIELD_SCHEMA,
94
+ projectRoot: STRING_FIELD_SCHEMA,
95
+ task: STRING_FIELD_SCHEMA,
96
+ targets: { type: "array", items: STRING_FIELD_SCHEMA, description: "空数组表示空范围(与运行时既有语义一致)" },
97
+ skill: STRING_FIELD_SCHEMA,
98
+ host: STRING_FIELD_SCHEMA,
99
+ type: STRING_FIELD_SCHEMA,
100
+ domain: STRING_FIELD_SCHEMA,
101
+ profile: STRING_FIELD_SCHEMA,
102
+ context: CONTEXT_FIELD_SCHEMA,
103
+ },
104
+ ...(conditional.length > 0 ? { allOf: conditional } : {}),
105
+ };
106
+ }
107
+
108
+ function isEmptyValue(value) {
109
+ return value === undefined || value === null || (typeof value === "string" && value.trim() === "");
110
+ }
111
+
112
+ function isNonEmptyString(value) {
113
+ return typeof value === "string" && value.trim() !== "";
114
+ }
115
+
116
+ function assertValidOperation(operation) {
117
+ if (!operation.id || typeof operation.summary !== "string") throw new Error(`操作定义不完整:${JSON.stringify(operation)}`);
118
+ if (typeof operation.readOnly !== "boolean") throw new Error(`操作 ${operation.id} 缺少 readOnly 声明`);
119
+ if (!Array.isArray(operation.required) || !Array.isArray(operation.optional)) throw new Error(`操作 ${operation.id} 缺少 required/optional 输入声明`);
120
+ if (operation.requireAny !== undefined && !Array.isArray(operation.requireAny)) throw new Error(`操作 ${operation.id} 的 requireAny 必须是字符串数组`);
121
+ if (typeof operation.mapping !== "string") throw new Error(`操作 ${operation.id} 缺少原 CLI 映射说明`);
122
+ }
123
+
124
+ function assertValidConfig(config) {
125
+ if (!config || typeof config !== "object") throw new Error("integration protocol 需要配置对象");
126
+ if (!config.packageName) throw new Error("integration protocol 配置缺少 packageName");
127
+ if (!Array.isArray(config.operations) || config.operations.length === 0) throw new Error("integration protocol 配置缺少 operations");
128
+ config.operations.forEach(assertValidOperation);
129
+ }
130
+
131
+ function createProtocol(config) {
132
+ assertValidConfig(config);
133
+
134
+ function base(operation, requestId) {
135
+ return {
136
+ protocolVersion: PROTOCOL_VERSION,
137
+ package: config.packageName,
138
+ packageVersion: config.packageVersion || null,
139
+ operation: isNonEmptyString(operation) ? operation : null,
140
+ requestId: isNonEmptyString(requestId) ? requestId : null,
141
+ };
142
+ }
143
+
144
+ function fail(operation, requestId, code, message, extra) {
145
+ return { ...base(operation, requestId), ok: false, error: { code, message, ...(extra || {}) }, diagnostics: [] };
146
+ }
147
+
148
+ function invalidField(input, field, expected) {
149
+ return fail(input.operation, input.requestId, "invalid-input", `字段 ${field} 非法:应为${expected}`, { field });
150
+ }
151
+
152
+ function validateContextField(input) {
153
+ const {context} = input;
154
+ if (context === null || typeof context !== "object" || Array.isArray(context)) return invalidField(input, "context", "非 null 对象(signals: string[] / domainRelevant: boolean)");
155
+ if (context.signals !== undefined && (!Array.isArray(context.signals) || context.signals.some((item) => !isNonEmptyString(item)))) {
156
+ return invalidField(input, "context.signals", "非空字符串数组");
157
+ }
158
+ if (context.domainRelevant !== undefined && typeof context.domainRelevant !== "boolean") return invalidField(input, "context.domainRelevant", "布尔值");
159
+ return null;
160
+ }
161
+
162
+ function validateStringFields(input) {
163
+ for (const field of OPTIONAL_STRING_FIELDS) {
164
+ if (input[field] !== undefined && !isNonEmptyString(input[field])) return invalidField(input, field, "非空字符串");
165
+ }
166
+ if (input.task !== undefined && !isNonEmptyString(input.task)) return invalidField(input, "task", "非空字符串");
167
+ return null;
168
+ }
169
+
170
+ function validateTargetsField(input) {
171
+ if (input.targets === undefined) return null;
172
+ if (!Array.isArray(input.targets)) return invalidField(input, "targets", "字符串数组");
173
+ if (input.targets.some((item) => !isNonEmptyString(item))) return invalidField(input, "targets", "非空字符串元素数组");
174
+ return null;
175
+ }
176
+
177
+ function validateFieldTypes(input) {
178
+ if (!isNonEmptyString(input.operation)) return invalidField(input, "operation", "非空字符串");
179
+ const stringError = validateStringFields(input);
180
+ if (stringError) return stringError;
181
+ const targetsError = validateTargetsField(input);
182
+ if (targetsError) return targetsError;
183
+ if (input.context !== undefined) {
184
+ const contextError = validateContextField(input);
185
+ if (contextError) return contextError;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ function unsupportedVersionError(input) {
191
+ if (input.protocolVersion === undefined || SUPPORTED_VERSIONS.includes(input.protocolVersion)) return null;
192
+ return fail(input.operation, input.requestId, "unsupported-protocol", `不支持的协议版本:${input.protocolVersion}`, { supportedProtocolVersions: [...SUPPORTED_VERSIONS] });
193
+ }
194
+
195
+ function findOperation(input) {
196
+ const operation = config.operations.find((item) => item.id === input.operation);
197
+ if (operation) return { operation };
198
+ return { error: fail(input.operation, input.requestId, "unknown-operation", `未知操作:${isNonEmptyString(input.operation) ? input.operation : "(缺失或非法)"}`, { availableOperations: config.operations.map((item) => item.id) }) };
199
+ }
200
+
201
+ function missingInputError(input, operation) {
202
+ const missingField = operation.required.find((field) => isEmptyValue(input[field]));
203
+ if (missingField !== undefined) {
204
+ return fail(operation.id, input.requestId, "missing-input", `操作 ${operation.id} 缺少必要输入:${missingField}`, { field: missingField });
205
+ }
206
+ if (operation.requireAny && !operation.requireAny.some((field) => !isEmptyValue(input[field]))) {
207
+ return fail(operation.id, input.requestId, "missing-input", `操作 ${operation.id} 需要以下输入至少之一:${operation.requireAny.join(" / ")}`, { field: [...operation.requireAny] });
208
+ }
209
+ return null;
210
+ }
211
+
212
+ function validateRequest(input) {
213
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
214
+ return { error: fail(null, null, "invalid-input", "request 必须是 JSON 对象") };
215
+ }
216
+ const versionError = unsupportedVersionError(input);
217
+ if (versionError) return { error: versionError };
218
+ const typeError = validateFieldTypes(input);
219
+ if (typeError) return { error: typeError };
220
+ const found = findOperation(input);
221
+ if (found.error) return found;
222
+ const { operation } = found;
223
+ const inputError = missingInputError(input, operation);
224
+ if (inputError) return { error: inputError };
225
+ return { operation };
226
+ }
227
+
228
+ function buildInventory(config) {
229
+ const inventory = config.inventory || {};
230
+ return {
231
+ skills: inventory.skills || [],
232
+ taskTypes: inventory.taskTypes || [],
233
+ commands: inventory.commands || [],
234
+ mcpTools: inventory.mcpTools || [],
235
+ loadErrors: inventory.loadErrors || [],
236
+ note: "业务执行仍走各包原 CLI/MCP;inventory 只声明公开调用映射与入口,不改变执行归属;loadErrors 非空表示目录派生存在异常,不得忽略",
237
+ };
238
+ }
239
+
240
+ function buildConstraints(config) {
241
+ return {
242
+ ...(config.constraints || {}),
243
+ instructionOnly: "未提供 programmatic 执行入口的能力均为指令指导型,由宿主/AI 按各包技能流程执行并提供证据",
244
+ evidence: "request 返回的判定与状态来自各包自身记录;宿主是否加载、读取、调用以宿主证据为准,本协议不代为宣称",
245
+ okSemantics: "ok=true 仅表示协议调用成功(判定/记录/查询完成);业务验证状态以 result 内的 validationStatus/executionStatus 等字段为准",
246
+ };
247
+ }
248
+
249
+ return {
250
+ PROTOCOL_VERSION,
251
+ SUPPORTED_VERSIONS,
252
+ ERROR_CODES,
253
+
254
+ describe() {
255
+ return {
256
+ ...base(null, null),
257
+ capabilities: config.capabilities || [],
258
+ operations: config.operations.map((operation) => ({ ...operation })),
259
+ inventory: buildInventory(config),
260
+ schemas: { request: requestSchema(config), envelope: envelopeSchema(config.packageName) },
261
+ constraints: buildConstraints(config),
262
+ errorCodes: [...ERROR_CODES],
263
+ };
264
+ },
265
+
266
+ request(input, runOperation) {
267
+ const { operation, error } = validateRequest(input);
268
+ if (error) return error;
269
+ const diagnostics = [];
270
+ try {
271
+ const result = runOperation(operation.id, input, diagnostics) ?? {};
272
+ return { ...base(operation.id, input.requestId), ok: true, result, diagnostics };
273
+ } catch (thrown) {
274
+ diagnostics.push(String((thrown && thrown.message) || thrown));
275
+ const envelope = fail(operation.id, input.requestId, "internal-error", "操作执行异常,详见 diagnostics");
276
+ envelope.diagnostics = diagnostics;
277
+ return envelope;
278
+ }
279
+ },
280
+ };
281
+ }
282
+
283
+ module.exports = { createProtocol, PROTOCOL_VERSION, SUPPORTED_VERSIONS, ERROR_CODES };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * protocol-cli.mjs — ui 的公开集成协议接线(薄层)
3
+ *
4
+ * 协议实现来自快照 bin/integration-protocol.cjs(单源 conformance/support,勿改);
5
+ * 本文件只提供 ui 的能力目录、操作映射,并复用 task-integration 的原执行器。
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { createRequire } from "node:module";
10
+ import { runTaskAction, taskCatalog } from "./task-integration.mjs";
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require("../package.json");
14
+ const capabilitiesDocument = require("./capabilities.json");
15
+ const { createProtocol } = require("./integration-protocol.cjs");
16
+
17
+ const BIN = "wl-ui";
18
+ const OPERATIONS = [
19
+ { id: "route", summary: "只读任务判定:视觉/样式域技能、约束、歧义与缺口", readOnly: true, required: ["task"], optional: ["targets", "profile", "skill", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} route --text "<task>"` },
20
+ { id: "explain", summary: "解释本次任务判定(只读,不记录)", readOnly: true, required: ["task"], optional: ["targets", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} explain --text "<task>"` },
21
+ { id: "task", summary: "判定并持久化任务计划(尚未执行扫描或修复)", readOnly: false, required: ["task"], optional: ["runId", "targets", "profile", "projectRoot"], sideEffects: "写入 .wl-skills-ui/runs/ 下本包任务记录", mapping: `${BIN} task --text "<task>" [--run-id <id>]` },
22
+ { id: "status", summary: "读取本包执行/校验记录与新鲜度", readOnly: true, required: [], optional: ["runId", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} status [--run-id <id>]` },
23
+ { id: "doctor-host", summary: "宿主入口静态诊断(不证明宿主已加载)", readOnly: true, required: [], optional: ["host", "projectRoot"], sideEffects: "无写入", mapping: `${BIN} doctor-host [--host <host>]` },
24
+ ];
25
+
26
+ function buildInventory() {
27
+ const commands = [
28
+ { name: "init", args: "[--project <path>] [--editor <id>] [--profile <id>] [--skills-only] [--dry-run]", summary: "写入编辑器规则与技能", execution: "programmatic", sideEffects: "写入目标项目编辑器规则目录" },
29
+ { name: "update", args: "[--project <path>] [--editor <id|all>] [--force] [--dry-run]", summary: "更新已安装 rules/MCP/提示", execution: "programmatic", sideEffects: "按 manifest 更新" },
30
+ { name: "diff", args: "[--project <path>]", summary: "对比安装与 manifest", execution: "programmatic", sideEffects: "无" },
31
+ { name: "clean", args: "[--project <path>] [--dry-run]", summary: "清理本包安装文件", execution: "programmatic", sideEffects: "删除本包登记文件" },
32
+ { name: "scan", args: "[--project <path>] [--profile <id>]", summary: "R 规则扫描", execution: "programmatic", sideEffects: "无(报告输出)" },
33
+ { name: "audit", args: "[--project <path>]", summary: "审计汇总", execution: "programmatic", sideEffects: "无" },
34
+ { name: "check", args: "[--project <path>]", summary: "扫描+回执记录", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
35
+ { name: "fix", args: "--run-id <id> --rule <id> [--confirm]", summary: "确定性修复(快照/回滚)", execution: "programmatic", sideEffects: "写源文件(确认制+快照回滚)" },
36
+ { name: "snapshot", args: "/ drift / exempt", summary: "漂移基线管理", execution: "programmatic", sideEffects: "写基线文件" },
37
+ { name: "contract", args: "<extract|validate|match>", summary: "ui-contract 提取/校验/匹配", execution: "programmatic", sideEffects: "extract 写契约文件" },
38
+ { name: "doctor", args: "[--project <path>]", summary: "项目接入诊断", execution: "programmatic", sideEffects: "无" },
39
+ { name: "task", args: "\"任务\" [--run-id <id>]", summary: "判定并持久化任务计划", execution: "programmatic", sideEffects: "写 .wl-skills-ui/runs/" },
40
+ { name: "route", args: "\"任务\"", summary: "只读判定", execution: "programmatic", sideEffects: "无" },
41
+ { name: "explain", args: "\"任务\"", summary: "只读判定解释", execution: "programmatic", sideEffects: "无" },
42
+ { name: "status", args: "[--run-id <id>]", summary: "读取本包回执", execution: "programmatic", sideEffects: "无" },
43
+ { name: "doctor-host", args: "[--host <name>]", summary: "宿主入口静态诊断", execution: "programmatic", sideEffects: "无" },
44
+ { name: "protocol", args: "describe | request --input-file <file>", summary: "本公开集成协议", execution: "programmatic", sideEffects: "见操作声明" },
45
+ ];
46
+ const skills = [];
47
+ const loadErrors = [];
48
+ try {
49
+ for (const skill of taskCatalog()) {
50
+ skills.push({ id: skill.id, description: skill.description, triggers: skill.triggers, status: skill.status, entry: skill.path, execution: "instructional" });
51
+ }
52
+ } catch (error) {
53
+ loadErrors.push(`skills 加载失败:${error.message}`);
54
+ }
55
+ // 与 mcp/server.js tools/list 对齐的 18 项(由 readiness 探针持续校验数量与名称一致)
56
+ const mcpTools = [
57
+ { name: "wl_ui_task", summary: "判定并持久化 UI 任务计划", write: "persist" },
58
+ { name: "wl_ui_route", summary: "只读任务判定", write: "readonly" },
59
+ { name: "wl_ui_explain", summary: "只读判定解释", write: "readonly" },
60
+ { name: "wl_ui_status", summary: "读取执行回执", write: "readonly" },
61
+ { name: "wl_ui_doctor_host", summary: "宿主入口静态诊断", write: "readonly" },
62
+ { name: "wl_ui_check", summary: "执行 UI 检查并记录回执", write: "guarded" },
63
+ { name: "wl_ui_scan", summary: "R 规则扫描", write: "readonly" },
64
+ { name: "wl_ui_fix_dry_run", summary: "修复预演(不写盘)", write: "readonly" },
65
+ { name: "wl_ui_skill_prompt", summary: "技能提示词", write: "readonly" },
66
+ { name: "wl_ui_route_intent", summary: "意图路由", write: "readonly" },
67
+ { name: "wl_ui_detect_skin", summary: "皮肤检测", write: "readonly" },
68
+ { name: "wl_ui_list_rules", summary: "规则清单", write: "readonly" },
69
+ { name: "wl_ui_describe_rule", summary: "规则详情", write: "readonly" },
70
+ { name: "wl_ui_drift", summary: "漂移对比", write: "readonly" },
71
+ { name: "wl_ui_recommend_flow", summary: "推荐流程", write: "readonly" },
72
+ { name: "wl_ui_contract_extract", summary: "契约提取", write: "guarded" },
73
+ { name: "wl_ui_contract_validate", summary: "契约校验", write: "readonly" },
74
+ { name: "wl_ui_contract_match", summary: "契约匹配", write: "readonly" },
75
+ ];
76
+ return { skills, commands, mcpTools, loadErrors };
77
+ }
78
+
79
+ export const protocol = createProtocol({
80
+ packageName: pkg.name,
81
+ packageVersion: pkg.version,
82
+ capabilities: capabilitiesDocument.capabilities || [],
83
+ constraints: { node: (pkg.engines && pkg.engines.node) || null, boundaryVersion: capabilitiesDocument.boundaryVersion || null },
84
+ operations: OPERATIONS,
85
+ inventory: buildInventory(),
86
+ });
87
+
88
+ export function runOperation(operation, input) {
89
+ return runTaskAction(operation, {
90
+ projectRoot: input.projectRoot || ".",
91
+ task: input.task,
92
+ targets: Array.isArray(input.targets) ? input.targets : [],
93
+ runId: input.runId,
94
+ skill: input.skill,
95
+ host: input.host,
96
+ profile: input.profile,
97
+ context: input.context,
98
+ });
99
+ }
100
+
101
+ function envelopeError(code, message, field) {
102
+ return { protocolVersion: 1, package: pkg.name, packageVersion: pkg.version, operation: null, requestId: null, ok: false, error: { code, message, ...(field ? { field } : {}) }, diagnostics: [] };
103
+ }
104
+
105
+ export function runCli(argv) {
106
+ const sub = argv[0];
107
+ if (sub === "describe") {
108
+ console.log(JSON.stringify(protocol.describe(), null, 2));
109
+ return 0;
110
+ }
111
+ if (sub === "request") {
112
+ const fileIndex = argv.indexOf("--input-file");
113
+ const file = fileIndex >= 0 ? argv[fileIndex + 1] : null;
114
+ if (!file) {
115
+ console.log(JSON.stringify(envelopeError("missing-input", "缺少必要输入:--input-file <request.json>", "input-file"), null, 2));
116
+ return 2;
117
+ }
118
+ let input;
119
+ try {
120
+ input = JSON.parse(readFileSync(path.resolve(file), "utf8"));
121
+ } catch (error) {
122
+ const message = error.code === "ENOENT" ? `请求文件不存在:${file}` : `请求文件无法解析:${error.message}`;
123
+ console.log(JSON.stringify(envelopeError("invalid-input", message, "input-file"), null, 2));
124
+ return 2;
125
+ }
126
+ const envelope = protocol.request(input, runOperation);
127
+ console.log(JSON.stringify(envelope, null, 2));
128
+ return envelope.ok ? 0 : 2;
129
+ }
130
+ console.error("用法:");
131
+ console.error(` ${BIN} protocol describe --json`);
132
+ console.error(` ${BIN} protocol request --input-file <request.json> --json`);
133
+ return 2;
134
+ }
@@ -9,7 +9,9 @@ description: Vue and Element Plus UI consistency, design tokens, styles, compone
9
9
  # wl-skills-ui task gateway
10
10
  For one user task reuse the same \`--run-id\` or \`WL_TASK_RUN_ID\` across installed applicable packages. Do not install unused sibling packages or create unrelated IDs.
11
11
 
12
- 1. Start each task with \`node node_modules/@agile-team/wl-skills-ui/bin/wl-ui.js task "<task>" --target <path>\`. Retain the run ID and briefly explain the decision, applicable rules and unverified items. A routing decision does not execute checks.
12
+ 多项目工作区先沿目标路径向上找到本包安装清单与项目 AGENTS,切到该项目根再调用工具;不要以聚合工作区根代替 projectRoot,也不要在聚合根安装。不同项目分别保存项目身份,同一用户任务复用 runId。
13
+
14
+ 1. Start each relevant task from its project root with \`node node_modules/@agile-team/wl-skills-ui/bin/wl-ui.js task "<task>" --target <path> --json\`. Before editing, visibly show the returned \`notice\`: actual package/version, decision, Skill or baseline, rule IDs/names, target, runId and unverified checks. Include baseline and gaps. Report missing/unsupported local commands and version drift explicitly. A routing decision does not execute checks.
13
15
  2. Read \`node_modules/@agile-team/wl-skills-ui/SKILL.md\`, then only the selected canonical Skill under \`node_modules/@agile-team/wl-skills-ui/skills\`. Resolve references relative to that Skill. Honor the actual project Profile; do not infer a table migration from a Skill match.
14
16
  3. Run the actual scan/check/fix tools with the same \`--run-id\`. For a code gate use \`wl-ui scan --target <path> --fail-on-error --run-id <id>\`. An ordinary Vue edit still receives UI baseline checks; tasks outside the domain are not applicable.
15
17
  4. End with \`wl-ui status --run-id <id>\` and report execution separately from validation. Zero files, skipped checks, dependency-only checks and unresolved review items are not proof of all UI constraints passing.
@@ -41,7 +41,7 @@ export function taskCatalog() {
41
41
  return catalogFiles(join(root, "skills")).map((file) => {
42
42
  const id = relative(join(root, "skills"), file).replace(/\\/g, "/").replace(/\/SKILL\.md$/, "");
43
43
  const content = readFileSync(file, "utf8");
44
- return { id, path: `node_modules/${pkg.name}/skills/${id}/SKILL.md`, description: content.match(/^# (.+)$/m)?.[1] || id, triggers: TRIGGERS[id] || [], rules: POLICY.baselineRules, checks: ["scan"], status: "enabled" };
44
+ return { id, path: `node_modules/${pkg.name}/skills/${id}/SKILL.md`, description: content.match(/^# (.+)$/m)?.[1] || id, triggers: TRIGGERS[id] || [], rules: getRules().filter((rule) => rule.skills?.includes(id)).map((rule) => rule.id), checks: ["scan"], status: "enabled" };
45
45
  });
46
46
  }
47
47
 
@@ -75,10 +75,15 @@ export function runTaskAction(action, input = {}) {
75
75
  if (action === "doctor-host") return hostDiagnostic({ ...opts, host: input.host || "unknown" });
76
76
  const profile = resolveProjectProfile({ projectRoot: opts.projectRoot, profile: input.profile }).profile;
77
77
  const routed = decideTask({ task: input.task || "", targets: opts.targets, skill: input.skill });
78
- const decision = { ...routed, profile: profile.id, applicableConstraints: routed.applicable === true ? getRules({ profile: profile.id }).map((rule) => rule.id) : [] };
78
+ const applicable = getRules({ profile: profile.id });
79
+ const enabled = new Set(applicable.map((rule) => rule.id));
80
+ const decision = { ...routed, profile: profile.id, applicableConstraints: routed.applicable === true ? [...enabled] : [] };
81
+ decision.baselineRules = decision.baselineRules.filter((id) => enabled.has(id));
82
+ decision.requiredRules = decision.requiredRules.filter((id) => enabled.has(id));
83
+ decision.ruleDetails = applicable.filter((rule) => decision.requiredRules.includes(rule.id)).map((rule) => ({ id: rule.id, name: rule.title, source: `node_modules/${pkg.name}/standards/rules.json` }));
79
84
  assetReadiness(decision, opts.projectRoot);
80
- if (action !== "task") return decision;
81
- return { ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) };
85
+ if (action !== "task") return core.attachNotice(decision, opts);
86
+ return core.attachNotice({ ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) }, opts);
82
87
  }
83
88
 
84
89
  function hostDiagnostic(opts) {
@@ -69,7 +69,10 @@ function scoreSkill(skill, text, contextText) {
69
69
  const hits = matches(text, skill.triggers);
70
70
  const negative = matches(text, skill.negative);
71
71
  const contextHits = matches(contextText, skill.contexts);
72
- const score = negative.length ? -100 : hits.length * 5 + contextHits.length * 2;
72
+ // 最长命中加权:更长的触发短语代表更具体的意图(如「表单弹窗布局」优先于「表单」「弹窗」),
73
+ // 只在同一命中数级别内打破平局,不改变命中数量的主导地位。
74
+ const longestHit = hits.reduce((max, phrase) => Math.max(max, String(phrase).length), 0);
75
+ const score = negative.length ? -100 : hits.length * 5 + contextHits.length * 2 + longestHit;
73
76
  return { id: skill.id, path: skill.path || null, score, hits, contextHits, negative, status: skill.status || "released" };
74
77
  }
75
78
 
@@ -262,6 +265,7 @@ function startTask(options) {
262
265
  const record = { ...baseRecord(metadata, "task-decision"), task: safeText(options.task), decision: selected, targets: strings(options.targets), inputSnapshot: captureSnapshot(options),
263
266
  ruleSnapshot: captureReadEvidence(options, [...strings(options.ruleFiles), ...strings(selected.requiredFiles)]),
264
267
  configSnapshot: contextSnapshot(options, "configFiles"), executionStatus: "not-executed", validationStatus: "unverified" };
268
+ attachNotice(record, options);
265
269
  record.recordPath = appendRecord(options, record);
266
270
  return record;
267
271
  }
@@ -403,12 +407,12 @@ function statusForRecords(records, options) {
403
407
  const started = records.filter((record) => record.kind === "tool-started");
404
408
  const current = validationContext(plan, tools, options);
405
409
  const failed = tools.some((record) => record.executionStatus === "failed");
406
- return {
410
+ return attachNotice({
407
411
  recordVersion: RECORD_VERSION, packageName: options.packageName, ...lastRecordContext(records.at(-1), options),
408
412
  ...planContext(plan), records, tools,
409
413
  executionStatus: executionState(failed, started, history),
410
414
  ...current, stages: { selected: plan ? plan.decision.status : "unverified", hostDiscovered: "unverified", contentLoaded: "unverified", toolsExecuted: history.length },
411
- };
415
+ }, options);
412
416
  }
413
417
 
414
418
  function validationContext(plan, tools, options) {
@@ -436,7 +440,7 @@ function uncoveredScope(plan, tools, options) {
436
440
 
437
441
  function planContext(plan) {
438
442
  if (!plan) return { task: null, decision: null, gaps: [] };
439
- return { task: plan.task, decision: plan.decision, gaps: plan.decision.gaps || [] };
443
+ return { task: plan.task, decision: plan.decision, targets: plan.targets, gaps: plan.decision.gaps || [] };
440
444
  }
441
445
 
442
446
  function lastRecordContext(latest, options) {
@@ -502,18 +506,91 @@ function doctorHost(options) {
502
506
  const skills = strings(options.skillPaths).map((relative) => inspectHostFile(root, relative, Infinity));
503
507
  const gateway = options.gatewayPath ? inspectHostFile(root, options.gatewayPath, Infinity) : null;
504
508
  const warnings = entries.filter((entry) => entry.oversized).map((entry) => `${entry.path} 超过 ${maximum} 字节参考预算;请核对宿主实际配置与内容加载范围`);
505
- const ready = gateway?.status === "present" && skills.length > 0 && skills.every((skill) => skill.status === "present") && entries.every((entry) => entry.status === "present" && !entry.oversized);
506
- return { recordVersion: RECORD_VERSION, packageName: options.packageName, host: options.host || "unspecified", projectRoot: root, entries, skills, gateway, entryReadiness: ready ? "ready" : "incomplete", hostDiscovery: "unverified", mcpConnection: "unverified", contentLoaded: "unverified", warnings, instructionBudgetSource: "reference-default-not-observed-host-config" };
509
+ const runtime = inspectRuntime(root, options);
510
+ warnings.push(...runtime.warnings);
511
+ const ready = hostReady(gateway, skills, entries, runtime);
512
+ return { recordVersion: RECORD_VERSION, packageName: options.packageName, packageVersion: options.packageVersion, host: options.host || "unspecified", projectRoot: root, entries, skills, gateway, runtime, entryReadiness: ready ? "ready" : "incomplete", hostDiscovery: "unverified", mcpConnection: "unverified", contentLoaded: "unverified", warnings, instructionBudgetSource: "reference-default-not-observed-host-config" };
513
+ }
514
+
515
+ function hostReady(gateway, skills, entries, runtime) {
516
+ return gateway?.status === "present" && skills.length > 0 && skills.every((skill) => skill.status === "present") && entries.every((entry) => entry.status === "present" && !entry.oversized) && ["aligned", "unverified"].includes(runtime.status);
517
+ }
518
+ function runtimeVersion(root, relative, diagnostics) {
519
+ try { const value = JSON.parse(fs.readFileSync(readEvidencePath(root, relative), "utf8")).version; if (typeof value !== "string" || !value.trim()) throw new Error("version 必须是非空字符串"); return value; }
520
+ catch (error) { if (error.code !== "ENOENT") diagnostics.push(`${relative}: ${error.message}`); return null; }
521
+ }
522
+ function declaredDependency(root, packageName) {
523
+ try { const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); return pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName] || null; }
524
+ catch { return null; }
525
+ }
526
+ function runtimeMismatch(versions, declared, local) {
527
+ return strings(versions).length > 1 || Boolean(local && /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(declared || "") && declared !== local);
528
+ }
529
+ function inspectRuntime(root, options) {
530
+ root = fs.realpathSync(path.resolve(root));
531
+ const key = (options.packageName || "").replace("@agile-team/wl-skills-", "");
532
+ const manifestPath = { kit: ".wl-skills-manifest.json", design: ".wl-skills-design/state.json", test: ".wl-skills-test/manifest.json" }[key] || `.wl-skills-${key}-manifest.json`;
533
+ const diagnostics = [];
534
+ const distributedVersion = runtimeVersion(root, manifestPath, diagnostics);
535
+ const localVersion = runtimeVersion(root, `node_modules/${options.packageName}/package.json`, diagnostics);
536
+ const declaredVersion = declaredDependency(root, options.packageName);
537
+ const mismatch = runtimeMismatch([distributedVersion, localVersion, options.packageVersion], declaredVersion, localVersion);
538
+ const state = distributedVersion ? "aligned" : "unverified";
539
+ return { status: mismatch ? "mismatch" : diagnostics.length ? "invalid" : state, distributedVersion, localVersion, runningVersion: options.packageVersion || null, declaredVersion, diagnostics,
540
+ warnings: mismatch ? [`${options.packageName} 规范/本地执行器/当前执行器版本不一致:${distributedVersion} / ${localVersion} / ${options.packageVersion};声明依赖=${declaredVersion};同步本包依赖、锁文件与受管入口后复验`] : diagnostics, hostInvocation: "unverified" };
541
+ }
542
+
543
+ /** A notice describes the receipt; it does not claim the host displayed it or read rules. */
544
+ function noticeProjectRoot(record, options) {
545
+ return options.projectRoot ? fs.realpathSync(path.resolve(options.projectRoot)) : record.notice?.projectRoot || null;
546
+ }
547
+ function noticeRules(item) {
548
+ const details = new Map((item.ruleDetails || []).map((rule) => [rule.id, rule]));
549
+ const ids = strings([...strings(item.baselineRules), ...strings(item.requiredRules), ...strings(item.constraints)]);
550
+ return ids.map((id) => ({ id, name: details.get(id)?.name || id, source: details.get(id)?.source || null }));
551
+ }
552
+ function noticeScope(record, options, item) {
553
+ return { targets: strings(options.originalTargets || record.targets || options.targets),
554
+ executionStatus: record.executionStatus || "not-executed", validationStatus: record.validationStatus || "unverified",
555
+ unverified: strings(item.unverified || ["host-discovery", "model-read-canonical-files", "planned-actions-not-executed"]) };
556
+ }
557
+ function noticeReason(item) { return safeText(item.reason || strings(item.reasons).join(";")); }
558
+ function attachNotice(record, options = {}) {
559
+ const item = record.decision || record;
560
+ record.packageName ||= options.packageName;
561
+ record.packageVersion ||= options.packageVersion;
562
+ record.notice = {
563
+ schemaVersion: 1, packageName: record.packageName || null, packageVersion: record.packageVersion || null,
564
+ projectRoot: noticeProjectRoot(record, options), runId: record.runId || null, decision: item.status || "unverified",
565
+ skills: strings(item.selectedSkills), requiredChecks: strings(item.requiredChecks), baselineRules: strings(item.baselineRules), rules: noticeRules(item),
566
+ reason: noticeReason(item), missingInputs: strings(item.missingInputs),
567
+ gaps: (item.gaps || []).map((gap) => ({ reason: safeText(gap.reason), suggestion: safeText(gap.suggestion) })),
568
+ ...noticeScope(record, options, item), displayEvidence: "unverified",
569
+ };
570
+ return record;
571
+ }
572
+ function noticeRuleText(notice) {
573
+ const rules = notice.rules.slice(0, 8).map((rule) => rule.name === rule.id ? rule.id : `${rule.id}(${rule.name})`).join("、");
574
+ const remaining = notice.rules.length > 8 ? `等 ${notice.rules.length} 条,完整清单见 JSON 回执` : "";
575
+ return `${rules}${remaining || (rules ? "" : "尚无确认规则")}`;
576
+ }
577
+ function noticePlanText(notice) {
578
+ return `${notice.requiredChecks.length ? `\n待执行检查:${notice.requiredChecks.join("、")}` : ""}` + `${notice.runId ? `\nrunId=${notice.runId}` : "\n只读判定,尚无任务记录"};执行=${notice.executionStatus};验证=${notice.validationStatus}`;
579
+ }
580
+ function noticeGapText(notice) {
581
+ return `${notice.missingInputs.length ? `\n缺少:${notice.missingInputs.join("、")}` : ""}` + `${notice.gaps.length ? `\n缺口:${notice.gaps.map((gap) => `${gap.reason};建议:${gap.suggestion}`).join("\n")}` : ""}`;
507
582
  }
508
-
509
583
  function formatDecision(record) {
510
- const item = record.decision || record;
511
- const label = { baseline: "基础约束", ambiguous: "候选需确认", gap: "规则缺口", "not-applicable": "不适用", "needs-context": "缺少上下文" }[item.status] || "待判定";
512
- return `[WL ${item.status}] ${strings(item.selectedSkills).join("、") || label};${safeText(item.reason || strings(item.reasons).join(";"), 300)}${record.runId ? `;runId=${record.runId}` : ""}`;
584
+ const notice = record.notice || attachNotice({ ...record }).notice;
585
+ const label = { baseline: "基础约束", ambiguous: "候选需确认", gap: "规则缺口", "not-applicable": "不适用", "needs-context": "缺少上下文" }[notice.decision] || "待判定";
586
+ const identity = notice.packageName ? `${notice.packageName.replace("@agile-team/wl-skills-", "")}@${notice.packageVersion || "未知"};` : "";
587
+ return `[WL ${notice.decision}] ${identity}${notice.skills.join("、") || label};${notice.reason}` + `\n适用规则:${noticeRuleText(notice)};范围:${notice.targets.slice(0, 3).join("、") || "未指定目标,检查范围待确认"}` + noticePlanText(notice) + noticeGapText(notice);
513
588
  }
514
589
 
515
590
  function formatStatus(status) {
516
- return `[WL 执行=${status.executionStatus} 验证=${status.validationStatus}] runId=${status.runId || "尚无记录"};工具=${status.tools.length};待验证=${status.pendingChecks.length};范围缺口=${(status.scopeGaps || []).length};规则缺口=${(status.gaps || []).length};宿主发现=${status.stages.hostDiscovered}`;
591
+ const identity = `${status.packageName?.replace("@agile-team/wl-skills-", "") || "未知包"}@${status.packageVersion || "未知"}`;
592
+ const checked = [...new Set(status.tools.flatMap((tool) => (tool.checkedFiles || []).map((file) => file.path)))];
593
+ return `[WL 执行=${status.executionStatus} 验证=${status.validationStatus}] ${identity};runId=${status.runId || "尚无记录"};工具=${status.tools.length};实际检查文件=${checked.length};待验证=${status.pendingChecks.length};范围缺口=${(status.scopeGaps || []).length};规则缺口=${(status.gaps || []).length};证据过期=${Boolean(status.stale)};宿主发现=${status.stages.hostDiscovered}`;
517
594
  }
518
595
 
519
596
  function aggregateStatus(statuses) {
@@ -522,4 +599,4 @@ function aggregateStatus(statuses) {
522
599
  return { recordVersion: RECORD_VERSION, runId: runIds.length === 1 ? runIds[0] : null, correlated: runIds.length === 1, packages: valid, mixedRuns: runIds.length > 1, hasFailure: valid.some((status) => status.validationStatus === "failed"), gaps: valid.flatMap((status) => status.gaps || []) };
523
600
  }
524
601
 
525
- module.exports = { RECORD_VERSION, STORAGE, evaluateTask, startTask, beginExecution, finishExecution, withExecution, captureSnapshot, readStatus, listGaps, doctorHost, formatDecision, formatStatus, aggregateStatus };
602
+ module.exports = { RECORD_VERSION, STORAGE, evaluateTask, startTask, beginExecution, finishExecution, withExecution, captureSnapshot, readStatus, listGaps, doctorHost, inspectRuntime, attachNotice, formatDecision, formatStatus, aggregateStatus };
@@ -34,7 +34,8 @@
34
34
  "artifacts": { "type": "array", "items": { "$ref": "#/definitions/file" } },
35
35
  "changedDuringCheck": { "type": "boolean" },
36
36
  "errorCode": { "type": "string" },
37
- "recordPath": { "type": ["string", "null"] }
37
+ "recordPath": { "type": ["string", "null"] },
38
+ "notice": { "$ref": "#/definitions/notice" }
38
39
  },
39
40
  "allOf": [
40
41
  { "if": { "properties": { "kind": { "const": "task-decision" } } }, "then": { "required": ["task", "decision"], "properties": { "executionStatus": { "const": "not-executed" }, "validationStatus": { "const": "unverified" } } } },
@@ -42,6 +43,100 @@
42
43
  { "if": { "properties": { "kind": { "const": "tool-finished" } } }, "then": { "required": ["tool", "executionId", "executionStartedAt", "exitCode", "durationMs", "checks", "artifacts", "inputBefore", "changedDuringCheck"], "properties": { "executionStatus": { "enum": ["completed", "failed"] } } } }
43
44
  ],
44
45
  "definitions": {
46
+ "notice": {
47
+ "type": "object",
48
+ "required": [
49
+ "schemaVersion",
50
+ "packageName",
51
+ "packageVersion",
52
+ "runId",
53
+ "decision",
54
+ "skills",
55
+ "rules",
56
+ "requiredChecks",
57
+ "executionStatus",
58
+ "validationStatus",
59
+ "displayEvidence"
60
+ ],
61
+ "properties": {
62
+ "schemaVersion": {
63
+ "const": 1
64
+ },
65
+ "packageName": {
66
+ "type": [
67
+ "string",
68
+ "null"
69
+ ]
70
+ },
71
+ "packageVersion": {
72
+ "type": [
73
+ "string",
74
+ "null"
75
+ ]
76
+ },
77
+ "runId": {
78
+ "type": [
79
+ "string",
80
+ "null"
81
+ ]
82
+ },
83
+ "projectRoot": {
84
+ "type": [
85
+ "string",
86
+ "null"
87
+ ]
88
+ },
89
+ "decision": {
90
+ "type": "string"
91
+ },
92
+ "skills": {
93
+ "type": "array",
94
+ "items": {
95
+ "type": "string"
96
+ }
97
+ },
98
+ "requiredChecks": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "string"
102
+ }
103
+ },
104
+ "rules": {
105
+ "type": "array",
106
+ "items": {
107
+ "type": "object",
108
+ "required": [
109
+ "id",
110
+ "name",
111
+ "source"
112
+ ],
113
+ "properties": {
114
+ "id": {
115
+ "type": "string"
116
+ },
117
+ "name": {
118
+ "type": "string"
119
+ },
120
+ "source": {
121
+ "type": [
122
+ "string",
123
+ "null"
124
+ ]
125
+ }
126
+ }
127
+ }
128
+ },
129
+ "executionStatus": {
130
+ "type": "string"
131
+ },
132
+ "validationStatus": {
133
+ "type": "string"
134
+ },
135
+ "displayEvidence": {
136
+ "const": "unverified"
137
+ }
138
+ }
139
+ },
45
140
  "file": {
46
141
  "type": "object", "required": ["path", "sha256", "bytes"],
47
142
  "properties": { "path": { "type": "string" }, "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "bytes": { "type": "integer", "minimum": 0 } }
package/bin/wl-ui.js CHANGED
@@ -78,7 +78,7 @@ const SUBCOMMANDS = new Set([
78
78
  "contract",
79
79
  "rules",
80
80
  "profiles",
81
- "task", "route", "explain", "status", "doctor-host",
81
+ "task", "route", "explain", "status", "doctor-host", "protocol",
82
82
  ]);
83
83
  let subcommand = "help";
84
84
 
@@ -95,7 +95,10 @@ if (rawArgs.length > 0 && SUBCOMMANDS.has(rawArgs[0])) {
95
95
  }
96
96
 
97
97
  // 非 init 子命令:直接委托给 scanner/index.mjs
98
- if (["task", "route", "explain", "status", "doctor-host"].includes(subcommand)) {
98
+ if (subcommand === "protocol") {
99
+ const { runCli } = await import("./protocol-cli.mjs");
100
+ process.exitCode = runCli(rawArgs);
101
+ } else if (["task", "route", "explain", "status", "doctor-host"].includes(subcommand)) {
99
102
  const { taskCli } = await import("./task-integration.mjs");
100
103
  taskCli(subcommand, rawArgs);
101
104
  process.exit(0);
@@ -571,7 +574,7 @@ function transformForEditor(content, editor) {
571
574
  ?.slice(2) || "Skill",
572
575
  );
573
576
 
574
- return header + "\nStart each task with local `wl-ui task \"<task>\" --target <path>`. Read only the selected Skill. Finish actual tools with the same `--run-id` and report `wl-ui status`; model declarations do not prove verification.\n\n" + body;
577
+ return header + "\nStart each task with local `wl-ui task \"<task>\" --target <path>`. Before editing visibly show the returned notice: actual package/version, decision, Skill or baseline, rule IDs/names, targets, runId and unexecuted checks. Report local command failures/version drift. Read only the selected Skill. Finish actual tools with the same `--run-id` and report `wl-ui status`; model declarations do not prove verification.\n\n" + body;
575
578
  }
576
579
 
577
580
  function selectedSkills(mode, profile) {
@@ -661,7 +664,7 @@ function buildSingleFileRouter(skills, profile) {
661
664
  `> Active profile: \`${profile?.id || "native-element"}\`.`,
662
665
  "",
663
666
  "Use the package router first, then open only the relevant Skill:",
664
- "Start each task with local `wl-ui task \"<task>\" --target <path>`; read only the selected canonical Skill. Finish actual checks with the same `--run-id`, then report `wl-ui status --run-id <id>`. Routing/model declarations do not prove execution or validation. Native discovery: `.agents/skills/wl-skills-ui/SKILL.md`.",
667
+ "Start each task with local `wl-ui task \"<task>\" --target <path>`; before editing visibly show notice (actual package/version, decision, Skill or baseline, rule IDs/names, targets, runId, unexecuted checks), including gaps and local command/version failures; read only the selected canonical Skill. Finish actual checks with the same `--run-id`, then report `wl-ui status --run-id <id>`. Routing/model declarations do not prove execution or validation. Native discovery: `.agents/skills/wl-skills-ui/SKILL.md`.",
665
668
  "Reuse one `--run-id` / `WL_TASK_RUN_ID` across installed applicable packages for this user task; do not install unused sibling packages.",
666
669
  "",
667
670
  "- `node_modules/@agile-team/wl-skills-ui/SKILL.md`",
@@ -1370,6 +1373,8 @@ wl-ui — @agile-team/wl-skills-ui 统一 CLI v${PKG.version}
1370
1373
  查看本包实际执行、验证、检查范围和过期状态
1371
1374
  wl-ui doctor-host [--host codex|claude|copilot|cursor] [--json]
1372
1375
  静态入口诊断;宿主发现与读取仍须宿主证据
1376
+ wl-ui protocol describe --json | wl-ui protocol request --input-file <request.json> --json
1377
+ 公开集成协议:能力目录与统一判定/状态 JSON 信封
1373
1378
  wl-ui init [--project <path>] [--editor <editor>] [--profile <id>]
1374
1379
  [--dry-run] [--skills-only]
1375
1380
  把 skills/ 写入目标项目的 AI 编辑器规则目录
@@ -2,6 +2,16 @@
2
2
 
3
3
  本页保留面向使用者的版本能力摘要;逐提交级变更、修复细节和旧版本记录见根目录 [`CHANGELOG.md`](../CHANGELOG.md)。
4
4
 
5
+ ## v1.16.0 — 公开集成协议与输入校验收口(2026-10-08)
6
+
7
+ ### Added
8
+
9
+ - 新增公开集成协议 describe/request 统一信封:能力清单与路由语料以真实打包产物验证;外部适配器仅依赖公开入口,移除适配器后本包独立能力不受影响。
10
+
11
+ ### Fixed
12
+
13
+ - 输入校验收口(context null / 空 targets 返回可解释拒绝);MCP 目录与真实 `tools/list` 18 项对齐。
14
+
5
15
  ## v1.15.0 — 任务判定、真实执行回执与原生技能入口
6
16
 
7
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-ui",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "企业级 UI 风格对齐框架 — Vue + Element Plus 双模式、任务路由与真实检查回执(tokens / element / vendors / layouts / runtime / scanner / fixer / skills)",
5
5
  "type": "module",
6
6
  "main": "./es/index.js",
@@ -59,7 +59,7 @@
59
59
  "scripts": {
60
60
  "build": "tsup && npm run sync:tokens",
61
61
  "sync:tokens": "node -e \"import('fs').then(fs=>fs.copyFileSync('design/tokens/base.css','dist/tokens.css'))\"",
62
- "test": "node --test scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs",
62
+ "test": "node --test scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs bin/__tests__/*.test.mjs",
63
63
  "test:visual": "playwright test --config tests/visual/playwright.config.ts",
64
64
  "test:visual:contracts": "playwright test --config tests/visual/playwright.config.ts tests/visual/specs/browser-compat.spec.ts",
65
65
  "test:visual:ui": "playwright test --config tests/visual/playwright.config.ts --ui",
@@ -80,7 +80,7 @@
80
80
  "pack:dry": "npm pack --dry-run --ignore-scripts",
81
81
  "release:check": "npm run verify && npm run test:visual:contracts && npm run pack:dry",
82
82
  "prepublishOnly": "npm run release:check",
83
- "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=bin/vendor/** --test-coverage-lines=70 --test-coverage-branches=65 --test-coverage-functions=70 scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs",
83
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude=bin/vendor/** --test-coverage-lines=70 --test-coverage-branches=65 --test-coverage-functions=70 scanner/__tests__/*.test.mjs runtime/__tests__/*.test.mjs bin/__tests__/*.test.mjs",
84
84
  "benchmark:runtime": "node scripts/benchmark-observer-hub.mjs"
85
85
  },
86
86
  "files": [