@rowansenne/workflow-run 0.24.1 → 0.26.1

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,28 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.26.1] - 2026-10-23
8
+
9
+ ### 修复
10
+ - **`upgrade --with-docnexus` 现在真的会把 DocNexus 升到最新版**:接入与升级统一改用 `@rowansenne/docnexus@latest`。此前传的是不带 tag 的包名,npm 会参考 `.docnexus/package.json` 里 `--save-exact` 锁定的版本,把旧版本原地装回来,命令看起来成功、DocNexus 却没升级。离线 / 本地验证用 `DOCNEXUS_PKG` 指向目录或 tgz 时不追加 `@latest`(本地来源没有 tag)。
11
+ - 修正文档里「upgrade 不会升级 DocNexus 包」的说法:`upgrade` 不带 `--with-docnexus` 时确实只升级 skills、DocNexus 包原地不动;带上该参数则会在重装技能的同时升级 DocNexus 包、刷新 skill 链接与 `CLAUDE.md` / `AGENTS.md` 段落,并导入新增模板。`--help` 与 README 命令表同步重写该参数说明。
12
+
13
+ ### 变更
14
+ - 架构页的分层图改为「代理 · 用户 / 用户 view / 工作流编排 / 工具 / 底层文档库」:原来的五层里,「编排」与「共用核心」合并为「工作流编排」(脚本由流程调用,不构成独立能力层),DocNexus 从「能力」移到「底层文档库」(它的产物与需求产物同在 `workflow-run/` 下),新增「用户 view」一条,容纳 `view.mjs`、`docnexus view`、`report.mjs` 这类只读回看入口。README 的架构描述同步更新。
15
+ - 架构页的 DocNexus 命令表补上 `docnexus view`(本地只读浏览知识库),并说明它是前台常驻、`Ctrl+C` 停止、`--port` 可指定端口。
16
+
17
+ ## [0.26.0] - 2026-10-22
18
+
19
+ ### 变更
20
+ - dev 不再向用户确认规模:S / M / L 由 agent 按标准判定并说明依据,直接按该规模推进,用户主动提出异议或范围变化时按新规模继续。S 规模符合快速通道条件时改为明确询问用户是否走快速通道,问完等本轮答复再继续:明确同意才走,拒绝或答复未明确同意按完整流程落盘交付;缺陷修复同样处理,M / L 规模先把根因与修复方式给用户确认再改代码。
21
+
22
+ ## [0.25.0] - 2026-10-22
23
+
24
+ ### 新增
25
+ - **项目画像新增开发环境配置 `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。
26
+ - `toolchain` 的值按 Maven 的实际规则取值:全局(`$MAVEN_HOME/conf/settings.xml`)与用户级(`~/.m2/settings.xml`)会合并、**以用户级为准**,所以推断用户级那份(原来优先取全局,会丢掉用户的私服与镜像配置),全局的存在时记在 `evidence` 里。`<localRepository>` 同样按有效配置推断,并且**忽略 `settings.xml` 注释里的示例节点**(原来会把注释中的 `/path/to/local/repo` 当成真实仓库)。
27
+ - 个人的 `profile.local.json` 里把 `toolchain` 某字段写 `""` 表示**不使用**团队那个值(例如用 Maven 默认配置),原来空字符串不算覆盖、仍继承团队路径。被清空的字段不再校验、不报缺失,`env --check` / `status` / `profile` 输出里列在 `cleared`。
28
+
7
29
  ## [0.24.1] - 2026-10-21
8
30
 
9
31
  ### 修复
@@ -340,7 +362,8 @@
340
362
  - 安装器改为单一 Node 实现,`init-skills.sh` / `init-skills.ps1` 只是薄封装。
341
363
  - `code-review` 改名为 `project-review`,升级时自动清理旧名。
342
364
 
343
- [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.21.0...HEAD
365
+ [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.26.0...HEAD
366
+ [0.26.0]: https://github.com/guilianerde/skills-init/compare/v0.25.0...v0.26.0
344
367
  [0.21.0]: https://github.com/guilianerde/skills-init/compare/v0.20.0...v0.21.0
345
368
  [0.20.0]: https://github.com/guilianerde/skills-init/compare/v0.19.0...v0.20.0
346
369
  [0.19.0]: https://github.com/guilianerde/skills-init/compare/v0.18.0...v0.19.0
package/README.md CHANGED
@@ -27,7 +27,7 @@ npx @rowansenne/workflow-run --with-docnexus --with-codegraph
27
27
 
28
28
  全量安装的分步说明见图文教程 [docs/full-install.html](docs/full-install.html)(下载后用浏览器打开)。
29
29
 
30
- 整体架构(五层结构、dev 主线流程、设计约定、产物与知识闭环)见 [docs/architecture.html](docs/architecture.html)(下载后用浏览器打开)。
30
+ 整体架构(代理 / 用户 view / 工作流编排 / 工具 / 底层文档库四层、dev 主线流程、设计约定、产物与知识闭环)见 [docs/architecture.html](docs/architecture.html)(下载后用浏览器打开)。
31
31
 
32
32
  非交互示例(bash、zsh、PowerShell 写法一样):
33
33
 
@@ -44,7 +44,7 @@ npx @rowansenne/workflow-run uninstall
44
44
  | 命令 | 作用 |
45
45
  | --- | --- |
46
46
  | `init`(默认,可省略) | 安装或重装 |
47
- | `upgrade` | 读安装清单里记录的环境与模式,非交互升级到当前版本:保留数据库配置等用户配置,skills 文件一律更新为新版(手改过的先备份到 `workflow-run/tmp/upgrade-backup/`);已接入 DocNexus 时顺带导入新版本新增的文档模板(只新增,不改动已有模板) |
47
+ | `upgrade` | 读安装清单里记录的环境与模式,非交互升级到当前版本:保留数据库配置等用户配置,skills 文件一律更新为新版(手改过的先备份到 `workflow-run/tmp/upgrade-backup/`)。加 `--with-docnexus` 时把 DocNexus 包也升到最新版(`@latest`),刷新 skill 链接与 `CLAUDE.md` / `AGENTS.md` 段落,并导入新增的文档模板(只新增,不改动已有模板);不加则 DocNexus 包原地不动,只顺带导入新增模板 |
48
48
  | `doctor` | 自检:已装版本与当前包对比、手改 / 缺失的文件、软链接是否断开、项目画像、DocNexus 模板是否齐全、各数据库连接能否只读 `SELECT 1`(`--offline` 跳过)、`.gitignore` 是否忽略了凭据。有错误时退出码 1 |
49
49
  | `uninstall` | 等价于 `--uninstall` |
50
50
  | `report` | 本地效果报告:只读统计 `workflow-run/docs/requirements/` 下的产物(`--since YYYY-MM-DD`、`--all-branches`、`--json`),见「workflow:入口与项目画像」 |
@@ -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
 
@@ -194,9 +210,9 @@ npx @rowansenne/workflow-run@latest doctor --offline
194
210
  /dev --bug <现象 | 报错 | 日志> # 缺陷修复
195
211
  ```
