@agile-team/wl-skills-ui 1.16.0 → 1.18.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
@@ -1,9 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.18.0] - 2026-10-11
4
+
5
+ - 任务路由前校验目标项目自身接入与平台;未接入/移动端零规则零任务写入,不继承父安装。
6
+ - 最近项目边界与跨项目拆分、独立包禁用、严格范围Schema、notice/doctor/status范围与过期证据。
7
+ - 原生及编辑器入口收窄适用描述;补充单包、移动端、开源未接入、工作区、异常配置和Maven模块回归。
8
+
3
9
  All notable changes to **@agile-team/wl-skills-ui** will be documented in this file.
4
10
 
5
11
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
12
 
13
+ ## [1.17.0] - 2026-10-09
14
+
15
+ - 统一可见 notice(包版本/判定/技能或基础约束/规则/目标/runId/待检查项)、开始与收尾提示、规范与执行器版本漂移诊断;保持单包使用与宿主证据未验证口径。
16
+ - 按本包规则与实际 Profile 映射 Skill,不重复前端注释规则;保留旧入口哈希证明迁移与外来内容保护。
17
+
7
18
  ## [Unreleased]
8
19
 
9
20
  ## [1.16.0] - 2026-10-08
package/README.md CHANGED
@@ -1,12 +1,22 @@
1
1
  # @agile-team/wl-skills-ui
2
2
 
3
- **企业级 UI 一致性与 AI 精确治理工具包 v1.16.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
3
+ > 项目范围:只对目标自身已接入且平台适用的项目判定规则;父工作区安装不启用子项目。移动端与未接入开源项目不套用。详见 [精准触发与范围配置](docs/project-scope.md)。
4
+
5
+ **企业级 UI 一致性与 AI 精确治理工具包 v1.17.0** — 面向 Vue 3 + Element Plus 的设计令牌、能力 Profile、样式皮肤、页面骨架、运行时能力、静态扫描、确定性修复、AI Skill、MCP 与脱敏 UI 模板契约。
4
6
 
5
7
  它把“视觉统一、问题识别、机械修复、项目适配和样例沉淀”拆成可验证、可组合的工程能力。脚本、规则和 MCP 先完成确定性工作,AI 只处理业务语义与人工决策,从而减少重复读源码、上下文 token 和误修改。
6
8
 
