@rowansenne/workflow-run 0.24.1 → 0.25.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,6 +4,13 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.25.0] - 2026-10-22
8
+
9
+ ### 新增
10
+ - **项目画像新增开发环境配置 `toolchain`**:记录 JDK(`java_home`)、Maven(`maven.settings` / `maven.repository`)、Gradle(`gradle_user_home`)、Node(`node`)的位置,路径可写 `$JAVA_HOME` / `${VAR}` / `%VAR%` / `~`,变量在 `env_vars` 里声明(不含凭据)。构建和测试因环境跑不起来时(找不到 java、依赖拉不下来、版本不对),dev 用它核对而不是猜。`infer-profile` 按构建文件、`JAVA_HOME` / `GRADLE_USER_HOME`、`~/.m2/settings.xml` 及其 `<localRepository>`、`~/.gradle`、锁定的 Node 版本推断草稿并附依据;新命令 `resolve.mjs env --check` 逐个字段给出展开后的路径、`exists`、没配的变量和本机实际的 `JAVA_HOME` / `MAVEN_HOME` / Node 目录,写入前核对、对不上时改个人的 `profile.local.json`(逐个子字段覆盖团队画像)。`status` 输出里也带上配好的 toolchain。
11
+ - `toolchain` 的值按 Maven 的实际规则取值:全局(`$MAVEN_HOME/conf/settings.xml`)与用户级(`~/.m2/settings.xml`)会合并、**以用户级为准**,所以推断用户级那份(原来优先取全局,会丢掉用户的私服与镜像配置),全局的存在时记在 `evidence` 里。`<localRepository>` 同样按有效配置推断,并且**忽略 `settings.xml` 注释里的示例节点**(原来会把注释中的 `/path/to/local/repo` 当成真实仓库)。
12
+ - 个人的 `profile.local.json` 里把 `toolchain` 某字段写 `""` 表示**不使用**团队那个值(例如用 Maven 默认配置),原来空字符串不算覆盖、仍继承团队路径。被清空的字段不再校验、不报缺失,`env --check` / `status` / `profile` 输出里列在 `cleared`。
13
+
7
14
  ## [0.24.1] - 2026-10-21
8
15
 
9
16
  ### 修复
package/README.md CHANGED
@@ -172,6 +172,13 @@ npx @rowansenne/workflow-run@latest doctor --offline
172
172
  "deploy": { "platform": "内部发布平台", "regions": ["华东-生产", "华南-生产", "海外-生产"] },
173
173
  "job_annotation": "@XxlJob",
174
174
  "data_review": true,
175
+ "toolchain": {
176
+ "java_home": "$JAVA_HOME",
177
+ "maven": { "settings": "$HOME/.m2/settings.xml", "repository": "/data/maven-repo" },
178
+ "gradle_user_home": "",
179
+ "node": ""
180
+ },
181
+ "env_vars": { "JAVA_HOME": "/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home" },
175
182
  "notes": ["技术方案的「上线服务」与「数据链路」为必填"]
176
183
  }