196
212
 
197
- * **功能开发**:理解需求(可读 PRD、查 DocNexus 历史决策)→ 调研现有代码 → 需求澄清(借鉴 grill-me:按决策树分轮提问,每题给推荐答案;事实自己查、决定交给用户;S 规模无未定决定时跳过澄清提问)→ **规模确认(S / M / L 都说明判定依据、对应流程并等用户确认)** → 按规模出方案(S 列步骤开始;M 对话内方案待确认;L 落盘方案文档待确认)→ 小步实现并编译 → 补测试并运行 → 用 project-review 自审 → 交付说明落盘到 `workflow-run/docs/requirements/<需求名>/<项目名>/delivery.md`(带分支元信息;改动、验证结果、配置清单、未执行的 SQL),已接入 DocNexus 时每次都问要不要沉淀(同意后以 `dev-notes` 分类写入,并把库内路径记回交付说明)。审查和上线方案会按分支自动读取这份交付说明。
198
- * **S 规模快速通道**:先经用户确认 S 规模及快速通道,再按条件执行:不涉及配置、SQL / DDL、对外接口、表结构,也没有需要澄清的业务决定时,编译、测试、自审照常,但只在对话里交付,不落盘交付说明、不问沉淀;实现中碰到上述任何一项或改动超过 3 个文件,就退回完整流程,规模变化时先确认新规模。规模确认本身不影响快速通道资格。缺陷修复同样适用(根因仍须在对话里写清楚)。
199
- * **缺陷修复**:收集现象 → 用失败测试复现 → 用证据定位根因 → **判定规模并等用户确认** → 最小修复让测试转绿 → 自审 → 交付(必须写明根因)。
213
+ * **功能开发**:理解需求(可读 PRD、查 DocNexus 历史决策)→ 调研现有代码 → 需求澄清(借鉴 grill-me:按决策树分轮提问,每题给推荐答案;事实自己查、决定交给用户;S 规模无未定决定时跳过澄清提问)→ **规模判定(S / M / L 说明判定依据,不再询问确认)** → 按规模出方案(S 列步骤开始、符合条件时问是否走快速通道;M 对话内方案待确认;L 落盘方案文档待确认)→ 小步实现并编译 → 补测试并运行 → 用 project-review 自审 → 交付说明落盘到 `workflow-run/docs/requirements/<需求名>/<项目名>/delivery.md`(带分支元信息;改动、验证结果、配置清单、未执行的 SQL),已接入 DocNexus 时每次都问要不要沉淀(同意后以 `dev-notes` 分类写入,并把库内路径记回交付说明)。审查和上线方案会按分支自动读取这份交付说明。
214
+ * **S 规模快速通道**:规模判定不询问用户;S 规模且不涉及配置、SQL / DDL、对外接口、表结构,也没有需要澄清的业务决定时,**问用户是否走快速通道**,问完等本轮答复——明确同意才走:编译、测试、自审照常,但只在对话里交付,不落盘交付说明、不问沉淀;拒绝或未明确同意就按完整流程落盘交付。实现中碰到上述任何一项或改动超过 3 个文件,就退回完整流程,规模变化时按新规模继续。缺陷修复同样适用(根因仍须在对话里写清楚)。
215
+ * **缺陷修复**:收集现象 → 用失败测试复现 → 用证据定位根因 → **判定规模并告知(S 规模问是否走快速通道;M / L 确认根因与修复方式后再改)** → 最小修复让测试转绿 → 自审 → 交付(必须写明根因)。
200
216
  * 守则:不写库、不改凭据、不擅自 commit / push、不装新依赖;测试没跑就写「未验证」。