7
9
  [![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
10
  [![Node](https://img.shields.io/badge/node-%3E%3D22-green.svg)]()
9
11
 
12
+ ## 如何确认本包正在起作用
13
+
14
+ 每次适用任务先运行项目本地 `wl-ui task "修改目标文件" --target src/Foo.vue --json`。编辑前展示真实 `notice`:包名与版本、判定、选中 Skill 或基础约束、具体规则、目标、runId 和尚未执行的检查。普通修改也需要基础约束提醒;相关但未覆盖的意图显示 gap 与建议;没有目标依据显示 needs-context,职责外显示 not-applicable,不强行匹配。
15
+
16
+ 执行实际检查时复用 `--run-id`,结束读取 `wl-ui status --run-id <id> --json`,分别报告执行和验证、实际检查文件、过期证据与未执行项。`notice.displayEvidence=unverified` 表示工具回执不能证明聊天界面展示;安装、路由或模型自报不能证明宿主加载/模型读取。
17
+
18
+ `wl-ui doctor-host --json` 对比已分发规范、本地执行器和正在运行的版本,漂移会显式报告。规范更新不会替代依赖升级:同步本包依赖、锁文件和受管入口;未使用的兄弟包无需安装。重开/刷新宿主加载后仍需观察真实任务调用,不能宣称所有 AI 自动触发。
19
+
10
20
  ## 你能获得什么
11
21
 
12
22
  | 目标 | 包提供的能力 | 带来的效果 |
@@ -500,7 +510,7 @@ npx wl-ui doctor --project . --print-overrides
500
510
 
501
511
  ## 版本与进一步阅读
502
512
 
503
- 当前 v1.16.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 依赖识别。
513
+ 当前 v1.18.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 依赖识别。
504
514
 
505
515
  - [版本摘要](docs/version-history.md)
506
516
  - [完整 Changelog](CHANGELOG.md)
package/SKILL.md CHANGED
@@ -1,11 +1,15 @@
1
1
  ---
2
2
  name: wl-skills-ui
3
3
  description: |
4
+ Only for PC Vue admin projects that adopted wl-skills-ui; excludes mobile and unadopted projects.
4
5
  Audit and align Vue 3 + Element Plus enterprise UI projects with deterministic
5
6
  scanning, profile-aware fixes, design tokens, vendor adapters, and runtime guards.
6
7
  Use for UI consistency, legacy skin migration, rule diagnostics, or safe UI fixes.
7
8
  ---
8
9
 
10
+ 先确认目标项目自身的接入证据(本包安装清单、直接依赖或 `.wl-skills-scope.json` 显式启用),再判定任务。只在目标已接入且平台适用时读取规则;父目录安装、兄弟包、Vue 文件或触发词不能证明接入。未接入的开源项目不套用规则、不建议自动安装;UniApp/小程序/App/PDA/移动 H5 不适用。跨项目目标分别判定,沿最近项目边界停止,不能越过未接入子项目找父清单。工具返回 `scope` 作为静态范围证据,宿主加载仍需实际事件。
11
+
12
+
9
13
  # wl-skills-ui
10
14
 
11
15
  Use deterministic project facts before reading or editing business source. Rules,
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { test } from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { mkdtempSync } from "node:fs";
6
+ import fs, { mkdtempSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import path from "node:path";
9
9
  import { spawnSync } from "node:child_process";
@@ -17,7 +17,9 @@ const pkg = require("../../package.json");
17
17
  const BIN = path.join(here, "..", "wl-ui.js");
18
18
 
19
19
  function tempRoot() {
20
- return mkdtempSync(path.join(tmpdir(), "wl-ui-protocol-"));
20
+ const root = mkdtempSync(path.join(tmpdir(), "wl-ui-protocol-"));
21
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
22
+ return root;
21
23
  }
22
24
 
23
25
  test("describe 返回协议版本、能力目录与五个统一操作", () => {
@@ -3,14 +3,16 @@
3
3
  */
4
4
  import { test } from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { mkdtempSync } from "node:fs";
6
+ import fs, { mkdtempSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import path from "node:path";
9
9
 
10
10
  const { protocol, runOperation } = await import("../protocol-cli.mjs");
11
11
 
12
12
  function tempRoot() {
13
- return mkdtempSync(path.join(tmpdir(), "wl-ui-evidence-"));
13
+ const root = mkdtempSync(path.join(tmpdir(), "wl-ui-evidence-"));
14
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
15
+ return root;
14
16
  }
15
17
 
16
18
  test("status 严格回查同一 runId:字段精确、不兜底", () => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { test, before } from "node:test";
6
6
  import assert from "node:assert/strict";
7
- import { mkdtempSync, rmSync } from "node:fs";
7
+ import fs, { mkdtempSync, rmSync } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import path from "node:path";
10
10
  import { spawnSync } from "node:child_process";
@@ -22,6 +22,7 @@ function tempRoot() {
22
22
  }
23
23
 
24
24
  function routeAt(projectRoot, task) {
25
+ if (!fs.existsSync(path.join(projectRoot, "package.json"))) fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify({ devDependencies: { "@agile-team/wl-skills-ui": "*" } }));
25
26
  return protocol.request({ operation: "route", projectRoot, task }, runOperation);
26
27
  }
27
28
 
@@ -40,7 +41,7 @@ for (const item of cases) {
40
41
  const expectedBare = item.skill ? "gap" : item.status;
41
42
  const expectedInstalled = item.installedStatus || (item.skill ? "matched" : item.status);
42
43
 
43
- test(`未安装:「${item.task}」→ ${expectedBare}${item.skill ? ` + ${item.skill}` : ""}`, () => {
44
+ test(`已声明接入但未初始化:「${item.task}」→ ${expectedBare}${item.skill ? ` + ${item.skill}` : ""}`, () => {
44
45
  const envelope = routeAt(tempRoot(), item.task);
45
46
  assert.equal(envelope.ok, true);
46
47
  const decision = envelope.result.decision || envelope.result;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+
3
+ // Independent snapshot: project adoption is checked before skill intent or writes.
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const crypto = require("node:crypto");
7
+ const CONFIG = ".wl-skills-scope.json";
8
+ const MANIFESTS = {
9
+ kit: ".wl-skills-manifest.json", ui: ".wl-skills-ui-manifest.json",
10
+ bd: ".wl-skills-bd-manifest.json", design: ".wl-skills-design/state.json", test: ".wl-skills-test/manifest.json",
11
+ };
12
+ const TYPES = new Set(["pc", "backend", "documents", "testing", "mobile", "workspace", "unknown"]);
13
+
14
+ function inside(root, target) {
15
+ const rel = path.relative(root, target);
16
+ return !path.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path.sep}`);
17
+ }
18
+
19
+ // Resolve missing paths through their nearest existing ancestor, including symlinks.
20
+ function realTarget(target) {
21
+ if (fs.existsSync(target)) return fs.realpathSync(target);
22
+ const parent = path.dirname(target);
23
+ if (parent === target) throw new Error("Cannot resolve target ancestor");
24
+ return path.join(realTarget(parent), path.basename(target));
25
+ }
26
+
27
+ function readJson(root, relative) {
28
+ const file = path.join(root, relative);
29
+ if (!fs.existsSync(file)) return { present: false, value: null };
30
+ try {
31
+ if (!inside(root, fs.realpathSync(file))) throw new Error("metadata path escapes project");
32
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected object");
34
+ return { present: true, value };
35
+ } catch (error) { return { present: true, value: null, error: `${relative}: ${error.message}` }; }
36
+ }
37
+
38
+ function boundary(root, fallback) {
39
+ const markers = ["package.json", ".git", CONFIG, ...Object.values(MANIFESTS)];
40
+ // A Java project's Maven modules share its adoption; a nested independent repo still stops lookup.
41
+ if (root === fallback || !fs.existsSync(path.join(fallback, "pom.xml"))) markers.push("pom.xml");
42
+ return markers.some((file) => fs.existsSync(path.join(root, file)));
43
+ }
44
+
45
+ function nearestRoot(target, fallback) {
46
+ let dir = fs.existsSync(target) && fs.statSync(target).isDirectory() ? target : path.dirname(target);
47
+ while (inside(fallback, dir)) {
48
+ if (boundary(dir, fallback) || dir === fallback) return dir;
49
+ dir = path.dirname(dir);
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function targetRoots(root, targets) {
55
+ const resolved = targets.map((target) => realTarget(path.resolve(root, target)));
56
+ if (resolved.some((target) => !inside(root, target))) return { error: "target-outside-authorized-project", roots: [], resolved };
57
+ return { roots: [...new Set(resolved.map((target) => nearestRoot(target, root)))], resolved };
58
+ }
59
+
60
+ function scopeConfig(root) {
61
+ const config = readJson(root, CONFIG);
62
+ if (!config.present || config.error) return config;
63
+ const { value } = config;
64
+ const allowed = ["schemaVersion", "projectType", "packages"];
65
+ if (value.schemaVersion !== 1 || !TYPES.has(value.projectType) || Object.keys(value).some((key) => !allowed.includes(key)) || !validPackageSwitches(value.packages)) {
66
+ return { ...config, error: `${CONFIG}: invalid schemaVersion/projectType/packages` };
67
+ }
68
+ return config;
69
+ }
70
+
71
+ function validPackageSwitches(packages) {
72
+ if (packages === undefined) return true;
73
+ return Boolean(packages && typeof packages === "object" && !Array.isArray(packages) && Object.entries(packages).every(([key, enabled]) => Object.hasOwn(MANIFESTS, key) && typeof enabled === "boolean"));
74
+ }
75
+
76
+ function mobileEvidence(root, pkg) {
77
+ const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
78
+ const mobile = deps.filter((name) => /^@dcloudio\/uni(?:-|$)|^@tarojs\/|^react-native$|^expo$|^@ionic\/|^@capacitor\//.test(name));
79
+ if (fs.existsSync(path.join(root, "pages.json")) && fs.existsSync(path.join(root, "manifest.json"))) mobile.push("pages.json+manifest.json");
80
+ return mobile;
81
+ }
82
+
83
+ function adoption(root, key, packageName, pkg, config) {
84
+ const manifest = readJson(root, MANIFESTS[key]);
85
+ validateAdoptionManifest(manifest, key, packageName);
86
+ const declared = [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies].some((deps) => deps?.[packageName]);
87
+ const explicit = config.value?.packages?.[key] === true;
88
+ const installed = manifest.present && !manifest.error;
89
+ return { adopted: explicit || declared || installed, sources: [
90
+ ...(explicit ? [`${CONFIG}:packages.${key}`] : []), ...(declared ? ["package.json:direct-dependency"] : []),
91
+ ...(installed ? [MANIFESTS[key]] : []),
92
+ ], error: manifest.error || null };
93
+ }
94
+
95
+ function validateAdoptionManifest(manifest, key, packageName) {
96
+ if (!manifest.present || manifest.error) return;
97
+ const { value } = manifest;
98
+ if (typeof value.version !== "string" || !value.version.trim() || value.package && value.package !== packageName) manifest.error = `${MANIFESTS[key]}: invalid package/version`;
99
+ }
100
+
101
+ function verdict(status, reason, extra = {}) {
102
+ return { schemaVersion: 1, status, reason, adopted: false, projectType: "unknown", evidence: [], inherited: false, ...extra };
103
+ }
104
+
105
+ function inspectProject(root, key, packageName) {
106
+ const config = scopeConfig(root);
107
+ const packageFile = readJson(root, "package.json");
108
+ if (config.error || packageFile.error) return verdict("needs-context", "invalid-project-metadata", { diagnostics: [config.error || packageFile.error] });
109
+ const pkg = packageFile.value || {};
110
+ if (!["dependencies", "devDependencies", "optionalDependencies"].every((field) => validDependencies(pkg[field]))) {
111
+ return verdict("needs-context", "invalid-project-metadata", { diagnostics: ["package.json: dependency declarations must be nonempty strings"] });
112
+ }
113
+ const exclusion = platformExclusion(root, key, pkg, config);
114
+ if (exclusion) return exclusion;
115
+ const own = adoption(root, key, packageName, pkg, config);
116
+ if (own.error) return verdict("needs-context", "invalid-package-adoption", { diagnostics: [own.error] });
117
+ if (!own.adopted) return verdict("not-applicable", "package-not-adopted-in-target-project");
118
+ return adoptedScope(root, key, pkg, config, own);
119
+ }
120
+
121
+ function validDependencies(deps) {
122
+ if (deps === undefined) return true;
123
+ return Boolean(deps && typeof deps === "object" && !Array.isArray(deps) && Object.values(deps).every((value) => typeof value === "string" && value.trim()));
124
+ }
125
+
126
+ function platformExclusion(root, key, pkg, config) {
127
+ const mobile = mobileEvidence(root, pkg);
128
+ const type = config.value?.projectType;
129
+ if (mobile.length || type === "mobile") return verdict("not-applicable", "mobile-project-excluded", { projectType: "mobile", evidence: mobile.length ? mobile : [CONFIG] });
130
+ if (type === "workspace") return verdict("not-applicable", "aggregate-workspace-is-not-project", { projectType: type, evidence: [CONFIG] });
131
+ if (config.value?.packages?.[key] === false) return verdict("not-applicable", "package-explicitly-disabled", { projectType: type || "unknown", evidence: [CONFIG] });
132
+ return null;
133
+ }
134
+
135
+ function unsupportedFrontend(root, key, pkg, type) {
136
+ if (!["kit", "ui"].includes(key)) return false;
137
+ if (type && type !== "pc") return "pc-frontend-package-excluded";
138
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
139
+ return !deps.vue && (deps.react || deps.svelte || deps["@angular/core"] || fs.existsSync(path.join(root, "pom.xml"))) ? "unsupported-project-framework" : false;
140
+ }
141
+
142
+ function defaultType(key) { return { kit: "pc", ui: "pc", bd: "backend", design: "documents", test: "testing" }[key]; }
143
+
144
+ function scopeFingerprint(root, key) {
145
+ const metadata = [CONFIG, "package.json", "pom.xml", MANIFESTS[key], "pages.json", "manifest.json"].map((file) => {
146
+ const full = path.join(root, file);
147
+ if (!fs.existsSync(full)) return [file, "missing"];
148
+ if (!inside(root, fs.realpathSync(full))) return [file, "external-metadata"];
149
+ try { return [file, crypto.createHash("sha256").update(fs.readFileSync(full)).digest("hex")]; }
150
+ catch (error) { return [file, `unreadable:${error.code}`]; }
151
+ });
152
+ return crypto.createHash("sha256").update(JSON.stringify(metadata)).digest("hex");
153
+ }
154
+
155
+ function adoptedScope(root, key, pkg, config, own) {
156
+ const type = config.value?.projectType;
157
+ if (type === "unknown") return verdict("needs-context", "project-platform-needs-context", { adopted: true, evidence: own.sources });
158
+ const unsupported = unsupportedFrontend(root, key, pkg, type);
159
+ if (unsupported) return verdict("not-applicable", unsupported, { adopted: true, projectType: type || "unknown", evidence: ["package.json/pom.xml", ...own.sources] });
160
+ return verdict("applicable", "target-project-explicitly-adopted-package", {
161
+ adopted: true, projectType: type || defaultType(key),
162
+ typeEvidence: type ? "explicit-config" : "adoption-default-not-host-observed", evidence: own.sources,
163
+ });
164
+ }
165
+
166
+ /** Parent installation, Vue files and keyword matches are never adoption evidence. */
167
+ function resolveScope({ projectRoot, packageName, targets = [] }) {
168
+ const root = fs.realpathSync(path.resolve(projectRoot || process.cwd()));
169
+ const key = packageName.replace("@agile-team/wl-skills-", "");
170
+ if (!Object.hasOwn(MANIFESTS, key)) throw new Error("Unknown WL package");
171
+ const located = targetRoots(root, targets.filter((item) => typeof item === "string" && item.trim()));
172
+ if (located.error || located.roots.length > 1) return { projectRoot: root, targets, scope: verdict("needs-context", located.error || "multiple-project-targets-split-required", { projectRoots: located.roots }) };
173
+ const actualRoot = located.roots[0] || root;
174
+ const actualTargets = located.resolved.map((target) => path.relative(actualRoot, target).split(path.sep).join("/") || ".");
175
+ return { projectRoot: actualRoot, targets: actualTargets, scope: { ...inspectProject(actualRoot, key, packageName), projectRoot: actualRoot, fingerprint: scopeFingerprint(actualRoot, key) } };
176
+ }
177
+
178
+ function excludedDecision(scope) {
179
+ const messages = {
180
+ "mobile-project-excluded": "目标为移动端/UniApp/小程序项目,不适用本轮WL项目约束",
181
+ "aggregate-workspace-is-not-project": "目标是聚合工作区,请指定实际已接入项目",
182
+ "package-explicitly-disabled": "目标项目已明确禁用本包",
183
+ "package-not-adopted-in-target-project": "目标项目未接入本包,不继承父目录或兄弟项目的安装",
184
+ "pc-frontend-package-excluded": "目标项目不是已接入的PC前端,PC规范不适用",
185
+ "unsupported-project-framework": "目标技术栈不是本包支持的Vue管理端,不套用PC Vue规范",
186
+ "multiple-project-targets-split-required": "目标跨越多个项目,请按项目分别判定并复用任务runId",
187
+ "target-outside-authorized-project": "目标超出当前授权项目边界,请从实际目标项目根调用",
188
+ "project-platform-needs-context": "项目已接入但平台未确认,请确认PC/后端/文档/测试或移动端范围",
189
+ };
190
+ const reason = messages[scope.reason] || "项目范围元数据无效,需要核对后再应用规则";
191
+ return { recordVersion: 1, status: scope.status, applicable: scope.status === "needs-context" ? null : false,
192
+ reason, reasons: [reason], scope, selectedSkills: [], candidates: [], baselineRules: [], requiredRules: [],
193
+ ruleDetails: [], requiredChecks: [], requiredFiles: [], missingInputs: scope.status === "needs-context" ? ["有效的目标项目范围"] : [],
194
+ gaps: [], constraints: [], ready: false, executionStatus: "not-executed", validationStatus: "unverified",
195
+ selectionEvidence: "project-scope-before-intent", hostDiscovery: "unverified", contentLoaded: "unverified", unverified: ["host-discovery", "model-read-canonical-files"] };
196
+ }
197
+
198
+ module.exports = { CONFIG, MANIFESTS, resolveScope, excludedDecision };
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "urn:wl-skills:project-scope:1",
4
+ "title": "Explicit WL project scope and per-package adoption",
5
+ "type": "object",
6
+ "required": [
7
+ "schemaVersion",
8
+ "projectType"
9
+ ],
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "schemaVersion": {
13
+ "const": 1
14
+ },
15
+ "projectType": {
16
+ "enum": [
17
+ "pc",
18
+ "backend",
19
+ "documents",
20
+ "testing",
21
+ "mobile",
22
+ "workspace",
23
+ "unknown"
24
+ ]
25
+ },
26
+ "packages": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "kit": {
31
+ "type": "boolean"
32
+ },
33
+ "ui": {
34
+ "type": "boolean"
35
+ },
36
+ "bd": {
37
+ "type": "boolean"
38
+ },
39
+ "design": {
40
+ "type": "boolean"
41
+ },
42
+ "test": {
43
+ "type": "boolean"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
@@ -80,7 +80,7 @@ export const protocol = createProtocol({
80
80
  packageName: pkg.name,
81
81
  packageVersion: pkg.version,
82
82
  capabilities: capabilitiesDocument.capabilities || [],
83
- constraints: { node: (pkg.engines && pkg.engines.node) || null, boundaryVersion: capabilitiesDocument.boundaryVersion || null },
83
+ constraints: { projectScope: { config: ".wl-skills-scope.json", schema: "bin/project-scope.schema.json", adoption: "own-manifest-or-direct-dependency-or-explicit-enable", inheritance: "never-across-project-boundaries", excluded: ["mobile", "unadopted-project", "aggregate-workspace"] }, node: (pkg.engines && pkg.engines.node) || null, boundaryVersion: capabilitiesDocument.boundaryVersion || null },
84
84
  operations: OPERATIONS,
85
85
  inventory: buildInventory(),
86
86
  });
@@ -3,13 +3,17 @@ export const GATEWAY_PATH = ".agents/skills/wl-skills-ui/SKILL.md";
3
3
  export function taskGateway() {
4
4
  return `---
5
5
  name: wl-skills-ui
6
- description: Vue and Element Plus UI consistency, design tokens, styles, component layout and visual rule checks. Decide the applicable UI workflow and verify actual source files using local package tools.
6
+ description: Only for adopted PC Vue/Web admin projects; excludes mobile and unadopted projects. Vue and Element Plus UI consistency, design tokens, styles, component layout and visual rule checks. Decide the applicable UI workflow and verify actual source files using local package tools.
7
7
  ---
8
8
 
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
+ 先确认目标项目自身的接入证据(本包安装清单、直接依赖或 \`.wl-skills-scope.json\` 显式启用),再判定任务。只在目标已接入且平台适用时读取规则;父目录安装、兄弟包、Vue 文件或触发词不能证明接入。未接入的开源项目不套用规则、不建议自动安装;UniApp/小程序/App/PDA/移动 H5 不适用。跨项目目标分别判定,沿最近项目边界停止,不能越过未接入子项目找父清单。工具返回 \`scope\` 作为静态范围证据,宿主加载仍需实际事件。
13
+
14
+ 多项目工作区先沿目标路径向上找到本包安装清单与项目 AGENTS,切到该项目根再调用工具;不要以聚合工作区根代替 projectRoot,也不要在聚合根安装。不同项目分别保存项目身份,同一用户任务复用 runId。
15
+
16
+ 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
17
  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
18
  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
19
  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.
@@ -2,6 +2,7 @@ import { readFileSync, readdirSync, existsSync, realpathSync } from "node:fs";
2
2
  import { join, relative, resolve, isAbsolute } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import core from "./task-observability.cjs";
5
+ import projectScope from "./project-scope.cjs";
5
6
  import { GATEWAY_PATH } from "./task-gateway.mjs";
6
7
  import { resolveProjectProfile } from "../standards/profiles-loader.mjs";
7
8
  import { getRules } from "../scanner/rules/index.mjs";
@@ -41,7 +42,7 @@ export function taskCatalog() {
41
42
  return catalogFiles(join(root, "skills")).map((file) => {
42
43
  const id = relative(join(root, "skills"), file).replace(/\\/g, "/").replace(/\/SKILL\.md$/, "");
43
44
  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" };
45
+ 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
46
  });
46
47
  }
47
48
 
@@ -70,15 +71,22 @@ function capabilityGap(decision, reason) {
70
71
  }
71
72
 
72
73
  export function runTaskAction(action, input = {}) {
73
- const opts = taskOptions(input.projectRoot || process.env.WL_PROJECT_ROOT || process.cwd(), { runId: input.runId, targets: input.targets || [] });
74
+ const resolved = projectScope.resolveScope({ projectRoot: input.projectRoot || process.env.WL_PROJECT_ROOT || process.cwd(), packageName: pkg.name, targets: input.targets || [] });
75
+ const opts = taskOptions(resolved.projectRoot, { runId: input.runId, targets: resolved.targets });
74
76
  if (action === "status") return core.readStatus(opts);
75
77
  if (action === "doctor-host") return hostDiagnostic({ ...opts, host: input.host || "unknown" });
78
+ if (resolved.scope.status !== "applicable") return core.attachNotice(projectScope.excludedDecision(resolved.scope), opts);
76
79
  const profile = resolveProjectProfile({ projectRoot: opts.projectRoot, profile: input.profile }).profile;
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) : [] };
80
+ const routed = decideTask({ task: input.task || "", targets: opts.targets, skill: input.skill, context: input.context });
81
+ const applicable = getRules({ profile: profile.id });
82
+ const enabled = new Set(applicable.map((rule) => rule.id));
83
+ const decision = { ...routed, scope: resolved.scope, profile: profile.id, applicableConstraints: routed.applicable === true ? [...enabled] : [] };
84
+ decision.baselineRules = decision.baselineRules.filter((id) => enabled.has(id));
85
+ decision.requiredRules = decision.requiredRules.filter((id) => enabled.has(id));
86
+ 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
87
  assetReadiness(decision, opts.projectRoot);
80
- if (action !== "task") return decision;
81
- return { ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) };
88
+ if (action !== "task") return core.attachNotice(decision, opts);
89
+ return core.attachNotice({ ...decision, ...core.startTask({ ...opts, task: input.task || "", decision }) }, opts);
82
90
  }
83
91
 
84
92
  function hostDiagnostic(opts) {