177
184
  ```
@@ -184,6 +191,15 @@ npx @rowansenne/workflow-run@latest doctor --offline
184
191
  | `deploy` | 技术 / 上线方案「上线服务」的服务名与环境说明 |
185
192
  | `job_annotation` | 技术方案「定时任务变更」识别任务类 |
186
193
  | `data_review` | 是否输出技术方案模块 7 与上线方案 2.3 的库表评审块 |
194
+ | `toolchain` / `env_vars` | 开发环境配置:JDK(`java_home`)、Maven(`maven.settings` / `maven.repository`)、Gradle(`gradle_user_home`)、Node(`node`)的位置,以及路径里用到的变量。路径写 `$JAVA_HOME` 这类变量或本机绝对路径,`~` 展开为用户目录;**不含凭据**。构建跑不起来时 dev 用它核对 |
195
+
196
+ **开发环境配置**(`toolchain`):JDK 在哪、Maven 用哪个 `settings.xml`、本地仓库在哪个盘,这些决定构建和测试能不能跑起来,统一由画像提供,模板里不写死:
197
+
198
+ ```bash
199
+ node .agents/skills/workflow/scripts/resolve.mjs env --check # 逐个字段核对在本机是否存在
200
+ ```
201
+
202
+ `/workflow profile` 时会用 `infer-profile` 自动推断草稿(构建文件对应的工具、`JAVA_HOME` / `GRADLE_USER_HOME`、`~/.m2/settings.xml` 与其 `<localRepository>`、`~/.gradle`、锁定的 Node 版本),每个值带依据,经你确认后写入。Maven 的两份 `settings.xml`(全局 `$MAVEN_HOME/conf` 与用户级 `~/.m2`)**会合并、以用户级为准**,所以取用户级那份、全局的记在依据里;`<localRepository>` 也只认有效配置,注释里的示例会被忽略。路径可以写 `$JAVA_HOME`、`${VAR}`、`%VAR%`(用 `env_vars` 或本机环境展开)或 `~`。`env --check` 对每个字段给出展开后的路径、是否存在、哪个变量没配,以及本机实际的 `JAVA_HOME` / `MAVEN_HOME` / Node 目录,方便对照。**JDK、Maven 的位置每台机器不同**:团队共用一份就写 `profile.json`,个人不同就写 `profile.local.json` 的同名字段(优先),不用改团队那份;个人那份里写 `""` 表示**不用**团队那个值(用工具默认配置),不写这个键才是沿用。这一块只放路径和普通环境变量,token、密码一律不写。
187
203
 
188
204
  **本机系统与终端**:安装 / 升级时检测一次,写进 `workflow/profile.local.json` 的 `env`(个人设置,已在 `.gitignore` 里,团队里各人各机器不同),例如 `{ "os": "windows", "shell": "powershell" }`。Windows 上区分 `pwsh`(PowerShell 7)和 `powershell`(Windows PowerShell 5.1,没有 `&&`),其他系统取 `$SHELL`。skill 写终端命令时读 `resolve.mjs env`(`profile` / `status` 的输出里也有),不再每次对话自己探测;skill 里给出的命令本身都是 `node …` / `git …`,在哪个终端都一样。换了终端或机器,重跑一次安装器即可。
189
205
 
@@ -445,6 +461,7 @@ npx @rowansenne/workflow-run uninstall
445
461
  | `workflow/scripts/resolve.mjs template <skill> <文件名>` | 判定用哪份模板:已接入 DocNexus 只用知识库,缺失时退出码 2 并给出导入来源;未接入时项目文件优先,其次默认模板 |
446
462
  | `workflow/scripts/resolve.mjs profile` | 读项目画像,列出空字段 |
447
463
  | `workflow/scripts/resolve.mjs env` | 本机系统与终端(安装时记录在 `profile.local.json`),写终端命令按它来 |
464
+ | `workflow/scripts/resolve.mjs env --check` | 核对画像里的开发环境配置(`toolchain`)在本机是否存在:每个字段给出 `resolved`(展开后)、`exists`、`unset_vars`,以及 `current`(本机实际的 `JAVA_HOME` / `MAVEN_HOME` / Node 目录)便于对照 |
448
465
  | `workflow/scripts/docnexus.mjs <参数>` / `--node <脚本> <参数>` | 在业务项目根运行 `.docnexus/` 中的 CLI 或它的 node 脚本,知识输出到 `workflow-run/docnexus/`;Windows 上直接用 node 跑 CLI 入口,不经过 `.cmd` |
449
466
  | `workflow/scripts/knowledge.mjs <skill> [--match 词] [--limit n] [--query 问题]` | 按 `readFrom` 的分类列出 DocNexus 文档(标题、摘要、路径,不含正文);`--query` 再在每个分类内 `recall --category`,直接使用新版按分类检索能力。未接入时退出码 2 |
450
467
  | `workflow/scripts/resolve.mjs category <skill>` | 该 skill 写入 DocNexus 时的显式分类(画像 `docnexus_categories` 优先,其次 skill.json),输出 `seal_args` 与 `metadata`;未声明时退出码 1 |
@@ -90,6 +90,15 @@ export function doctor(root, offline) {
90
90
  } else bad('缺少 workflow/scripts/resolve.mjs:运行 npx @rowansenne/workflow-run upgrade');
91
91
  if (st) {
92
92
  if (st.profile.ok) ok('项目画像已填写'); else meh(`项目画像未完成(空字段:${st.profile.empty.join('、')}):运行 /workflow profile`);
93
+ // 开发环境配置:配过才查,逐个字段核对路径在本机是否存在(画像可能抄的是别人的机器)
94
+ if (st.toolchain?.configured) {
95
+ const r = spawnSync(process.execPath, [resolveJs, '--root', root, 'env', '--check'], { encoding: 'utf8' });
96
+ let tc = null;
97
+ try { tc = JSON.parse(r.stdout); } catch { /* 单独在下面报 */ }
98
+ if (!tc) bad('toolchain 检查失败:resolve.mjs env --check 执行异常');
99
+ else if (tc.ok) ok(`开发环境配置已核对(${Object.keys(tc.fields).join('、')})`);
100
+ else meh(`开发环境配置里这些路径在本机不存在或变量没配:${tc.missing.join('、')}(运行 resolve.mjs env --check 看 resolved 与 current;只是个人机器不同就写进 profile.local.json)`);
101
+ }
93
102
  if (st.wiki.ok) ok('wiki 已配置'); else console.log(' -- wiki 未配置(可选;也可能通过 wiki MCP 提供)');
94
103
  if (st.docnexus.ok) {
95
104
  // 运行环境在 .docnexus/,所有 Agent 都从业务项目根使用配置的知识输出。
package/dev/SKILL.md.tpl CHANGED
@@ -27,8 +27,9 @@ description: "Implement a feature or fix a bug in this project end to end: under
27
27
 
28
28
  ## Step 0 — 前置检查(两种模式共用)
29
29
 
30
- 1. **项目画像**:运行 `node <skills>/workflow/scripts/resolve.mjs profile`,读输出的 `profile` 与 `empty`(空字段列表;字段含义见 `workflow` skill)。本 skill 用到 `main_branch`、`build`、`test`、`test_single`(`{target}` 替换为测试类或文件)、`lint`、`config_center`、`notes`。
30
+ 1. **项目画像**:运行 `node <skills>/workflow/scripts/resolve.mjs profile`,读输出的 `profile`、`empty`(空字段列表)与 `toolchain`(开发环境配置,见 `workflow` skill)。本 skill 用到 `main_branch`、`build`、`test`、`test_single`(`{target}` 替换为测试类或文件)、`lint`、`config_center`、`notes`。
31
31
  - 需要的字段为空 → 按 `<skills>/workflow/references/profile.md`「填写与更新」的步骤推断,经用户确认后写回;用户不确认就只在本次使用,不落盘。
32
+ - **开发环境配置**:`toolchain.configured` 为 true 时,编译 / 测试按它跑(JDK 用 `java_home`、Maven 用 `maven.settings` / `maven.repository`,写命令时把对应的 `env_vars`(如 `JAVA_HOME`)带上,例如 `JAVA_HOME=<值> <profile.build>`)。构建或测试失败且像是环境问题(找不到 java、依赖下载不下来、版本不对)时,先运行 `node <skills>/workflow/scripts/resolve.mjs env --check`:字段 `exists: false` 或带 `unset_vars` 就是环境没配对,把 `resolved` 和 `current`(本机实际值)一起给用户看,问清楚再继续,**不要自己改画像**。配过但本机对不上时属于个人差异,按 `references/profile.md` 写进 `profile.local.json`。
32
33
  2. **工作区状态**:`git status --short` 与当前分支。
33
34
  - 有未提交改动或未跟踪文件 → 列出这些文件,**问用户哪些属于本需求**(例如用户先手写了一部分,让你接着做);可以和后面的澄清问题一起问。不要替用户 stash / 丢弃。
34
35
  - 属于本需求的:和你之后的改动同等对待,影响面、相关测试、自审、交付说明「改动」节都纳入。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowansenne/workflow-run",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "项目级 Agent Skills 一键初始化:workflow / dev / project-review / code-graph / database-query / md-to-wiki / 技术与上线方案生成",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -59,7 +59,7 @@ dev(需求 → 方案 → 实现 → 测试 → 自审)
59
59
 
60
60
  | 文件 | 内容 |
61
61
  |---|---|
62
- | `references/profile.md` | 项目画像 `profile.json` 各字段含义、填写与更新步骤(`resolve.mjs infer-profile`) |
62
+ | `references/profile.md` | 项目画像 `profile.json` 各字段含义(含开发环境配置 `toolchain` / `env_vars`)、填写与更新步骤(`resolve.mjs infer-profile`、`env --check`) |
63
63
  | `references/templates.md` | 模板来源判定(`resolve.mjs template`)、创建与修改模板(`/workflow template`)、模板随知识库更新 |
64
64
  | `references/docnexus.md` | 写入 DocNexus 的分类(`resolve.mjs category`)、按分类参考知识库(`knowledge.mjs`) |
65
65
  | `references/status.md` | `/workflow status` 的输出格式 |
@@ -80,13 +80,15 @@ dev(需求 → 方案 → 实现 → 测试 → 自审)
80
80
  | 数据库 | `database-query/config.json` 存在且 `connections` 非空 | 查表结构、核对数据 |
81
81
  | 代码图谱(CodeGraph) | 项目根有 `.codegraph/` 索引,且 `codegraph` 命令可用(`resolve.mjs status` 的 `codegraph.ok`) | 读代码、查调用链与影响面、找受影响的测试;用法见 `references/codegraph.md` |
82
82
  | 项目画像 | `workflow/profile.json` 中 `main_branch` 与 `build` / `test` 非空 | dev 构建与测试 |
83
+ | 开发环境配置 | `profile.json` 的 `toolchain`(可被 `profile.local.json` 覆盖)配了 JDK / Maven / Node 的路径;`resolve.mjs env --check` 逐个核对在本机是否存在 | dev 跑编译与测试(跑不起来时先看它) |
83
84
 
84
85
  ## Rules
85
86
 
86
87
  - 本 skill 只路由和维护 `profile.json`,不写业务代码;具体工作交给对应 skill,并遵守该 skill 的规则。
87
- - `profile.json` 只记录项目事实,不写密码、token、内网凭据。
88
+ - `profile.json` 只记录项目事实(含 `toolchain` 开发环境配置:JDK / Maven / Node 的路径),**不写密码、token、内网凭据**;路径里的变量在 `env_vars` 里声明。
89
+ - 写 `toolchain` 前先用 `resolve.mjs env --check` 核对路径在本机存在;本机对不上、又只是个人差异的,写进个人的 `profile.local.json`(覆盖 `profile.json`),不要改团队那份。
88
90
  - 用户的请求明显只属于某个 skill 时,不要先跑完整链路或状态检查,直接进入那个 skill。
89
91
 
90
92
  ## 终端命令
91
93
 
92
- 本 skill 给出的 `node …`、`git …` 命令在 bash、zsh、PowerShell 里写法一样,原样运行即可。需要自己写其他终端命令时,先看本机的系统和终端:`node <skills>/workflow/scripts/resolve.mjs env`(安装时记录,一次对话查一次就够;`resolve.mjs profile` / `status` 的输出里也带着 `env`)。按输出的 `os`、`shell`、`note` 写命令,不要再用 `uname`、`$PSVersionTable`、`ver` 之类去探测。
94
+ 本 skill 给出的 `node …`、`git …` 命令在 bash、zsh、PowerShell 里写法一样,原样运行即可。构建 / 测试因环境跑不起来(找不到 java、依赖拉不下来)时,用 `node <skills>/workflow/scripts/resolve.mjs env --check` 核对画像里的开发环境配置(`toolchain`)在本机是否存在,把 `resolved` 与 `current` 给用户看。需要自己写其他终端命令时,先看本机的系统和终端:`node <skills>/workflow/scripts/resolve.mjs env`(安装时记录,一次对话查一次就够;`resolve.mjs profile` / `status` 的输出里也带着 `env`)。按输出的 `os`、`shell`、`note` 写命令,不要再用 `uname`、`$PSVersionTable`、`ver` 之类去探测。
@@ -11,5 +11,15 @@
11
11
  },
12
12
  "job_annotation": "",
13
13
  "data_review": false,
14
+ "toolchain": {
15
+ "java_home": "",
16
+ "maven": {
17
+ "settings": "",
18
+ "repository": ""
19
+ },
20
+ "gradle_user_home": "",
21
+ "node": ""
22
+ },
23
+ "env_vars": {},
14
24
  "notes": []
15
25
  }
@@ -12,6 +12,9 @@
12
12
  | `deploy.regions` | 可选的部署环境 / Region 列表 | 技术 / 上线方案 |
13
13
  | `job_annotation` | 定时任务注解(如 `@XxlJob`、`@Scheduled`) | 技术方案「定时任务变更」 |
14
14
  | `data_review` | 上线前是否需要数据团队评审库表(`true` / `false`) | 技术方案模块 7、上线方案 2.3 |
15
+ | `toolchain` | 可选,开发环境配置:`java_home`、`maven.{settings,repository}`、`gradle_user_home`、`node`,都是路径。**写本机绝对路径,或在 `env_vars` 里声明的变量**(如 `$JAVA_HOME`、`%JAVA_HOME%`);路径里有空格照常写。写入前用 `resolve.mjs env --check` 核对文件 / 目录确实存在。各人机器不同:团队共用一份环境约定时写在这里,个人机器不同就写进 `profile.local.json` 的同名字段(覆盖 `profile.json`;**写 `""` 表示不用团队那个值**,见下)。`maven.settings` 按 Maven 的实际规则取值:全局(`$MAVEN_HOME/conf/settings.xml`)与用户级(`~/.m2/settings.xml`)**会合并、以用户级为准**,所以推断出的通常是用户级那份,全局的记在 `evidence` 里 | dev 构建与测试(编译 / 测试跑不起来时先核对)、交付说明与上线方案的「环境」说明 |
16
+ | `env_vars` | 可选,`toolchain` 里用到的变量与构建需要额外设置的环境变量,如 `{"JAVA_HOME": "/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home", "MAVEN_OPTS": "-Xmx2g"}`。**不含凭据**(token、密码、内网密钥一律不写) | `toolchain` 的变量展开、dev 写终端命令 |
17
+ | `toolchain_note` | 可选,一句话的环境说明(如「启动需要本地 Redis」这类与构建环境有关的约定放 `notes`,这里只写工具链差异,如「公司镜像源在 settings.xml 里配好了」) | dev 排查环境问题 |
15
18
  | `notes` | 其他项目约定,一条一句(如「启动需要本地 Redis」) | 全部 |
16
19
  | `artifacts` | 可选:`{ "project": "order-service", "commit_prompt": true }`。`project` 是这个项目的产物目录名(产物在 `workflow-run/docs/requirements/<需求名>/<项目名>/`,随代码提交和独立文档仓库两种用法都一样):随代码提交时不配置就取 git 远端仓库名,第一次落盘时自动写进来;独立文档仓库时要先配置;`commit_prompt: false` 关闭写完后的提交提示。个人设置可写在 `workflow/profile.local.json`(不提交)覆盖 | 见 `references/requirements.md`,用 `requirement.mjs config` 修改 |
17
20
  | `review` | 可选,审查范围与项目规则,如 `{"exclude": ["**/legacy/**"], "include": ["docs/api/**/*.md"], "rules": [{"path": "**/*Mapper.xml", "rule": "分页查询必须带 ORDER BY 主键"}]}`。`exclude` 不审;`include` 把默认跳过的生成文件、文档、本工具产物加回来(删除、二进制、敏感文件加不回来);`rules` 按 glob 匹配文件路径(不含 `/` 的模式匹配文件名,`**` 跨目录,`{a,b}` 多选),匹配到的规则和审查清单一起检查,不替换清单 | `review-scope.mjs`(project-review、dev 自审) |
@@ -25,12 +28,37 @@
25
28
 
26
29
  - `env`:`{ "os": "windows", "shell": "pwsh" }`,安装 / 升级时由安装器检测并写入,不用手填。`os` 是 `windows` / `macos` / `linux`;`shell` 在 Windows 上是 `pwsh`(PowerShell 7)或 `powershell`(Windows PowerShell 5.1,没有 `&&`),其他系统是 `$SHELL`(如 `zsh`、`bash`)。`resolve.mjs env` / `profile` / `status` 输出它,skill 写终端命令时按它来,不再自行探测;没记录时 `source` 为 `runtime`、`shell` 为 `null`,重跑安装器即可补上。
27
30
  - `artifacts`:覆盖 `profile.json` 的同名字段(目前用于 `commit_prompt`),用 `requirement.mjs config --local` 修改。
31
+ - `toolchain` / `env_vars`:覆盖 `profile.json` 的同名字段。**JDK、Maven、Node 的位置每台机器不一样**,团队共用的画像里写的是约定值,个人机器不同就写这里(`profile.local.json` 优先)。`/workflow profile` 检测到不一致时会问你是写团队的那份还是自己这份。
32
+ - **`""` 和「没写」不一样**:字段写 `""` 表示本机**不使用**团队那个值(例如团队配了内网 `settings.xml`,你的机器想用 Maven 默认配置 → `"maven": { "settings": "" }`);没写这个键才表示沿用团队值。被清空的字段不再校验、也不报缺失,`env --check` 输出里列在 `cleared`。
33
+
34
+ ## 开发环境配置(`toolchain` / `env_vars`)
35
+
36
+ 构建和测试命令能不能跑起来,往往取决于 JDK 在哪、Maven 用哪个 `settings.xml`、本地仓库在哪个盘。这些不写死在模板里,统一由画像提供:
37
+
38
+ ```json
39
+ {
40
+ "toolchain": {
41
+ "java_home": "$JAVA_HOME",
42
+ "maven": { "settings": "$HOME/.m2/settings.xml", "repository": "/data/maven-repo" },
43
+ "gradle_user_home": "",
44
+ "node": ""
45
+ },
46
+ "env_vars": { "JAVA_HOME": "/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home" }
47
+ }
48
+ ```
49
+
50
+ - 路径里的 `$VAR` / `${VAR}` / `%VAR%` 用 `env_vars` 或进程环境展开;`$HOME`(`~`)展开为当前用户目录。展开后是空的,`env --check` 会按「变量没配」报出来。
51
+ - **Maven 的取值顺序**:`maven.settings` 推断用户级 `~/.m2/settings.xml`(存在时),否则全局 `$MAVEN_HOME/conf/settings.xml`——Maven 会合并两份、用户级优先,私服和镜像通常配在用户级。`maven.repository` 取有效配置里的 `<localRepository>`(用户级优先,其次全局),都没有才是默认 `~/.m2/repository`。`settings.xml` 里注释掉的示例节点不算有效配置。
52
+ - **本项目 Node 版本可能和系统默认不同**,`node` 指本项目要用的 Node(如 `nvm` 的某个版本目录);不填就用 `PATH` 里的。
53
+ - 写入前必须核对:`node <skills>/workflow/scripts/resolve.mjs env --check` 逐个字段给出 `resolved`(展开后的路径)、`exists`,以及 `current`(本机当前进程的 `JAVA_HOME` / `MAVEN_HOME` / `NODE`)便于对照。**不存在或对不上的不要写进画像**,先问用户;用户也不知道就留空,不猜。
54
+ - 这里只放路径和普通环境变量。**token、密码、密钥、内网凭据一律不写**(画像会随代码提交);需要凭据的走 `database-query/config.json` 的 `password_env` 这类机制,或写在个人的 `profile.local.json` 之外由用户自己 export。
28
55
 
29
56
  ## 填写与更新
30
57
 
31
58
  由 `/workflow profile` 触发,或其他 skill 首次需要某个字段但它为空时触发:
32
59
 
33
- 1. 运行 `node <skills>/workflow/scripts/resolve.mjs infer-profile`:按固定规则从构建文件、Makefile、依赖与配置文件、`git symbolic-ref refs/remotes/origin/HEAD` 推断草稿,`evidence` 给出每个字段的依据,`unknown` 是推断不出的字段。脚本覆盖不到的约定再补读 `CLAUDE.md` / `AGENTS.md`、CI 配置。
60
+ 1. 运行 `node <skills>/workflow/scripts/resolve.mjs infer-profile`:按固定规则从构建文件、Makefile、依赖与配置文件、`git symbolic-ref refs/remotes/origin/HEAD` 推断草稿,`evidence` 给出每个字段的依据,`unknown` 是推断不出的字段。`toolchain` 也一起推断(构建文件对应的工具、`JAVA_HOME` / `MAVEN_HOME` / `NODE` 进程变量、`~/.m2/settings.xml` 与其 `<localRepository>`、`~/.gradle`),同样带依据。脚本覆盖不到的约定再补读 `CLAUDE.md` / `AGENTS.md`、CI 配置。
34
61
  2. 推断不出的字段(发布平台、Region、是否数据评审)问用户;用户不知道就留空,**不要沿用其他项目的取值**。
35
62
  3. 把拟写入的完整 JSON 给用户确认,确认后再写入;未确认则只在本次对话中使用。
36
- 4. 只改用户确认的字段,保留其他已有字段。
63
+ 4. 只改用户确认的字段,保留其他已有字段。`toolchain` / `env_vars` 逐个子字段合并:只覆盖用户确认过的键,`maven.settings` 这类没确认的不动。
64
+ 5. 写完再跑一次 `resolve.mjs env --check`,把不存在或展开为空的路径告诉用户(推断依据可能来自别的机器,尤其是 `JAVA_HOME`)。团队画像里的路径在本机对不上时,问用户是写进自己的 `profile.local.json` 还是更新团队那份。
@@ -38,7 +38,86 @@ function makeTargets(root) {
38
38
  return { build: has('build'), test: has('test'), lint: has('lint') };
39
39
  }
40
40
 
41
- export function inferProfile(root) {
41
+ // Maven settings.xml:注释里的示例节点不是有效配置(mvn 也会忽略),解析前先去掉 <!-- ... -->
42
+ export function stripXmlComments(xml) {
43
+ return xml.replace(/<!--[\s\S]*?-->/g, '');
44
+ }
45
+ const localRepositoryOf = (xml) => /<localRepository>\s*([^<\s]+)\s*<\/localRepository>/.exec(xml)?.[1] ?? '';
46
+
47
+ // 开发环境配置(toolchain):JDK / Maven / Gradle / Node 的位置。
48
+ // 值来自本机当前环境(进程变量、用户目录下的配置文件),但画像会随代码提交、别的机器未必一样,
49
+ // 所以每个值都带 evidence 说明依据;写入前由 resolve.mjs env --check 在本机核对存在性,再由用户确认。
50
+ // 项目文件(pom.xml 要求的 JDK 版本、.nvmrc 锁定的 Node 版本)只作为对照依据写进 evidence,不当路径用。
51
+ export function inferToolchain(root, { env = process.env } = {}) {
52
+ const tc = { java_home: '', maven: { settings: '', repository: '' }, gradle_user_home: '', node: '' };
53
+ const evidence = {};
54
+ const set = (k, v, why) => { const [a, b] = k.split('.'); if (v && !(b ? tc[a][b] : tc[a])) { if (b) tc[a][b] = v; else tc[a] = v; evidence[k] = why; } };
55
+ const exists = (f) => fs.existsSync(path.join(root, f));
56
+ const home = env.HOME || env.USERPROFILE || '';
57
+ const at = (...p) => (home ? path.join(home, ...p) : '');
58
+ const file = (p) => p && fs.existsSync(p);
59
+ const dir = (p) => { try { return !!p && fs.statSync(p).isDirectory(); } catch { return false; } };
60
+
61
+ // 项目是否用 Java / Node,决定该推断哪些工具链
62
+ const pom = read(path.join(root, 'pom.xml'));
63
+ const gradleFile = ['build.gradle.kts', 'build.gradle'].find(exists);
64
+ const javaProject = !!(pom || gradleFile || exists('.java-version'));
65
+ const nodeProject = exists('package.json') || exists('.nvmrc') || exists('.node-version');
66
+
67
+ // JDK:先记下项目要求的版本(只作依据,版本不是路径),路径取本机 JAVA_HOME;没有就问用户
68
+ if (javaProject) {
69
+ if (pom) {
70
+ const src = /<(?:maven\.compiler\.)?(?:source|release|target)>([\d.]+)</.exec(pom)?.[1]
71
+ ?? /<java\.version>([\d.]+)</.exec(pom)?.[1];
72
+ if (src) evidence['java_home.version'] = `pom.xml 要求 JDK ${src}(版本不是路径,需用户给出安装位置)`;
73
+ }
74
+ if (env.JAVA_HOME) set('java_home', env.JAVA_HOME, '本机 JAVA_HOME(别的机器可能不同,先确认)');
75
+ }
76
+
77
+ // Maven:settings.xml 与本地仓库。
78
+ // Maven 实际会合并全局($MAVEN_HOME/conf/settings.xml)与用户级(~/.m2/settings.xml)两份配置,用户级优先
79
+ // (见 https://maven.apache.org/settings.html),所以以用户级为主、全局只作补充依据,两者都记下来。
80
+ if (javaProject && !gradleFile) {
81
+ const globalSettings = env.MAVEN_HOME ? path.join(env.MAVEN_HOME, 'conf', 'settings.xml') : '';
82
+ const userSettings = at('.m2', 'settings.xml');
83
+ const hasGlobal = file(globalSettings);
84
+ const hasUser = file(userSettings);
85
+ const settings = hasUser ? userSettings : (hasGlobal ? globalSettings : '');
86
+ if (settings) {
87
+ set('maven.settings', settings, hasUser
88
+ ? `用户级 settings.xml(优先于全局${hasGlobal ? ` ${globalSettings}` : ''};Maven 会合并两份,用户配置生效)`
89
+ : `全局 settings.xml(没有用户级 ~/.m2/settings.xml,Maven 只用这一份)`);
90
+ if (hasUser && hasGlobal) evidence['maven.settings.global'] = `同时存在全局 ${globalSettings},Maven 会合并、以用户级为准;改镜像 / 私服通常改这一份`;
91
+ }
92
+ // 本地仓库:按「有效配置」推断 —— 用户级 settings.xml 里的 <localRepository> 优先,
93
+ // 其次全局 settings.xml,最后才是默认目录。注释里的示例不算(mvn 自己也会忽略)。
94
+ const userRepo = localRepositoryOf(stripXmlComments(read(userSettings)));
95
+ const globalRepo = localRepositoryOf(stripXmlComments(read(globalSettings)));
96
+ const localRepo = userRepo || globalRepo || '';
97
+ if (localRepo) set('maven.repository', localRepo, `${userRepo ? path.basename(userSettings) : path.basename(globalSettings || 'settings.xml')} 里的 <localRepository>`);
98
+ else if (dir(at('.m2', 'repository'))) set('maven.repository', at('.m2', 'repository'), '默认本地仓库目录 ~/.m2/repository');
99
+ }
100
+
101
+ // Gradle:GRADLE_USER_HOME,默认 ~/.gradle
102
+ if (gradleFile) {
103
+ if (env.GRADLE_USER_HOME) set('gradle_user_home', env.GRADLE_USER_HOME, '本机 GRADLE_USER_HOME');
104
+ else if (dir(at('.gradle'))) set('gradle_user_home', at('.gradle'), '默认 Gradle 用户目录 ~/.gradle');
105
+ }
106
+
107
+ // Node:项目锁定的版本优先(.nvmrc / .node-version / package.json engines),其次本机 node 所在目录
108
+ if (nodeProject) {
109
+ const pinned = read(path.join(root, '.nvmrc')).trim() || read(path.join(root, '.node-version')).trim();
110
+ const engines = readJson(path.join(root, 'package.json'))?.engines?.node ?? '';
111
+ if (pinned || engines) evidence['node.version'] = `${pinned ? '.nvmrc / .node-version' : 'package.json engines'} 要求 Node ${pinned || engines}(不是路径;用 nvm 等版本管理器时给出对应目录)`;
112
+ const nodeDir = env.NODE_HOME || (process.execPath && path.dirname(process.execPath));
113
+ if (nodeDir) set('node', nodeDir, '当前运行本脚本的 Node 所在目录(别的机器可能不同)');
114
+ }
115
+
116
+ // 只推断,不写文件;展开与存在性核对交给 resolve.mjs env --check
117
+ return { toolchain: tc, evidence };
118
+ }
119
+
120
+ export function inferProfile(root, { env = process.env } = {}) {
42
121
  const draft = { main_branch: '', build: '', test: '', test_single: '', lint: '', config_center: '', job_annotation: '' };
43
122
  const evidence = {};
44
123
  const set = (k, v, why) => { if (v && !draft[k]) { draft[k] = v; evidence[k] = why; } };
@@ -123,10 +202,22 @@ export function inferProfile(root) {
123
202
  }
124
203
  }
125
204
 
205
+ // 开发环境配置(toolchain):路径多半是本机值,写入前必须用 env --check 核对并让用户确认
206
+ const { toolchain, evidence: tcEvidence } = inferToolchain(root, { env });
207
+ for (const [k, v] of Object.entries(tcEvidence)) evidence[`toolchain.${k}`] = v;
208
+
209
+ const tcEmpty = [
210
+ ...(toolchain.java_home ? [] : ['toolchain.java_home']),
211
+ ...(toolchain.maven.settings ? [] : ['toolchain.maven.settings']),
212
+ ...(toolchain.maven.repository ? [] : ['toolchain.maven.repository']),
213
+ ...(toolchain.gradle_user_home ? [] : ['toolchain.gradle_user_home']),
214
+ ...(toolchain.node ? [] : ['toolchain.node']),
215
+ ];
216
+
126
217
  return {
127
- draft,
218
+ draft: { ...draft, toolchain },
128
219
  evidence,
129
- unknown: [...Object.keys(draft).filter((k) => !draft[k]), 'deploy.platform', 'deploy.regions', 'data_review'],
130
- note: '草稿仅供确认:给用户看完整 JSON,确认后再写入 workflow/profile.json;已有非空字段不要被草稿覆盖,除非用户同意。',
220
+ unknown: [...Object.keys(draft).filter((k) => !draft[k]), ...tcEmpty, 'deploy.platform', 'deploy.regions', 'data_review'],
221
+ note: '草稿仅供确认:给用户看完整 JSON,确认后再写入 workflow/profile.json;已有非空字段不要被草稿覆盖,除非用户同意。toolchain 的路径多是本机值,先用 resolve.mjs env --check 核对存在性再写;团队画像里对不上的写进个人的 profile.local.json。',
131
222
  };
132
223
  }
@@ -10,6 +10,7 @@
10
10
  // node .agents/skills/workflow/scripts/resolve.mjs category <skill> # 该 skill 写入 DocNexus 时的显式分类
11
11
  // node .agents/skills/workflow/scripts/resolve.mjs status # 能力探测(/workflow status)
12
12
  // node .agents/skills/workflow/scripts/resolve.mjs env # 本机系统与终端(安装时记录),写终端命令按它来
13
+ // node .agents/skills/workflow/scripts/resolve.mjs env --check # 核对画像里的开发环境配置(JDK / Maven / Node 路径)在本机是否存在
13
14
  // 输出 JSON。退出码:0 正常;2 缺该模板(知识库里没有,或未接入 DocNexus 而该模板没有默认版本;按输出的 hint 处理);1 参数错误 / 该 skill 没有声明分类
14
15
 
15
16
  import fs from 'node:fs';
@@ -145,6 +146,78 @@ export function readEnv() {
145
146
  return { ...env, ...(note ? { note } : {}) };
146
147
  }
147
148
 
149
+ // 开发环境配置(toolchain):画像里的路径可能写 $JAVA_HOME / ${VAR} / %VAR% / ~,用 env_vars 与进程环境展开。
150
+ // env_vars 优先(画像里显式声明的是对的),缺失的键回退到本机进程环境(HOME、USERPROFILE 这类)。
151
+ // 展开后为空 = 变量确实没配(不是「留空」,要报出来);路径里有空格照常,不做 shell 求值。
152
+ const VAR_RE = /\$\{(\w+)\}|\$(\w+)|%(\w+)%/g;
153
+ export function expandPath(v, vars = {}, env = process.env) {
154
+ if (typeof v !== 'string' || !v) return { raw: v ?? '', resolved: '', missing: [] };
155
+ const look = (n) => (vars[n] !== undefined && vars[n] !== '' ? vars[n] : (env[n] ?? ''));
156
+ const home = look('HOME') || look('USERPROFILE') || '';
157
+ let s = v;
158
+ if (s === '~' || s.startsWith('~/') || s.startsWith('~\\')) s = home ? home + s.slice(1) : s;
159
+ const missing = [];
160
+ for (const m of v.matchAll(VAR_RE)) { const n = m[1] ?? m[2] ?? m[3]; if (!look(n)) missing.push(n); }
161
+ const resolved = s.replace(VAR_RE, (m, a, b, c) => look(a ?? b ?? c)).replace(/[\\/]+$/, '');
162
+ return { raw: v, resolved, missing };
163
+ }
164
+
165
+ // 画像里的 toolchain(profile.json)与个人覆盖(profile.local.json),逐个子字段合并,后者优先。
166
+ // 「没声明」和「显式写空」是两回事:本机写 "" 表示**不使用**团队那个值(用工具自己的默认配置),
167
+ // 所以要按 null / "" 区分来源,不能只看值真不真。
168
+ export function readToolchain(root) {
169
+ const dir = path.join(SKILLS_DIR, 'workflow');
170
+ const team = readJson(path.join(dir, 'profile.json')) ?? {};
171
+ const mine = readJson(path.join(dir, 'profile.local.json')) ?? {};
172
+ const declared = (o) => typeof o === 'string';
173
+ // 个人声明过(含 "")就以个人为准;否则用团队值。cleared 表示被个人显式留空
174
+ const merge = (t, m) => {
175
+ if (declared(m)) return m === '' ? { value: '', source: 'local', cleared: true } : { value: m, source: 'local' };
176
+ if (declared(t) && t !== '') return { value: t, source: 'team' };
177
+ return { value: '', source: null };
178
+ };
179
+ const vars = { ...(team.env_vars ?? {}), ...(mine.env_vars ?? {}) };
180
+ const fields = {
181
+ java_home: merge(team.toolchain?.java_home, mine.toolchain?.java_home),
182
+ 'maven.settings': merge(team.toolchain?.maven?.settings, mine.toolchain?.maven?.settings),
183
+ 'maven.repository': merge(team.toolchain?.maven?.repository, mine.toolchain?.maven?.repository),
184
+ gradle_user_home: merge(team.toolchain?.gradle_user_home, mine.toolchain?.gradle_user_home),
185
+ node: merge(team.toolchain?.node, mine.toolchain?.node),
186
+ };
187
+ const out = {};
188
+ const cleared = [];
189
+ for (const [k, { value, source, cleared: c }] of Object.entries(fields)) {
190
+ if (c) { cleared.push(k); continue; } // 个人明确不用这个字段:不展开、不校验、也不报缺失
191
+ if (value) out[k] = { ...expandPath(value, vars), source };
192
+ }
193
+ // toolchain 为空时也给出 env_vars,便于 agent 知道该用什么变量写路径
194
+ return { fields: out, env_vars: vars, cleared, configured: Object.keys(out).length > 0 };
195
+ }
196
+
197
+ // 每个字段的路径在本机是否存在(写入画像前、构建跑不起来时核对用)。
198
+ // 只判断存在性,不判断版本对不对;current 是本机当前进程的值,便于和画像对照
199
+ export function checkToolchain(root, { env = process.env } = {}) {
200
+ const tc = readToolchain(root);
201
+ const check = (f) => {
202
+ // 变量没配时展开结果可能是别处真存在的路径($MISSING/bin 展开成 /bin),必须按「没配」算不通过
203
+ let exists = false;
204
+ try { exists = !f.missing.length && !!f.resolved && fs.existsSync(f.resolved); } catch { exists = false; }
205
+ const kind = exists ? (() => { try { return fs.statSync(f.resolved).isDirectory() ? 'dir' : 'file'; } catch { return 'other'; } })() : null;
206
+ return { ...f, exists, kind, ...(f.missing.length ? { unset_vars: f.missing } : {}), ...(f.raw !== f.resolved ? { expanded: true } : {}) };
207
+ };
208
+ const fields = Object.fromEntries(Object.entries(tc.fields).map(([k, f]) => [k, check(f)]));
209
+ const bad = Object.entries(fields).filter(([, f]) => !f.exists).map(([k]) => k);
210
+ return {
211
+ fields, configured: tc.configured, env_vars: tc.env_vars, ok: bad.length === 0,
212
+ missing: bad,
213
+ // 个人在 profile.local.json 里显式留空、表示不用团队那个值的字段(用工具默认配置)
214
+ ...(tc.cleared.length ? { cleared: tc.cleared } : {}),
215
+ // 本机当前值:和画像对不上时,多半是画像抄的是别人的机器
216
+ current: { JAVA_HOME: env.JAVA_HOME ?? null, MAVEN_HOME: env.MAVEN_HOME ?? null, GRADLE_USER_HOME: env.GRADLE_USER_HOME ?? null, node: path.dirname(process.execPath) },
217
+ how: tc.configured ? (bad.length ? `核对 toolchain 里不存在或变量没配的字段:${bad.join('、')}(本机值见 current);改本机的那份用 profile.local.json,见 workflow/references/profile.md` : null) : '还没有配置开发环境:运行 /workflow profile 由脚本推断后确认,见 workflow/references/profile.md',
218
+ };
219
+ }
220
+
148
221
  export function readProfile(root) {
149
222
  const f = path.join(SKILLS_DIR, 'workflow', 'profile.json');
150
223
  const profile = readJson(f);
@@ -152,7 +225,7 @@ export function readProfile(root) {
152
225
  const isEmpty = (v) => v == null || v === '' || (Array.isArray(v) && !v.length);
153
226
  const empty = profile ? PROFILE_FIELDS.filter((k) => isEmpty(get(k))) : PROFILE_FIELDS;
154
227
  return {
155
- path: rel(root, f), exists: !!profile, profile: profile ?? {}, empty, env: readEnv(),
228
+ path: rel(root, f), exists: !!profile, profile: profile ?? {}, empty, env: readEnv(), toolchain: readToolchain(root),
156
229
  complete: !!profile && PROFILE_REQUIRED.every((k) => !empty.includes(k)),
157
230
  };
158
231
  }
@@ -169,6 +242,10 @@ export function status(root) {
169
242
  // 本机系统与终端(安装时记录),写命令按它来
170
243
  env: readEnv(),
171
244
  profile: { ok: p.complete, empty: p.empty, how: p.complete ? null : '运行 /workflow profile' },
245
+ // 开发环境配置:只在配过时给出(没配不算问题,构建跑不起来时才需要)
246
+ toolchain: p.toolchain.configured
247
+ ? { configured: true, fields: Object.fromEntries(Object.entries(p.toolchain.fields).map(([k, v]) => [k, v.resolved])), ...(p.toolchain.cleared.length ? { cleared: p.toolchain.cleared } : {}) }
248
+ : { configured: false, ...(p.toolchain.cleared.length ? { cleared: p.toolchain.cleared } : {}) },
172
249
  wiki: {
173
250
  ok: wikiCfg?.enabled === true && !!wikiCfg?.wiki?.base_url,
174
251
  how: '填写 md-to-wiki/wiki-config.json(enabled + base_url + api_token),或接入 wiki MCP(脚本无法探测 MCP,请另行检查可用工具)',
@@ -235,9 +312,12 @@ function main(argv) {
235
312
  return r.ok ? 0 : 1;
236
313
  }
237
314
  case 'status': print(status(root)); return 0;
238
- case 'env': print(readEnv()); return 0;
315
+ case 'env':
316
+ // env(默认):本机系统与终端;--check:核对画像里的开发环境配置在本机是否存在
317
+ if (rest.includes('--check') || rest.includes('--resolve')) { print(checkToolchain(root)); return 0; }
318
+ print(readEnv()); return 0;
239
319
  }
240
- console.error('用法: resolve.mjs [--root <项目根>] template <skill> <文件名> | templates | profile | infer-profile | category <skill> | status | env');
320
+ console.error('用法: resolve.mjs [--root <项目根>] template <skill> <文件名> | templates | profile | infer-profile | category <skill> | status | env [--check]');
241
321
  return 1;
242
322
  }
243
323