201
217
  * 其他 skill 都是可选增强:wiki、DocNexus、数据库没配置时自动跳过,并在交付说明中注明。
202
218
 
@@ -286,7 +302,7 @@ node .agents/skills/workflow/scripts/docnexus.mjs usage export > workflow-run/do
286
302
  npx @rowansenne/workflow-run upgrade --with-docnexus
287
303
  ```
288
304
 
289
- 新项目用 `npx @rowansenne/workflow-run --with-docnexus`。离线或本地验证可设 `DOCNEXUS_PKG=/path/to/package-or-source` 指定安装来源;默认从 npm 安装,必须使用支持 format 7 和 `--output-dir` 的版本。
305
+ 新项目用 `npx @rowansenne/workflow-run --with-docnexus`。默认从 npm 安装最新版(`@rowansenne/docnexus@latest`),接入和升级都取最新版;离线或本地验证可设 `DOCNEXUS_PKG=/path/to/package-or-source` 指定安装来源(此时不再追加 `@latest`),必须使用支持 format 7 和 `--output-dir` 的版本。
290
306
 
291
307
  ## CodeGraph 代码图谱与 code-graph skill(可选)
292
308
 
@@ -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 |
package/bin/lib/core.mjs CHANGED
@@ -24,12 +24,16 @@ export const resolveSkillSelection = (names) => withDependencies(SKILL_LIST, nam
24
24
  export const SKILLS = META.skills;
25
25
  // 用户配置:重跑不覆盖;值为对应空模板(用于判断单一来源中的文件是否还没被填写过)
26
26
  export const USER_FILES = META.userFiles;
27
+ // 接入 / 重装都用最新版:@latest 让 npm 显式解析最新 tag,不受 .docnexus/package.json 里
28
+ // 已锁版本的牵制(否则重跑会把旧版本原地装回来)。离线或本地验证时 DOCNEXUS_PKG 可指向
29
+ // 本地目录 / tgz,此时不再追加 @latest(本地来源没有 tag)。
27
30
  export const DOCNEXUS_PKG = process.env.DOCNEXUS_PKG || '@rowansenne/docnexus';
31
+ export const DOCNEXUS_SPEC = process.env.DOCNEXUS_PKG ? DOCNEXUS_PKG : `${DOCNEXUS_PKG}@latest`;
28
32
  export const CODEGRAPH_PKG = process.env.CODEGRAPH_PKG || '@colbymchenry/codegraph@1'; // code-graph skill 按 1.x 的库接口编写
29
33
  // 工作流产物与 DocNexus 知识输出的父目录;DocNexus 运行环境另在项目根 .docnexus/。
30
34
  export const HOME = 'workflow-run';
31
35
  // 升级 DocNexus 的命令(在项目根执行;不用 cd / &&,PowerShell 5.1 也能直接运行)
32
- export const docnexusUpgradeCmd = () => `npm install --prefix ./.docnexus --save-exact ${DOCNEXUS_PKG}@latest`;
36
+ export const docnexusUpgradeCmd = () => `npm install --prefix ./.docnexus --save-exact ${DOCNEXUS_SPEC}`;
33
37
  export const MANIFEST = '.workflow-run.json'; // 安装清单,位于每个安装目录(单一来源时只有 .agents/skills 一份)
34
38
 
35
39
  // ---------------------------------------------------------------- 输出
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import { templateStatus, STATUS_TEXT, TEMPLATES } from '../seed-templates.mjs';
7
- import { TPL_DIR, IS_WIN, DOCNEXUS_PKG, MANIFEST, PKG, CANON_DIR, step, ok, warn, fail, writeFile, readJson, toPosix, lstat, findManifests, removeLink, docnexusBin, docnexusWorkspace, docnexusInternal, HOME, INSTALL_BASES } from './core.mjs';
7
+ import { TPL_DIR, IS_WIN, DOCNEXUS_SPEC, MANIFEST, PKG, CANON_DIR, step, ok, warn, fail, writeFile, readJson, toPosix, lstat, findManifests, removeLink, docnexusBin, docnexusWorkspace, docnexusInternal, HOME, INSTALL_BASES } from './core.mjs';
8
8
 
9
9
  import { docnexusConfigured, docnexusConnected } from '../../workflow/scripts/docnexus-layout.mjs';
10
10
  export { docnexusConnected } from '../../workflow/scripts/docnexus-layout.mjs';
@@ -177,7 +177,7 @@ export function refreshDocNexusLinks(root, sel) {
177
177
  }
178
178
 
179
179
  export function setupDocNexus(root, sel) {
180
- const npmArgs = ['install', '--prefix', './.docnexus', '--save-exact', DOCNEXUS_PKG];
180
+ const npmArgs = ['install', '--prefix', './.docnexus', '--save-exact', DOCNEXUS_SPEC];
181
181
  step(`接入 DocNexus(运行环境 .docnexus/;知识输出 ${HOME}/docnexus/)`);
182
182
  if (!run(IS_WIN ? 'npm.cmd' : 'npm', npmArgs, root)) { warn('npm 安装 DocNexus 失败,已跳过 DocNexus 接入(skills 已正常安装)'); return 'failed'; }
183
183
  const bin = docnexusBin(root);
@@ -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 都从业务项目根使用配置的知识输出。
@@ -35,7 +35,7 @@ const HELP = `用法: workflow-run [命令] [选项]
35
35
  命令:
36
36
  init(默认) 安装或重装 skills
37
37
  upgrade 按安装清单里的环境与模式非交互升级到当前版本(保留数据库配置等用户配置;skills 文件一律更新为新版,手改过的先备份到 workflow-run/tmp/upgrade-backup/);
38
- 已接入 DocNexus 时顺带导入新增的文档模板(只新增,不覆盖已有模板)
38
+ 加 --with-docnexus 时把 DocNexus 包也升到最新版(@latest),刷新 skill 链接与 CLAUDE.md / AGENTS.md 段落,并导入新增的文档模板(只新增,不覆盖已有模板);不加则 DocNexus 包原地不动,只顺带导入新增模板
39
39
  doctor 自检:版本、手改 / 缺失文件、软链接、项目画像、DocNexus 模板、数据库连通性、.gitignore
40
40
  uninstall 按安装清单卸载本项目装的 skills 与链接(用户配置备份到 workflow-run/tmp/uninstall-backup/)
41
41
  seed-templates 只把自带的文档模板导入 DocNexus(workflow-run/docnexus/library/templates/,已有的不覆盖)
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
  - 属于本需求的:和你之后的改动同等对待,影响面、相关测试、自审、交付说明「改动」节都纳入。
@@ -57,7 +58,7 @@ description: "Implement a feature or fix a bug in this project end to end: under
57
58
 
58
59
  | 情况 | 做法 |
59
60
  |---|---|
60
- | S 规模且 F2 后没有未定的决定 | 跳过需求澄清提问,写明本次的理解和假设;仍须按 F4 确认规模 |
61
+ | S 规模且 F2 后没有未定的决定 | 跳过需求澄清提问,写明本次的理解和假设;规模在 F4 判定并告知用户,不再确认 |
61
62
  | 只有一两个小决定 | 问一轮即可 |
62
63
  | M / L 规模,或用户说「拷问一下 / grill me / 先把需求问清楚」 | 按轮问到前沿为空,输出需求要点、验收标准、不在本次范围、已确认的决定,**等用户确认理解一致**再进 F4 |
63
64
 
@@ -67,17 +68,17 @@ description: "Implement a feature or fix a bug in this project end to end: under
67
68
 
68
69
  | 规模 | 判定 | 做法 |
69
70
  |---|---|---|
70
- | **S** | ≤3 个文件,无接口签名 / 表结构 / 配置项变更,不涉及权限 / 资金 | 用户确认规模后,在对话中列改动步骤并开始 |
71
+ | **S** | ≤3 个文件,无接口签名 / 表结构 / 配置项变更,不涉及权限 / 资金 | 在对话中列改动步骤并开始;符合快速通道条件时先问用户是否走(见下) |
71
72
  | **M** | 新增或修改接口、配置项、少量表字段 | 在对话中给出简要方案(方案模板的精简版),**等用户确认后再写代码** |
72
73
  | **L** | 新模块、跨服务、表结构较大变更、数据迁移 | 按方案模板写方案文档落盘(见 Artifact Location),用户确认后再写代码;完成后建议用 `create-tech-md-doc` 生成正式技术方案 |
73
74
 
74
75
  规模拿不准时按大一级处理。涉及权限(鉴权、角色、数据可见范围)或资金(金额计算、扣款、退款、对账)的改动,文件再少也至少按 M:需求理解(F3)和方案都要用户确认。
75
76
 
76
- **规模确认(S / M / L 都必做,含快速通道)**:判定后向用户说明推荐规模、依据(预计改动文件 / 模块、接口 / 数据 / 配置变化及风险),以及对应流程(是否走快速通道、方案与交付说明是否落盘),明确询问是否按该规模处理。**等用户明确确认后,再按该规模推进后续流程、绑定需求或写代码**;只告知判定结果或默认选中推荐项不算确认。可以与 F3 的理解确认或简要方案确认在同一轮提出,但必须明确列出规模并收到确认;本轮对话中已明确确认的同一规模不重复询问。用户调整范围或实现中发现规模变化时,说明变化依据,确认新规模后再继续。规模确认属于流程选择,不算 F3 的业务决定,不影响 S 规模快速通道资格。
77
+ **规模判定(S / M / L 都必做,不再询问用户)**:规模由你按上表判定,向用户说明判定的规模与依据(预计改动文件 / 模块、接口 / 数据 / 配置变化及风险)即可,**不询问、不等确认**,直接按该规模推进后续流程;用户主动提出不同意见时,按用户说的规模继续。用户调整范围或实现中发现规模变化时,说明变化依据,按新规模继续。规模判定属于流程选择,不算 F3 的业务决定,不影响 S 规模快速通道资格。
77
78
 
78
- **M / L 规模先定需求名**:用户确认规模、确定要落盘时,按 Artifact Location 运行 `requirement.mjs which`,当前分支没绑定需求就在这一轮问用户这个需求叫什么(可以和规模确认、澄清问题一起问),之后的方案、交付、审查、技术方案、上线方案都归到这个需求下。
79
+ **M / L 规模先定需求名**:判定为 M / L、确定要落盘时,按 Artifact Location 运行 `requirement.mjs which`,当前分支没绑定需求就在这一轮问用户这个需求叫什么(可以和简要方案确认、澄清问题一起问),之后的方案、交付、审查、技术方案、上线方案都归到这个需求下。
79
80
 
80
- **S 规模快速通道**:用户已确认 S 规模,且改动**不涉及**配置项、SQL / DDL、对外接口(HTTP 接口签名、MQ 消息体、对外 DTO)、表结构、权限、资金,也没有 F3 中确认过的业务决定 → 走快速通道:F5~F7 照常(编译、测试、自审都不省),F8 只在对话里交付,不落盘交付说明、不问 DocNexus 沉淀(见 F8「快速通道交付」)。规模确认时说明「按快速通道处理,不落盘交付说明」。实现中一旦碰到上面任何一项、改动超过 3 个文件,或用户要求落盘 → 退出快速通道,按完整 F8 交付;重新评估后规模变化的,先确认新规模再继续。小改动也要走完整流程是用户绕开 dev 的主要原因,所以只在上面这些会影响上线和团队的情况下才落盘。
81
+ **S 规模快速通道(问用户)**:判定为 S 规模,且改动**不涉及**配置项、SQL / DDL、对外接口(HTTP 接口签名、MQ 消息体、对外 DTO)、表结构、权限、资金,也没有 F3 中确认过的业务决定时,**明确询问用户是否走快速通道**:说明快速通道下 F5~F7 照常(编译、测试、自审都不省),F8 只在对话里交付,不落盘交付说明、不问 DocNexus 沉淀(见 F8「快速通道交付」)。**问完等本轮答复再继续:明确同意才走快速通道;拒绝或答复没有明确同意,就按完整 F8 落盘交付。** 实现中一旦碰到上面任何一项、改动超过 3 个文件,或用户要求落盘 → 退出快速通道,按完整 F8 交付;重新评估后规模变化的,说明变化依据,按新规模继续。小改动也要走完整流程是用户绕开 dev 的主要原因,所以只在上面这些会影响上线和团队的情况下才落盘。
81
82
 
82
83
  方案模板:在项目根运行 `node <skills>/workflow/scripts/resolve.mjs template dev plan-template.md`,读输出的 `path`;退出码 2(已接入 DocNexus 但缺模板)时停下并按 `hint` 处理。L 规模的方案文档落盘后运行 `node <skills>/workflow/scripts/lint-doc.mjs <方案路径> --skill dev` 自检,再请用户确认。
83
84
 
@@ -143,13 +144,13 @@ description: "Implement a feature or fix a bug in this project end to end: under
143
144
 
144
145
  ### B4 — 修复与验证
145
146
 
146
- - 修复前按 F4 的标准判定规模,并完成同样的规模确认(S / M / L 都适用,说明是否走快速通道及落盘要求);用户明确确认后再改代码。修复中规模变化时先确认新规模再继续。
147
+ - 修复前按 F4 的标准判定规模并告知用户(规模不询问确认);S 规模符合快速通道条件时照 F4 问用户是否走快速通道,问完等本轮答复再改代码:明确同意才走快速通道,拒绝或未明确同意按完整流程落盘交付。M / L 规模把根因结论和修复方式当作简要方案给用户确认后再改代码。修复中规模变化时说明变化依据,按新规模继续。
147
148
  - 做能消除根因的**最小改动**;B2 的测试由红变绿,相关已有测试保持通过(运行方式同 F6)。
148
149
  - 如果历史脏数据需要修正,写成 SQL 脚本交给用户,不直接执行。
149
150
 
150
151
  ### B5 — 自审与交付
151
152
 
152
- 同 F7、F8(交付说明同样落盘,「类型」写缺陷修复)。快速通道同样适用于缺陷修复:S 规模、不涉及配置 / SQL / 对外接口 / 表结构 / 权限 / 资金,且同类风险排查没有发现其他调用方受影响时,在对话里交付,但根因(证据链、修复方式)必须在对话里写清楚。交付说明的「根因」节必须写:证据链、修复方式、为什么这样修、同类风险排查结果;DocNexus 沉淀时根因是首要候选。
153
+ 同 F7、F8(交付说明同样落盘,「类型」写缺陷修复)。快速通道同样适用于缺陷修复:S 规模、用户在 B4 已同意走快速通道,不涉及配置 / SQL / 对外接口 / 表结构 / 权限 / 资金,且同类风险排查没有发现其他调用方受影响时,在对话里交付,但根因(证据链、修复方式)必须在对话里写清楚。交付说明的「根因」节必须写:证据链、修复方式、为什么这样修、同类风险排查结果;DocNexus 沉淀时根因是首要候选。
153
154
 
154
155
  ## Rules
155
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowansenne/workflow-run",
3
- "version": "0.24.1",
3
+ "version": "0.26.1",
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