@rowansenne/workflow-run 0.18.0 → 0.20.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 +21 -1
- package/README.md +16 -16
- package/bin/lib/codegraph.mjs +2 -2
- package/bin/lib/core.mjs +4 -2
- package/bin/lib/db.mjs +1 -1
- package/bin/lib/docnexus.mjs +8 -3
- package/bin/lib/doctor.mjs +9 -8
- package/bin/lib/install.mjs +9 -6
- package/bin/lib/uninstall.mjs +4 -4
- package/bin/workflow-run.mjs +14 -14
- package/create-release-md-doc/SKILL.md.tpl +2 -2
- package/create-tech-md-doc/SKILL.md.tpl +2 -2
- package/dev/SKILL.md.tpl +2 -2
- package/package.json +2 -1
- package/project-review/SKILL.md.tpl +24 -10
- package/project-review/references/checklist.md.tpl +1 -0
- package/project-review/references/diff-strategy.md +32 -1
- package/project-review/references/report.md.tpl +5 -3
- package/workflow/references/codegraph.md +1 -1
- package/workflow/references/requirements.md +3 -2
- package/workflow/scripts/artifacts.mjs +2 -2
- package/workflow/scripts/infer-profile.mjs +1 -1
- package/workflow/scripts/requirement.mjs +51 -19
- package/workflow/scripts/view.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.20.0] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### 变更
|
|
10
|
+
- **project-review 独立复核**:能派子 agent 的环境(Claude Code、Codex、Qwen Code 等)里,CRITICAL / HIGH 每条交给一个只拿到结论、位置、触发条件的子 agent 重新读代码判定「成立 / 可能成立 / 不成立」,不成立的删掉并在「未覆盖与待确认」记复核排除的条数;派不了时自己复核,先找不成立的理由。复核方式写进报告「审查方式」。
|
|
11
|
+
- **project-review 分角度并行查找**(深审大改动,用户确认后):深审、改动超过 30 个文件或 1500 行、环境能派子 agent 时,先问用户要不要分 5 个角度(正确性与字段完整性 / 并发事务与幂等 / 安全与异常 / 数据库与配置 / 性能测试与语言专项)各派一个子 agent 同时查;同意后主 agent 合并去重、做影响面,再交独立复核。不同意照旧分批精读。
|
|
12
|
+
- **project-review 审查深度与降噪**:分快审 / 标准(默认)/ 深审,写进「审查方式」。快审只查正确性、空值、安全、事务与幂等;标准不查风格;深审全部维度并复核 MEDIUM。新增「只报会出问题的」:风格、linter 能拦的、个人偏好、写不出后果的建议不报,LOW 只写有实际收益的优化。
|
|
13
|
+
- **需求目录里每种产物只留最新版,文件名固定**(`review.md`、`tech-doc.md` …,不再生成 `_v2`、`_v3`),上线方案引用技术方案、复审引用的产物等不会因为出了新版而失效。版本号记在 `meta.json` 的 `versions`。`requirement.mjs path` 发现已有旧版时,先把它复制到 `workflow-run/tmp/old-artifacts/<需求>/<种类>_v<N>.md`(`previous` 指向它,复审对照用),新版直接写到原文件;`sync <path>` 时内容变了才算新版本,输出 `archived` 列出归档的文件,由 skill 提示用户确认后自己删除。`/workflow report`、`/workflow view` 只统计目录里的最新版,`view` 显示版本号。
|
|
14
|
+
- `/workflow view` 的审查记录按报告内容判断首次 / 复审(原来按排序位置,只剩一份报告时会把复审标成首次)。
|
|
15
|
+
|
|
16
|
+
## [0.19.0] - 2026-10-18
|
|
17
|
+
|
|
18
|
+
### 变更(破坏性)
|
|
19
|
+
- **去掉「同时安装到 Codex 用户级 `~/.codex/skills`」的询问**:只装到项目里。之前选过的,`~/.codex/skills` 下本工具的 skill 需要自己删除。
|
|
20
|
+
- 目录里没有安装清单时,和新版不同的文件仍然备份后覆盖,但不再逐个报「被手动修改过」,改为一行汇总(哪个目录、备份了几个文件)。
|
|
21
|
+
|
|
22
|
+
### 新增
|
|
23
|
+
- **支持腾讯 WorkBuddy**(`--envs workbuddy`,交互菜单第 5 项):skills 按 skill 软链接到 `.codebuddy/skills/`(WorkBuddy 读项目的 `.codebuddy/`,与 CodeBuddy Code 一致;`--copy` 时独立拷贝),DocNexus 的 skills 也链接过去,段落写在 `AGENTS.md`。`doctor`、`uninstall`、数据库配置查找、画像查找都认这个目录。
|
|
24
|
+
|
|
7
25
|
## [0.18.0] - 2026-10-18
|
|
8
26
|
|
|
9
27
|
### 变更(破坏性,不兼容旧版本)
|
|
@@ -242,7 +260,9 @@
|
|
|
242
260
|
- 安装器改为单一 Node 实现,`init-skills.sh` / `init-skills.ps1` 只是薄封装。
|
|
243
261
|
- `code-review` 改名为 `project-review`,升级时自动清理旧名。
|
|
244
262
|
|
|
245
|
-
[Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.
|
|
263
|
+
[Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.20.0...HEAD
|
|
264
|
+
[0.20.0]: https://github.com/guilianerde/skills-init/compare/v0.19.0...v0.20.0
|
|
265
|
+
[0.19.0]: https://github.com/guilianerde/skills-init/compare/v0.18.0...v0.19.0
|
|
246
266
|
[0.18.0]: https://github.com/guilianerde/skills-init/compare/v0.17.0...v0.18.0
|
|
247
267
|
[0.17.0]: https://github.com/guilianerde/skills-init/compare/v0.16.0...v0.17.0
|
|
248
268
|
[0.16.0]: https://github.com/guilianerde/skills-init/compare/v0.15.0...v0.16.0
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
技能目录,团队共享(可提交 git)。
|
|
10
10
|
|
|
11
|
-
支持环境:**Codex CLI / Claude Code / 豆包办公(Doubao)/ 千问(Qwen Code
|
|
11
|
+
支持环境:**Codex CLI / Claude Code / 豆包办公(Doubao)/ 千问(Qwen Code)/ 腾讯 WorkBuddy**。
|
|
12
12
|
|
|
13
13
|
> 本工具在项目里的产出(需求产物、DocNexus 工作区、项目模板、临时文件)都放在项目根的
|
|
14
14
|
> `workflow-run/`
|
|
@@ -64,18 +64,15 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
|
|
|
64
64
|
| claude-code | `.claude/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | Claude Code 自动发现,无需额外配置 |
|
|
65
65
|
| doubao | `.agents/skills/`(与 codex 共用) | 豆包办公自动发现(新会话生效) |
|
|
66
66
|
| qwen | `.qwen/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | Qwen Code 自动发现;`/skills` 面板可查看与切换 |
|
|
67
|
+
| workbuddy | `.codebuddy/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | WorkBuddy 打开项目目录后读取(项目级优先于用户级 `~/.workbuddy/skills/`) |
|
|
67
68
|
|
|
68
|
-
**单一来源 + 软链接**:skills 只在 `.agents/skills/` 装一份完整文件,`.claude/skills`、`.qwen/skills` 下按 skill 建相对软链接(`../../.agents/skills/<skill>`),改模板、填配置都只需改一处。因此即使只选了 claude 或
|
|
69
|
+
**单一来源 + 软链接**:skills 只在 `.agents/skills/` 装一份完整文件,`.claude/skills`、`.qwen/skills`、`.codebuddy/skills` 下按 skill 建相对软链接(`../../.agents/skills/<skill>`),改模板、填配置都只需改一处。因此即使只选了 claude、qwen 或 workbuddy,`.agents/skills/` 也会生成。
|
|
69
70
|
|
|
70
71
|
* 按 skill 建链接而不是链接整个目录,这样 `.claude/skills/` 里还能放其他 skill(例如 DocNexus 的链接)。
|
|
71
72
|
* 之前用 `--copy`(或建不了链接时)装的独立拷贝,再不带 `--copy` 安装时会换成链接:拷贝里填过的用户配置(`config.json`、`schema.md`、`wiki-config.json`、`profile.json`)先并入 `.agents/skills/`,原目录移到 `workflow-run/tmp/backup/<时间>/<环境>/`;两份都填过且内容不同时不会自动合并,会保留那个目录并提示手动处理。
|
|
72
73
|
* Windows:安装与升级(`init` / `upgrade`)**必须在管理员终端执行**(建目录软链接需要该权限);在普通终端运行会直接停下、不做任何改动,并提示改用「以管理员身份运行」的终端。`doctor`、`seed-templates`、`uninstall` 不要求管理员。少数文件系统仍建不了软链接时,该环境自动退回独立拷贝。在 Windows 上 clone 含软链接的仓库需要 `git config core.symlinks true`。
|
|
73
74
|
* 不想用软链接:加 `--copy`,每个环境各装一份完整文件。
|
|
74
75
|
|
|
75
|
-
> Codex 也可选择额外安装到用户级
|
|
76
|
-
> `~/.codex/skills/`
|
|
77
|
-
> (所有项目可用),脚本会询问。
|
|
78
|
-
|
|
79
76
|
每个 skill 目录结构(四环境一致):
|
|
80
77
|
|
|
81
78
|
|
|
@@ -181,7 +178,7 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
|
|
|
181
178
|
|
|
182
179
|
| 参数 | 说明 |
|
|
183
180
|
| ----------------------- | --------------------------------------------------------------------- |
|
|
184
|
-
| `--envs` | 环境列表,逗号分隔:`codex,claude,doubao,qwen`;`all` = 全部 |
|
|
181
|
+
| `--envs` | 环境列表,逗号分隔:`codex,claude,doubao,qwen,workbuddy`;`all` = 全部 |
|
|
185
182
|
| `--skip-db` | 跳过数据库连接配置(database-query 不生成 config.json) |
|
|
186
183
|
| `--with-docnexus` | 可选:接入 [DocNexus](https://www.npmjs.com/package/@rowansenne/docnexus) 项目文档库(需 Node >= 22.13),见下方「DocNexus 文档库(可选)」 |
|
|
187
184
|
| `--with-codegraph` | 可选:接入 [CodeGraph](https://github.com/colbymchenry/codegraph) 代码图谱,见下方「CodeGraph 代码图谱(可选)」 |
|
|
@@ -210,12 +207,13 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
|
|
|
210
207
|
| --- | --- | --- |
|
|
211
208
|
| claude | `.claude/skills/` | `CLAUDE.md` |
|
|
212
209
|
| codex / doubao | `.agents/skills/`(两者共用) | `AGENTS.md` |
|
|
210
|
+
| workbuddy | `.codebuddy/skills/` | `AGENTS.md`(WorkBuddy 读项目的 `AGENTS.md`,与 `CODEBUDDY.md` 并存) |
|
|
213
211
|
| qwen | `.qwen/skills/` | 不写 `QWEN.md`;需要自动召回时可在 `QWEN.md` 引用 `AGENTS.md` 的 DocNexus 段落,或在 `.qwen/settings.json` 的 `context.fileName` 加入 `AGENTS.md` |
|
|
214
212
|
|
|
215
213
|
说明:
|
|
216
214
|
|
|
217
215
|
* DocNexus 的 skills 不在本仓库中,始终以安装的 npm 包为准;重跑脚本会按包版本重新同步与链接(`docnexus init` 可重复执行,不影响已有记忆)。
|
|
218
|
-
* `.qwen/skills/` 下已存在的同名**真实目录**不会被替换,脚本会提示跳过。
|
|
216
|
+
* `.qwen/skills/`、`.codebuddy/skills/` 下已存在的同名**真实目录**不会被替换,脚本会提示跳过。
|
|
219
217
|
* npm 安装或 `docnexus init` 失败时只跳过 DocNexus,本仓库的 skills 照常安装,脚本最后以退出码 1 结束;修好后重跑即可。
|
|
220
218
|
* 安装后 `dev`、`project-review` 会先按 `docnexus-recall` 查项目约定;`dev`、`project-review` 交付时**每次都问**要不要沉淀(写入后把库内路径记回交付说明 / 审查报告的「DocNexus」元信息行),`create-tech-md-doc`、`create-release-md-doc` 结束时有值得记的内容才**提议**沉淀;**任何写入 DocNexus 的操作都必须经你授权**。
|
|
221
219
|
* 离线或测试时可用环境变量指定安装来源,例如 `DOCNEXUS_PKG=/path/to/rowansenne-docnexus-0.13.0.tgz npx @rowansenne/workflow-run --with-docnexus`(PowerShell:`$env:DOCNEXUS_PKG=...`)。
|
|
@@ -224,7 +222,7 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
|
|
|
224
222
|
### DocNexus 的位置
|
|
225
223
|
|
|
226
224
|
* DocNexus 装在 `workflow-run/docnexus/`,以 `workflow-run/` 为项目根运行(`docnexus init --agent none`:它自己不往 `workflow-run/` 里链接 skills、写 `CLAUDE.md`)。
|
|
227
|
-
* 本项目里由 workflow-run 接上:把 `workflow-run/docnexus/skills/*` 相对链接到 `.claude/skills` / `.agents/skills` / `.qwen/skills`,在 `CLAUDE.md` / `AGENTS.md` 写一段说明(概念索引 `@workflow-run/docnexus/CONCEPTS.md`;docnexus 系列 skill 的命令改用 `node <skills>/workflow/scripts/docnexus.mjs` 运行,不用先 `cd`),用自己的标记,不碰手写内容。升级时刷新,卸载时撤掉,`workflow-run/docnexus/` 本身不动。
|
|
225
|
+
* 本项目里由 workflow-run 接上:把 `workflow-run/docnexus/skills/*` 相对链接到 `.claude/skills` / `.agents/skills` / `.qwen/skills` / `.codebuddy/skills`,在 `CLAUDE.md` / `AGENTS.md` 写一段说明(概念索引 `@workflow-run/docnexus/CONCEPTS.md`;docnexus 系列 skill 的命令改用 `node <skills>/workflow/scripts/docnexus.mjs` 运行,不用先 `cd`),用自己的标记,不碰手写内容。升级时刷新,卸载时撤掉,`workflow-run/docnexus/` 本身不动。
|
|
228
226
|
* 各 skill 的脚本已按位置处理:`resolve.mjs status` 给出 `docnexus.root` 与 `run`(`node <skills>/workflow/scripts/docnexus.mjs`),`knowledge.mjs` 在那里运行并给出从项目根读正文的路径。
|
|
229
227
|
* `workflow-run/` 是独立文档仓库时,知识库随文档仓库提交,各服务共用;沉淀后的改动和产物一样由你确认后提交(`requirement.mjs pending` 单独列出)。DocNexus 的依赖和检索库(`node_modules/`、`store/`)不进仓库,各自 `--with-docnexus` 装一次。
|
|
230
228
|
|
|
@@ -267,7 +265,7 @@ dev、project-review、技术方案、上线方案都在对应环节调用它:
|
|
|
267
265
|
2. 所选环境里有 claude / codex 时,执行 `codegraph install --target=<claude,codex> --location=local --yes`,把 MCP 配置写进本项目:
|
|
268
266
|
- claude:`.mcp.json`、`.claude/settings.json`(自动放行 `codegraph_explore`)、`.claude/CLAUDE.md`;
|
|
269
267
|
- codex:`.codex/config.toml`、`AGENTS.md`;Codex 只在受信任的项目里加载项目级配置。
|
|
270
|
-
- doubao、qwen 不在 CodeGraph 支持的 agent
|
|
268
|
+
- doubao、qwen、workbuddy 不在 CodeGraph 支持的 agent 里,在这些环境用 code-graph skill 和 `codegraph` CLI。
|
|
271
269
|
3. 执行 `codegraph init <项目根> --yes` 建索引,生成 `.codegraph/`(本地 SQLite),并把它加入 `.gitignore`。之后 agent 使用期间自动增量同步;`cg.mjs` 发现索引落后于工作区时也会先 `codegraph sync`。
|
|
272
270
|
|
|
273
271
|
* 能力探测:`resolve.mjs status` 的 `codegraph`(`ok` = 有 `.codegraph/` 且 `codegraph` 命令可用)。`/workflow status` 和 `doctor` 都会显示。
|
|
@@ -312,7 +310,8 @@ mv .agents/skills/.skills-init.json .agents/skills/.workflow-run.json
|
|
|
312
310
|
| 情况 | 处理 |
|
|
313
311
|
| --- | --- |
|
|
314
312
|
| 当前内容与清单记录一致(没改过) | 直接更新 |
|
|
315
|
-
|
|
|
313
|
+
| 与清单记录不同(手改过) | 原文件先备份到 `workflow-run/tmp/upgrade-backup/<时间>/<相对项目根的路径>`,再更新为新版;结尾列出这些文件和备份目录 |
|
|
314
|
+
| 目录里没有清单,无从判断 | 和新版不同的文件同样备份后更新;结尾只用一行说明哪个目录没有清单、备份了几个文件,不当作手改过 |
|
|
316
315
|
| 清单里有、新版里已没有 | 没改过直接删除;改过的备份后删除 |
|
|
317
316
|
|
|
318
317
|
* 这样 `SKILL.md`、`skill.json`、脚本始终是同一个版本,不会出现「新 SKILL.md 配旧 skill.json」的混搭。
|
|
@@ -325,12 +324,11 @@ mv .agents/skills/.skills-init.json .agents/skills/.workflow-run.json
|
|
|
325
324
|
npx @rowansenne/workflow-run uninstall
|
|
326
325
|
```
|
|
327
326
|
|
|
328
|
-
按清单移除本项目安装的 skill 文件、`.claude/skills
|
|
327
|
+
按清单移除本项目安装的 skill 文件、`.claude/skills`、`.qwen/skills`、`.codebuddy/skills` 下指向 `.agents/skills` 的软链接,以及清单本身:
|
|
329
328
|
|
|
330
329
|
* 用户配置(`config.json`、`schema.md`、`wiki-config.json`、`profile.json`)和手改过的模板文件先移到 `workflow-run/tmp/uninstall-backup/<时间戳>/<安装目录>/<skill>/`,结尾列出;
|
|
331
330
|
* 不在清单里的文件、你自己的 skill 和链接都不动;skill 目录里还有你自己加的文件时保留该目录并提示;目录清空后才删除;
|
|
332
331
|
* `.gitignore` 里追加的规则和 DocNexus 不处理;没有清单时什么也不做。
|
|
333
|
-
* 用户级 `~/.codex/skills` 不在卸载范围内。
|
|
334
332
|
|
|
335
333
|
### skill.json
|
|
336
334
|
|
|
@@ -479,13 +477,13 @@ workflow-run/docs/requirements/<需求名>/
|
|
|
479
477
|
├── meta.json # 需求名、绑定的分支、各产物(版本、日期、wiki 发布记录、DocNexus 沉淀),给以后的可视化用
|
|
480
478
|
├── plan.md # 开发方案(dev,L 规模)
|
|
481
479
|
├── delivery.md # 交付说明(dev,每次完整交付都写)
|
|
482
|
-
├── review.md # 审查报告(project-review
|
|
480
|
+
├── review.md # 审查报告(project-review);复审也写这个文件,上一版归档到 workflow-run/tmp/old-artifacts/
|
|
483
481
|
├── tech-doc.md # 技术方案
|
|
484
482
|
└── release-doc.md # 上线方案
|
|
485
483
|
```
|
|
486
484
|
|
|
487
485
|
* **需求名由你定**:dev 确定是 M / L 规模(要落盘)时,或从 `/workflow` 完整链路开始时,如果当前分支还没有绑定需求,会问你这个需求叫什么(默认值按分支名生成;也可以选已有的需求,例如修 bug 的新分支挂到原需求下)。你不起名就按分支名生成。绑定后,同一分支上的各 skill 都写到这个目录,不再问。一个需求可以绑定多个分支。
|
|
488
|
-
* 路径由 `workflow/scripts/requirement.mjs path <种类>`
|
|
486
|
+
* 路径由 `workflow/scripts/requirement.mjs path <种类>` 给出,每种产物文件名固定,目录里只留最新版,引用不会失效;版本号记在 `meta.json` 的 `versions`。已有旧版时,上一版先复制到 `workflow-run/tmp/old-artifacts/<需求>/<种类>_v<N>.md`(不提交),新版直接覆盖写入;skill 会列出归档的文件,请你确认后自己删除。上一版发布过 wiki 时,新版沿用同一个页面。
|
|
489
487
|
* 需求目录写在项目根的 `.ignore` 里,ripgrep 和 agent 的代码搜索会跳过它,产物多了不会污染搜索结果(找历史决策用 DocNexus)。git 不受影响,建议随代码提交。
|
|
490
488
|
|
|
491
489
|
**独立文档仓库(可选)**:不想让产物进代码仓库、或一个需求横跨多个服务时,装好 skills 后在项目根运行一条命令:
|
|
@@ -659,4 +657,6 @@ node scripts/eval-triggers.mjs --only dev # 只跑期望为 dev 的用例
|
|
|
659
657
|
|
|
660
658
|
* **Claude Code** 项目级为 `.claude/skills/`,自动发现。
|
|
661
659
|
|
|
662
|
-
* **Qwen Code** 项目级为 `.qwen/skills/`,自动发现。
|
|
660
|
+
* **Qwen Code** 项目级为 `.qwen/skills/`,自动发现。
|
|
661
|
+
|
|
662
|
+
* **腾讯 WorkBuddy** 读项目工作目录下的 `.codebuddy/`(与 CodeBuddy Code 一致),项目级技能在 `.codebuddy/skills/`,同名时优先于用户级;它也读 `AGENTS.md`。
|
package/bin/lib/codegraph.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// CodeGraph(https://github.com/colbymchenry/codegraph):预建的代码知识图谱(符号、调用边、影响面),100% 本地。
|
|
2
2
|
// --with-codegraph 时:没有 CLI 就 npm 全局安装 → 给 claude / codex 写项目级 MCP 配置 → 建本项目索引(.codegraph/)。
|
|
3
|
-
// doubao / qwen 不在 CodeGraph 支持的 agent 里,skill 在这些环境直接调用 codegraph CLI。
|
|
3
|
+
// doubao / qwen / workbuddy 不在 CodeGraph 支持的 agent 里,skill 在这些环境直接调用 codegraph CLI。
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -19,7 +19,7 @@ export function codegraphVersion(cwd) {
|
|
|
19
19
|
|
|
20
20
|
export const codegraphIndexed = (root) => fs.existsSync(path.join(root, '.codegraph'));
|
|
21
21
|
|
|
22
|
-
// 我们的环境 → codegraph install 的 target id;doubao / qwen 没有对应 target
|
|
22
|
+
// 我们的环境 → codegraph install 的 target id;doubao / qwen / workbuddy 没有对应 target
|
|
23
23
|
const TARGETS = { claude: 'claude', codex: 'codex' };
|
|
24
24
|
|
|
25
25
|
const run = (cmd, args, cwd) => spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: IS_WIN, env: ENV }).status === 0;
|
package/bin/lib/core.mjs
CHANGED
|
@@ -11,8 +11,10 @@ export const TPL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url))
|
|
|
11
11
|
export const PKG = JSON.parse(fs.readFileSync(path.join(TPL_DIR, 'package.json'), 'utf8'));
|
|
12
12
|
export const IS_WIN = process.platform === 'win32';
|
|
13
13
|
|
|
14
|
-
export const ENV_NAMES = ['codex', 'claude', 'doubao', 'qwen'];
|
|
14
|
+
export const ENV_NAMES = ['codex', 'claude', 'doubao', 'qwen', 'workbuddy'];
|
|
15
15
|
export const CANON_DIR = path.join('.agents', 'skills'); // 单一来源目录
|
|
16
|
+
// 建软链接(或 --copy 各装一份)的环境 → 项目级 skills 目录。WorkBuddy 读项目的 .codebuddy/(与 CodeBuddy Code 一致)
|
|
17
|
+
export const LINK_ENVS = { claude: path.join('.claude', 'skills'), qwen: path.join('.qwen', 'skills'), workbuddy: path.join('.codebuddy', 'skills') };
|
|
16
18
|
// skill 列表、用户配置、文档模板都来自各 skill 目录的 skill.json(见 bin/skills.mjs)
|
|
17
19
|
export const SKILL_LIST = loadSkills(TPL_DIR);
|
|
18
20
|
export const META = deriveMeta(SKILL_LIST);
|
|
@@ -113,7 +115,7 @@ export function timestamp() {
|
|
|
113
115
|
return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}-${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}`;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
export const INSTALL_BASES = [CANON_DIR,
|
|
118
|
+
export const INSTALL_BASES = [CANON_DIR, ...Object.values(LINK_ENVS)];
|
|
117
119
|
// 找到本项目的安装清单(单一来源优先;--copy 安装时各目录各有一份)
|
|
118
120
|
export function findManifests(root) {
|
|
119
121
|
return INSTALL_BASES.map((rel) => ({ rel, base: path.join(root, rel), m: readManifest(path.join(root, rel)) })).filter((x) => x.m);
|
package/bin/lib/db.mjs
CHANGED
|
@@ -15,7 +15,7 @@ export function defaultConn(cfg) {
|
|
|
15
15
|
}
|
|
16
16
|
export function findExistingDbConfig(root) {
|
|
17
17
|
for (const rel of ['.codex/database-query/config.json', '.agents/skills/database-query/config.json',
|
|
18
|
-
'.claude/skills/database-query/config.json', '.qwen/skills/database-query/config.json']) {
|
|
18
|
+
'.claude/skills/database-query/config.json', '.qwen/skills/database-query/config.json', '.codebuddy/skills/database-query/config.json']) {
|
|
19
19
|
const f = path.join(root, rel);
|
|
20
20
|
const cfg = readJson(f);
|
|
21
21
|
const d = defaultConn(cfg);
|
package/bin/lib/docnexus.mjs
CHANGED
|
@@ -46,8 +46,11 @@ export function linkDocNexusSkills(root, dirs) {
|
|
|
46
46
|
// 用自己的标记,不碰 DocNexus 自己的段落
|
|
47
47
|
const CTX_START = '<!-- workflow-run:docnexus:start -->';
|
|
48
48
|
const CTX_END = '<!-- workflow-run:docnexus:end -->';
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// AGENTS.md 由 codex / doubao / workbuddy 共用:命令指向装了 workflow 的那个目录(--copy 只装 workbuddy 时没有 .agents/skills)
|
|
50
|
+
function contextBlock(root, file) {
|
|
51
|
+
const has = (dir) => fs.existsSync(path.join(root, dir, 'workflow', 'scripts', 'docnexus.mjs'));
|
|
52
|
+
const dir = file === 'CLAUDE.md' ? '.claude/skills' : ['.agents/skills', '.codebuddy/skills'].find(has) ?? '.agents/skills';
|
|
53
|
+
const run = `${dir}/workflow/scripts/docnexus.mjs`;
|
|
51
54
|
const index = file === 'CLAUDE.md' ? `@${HOME}/docnexus/CONCEPTS.md` : `\`${HOME}/docnexus/CONCEPTS.md\`(每个任务开始时读)`;
|
|
52
55
|
return [
|
|
53
56
|
CTX_START,
|
|
@@ -79,7 +82,7 @@ export function writeDocNexusContext(root, files) {
|
|
|
79
82
|
for (const file of files) {
|
|
80
83
|
const f = path.join(root, file);
|
|
81
84
|
const cur = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
|
|
82
|
-
const next = insertBlock(cur, CTX_START, CTX_END, contextBlock(file));
|
|
85
|
+
const next = insertBlock(cur, CTX_START, CTX_END, contextBlock(root, file));
|
|
83
86
|
if (next !== cur) { fs.writeFileSync(f, next); ok(`${file} 已写入 DocNexus 段落(工作区在 ${HOME}/docnexus/)`); }
|
|
84
87
|
}
|
|
85
88
|
}
|
|
@@ -91,6 +94,8 @@ function agentTargets(sel) {
|
|
|
91
94
|
if (sel.includes('claude')) { dirs.add(path.join('.claude', 'skills')); files.add('CLAUDE.md'); }
|
|
92
95
|
if (sel.includes('codex') || sel.includes('doubao')) { dirs.add(CANON_DIR); files.add('AGENTS.md'); }
|
|
93
96
|
if (sel.includes('qwen')) dirs.add(path.join('.qwen', 'skills'));
|
|
97
|
+
// WorkBuddy 读项目的 AGENTS.md(与 CODEBUDDY.md 并存)
|
|
98
|
+
if (sel.includes('workbuddy')) { dirs.add(path.join('.codebuddy', 'skills')); files.add('AGENTS.md'); }
|
|
94
99
|
return { dirs: [...dirs], files: [...files] };
|
|
95
100
|
}
|
|
96
101
|
|
package/bin/lib/doctor.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { PKG, CANON_DIR, HOME, DOCNEXUS_PKG, docnexusUpgradeCmd, SKILLS, step, ok, warn, fail, toPosix, fileHash, readManifest, lstat, findManifests, cmpVersion } from './core.mjs';
|
|
6
|
+
import { PKG, CANON_DIR, INSTALL_BASES, LINK_ENVS, HOME, DOCNEXUS_PKG, docnexusUpgradeCmd, SKILLS, step, ok, warn, fail, toPosix, fileHash, readManifest, lstat, findManifests, cmpVersion } from './core.mjs';
|
|
7
7
|
import { reportTemplateStatus, docnexusSupportsCategory, docnexusSupportsRecallCategory } from './docnexus.mjs';
|
|
8
8
|
import { codegraphVersion } from './codegraph.mjs';
|
|
9
9
|
|
|
@@ -59,19 +59,20 @@ export function doctor(root, offline) {
|
|
|
59
59
|
const canon = found.find((x) => x.rel === CANON_DIR);
|
|
60
60
|
if (canon && m.mode !== 'copy') {
|
|
61
61
|
step('软链接');
|
|
62
|
-
for (const env of
|
|
62
|
+
for (const [env, rel] of Object.entries(LINK_ENVS)) {
|
|
63
63
|
if (!(m.envs ?? []).includes(env)) continue;
|
|
64
|
-
const dir = path.join(root,
|
|
64
|
+
const dir = path.join(root, rel);
|
|
65
|
+
const d = toPosix(rel);
|
|
65
66
|
let good = 0;
|
|
66
67
|
for (const skill of Object.keys(canon.m.skills)) {
|
|
67
68
|
const link = path.join(dir, skill);
|
|
68
69
|
const st = lstat(link);
|
|
69
|
-
if (!st) bad(
|
|
70
|
-
else if (!st.isSymbolicLink()) { if (!readManifest(dir)) meh(
|
|
71
|
-
else if (!fs.existsSync(link)) bad(
|
|
70
|
+
if (!st) bad(`${d}/${skill} 不存在:运行 workflow-run upgrade 重建`);
|
|
71
|
+
else if (!st.isSymbolicLink()) { if (!readManifest(dir)) meh(`${d}/${skill} 是普通目录而非软链接(可能是建链接失败后的独立拷贝)`); else good++; }
|
|
72
|
+
else if (!fs.existsSync(link)) bad(`${d}/${skill} 是断开的软链接`);
|
|
72
73
|
else good++;
|
|
73
74
|
}
|
|
74
|
-
if (good === Object.keys(canon.m.skills).length) ok(
|
|
75
|
+
if (good === Object.keys(canon.m.skills).length) ok(`${d} 全部指向 ${toPosix(CANON_DIR)}`);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -89,7 +90,7 @@ export function doctor(root, offline) {
|
|
|
89
90
|
if (st.wiki.ok) ok('wiki 已配置'); else console.log(' -- wiki 未配置(可选;也可能通过 wiki MCP 提供)');
|
|
90
91
|
if (st.docnexus.ok) {
|
|
91
92
|
// DocNexus 以 workflow-run/ 为项目根:本项目要有 docnexus skill 的链接和 CLAUDE.md / AGENTS.md 段落,agent 才知道去哪里找
|
|
92
|
-
const linked =
|
|
93
|
+
const linked = INSTALL_BASES.some((b) => lstat(path.join(root, b, 'docnexus-recall'))?.isSymbolicLink());
|
|
93
94
|
const ctx = ['CLAUDE.md', 'AGENTS.md'].some((f) => { try { return fs.readFileSync(path.join(root, f), 'utf8').includes('workflow-run:docnexus:start'); } catch { return false; } });
|
|
94
95
|
if (linked && ctx) ok(`DocNexus 在 ${HOME}/docnexus/,本项目已链接它的 skills 并写入段落`);
|
|
95
96
|
else meh(`DocNexus 在 ${HOME}/docnexus/,但本项目${linked ? '' : '缺少 docnexus skill 链接'}${!linked && !ctx ? '、' : ''}${ctx ? '' : '缺少 CLAUDE.md / AGENTS.md 里的 DocNexus 段落'}:运行 workflow-run upgrade`);
|
package/bin/lib/install.mjs
CHANGED
|
@@ -7,8 +7,9 @@ import { writeDbConfig } from './db.mjs';
|
|
|
7
7
|
import { layout, ensureIgnore, homeIsRepo, IGNORE_LINE, TMP_DIR } from '../../workflow/scripts/requirement.mjs';
|
|
8
8
|
import { detectEnv } from '../../workflow/scripts/resolve.mjs';
|
|
9
9
|
|
|
10
|
-
// 结尾汇总:被手改过、已用新版覆盖(原文件已备份)的 / 模板已删除、手改过而备份后删除的文件
|
|
11
|
-
|
|
10
|
+
// 结尾汇总:被手改过、已用新版覆盖(原文件已备份)的 / 模板已删除、手改过而备份后删除的文件 /
|
|
11
|
+
// 没有安装清单、内容与新版不同而备份后覆盖的文件(无从判断是否手改)
|
|
12
|
+
export const report = { overwritten: [], removed: [], unlisted: [], unlistedDirs: [], backupDir: null };
|
|
12
13
|
|
|
13
14
|
// skills 目录里的文件一律以安装包为准:手改过的先备份到 <项目根>/workflow-run/tmp/upgrade-backup/<时间>/(同一次运行共用一个目录),
|
|
14
15
|
// 再覆盖或删除。项目自己的定制应放在不会被升级覆盖的地方:workflow/profile.json、workflow-run/templates/、DocNexus、CLAUDE.md / AGENTS.md
|
|
@@ -17,8 +18,7 @@ function backup(root, file) {
|
|
|
17
18
|
stamp ??= timestamp();
|
|
18
19
|
const dir = path.join(root, TMP_DIR, 'upgrade-backup', stamp);
|
|
19
20
|
const rel = path.relative(root, file);
|
|
20
|
-
|
|
21
|
-
const target = rel.startsWith('..') || path.isAbsolute(rel) ? path.join(dir, 'outside-project', file.replace(/^[A-Za-z]:/, '').replace(/^[\\/]+/, '')) : path.join(dir, rel);
|
|
21
|
+
const target = path.join(dir, rel);
|
|
22
22
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
23
23
|
fs.copyFileSync(file, target);
|
|
24
24
|
report.backupDir = toPosix(path.relative(root, dir));
|
|
@@ -34,12 +34,13 @@ export function pruneFile(file, recorded, stop, label, root) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// 模板文件:总是写成新版。和上次安装记录的哈希不同(被手改过)时先备份原文件。
|
|
37
|
-
//
|
|
37
|
+
// 没有清单时无从判断是否手改,内容不同就备份后覆盖(单独汇总,不报「手动修改过」)
|
|
38
38
|
export function installTemplate(dst, content, recorded, hasManifest, ctx, label) {
|
|
39
39
|
if (!fs.existsSync(dst)) { writeFile(dst, content); return; }
|
|
40
40
|
const cur = fileHash(dst);
|
|
41
41
|
if (cur === sha256(content)) return;
|
|
42
|
-
if (!hasManifest
|
|
42
|
+
if (!hasManifest) { backup(ctx.root, dst); report.unlisted.push(label); }
|
|
43
|
+
else if (cur !== recorded) { backup(ctx.root, dst); report.overwritten.push(label); }
|
|
43
44
|
writeFile(dst, content);
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -59,6 +60,7 @@ export function pruneSkill(base, skill, prevSkill, entry, root) {
|
|
|
59
60
|
export async function installSkills(base, db, ctx) {
|
|
60
61
|
const selected = ctx.skills ?? SKILLS;
|
|
61
62
|
const prev = readManifest(base);
|
|
63
|
+
const unlistedBefore = report.unlisted.length;
|
|
62
64
|
const manifest = { installer: PKG.name, version: PKG.version, installedAt: new Date().toISOString(), envs: ctx.envs, mode: ctx.mode, skills: {}, templates: prev?.templates ?? {} };
|
|
63
65
|
for (const skill of selected) {
|
|
64
66
|
const srcDir = path.join(TPL_DIR, skill);
|
|
@@ -109,6 +111,7 @@ export async function installSkills(base, db, ctx) {
|
|
|
109
111
|
// 清单里有、本版本已不再提供的 skill:按同样规则清理
|
|
110
112
|
else pruneSkill(base, skill, prevSkill, null, ctx.root);
|
|
111
113
|
}
|
|
114
|
+
if (report.unlisted.length > unlistedBefore) report.unlistedDirs.push(toPosix(path.relative(ctx.root, base)));
|
|
112
115
|
writeFile(path.join(base, MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
113
116
|
}
|
|
114
117
|
|
package/bin/lib/uninstall.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { IS_WIN, CANON_DIR, MANIFEST, step, ok, warn, toPosix, fileHash, readManifest, removeEmptyDirs, lstat, timestamp, removeLink, docnexusRoot } from './core.mjs';
|
|
5
|
+
import { IS_WIN, CANON_DIR, INSTALL_BASES, MANIFEST, step, ok, warn, toPosix, fileHash, readManifest, removeEmptyDirs, lstat, timestamp, removeLink, docnexusRoot } from './core.mjs';
|
|
6
6
|
import { removeDocNexusContext } from './docnexus.mjs';
|
|
7
7
|
|
|
8
8
|
// 软链接是否指向 absTarget(按解析后的绝对路径比较,兼容 Windows 上 readlink 返回的 \\?\ 前缀)
|
|
@@ -17,14 +17,14 @@ export function linkPointsTo(link, absTarget) {
|
|
|
17
17
|
export function rmdirIfEmpty(dir) { try { fs.rmdirSync(dir); return true; } catch { return false; } }
|
|
18
18
|
|
|
19
19
|
export function uninstall(root) {
|
|
20
|
-
const bases =
|
|
20
|
+
const bases = INSTALL_BASES;
|
|
21
21
|
const found = bases.map((rel) => [rel, readManifest(path.join(root, rel))]).filter(([, m]) => m);
|
|
22
22
|
if (!found.length) { warn(`未找到安装清单(${bases.map((b) => toPosix(path.join(b, MANIFEST))).join(' / ')}),没有可卸载的内容`); return 0; }
|
|
23
23
|
const backupRoot = path.join(root, 'workflow-run', 'tmp', 'uninstall-backup', timestamp());
|
|
24
24
|
const backedUp = [];
|
|
25
25
|
const leftovers = [];
|
|
26
26
|
|
|
27
|
-
// 1. .claude / .qwen 下指向单一来源、且在清单里的 skill 链接;用户自己的链接和目录不动
|
|
27
|
+
// 1. .claude / .qwen / .codebuddy 下指向单一来源、且在清单里的 skill 链接;用户自己的链接和目录不动
|
|
28
28
|
const canon = found.find(([rel]) => rel === CANON_DIR)?.[1];
|
|
29
29
|
for (const rel of bases.slice(1)) {
|
|
30
30
|
const dir = path.join(root, rel);
|
|
@@ -81,7 +81,7 @@ export function uninstall(root) {
|
|
|
81
81
|
fs.rmSync(path.join(base, MANIFEST), { force: true });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// 3. 清空后的环境目录一并移除(.claude
|
|
84
|
+
// 3. 清空后的环境目录一并移除(.claude / .qwen / .codebuddy 里还有别的东西时保留)
|
|
85
85
|
for (const rel of bases) if (rmdirIfEmpty(path.join(root, rel))) rmdirIfEmpty(path.dirname(path.join(root, rel)));
|
|
86
86
|
|
|
87
87
|
step('卸载完成');
|
package/bin/workflow-run.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// npx @rowansenne/workflow-run [命令] [选项] # 在项目根执行(唯一的安装方式)
|
|
6
6
|
//
|
|
7
7
|
// 单一来源:skills 只完整安装到 .agents/skills(codex / doubao 直接使用);
|
|
8
|
-
// .claude/skills、.qwen/skills
|
|
8
|
+
// .claude/skills、.qwen/skills、.codebuddy/skills(WorkBuddy)下按 skill 建相对软链接指向它;--copy 退回各环境独立拷贝。
|
|
9
9
|
// 重跑安全:SKILL.md / references / 模板会更新;用户已填写的 wiki-config.json、schema.md、
|
|
10
10
|
// workflow/profile.json 保留不覆盖;database-query/config.json 仅在本次提供了数据库配置时写入。
|
|
11
11
|
// 安装清单:每个安装目录写 .workflow-run.json,记录模板文件的 sha256。重跑时据此判断模板文件是否被手改过
|
|
@@ -15,10 +15,9 @@
|
|
|
15
15
|
// install(安装 / 软链接 / 改名清理 / .gitignore)、uninstall、docnexus(接入与模板记录)、doctor(upgrade / doctor)。
|
|
16
16
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
|
-
import os from 'node:os';
|
|
19
18
|
import path from 'node:path';
|
|
20
19
|
import { seedTemplates, updateTemplates } from './seed-templates.mjs';
|
|
21
|
-
import { TPL_DIR, PKG, ENV_NAMES, CANON_DIR, HOME, SKILLS, resolveSkillSelection, step, ok, warn, CliError, fail, isAdminTerminal } from './lib/core.mjs';
|
|
20
|
+
import { TPL_DIR, PKG, ENV_NAMES, CANON_DIR, LINK_ENVS, HOME, SKILLS, resolveSkillSelection, step, ok, warn, CliError, fail, isAdminTerminal } from './lib/core.mjs';
|
|
22
21
|
import { ask, closePrompt } from './lib/prompt.mjs';
|
|
23
22
|
import { findExistingDbConfig, getDb } from './lib/db.mjs';
|
|
24
23
|
import { report, installSkills, linkSkills, updateGitignore, setupHome, recordEnv } from './lib/install.mjs';
|
|
@@ -46,7 +45,7 @@ const HELP = `用法: workflow-run [命令] [选项]
|
|
|
46
45
|
不带地址时显示当前状态
|
|
47
46
|
|
|
48
47
|
init 选项:
|
|
49
|
-
--envs <列表> 环境,逗号分隔:codex,claude,doubao,qwen;all = 全部(不传则交互选择)
|
|
48
|
+
--envs <列表> 环境,逗号分隔:codex,claude,doubao,qwen,workbuddy;all = 全部(不传则交互选择)
|
|
50
49
|
--skip-db 跳过数据库连接配置
|
|
51
50
|
--yes 非交互:使用已有数据库配置;都没有且未 --skip-db 时报错退出
|
|
52
51
|
--with-docnexus 同时接入 DocNexus 文档库(装在 workflow-run/docnexus/,需 Node >= 22.13 与 npm),并导入文档模板
|
|
@@ -126,9 +125,10 @@ async function resolveEnvs(o) {
|
|
|
126
125
|
console.log(' 2) claude -> .claude/skills (Claude Code 项目级)');
|
|
127
126
|
console.log(' 3) doubao -> .agents/skills (豆包办公项目级)');
|
|
128
127
|
console.log(' 4) qwen -> .qwen/skills (Qwen Code 项目级)');
|
|
128
|
+
console.log(' 5) workbuddy -> .codebuddy/skills(腾讯 WorkBuddy 项目级)');
|
|
129
129
|
const choice = await ask('输入编号(逗号分隔,如 1,3;直接回车 = 全部): ');
|
|
130
130
|
if (!choice) return [...ENV_NAMES];
|
|
131
|
-
const sel = choice.split(/[,,\s]+/).filter((p) => /^[1-
|
|
131
|
+
const sel = choice.split(/[,,\s]+/).filter((p) => /^[1-5]$/.test(p)).map((p) => ENV_NAMES[Number(p) - 1]);
|
|
132
132
|
if (!sel.length) { warn('未识别到有效编号,按全部安装'); return [...ENV_NAMES]; }
|
|
133
133
|
return [...new Set(sel)].sort();
|
|
134
134
|
}
|
|
@@ -207,16 +207,10 @@ async function main() {
|
|
|
207
207
|
const links = [];
|
|
208
208
|
if (o.copy) {
|
|
209
209
|
if (sel.includes('codex') || sel.includes('doubao')) targets.push(['codex/doubao', CANON_DIR]);
|
|
210
|
-
if (sel.includes(
|
|
211
|
-
if (sel.includes('qwen')) targets.push(['qwen', path.join('.qwen', 'skills')]);
|
|
210
|
+
for (const [env, dir] of Object.entries(LINK_ENVS)) if (sel.includes(env)) targets.push([env, dir]);
|
|
212
211
|
} else {
|
|
213
212
|
targets.push(['单一来源', CANON_DIR]);
|
|
214
|
-
if (sel.includes(
|
|
215
|
-
if (sel.includes('qwen')) links.push(['qwen', path.join('.qwen', 'skills')]);
|
|
216
|
-
}
|
|
217
|
-
if (sel.includes('codex') && !o.yes) {
|
|
218
|
-
const a = (await ask('是否同时安装到 Codex 用户级 ~/.codex/skills(所有项目可用)?[y/N]: ')).toLowerCase();
|
|
219
|
-
if (a === 'y') targets.push(['codex(user)', path.join(os.homedir(), '.codex', 'skills')]);
|
|
213
|
+
for (const [env, dir] of Object.entries(LINK_ENVS)) if (sel.includes(env)) links.push([env, dir]);
|
|
220
214
|
}
|
|
221
215
|
|
|
222
216
|
const ctx = { root: o.root, yes: o.yes, envs: sel, mode: o.copy ? 'copy' : 'symlink', skills };
|
|
@@ -260,6 +254,7 @@ async function main() {
|
|
|
260
254
|
console.log(' claude : Claude Code 自动发现 .claude/skills,无需额外配置');
|
|
261
255
|
console.log(' doubao : 豆包办公自动发现 .agents/skills(新会话生效)');
|
|
262
256
|
console.log(' qwen : Qwen Code 自动发现 .qwen/skills;/skills 面板可查看与切换');
|
|
257
|
+
console.log(' workbuddy : WorkBuddy 打开本项目目录后读取 .codebuddy/skills(项目级优先于用户级);DocNexus 段落写在 AGENTS.md');
|
|
263
258
|
if (db) ok('database-query 连接已生成并写入;凭据已加入 .gitignore,请勿提交。');
|
|
264
259
|
else if (fs.existsSync(path.join(o.root, CANON_DIR, 'database-query', 'config.json'))) ok('database-query/config.json 已存在,保留不动。');
|
|
265
260
|
else warn('注意:本次未写入数据库连接。可重跑补配,或手动放置 database-query/config.json(已有的保留不动)。');
|
|
@@ -270,11 +265,16 @@ async function main() {
|
|
|
270
265
|
if (docnexusConnected(o.root)) reportTemplateStatus(o.root);
|
|
271
266
|
if (docnexus === 'failed') warn('注意:DocNexus 接入失败(见上方输出),其他 skills 不受影响;修复后可重跑(加 --with-docnexus)。');
|
|
272
267
|
if (codegraph === 'failed') warn('注意:CodeGraph 接入失败(见上方输出),其他 skills 不受影响;skills 会退回 grep / 读文件;修复后可重跑(加 --with-codegraph)。');
|
|
268
|
+
const keep = '(skills 目录里的文件以安装包为准;项目定制请放到 workflow/profile.json、workflow-run/templates/、DocNexus 或 CLAUDE.md / AGENTS.md)';
|
|
273
269
|
if (report.overwritten.length || report.removed.length) {
|
|
274
|
-
warn(`以下文件被手动修改过,已按新版${report.removed.length ? '覆盖或删除' : '覆盖'},原文件备份在 ${report.backupDir}
|
|
270
|
+
warn(`以下文件被手动修改过,已按新版${report.removed.length ? '覆盖或删除' : '覆盖'},原文件备份在 ${report.backupDir}/${keep}:`);
|
|
275
271
|
for (const f of report.overwritten) warn(` ${f}`);
|
|
276
272
|
for (const f of report.removed) warn(` ${f}(新版已不提供,已删除)`);
|
|
277
273
|
}
|
|
274
|
+
// 目录里没有安装清单(不是本工具装的,或清单丢了):无从判断是否手改,内容和新版不同就备份后覆盖,汇总成一行
|
|
275
|
+
if (report.unlisted.length) {
|
|
276
|
+
ok(`${report.unlistedDirs.join('、')} 里没有安装清单,${report.unlisted.length} 个和新版不同的文件已备份到 ${report.backupDir}/ 后覆盖(无从判断是否手改过)`);
|
|
277
|
+
}
|
|
278
278
|
console.log('先运行 /workflow profile 填写项目画像;说明见 workflow-run README。');
|
|
279
279
|
// skills 已装好,但显式要求的 DocNexus 未接入:返回非 0 便于自动化感知
|
|
280
280
|
return docnexus === 'failed' || codegraph === 'failed' || seedFailed ? 1 : 0;
|
|
@@ -69,10 +69,10 @@ description: "Generate a Markdown release/deployment plan (上线方案) from th
|
|
|
69
69
|
|
|
70
70
|
### Step 4 — 写文件
|
|
71
71
|
|
|
72
|
-
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path release --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/release-doc.md
|
|
72
|
+
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path release --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/release-doc.md`,文件名固定;已有旧版时直接写新版,上一版由脚本归档到输出的 `previous`)。规则见 `<skills>/workflow/references/requirements.md`。
|
|
73
73
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
|
|
74
74
|
- `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
|
|
75
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
75
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
76
76
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-release-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
|
|
77
77
|
- 再把路径和摘要告诉用户。
|
|
78
78
|
|
|
@@ -67,10 +67,10 @@ description: "Generate a Markdown technical design doc (技术方案) from the d
|
|
|
67
67
|
|
|
68
68
|
### Step 4 — 写文件
|
|
69
69
|
|
|
70
|
-
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path tech --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/tech-doc.md
|
|
70
|
+
- 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path tech --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/tech-doc.md`,文件名固定;已有旧版时直接写新版,上一版由脚本归档到输出的 `previous`)。规则见 `<skills>/workflow/references/requirements.md`。
|
|
71
71
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
|
|
72
72
|
- `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
|
|
73
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
73
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
74
74
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-tech-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
|
|
75
75
|
- 再把路径和摘要告诉用户。
|
|
76
76
|
|
package/dev/SKILL.md.tpl
CHANGED
|
@@ -155,8 +155,8 @@ description: "Implement a feature or fix a bug in this project end to end: under
|
|
|
155
155
|
产物按需求归档在 `workflow-run/docs/requirements/<需求名>/`,规则见 `<skills>/workflow/references/requirements.md`:
|
|
156
156
|
|
|
157
157
|
- **需求名**:确定规模为 M / L(要落盘)时,先运行 `node <skills>/workflow/scripts/requirement.mjs which`;当前分支还没绑定需求就问用户这个需求叫什么(默认值用输出的 `suggested_name`),再 `requirement.mjs bind --name "<需求名>"`;用户不给名字时 `bind` 按分支名生成。S 规模快速通道不落盘,不问。缺陷修复同样处理(可以选择挂到原需求下)。
|
|
158
|
-
- **路径**:交付说明用 `requirement.mjs path delivery`,L 规模方案(或用户要求落盘的 M 规模方案)用 `requirement.mjs path plan`,写到输出的 `path
|
|
159
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
158
|
+
- **路径**:交付说明用 `requirement.mjs path delivery`,L 规模方案(或用户要求落盘的 M 规模方案)用 `requirement.mjs path plan`,写到输出的 `path`;文件名固定,已有旧版时直接写新版,上一版由脚本归档(输出的 `previous`)。
|
|
159
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
|
|
160
160
|
|
|
161
161
|
## 终端命令
|
|
162
162
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rowansenne/workflow-run",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "项目级 Agent Skills 一键初始化:workflow / dev / project-review / code-graph / database-query / md-to-wiki / 技术与上线方案生成",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
7
7
|
"claude-code",
|
|
8
8
|
"codex",
|
|
9
9
|
"qwen-code",
|
|
10
|
+
"workbuddy",
|
|
10
11
|
"skills",
|
|
11
12
|
"workflow",
|
|
12
13
|
"code-review"
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-review
|
|
3
|
-
description: "Review source code (any language; Java/Spring, Go, TypeScript, Python, etc.) for bugs, correctness, missing fields, null-safety, concurrency, exception handling, resource leaks
|
|
3
|
+
description: "Review source code (any language; Java/Spring, Go, TypeScript, Python, etc.) for bugs, correctness, missing fields, null-safety, concurrency, exception handling, resource leaks and security issues (style only when asked). Use when the user asks to review code, review a branch/PR/MR against another branch (e.g. compare `main`), check a class/method/entity for missing fields or bugs, compare two classes to see whether a converter/mapper (DTO ↔ VO ↔ Entity, BeanUtils/MapStruct) drops or mismaps fields, verify changes before merge, or wants a structured code audit with an actionable findings report. Trigger phrases: 帮我审代码 / 审这个分支 / 这个类这段代码有没有问题 / 还缺哪些字段 / 是不是漏字段了 / 对照一下两个类 / 转换方法漏没漏字段 / review this branch / compare with main / code review / 影响面分析. Reports findings only and does not change code unless asked. Not for implementing or fixing (dev), reviewing the wording of a document, or generating tech/release docs."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Review
|
|
7
7
|
|
|
8
8
|
## Workflow
|
|
9
9
|
|
|
10
|
-
1.
|
|
10
|
+
1. **确定审查范围与深度**:范围是单文件 / 单方法、分支对比(如 `branch` vs `main`),还是未提交改动(`git diff HEAD`,见 `references/diff-strategy.md`「未提交改动」)。深度按用户的说法定,没说就用「标准」,写进报告「审查方式」:
|
|
11
|
+
- **快审**(「快速看一下」「扫一眼」):只查正确性、空值、安全、事务与幂等,不查性能、测试、风格;「优化建议(LOW)」写「无」。CRITICAL / HIGH 照样复核。
|
|
12
|
+
- **标准**(默认):检查清单全部维度,**风格类(清单「一致性 / 风格」一节)不查**;「优化建议(LOW)」只写有实际收益的(性能、可维护性上的具体问题),没有就写「无」。
|
|
13
|
+
- **深审**(「仔细审」「全面审」,或资金 / 权限 / 对外接口的大改动合并前):全部维度,包括风格;MEDIUM 也复核。
|
|
11
14
|
2. **建立理解**:先读实体、接口、DTO、调用链,理清数据流再下结论;不要凭文件名猜内容。
|
|
12
15
|
- **可选:查项目约定(DocNexus)**:DocNexus 可用时(`resolve.mjs status` 的 `docnexus.ok`),先运行 `node <skills>/workflow/scripts/knowledge.mjs project-review --match <模块名 / 业务名词> --query "<一句话:改动涉及的模块与业务>"`,看以往审查沉淀的反复问题和开发约定(`--query` 在这些分类内按正文 recall;输出 `recall_by_category: false` 时改按 `docnexus-recall` 做不带分类的 recall),找相关的编码约定、历史决策与已知坑,作为审查依据并在报告中引用 `docnexus/library/<path>`。未检测到或查询失败就跳过,并在「未覆盖与待确认」中注明;审查过程中**不写入** DocNexus(写入见第 8 步)。
|
|
13
16
|
- **同分支已有产物**:运行 `node <skills>/workflow/scripts/artifacts.mjs --branch <dev分支>`(单文件 / 未提交改动用当前分支,省略 `--branch`),看输出的 `latest`:
|
|
@@ -15,20 +18,31 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
15
18
|
- `latest.delivery` 存在(dev 的交付说明)→ 其中「已确认的决定」并入需求对照的依据;「配置清单」「SQL 脚本」和 diff 对照,diff 里有而交付说明漏列的配置 key / DDL 记为 MEDIUM(上线时会被漏掉)。
|
|
16
19
|
- `latest.review` 存在 → 这是**复审**:读上次报告的问题表,并用 `git log --oneline --since=<上次报告日期> <dev分支>` 看之后改了什么。本次报告对上次的每条问题标「已修复 / 仍存在 / 不再适用」(「已修复」要看到对应改动),本次新发现的标「新增」。
|
|
17
20
|
- **涉及数据库**:diff 里有 SQL、Mapper、实体或 DDL 时,按 `<skills>/workflow/references/tables.md` 运行 `node <skills>/workflow/scripts/tables.mjs --diff <基线>...<dev>`(未提交改动用 `--base <基线>`)找出涉及的表;数据库已配置(`resolve.mjs status` 的 `database.ok`)时用 `query.mjs --schema --tables <表> --indexes` 只读拉取实际字段、索引与估算行数,作为第 4、6 步判断索引支撑、字段存在与类型、DDL 锁表与滚动发布兼容的依据。查不了库时注明「未与实际库核对」写进「未覆盖与待确认」,这类发现不升到 HIGH 以上,除非 diff 本身就能证明。
|
|
18
|
-
3. **分支对比**:基线默认取 `workflow/profile.json` 的 `main_branch`(未配置则 `main`)。按 `references/diff-strategy.md` 先量规模:小改动全量精读;改动多时按风险分级(P0 资金 / 事务 / 权限 / SQL / 对外接口 / MQ /
|
|
21
|
+
3. **分支对比**:基线默认取 `workflow/profile.json` 的 `main_branch`(未配置则 `main`)。按 `references/diff-strategy.md` 先量规模:小改动全量精读;改动多时按风险分级(P0 资金 / 事务 / 权限 / SQL / 对外接口 / MQ / 配置优先)分批精读,审不到的写进报告。**深审且改动属于「更大」、环境又能派子 agent 时,先问用户要不要分角度并行查找**(`references/diff-strategy.md`「分角度并行查找」),用户同意才走,第 4 步改由各子 agent 按角度过清单。只审本次改动引入的问题,同时检查改动是否破坏既有调用方。
|
|
19
22
|
4. **逐项过检查清单**(在项目根运行 `node <skills>/workflow/scripts/resolve.mjs template project-review checklist.md`,读输出的 `path`;退出码 2 时停下并按 `hint` 处理):对每个发现给出 文件+行号、问题描述、严重级别、修改建议。
|
|
20
23
|
5. **影响面分析**:改动是否影响其他调用方/模块。先运行 `node <skills>/code-graph/scripts/cg.mjs impact --diff <基线>...<dev>`(code-graph skill;未提交改动用 `--base <基线>`):列出受影响的接口、MQ 消费者、定时任务、测试、表和配置 key,报告「影响面」按入口归纳并带文件:行号;退出码 2(没有索引等)时用 grep 或精读调用链确认。Spring 注入、AOP、MyBatis XML、MQ topic、配置 key 图谱看不到,照常 grep(见 `<skills>/workflow/references/codegraph.md`)。查不到调用方的符号不能当作安全,要明说"未确认"。
|
|
21
|
-
6. **复核**:交付前逐条复核 CRITICAL / HIGH
|
|
24
|
+
6. **复核**:交付前逐条复核 CRITICAL / HIGH(深审时 MEDIUM 也复核;其余 MEDIUM 至少做下面第 1 条)。审查结论会被用来决定能不能合并,一条误报会让人白改代码,也会让后面的真问题不被当回事。找出问题的人自己复核容易顺着原来的思路认同自己,所以**能交给独立的一方就交出去**:
|
|
25
|
+
- **独立复核(首选)**:当前环境能派子 agent / 子任务时(例如 Claude Code 的 Agent 工具、Codex / Qwen Code 的子 agent),每条发现派一个,互不相关的可以同时派。只给它:一句话结论、位置(文件:行号)、你写的触发条件、对比分支(或「未提交改动」)。**不给你的推理过程**,也不告诉它你认为成立。要它自己读代码,按下面 1~4 条检查,回答「成立 / 可能成立 / 不成立」并引用代码作为依据。
|
|
26
|
+
- **自行复核**:环境派不了子 agent 时自己做,先找它**不成立**的理由,找不到再确认成立。
|
|
27
|
+
检查项(独立复核和自行复核都按这 4 条):
|
|
22
28
|
1. 回到被审的代码(分支对比用 `git show <dev>:<文件>`,未提交改动读工作区)确认引用的片段和行号真实存在。
|
|
23
29
|
2. 写出**触发条件**:什么输入 / 状态下会出错、错成什么样。写不出来 → 降为 MEDIUM 并标「⚠ 待确认」。
|
|
24
30
|
3. 找有没有别处已经兜住:调用方校验、`@Validated` / 全局异常处理、数据库约束、框架默认行为、配置。涉及索引、字段、唯一约束、表大小的发现,以第 2 步拉到的实际表结构为准(复核列写「已与 <连接名> 核对」)。已兜住 → 删掉或降级,并在说明里写依据。
|
|
25
31
|
4. 确认是本次改动引入的:改动前就存在的问题移到「未覆盖与待确认」,不计入合并结论。
|
|
26
|
-
|
|
32
|
+
结果:「成立」在报告「复核」列标「✅」;「可能成立」标「⚠」;「不成立」从问题表里删掉(独立复核否掉的,在「未覆盖与待确认」里记一句「复核排除 N 条」)。复核方式(独立 / 自行)写进报告「审查方式」元信息。
|
|
27
33
|
7. **输出报告**:按报告模板输出(`node <skills>/workflow/scripts/resolve.mjs template project-review report.md` 取 `path`;退出码 2 时停下并按 `hint` 处理),落盘规则见 Artifact Location。
|
|
28
34
|
8. **沉淀到 DocNexus(需用户授权)**:`resolve.mjs status` 的 `docnexus.ok` 为 true 时,交付报告后**每次都问**用户「要不要启动 DocNexus 流程,把本次审查沉淀到 DocNexus?」,并从本次发现里列出建议沉淀的内容:同类问题再次出现(知识库里已有类似沉淀,或本次多处出现)、新确立的编码约定、容易踩的坑;没有特别值得记的也照问,并说明这一点。未接入时不提问,报告的「DocNexus」元信息行写「未接入」。
|
|
29
35
|
- 用户同意 → 按 DocNexus 入口 skill `docnexus` 的 capture 流程写入;正文只写知识,不写分支、路径;有 facts 时按 `<skills>/workflow/references/docnexus.md`「沉淀的来源」的格式写 provenance;分类用 `node <skills>/workflow/scripts/resolve.mjs category project-review` 的结果作为调用方显式分类封存(见 `<skills>/workflow/references/docnexus.md`「写入 DocNexus 的分类」)。只沉淀问题模式和约定,不沉淀某次审查的逐条清单,也不沉淀「⚠ 待确认」的发现;知识库已有同类沉淀时更新那一篇(带 `--base-version`),不要重复新建。写入成功后把报告的「DocNexus」元信息行改成 `docnexus/library/<path>`(多篇用「、」分隔),并回报给用户。
|
|
30
36
|
- 用户不同意 → 元信息行保持「未沉淀」。
|
|
31
37
|
|
|
38
|
+
## 只报会出问题的
|
|
39
|
+
|
|
40
|
+
报告越短,真问题越不会被漏看。下面这些不报:
|
|
41
|
+
- 风格、命名、格式、注释措辞(快审和标准审查都不报;深审或用户要求时才看);
|
|
42
|
+
- 编译器、linter、CI 已经会拦下的(未使用的变量、格式、import 顺序);
|
|
43
|
+
- 个人偏好、没有违反项目约定(`workflow/profile.json` 的 `notes`、DocNexus 约定、审查清单)的写法差异;
|
|
44
|
+
- 写不出后果的「可以更好」:每条发现都要说清不改会怎样。
|
|
45
|
+
|
|
32
46
|
## Review Dimensions
|
|
33
47
|
|
|
34
48
|
- 正确性 / 逻辑 bug
|
|
@@ -38,7 +52,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
38
52
|
- 异常处理与资源释放(连接、流、锁、事务)
|
|
39
53
|
- 安全(注入 / XSS / 越权 / 敏感信息 / 硬编码密钥)
|
|
40
54
|
- 性能(N+1、循环内查询、大对象、无谓复制)
|
|
41
|
-
- 一致性 / 命名 /
|
|
55
|
+
- 一致性 / 命名 / 风格(只在深审或用户要求时)
|
|
42
56
|
- 幂等与消息一致性(重复消费 / 回调、事务内发消息)
|
|
43
57
|
- 数据库变更安全(锁表、滚动发布兼容、索引支撑;以实际表结构为准,见第 2 步「涉及数据库」)
|
|
44
58
|
- 测试(改动有没有被测试覆盖、断言是否有效)
|
|
@@ -50,7 +64,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
50
64
|
- **CRITICAL**:崩溃 / 数据错误 / 资金影响 / 安全漏洞,必须改
|
|
51
65
|
- **HIGH**:明显 bug,特定场景可触发
|
|
52
66
|
- **MEDIUM**:潜在问题,边界未处理或语义存疑
|
|
53
|
-
- **LOW
|
|
67
|
+
- **LOW**:有实际收益的优化建议(性能、可维护性);风格类只在深审时报
|
|
54
68
|
|
|
55
69
|
## Output Template
|
|
56
70
|
|
|
@@ -62,10 +76,10 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
62
76
|
|
|
63
77
|
审查报告按需求归档在 `workflow-run/docs/requirements/<需求名>/`,规则见 `<skills>/workflow/references/requirements.md`:
|
|
64
78
|
|
|
65
|
-
- 路径用 `node <skills>/workflow/scripts/requirement.mjs path review`(分支对比按 dev 分支加 `--branch <dev分支>`),写到输出的 `path
|
|
79
|
+
- 路径用 `node <skills>/workflow/scripts/requirement.mjs path review`(分支对比按 dev 分支加 `--branch <dev分支>`),写到输出的 `path`(文件名固定为 `review.md`);已有报告时脚本先把它归档,输出的 `previous` 是上次报告,复审对照它。
|
|
66
80
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" [--branch <dev分支>]`;用户不给名字时 `bind` 按分支名生成。
|
|
67
81
|
- 报告同时完整输出到对话;落盘文件与对话内容一致
|
|
68
|
-
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`
|
|
82
|
+
- 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`
|
|
69
83
|
- 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <报告路径> --skill project-review --facts` 自检:「错误」(缺必填章节、缺元信息行、残留 `{{...}}`)先修正;「在 diff 和代码里都找不到」的名字逐个核实,写错了就改成真实的名字
|
|
70
84
|
|
|
71
85
|
## Verification
|
|
@@ -73,7 +87,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
73
87
|
- 每个结论都要能定位到具体文件+行号,并引用真实代码片段。
|
|
74
88
|
- 读不到的内容如实说明,不要臆测。
|
|
75
89
|
- 涉及跨分支对比时,确保 diff 基于正确的 base 分支(默认 `main`)。
|
|
76
|
-
- CRITICAL / HIGH 都经过第 6
|
|
90
|
+
- CRITICAL / HIGH 都经过第 6 步复核(能派子 agent 时独立复核),写得出触发条件;复核不了的标「⚠ 待确认」,不计入「不建议合并」的结论。
|
|
77
91
|
- 分批审查时,没精读的文件列在「未覆盖与待确认」里,不能让读者以为全部审过。
|
|
78
92
|
|
|
79
93
|
## 终端命令
|
|
@@ -39,6 +39,37 @@ git diff <main>...<dev> --name-only -G '@Transactional|synchronized|Lock\b|@Vers
|
|
|
39
39
|
- 每批结束先把发现记下来(文件:行号 + 一句话)再进入下一批,防止后面的批次挤掉前面的结论;批次多时可以边审边把发现写进报告草稿。
|
|
40
40
|
- 时间或上下文不够时,**P2 可以只看 `--stat`**,但要在报告「未覆盖与待确认」里列出只扫过 `--stat` 的文件;P0 不能跳过。
|
|
41
41
|
|
|
42
|
-
## 4.
|
|
42
|
+
## 4. 分角度并行查找(深审大改动,用户确认后)
|
|
43
|
+
|
|
44
|
+
一个人拿着整份清单从头扫到尾,清单越长、diff 越大,后面的维度越容易被草草带过。改动大时可以让几个子 agent 各盯一类问题,同时看同一份 diff。它比分批精读慢、用量多几倍,所以**三个条件都满足才问用户**,用户同意才走:
|
|
45
|
+
|
|
46
|
+
1. 审查深度是**深审**(见 SKILL.md 第 1 步);
|
|
47
|
+
2. 规模属于上表的「更大」(排除后 > 30 个文件或 > 1500 行改动);
|
|
48
|
+
3. 当前环境能派子 agent / 子任务(Claude Code 的 Agent 工具、Codex / Qwen Code 的子 agent 等)。
|
|
49
|
+
|
|
50
|
+
问法:「改动较大(<N> 个文件 / <M> 行),要不要分 5 个角度派子 agent 并行查找?更全面,但耗时和用量是平时的几倍。」用户不同意、或没回答就继续,按上一节分批精读。
|
|
51
|
+
|
|
52
|
+
**5 个角度**(对应检查清单的小节):
|
|
53
|
+
|
|
54
|
+
| 角度 | 清单小节 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| 正确性与字段完整性 | 1 正确性、2 空值、3 字段完整性 |
|
|
57
|
+
| 并发、事务与幂等 | 4 并发与线程安全、11 幂等与消息一致性 |
|
|
58
|
+
| 安全与异常 | 5 异常处理与资源释放、6 安全 |
|
|
59
|
+
| 数据库与配置 | 10 配置、12 数据库变更安全 |
|
|
60
|
+
| 性能、测试与语言专项 | 7 性能、13 测试、14 语言专项(深审时 8 风格也放这里) |
|
|
61
|
+
|
|
62
|
+
改动里没有对应内容的角度不派(例如没有 SQL / Mapper / DDL / 配置改动时不派「数据库与配置」)。
|
|
63
|
+
|
|
64
|
+
**每个子 agent 只给**:审查范围与对比分支(读 diff 的命令)、自己那几节清单的原文、第 2 步收集的背景(需求对照要点、实际表结构、DocNexus 约定、`config_center`),以及「只报本次改动引入的问题,按 P0 → P1 → P2 读,每条给 文件:行号、一句话结论、触发条件、级别;读不完的文件列出来」。不给其他角度的清单,也不给你已经发现的问题,免得它顺着你的思路走。
|
|
65
|
+
|
|
66
|
+
**收回之后**(由你自己做):
|
|
67
|
+
- 合并去重:同一位置、同一原因的发现合成一条,级别取高的;
|
|
68
|
+
- 子 agent 列出的没读完的文件汇总到「未覆盖与待确认」;
|
|
69
|
+
- 影响面(第 5 步)仍由你自己做,再对合并后的发现做第 6 步复核(独立复核时另派子 agent,不交回给找出它的那个)。
|
|
70
|
+
|
|
71
|
+
报告「审查方式」写「深审,分 <K> 个角度并行查找,独立复核」。
|
|
72
|
+
|
|
73
|
+
## 5. 未提交改动
|
|
43
74
|
|
|
44
75
|
审查工作区(dev 自审或用户说「我刚改完,帮我看看」):`git diff HEAD`(已暂存和未暂存一起);分支已有提交时再加 `git diff <main>...HEAD`。新建但未跟踪的文件用 `git status --short` 找出来逐个读。
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> 审查范围:{{分支对比 / 单文件 / 未提交改动}}:{{范围说明}} | 日期:{{YYYY-MM-DD}}
|
|
4
4
|
> 对比分支:`{{dev分支}}` ←→ `{{基线分支}}`(单文件 / 未提交改动两边都写当前分支)
|
|
5
|
-
> 审查方式:{{全量精读 / 分批精读(N 批,按风险排序)/ 部分摘要化}} | 清单来源:{{DocNexus / 项目文件 / skill 默认}}
|
|
6
|
-
> 参考产物:{{本分支的开发方案 / 交付说明 /
|
|
5
|
+
> 审查方式:{{快审 / 标准 / 深审}},{{全量精读 / 分批精读(N 批,按风险排序)/ 分 K 个角度并行查找 / 部分摘要化}},{{独立复核 / 自行复核}} | 清单来源:{{DocNexus / 项目文件 / skill 默认}}
|
|
6
|
+
> 参考产物:{{本分支的开发方案 / 交付说明 / 技术方案路径;复审时加「上次审查 v<N>(<日期>)」;没有写「无」}}
|
|
7
7
|
> DocNexus:{{沉淀后写 docnexus/library/<path>;未沉淀写「未沉淀」,未接入写「未接入」}}
|
|
8
8
|
|
|
9
9
|
## 结论摘要 *
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
|
|
35
35
|
## 优化建议(LOW)
|
|
36
36
|
|
|
37
|
+
<!-- 只写有实际收益的(性能、可维护性);风格类只在深审时写。快审写「无」 -->
|
|
38
|
+
|
|
37
39
|
| # | 位置 | 建议 |
|
|
38
40
|
|---|---|---|
|
|
39
41
|
|
|
@@ -55,4 +57,4 @@
|
|
|
55
57
|
|
|
56
58
|
## 未覆盖与待确认 *
|
|
57
59
|
|
|
58
|
-
{{分批审查时只扫过 --stat
|
|
60
|
+
{{分批审查时只扫过 --stat 的文件、复核后降级或待确认的发现、复核排除的条数、改动前就存在的问题、按审查深度没查的维度;都没有写「无」}}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
## 读代码:CodeGraph 原生工具
|
|
19
19
|
|
|
20
20
|
- **有 MCP 工具时**(Claude Code / Codex 经 `--with-codegraph` 写了配置):用 `codegraph_explore`,一次调用返回相关符号的源码(带行号)、调用路径和影响面摘要。问题里点名类名 / 方法名 / 文件,结果更准。
|
|
21
|
-
- **没有 MCP 工具时**(doubao、qwen、子 agent,或 MCP 没加载):在项目根用同名 CLI,输出相同:
|
|
21
|
+
- **没有 MCP 工具时**(doubao、qwen、workbuddy、子 agent,或 MCP 没加载):在项目根用同名 CLI,输出相同:
|
|
22
22
|
|
|
23
23
|
| 要做的事 | 命令 |
|
|
24
24
|
|---|---|
|
|
@@ -7,7 +7,7 @@ workflow-run/docs/requirements/<需求名>/
|
|
|
7
7
|
├── meta.json # 需求名、绑定的分支、各产物(种类、版本、日期、wiki 发布记录、DocNexus 沉淀)
|
|
8
8
|
├── plan.md # 开发方案(dev,L 规模或用户要求时)
|
|
9
9
|
├── delivery.md # 交付说明(dev)
|
|
10
|
-
├── review.md # 审查报告(project-review
|
|
10
|
+
├── review.md # 审查报告(project-review);复审也写这个文件,上一版归档(见「落盘」)
|
|
11
11
|
├── tech-doc.md # 技术方案(create-tech-md-doc)
|
|
12
12
|
└── release-doc.md # 上线方案(create-release-md-doc)
|
|
13
13
|
```
|
|
@@ -41,10 +41,11 @@ node <skills>/workflow/scripts/requirement.mjs which
|
|
|
41
41
|
node <skills>/workflow/scripts/requirement.mjs path <plan|delivery|review|tech|release>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path`
|
|
44
|
+
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path` 相同;文档仓库模式下是「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样。每种产物的文件名固定(`review.md`、`tech-doc.md` …),引用它的地方不会因为出了新版而失效。已有时,脚本先把当前版复制到 `workflow-run/tmp/old-artifacts/<需求名>/<种类>_v<N>.md`,输出的 `previous` 就是这份(复审对照上次的报告读它),`version` 是这次的版本号;然后把新版直接写到 `path`。
|
|
45
45
|
- 退出码 2:当前分支还没绑定需求,按上一节问用户需求名,`bind` 之后再运行一次。
|
|
46
46
|
- `wiki_record` 非空:上一版已经发布到 wiki。新版的**第一行原样写这条记录**,md-to-wiki 再发布时会更新同一个页面,不会新建页面。
|
|
47
47
|
- 写完运行 `requirement.mjs sync <path>`,更新 `meta.json`。md-to-wiki 发布后改了第一行的记录,或补写了 DocNexus 元信息行,也要再 sync 一次。
|
|
48
|
+
- sync 时新版内容和归档的上一版不同,才算新版本(`meta.json` 的 `versions` 加一);没写就放弃的话版本号不变。输出里 `archived` 非空时,告诉用户归档了哪些文件、在哪里(`workflow-run/tmp/old-artifacts/<需求名>/`,文档仓库模式是 `<需求名>/<项目名>/`,不提交),请用户确认不再需要后自己删除;**不要替用户删**。随代码或文档仓库提交过的旧版也能从 git 历史找回。
|
|
48
49
|
- sync 输出里 `commit` 非空:按下一节「文档仓库的提交」处理。
|
|
49
50
|
|
|
50
51
|
## 独立文档仓库(克隆到 workflow-run/)
|
|
@@ -63,9 +63,9 @@ export function collect(root, { allProjects = false } = {}) {
|
|
|
63
63
|
const k = KINDS.find((x) => x.kind === f.kind);
|
|
64
64
|
const text = fs.readFileSync(f.abs, 'utf8');
|
|
65
65
|
// 排序键:文档头里的日期 + 版本号(需求目录里的文件名不带日期)
|
|
66
|
-
out.push({ kind: k.kind, label: k.label, path: f.rel, cite: f.cite, requirement: f.slug, project: f.project, branch: parseBranch(text), date: `${headerDate(text)}#${String(f.version).padStart(3, '0')}`, mtime: fs.statSync(f.abs).mtimeMs, wiki: parseWiki(text) });
|
|
66
|
+
out.push({ kind: k.kind, label: k.label, path: f.rel, cite: f.cite, requirement: f.slug, project: f.project, version: f.version, branch: parseBranch(text), date: `${headerDate(text)}#${String(f.version).padStart(3, '0')}`, mtime: fs.statSync(f.abs).mtimeMs, wiki: parseWiki(text) });
|
|
67
67
|
}
|
|
68
|
-
//
|
|
68
|
+
// 同一天、无日期的文件按修改时间排
|
|
69
69
|
return out.sort(byDate);
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
8
8
|
|
|
9
9
|
const read = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } };
|
|
10
10
|
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
11
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'build', 'dist', 'out', '.gradle', '.idea', 'vendor', 'tmp', '.agents', '.claude', '.qwen', 'docnexus', 'workflow-run']);
|
|
11
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'build', 'dist', 'out', '.gradle', '.idea', 'vendor', 'tmp', '.agents', '.claude', '.qwen', '.codebuddy', 'docnexus', 'workflow-run']);
|
|
12
12
|
|
|
13
13
|
function git(root, args) {
|
|
14
14
|
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
// 用法(在项目根执行;--root 可指定项目根;--branch 缺省为当前 git 分支):
|
|
13
13
|
// requirement.mjs which [--branch b] # 当前分支绑定的需求;没绑定时给出建议名、最近的需求(文档仓库模式还列出其他项目的需求)
|
|
14
14
|
// requirement.mjs bind [--name <需求名>] [--branch b] [--base m] # 绑定(没有就新建);不给 --name 按分支名生成
|
|
15
|
-
// requirement.mjs path <plan|delivery|review|tech|release> [--branch b] #
|
|
16
|
-
//
|
|
15
|
+
// requirement.mjs path <plan|delivery|review|tech|release> [--branch b] # 本次落盘的文件路径(文件名固定;已有时上一版复制到
|
|
16
|
+
// # workflow-run/tmp/old-artifacts/<种类>_v<N>.md,输出 previous)
|
|
17
|
+
// requirement.mjs sync [<需求目录或其中的文件> | --all] # 按目录里的文件重建 meta.json;指定了目录 / 文件时确认新版本号、输出归档了哪些上一版;
|
|
18
|
+
// # 文档仓库模式下带上提交提示
|
|
17
19
|
// requirement.mjs pending [--branch b] # 文档仓库模式:当前需求在文档仓库里未提交的改动与建议的提交命令
|
|
18
20
|
// requirement.mjs list # 所有需求(名称、分支、产物数、更新时间)
|
|
19
21
|
// requirement.mjs config [--project <项目名>] [--commit-prompt on|off] [--local] # 查看 / 修改产物配置
|
|
@@ -34,11 +36,11 @@ export const IGNORE_LINE = `${HOME}/docs/`;
|
|
|
34
36
|
export const homeDir = (root) => path.join(root, HOME);
|
|
35
37
|
// workflow-run/ 自己是 git 仓库(团队文档仓库克隆到这里)= 独立文档仓库模式
|
|
36
38
|
export const homeIsRepo = (root) => fs.existsSync(path.join(root, HOME, '.git'));
|
|
37
|
-
// 产物种类 →
|
|
39
|
+
// 产物种类 → 需求目录里的文件名(只放最新版 <base>.md;版本号在 meta.json 的 versions,旧版归档为 <base>_v<N>.md)
|
|
38
40
|
export const FILES = { plan: 'plan', delivery: 'delivery', review: 'review', tech: 'tech-doc', release: 'release-doc' };
|
|
39
41
|
const LABELS = { plan: '开发方案', delivery: '交付说明', review: '审查报告', tech: '技术方案', release: '上线方案' };
|
|
40
42
|
// 安装后的 skills 目录(安装器从安装包调用本脚本时,到项目里找画像)
|
|
41
|
-
const INSTALL_BASES = ['.agents/skills', '.claude/skills', '.qwen/skills'];
|
|
43
|
+
const INSTALL_BASES = ['.agents/skills', '.claude/skills', '.qwen/skills', '.codebuddy/skills'];
|
|
42
44
|
|
|
43
45
|
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
44
46
|
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
@@ -181,11 +183,12 @@ export function artifactFiles(root, { allProjects = false } = {}) {
|
|
|
181
183
|
: requirements(root, L).map((r) => ({ abs: r.abs, slug: r.slug, project: L.project }));
|
|
182
184
|
const out = [];
|
|
183
185
|
for (const d of dirs) {
|
|
186
|
+
const versions = versionsOf(d.abs);
|
|
184
187
|
for (const name of fs.readdirSync(d.abs)) {
|
|
185
188
|
const p = parseFile(name);
|
|
186
189
|
if (!p) continue;
|
|
187
190
|
const abs = path.join(d.abs, name);
|
|
188
|
-
out.push({ abs, rel: relOf(root, abs), cite: L.ref(abs), slug: d.slug, project: d.project, ...p });
|
|
191
|
+
out.push({ abs, rel: relOf(root, abs), cite: L.ref(abs), slug: d.slug, project: d.project, ...p, ...(name === `${FILES[p.kind]}.md` ? { version: versions[p.kind] ?? 1 } : {}) });
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
return out;
|
|
@@ -198,16 +201,36 @@ export function find(root, branch, L = layout(root)) {
|
|
|
198
201
|
return hits[0] ?? null;
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
// 同种产物只留最新版,文件名固定(review.md、tech-doc.md …),文档里的引用不会失效。版本号记在 meta.json 的 versions。
|
|
205
|
+
// path 时把当前版复制到 workflow-run/tmp/old-artifacts/<需求目录(相对 requirements/)>/<种类>_v<N>.md(不提交),
|
|
206
|
+
// 写完 sync <path> 时新版内容和这份副本不同才算新版本(versions 加一);没写就放弃的话版本号不变
|
|
207
|
+
export const OLD_DIR = `${TMP_DIR}/old-artifacts`;
|
|
208
|
+
const archivePath = (root, dirAbs, kind, v) => path.join(root, OLD_DIR, path.relative(path.join(root, REQ_DIR), dirAbs), `${FILES[kind]}_v${v}.md`);
|
|
209
|
+
const versionsOf = (dirAbs) => readJson(path.join(dirAbs, 'meta.json'))?.versions ?? {};
|
|
210
|
+
|
|
201
211
|
// 按目录里的文件重建 meta.json(保留需求名、分支、创建时间)
|
|
202
|
-
|
|
212
|
+
// bump:写完新版后(sync <path>)确认新版本——和 path 时归档的上一版副本内容不同就把版本号加一,返回归档了哪些
|
|
213
|
+
export function sync(root, dirRel, { bump = false } = {}) {
|
|
203
214
|
const dir = path.resolve(root, dirRel);
|
|
204
215
|
const f = path.join(dir, 'meta.json');
|
|
205
216
|
const old = readJson(f) ?? {};
|
|
217
|
+
const versions = { ...(old.versions ?? {}) };
|
|
218
|
+
const archived = [];
|
|
219
|
+
if (bump) {
|
|
220
|
+
for (const kind of Object.keys(FILES)) {
|
|
221
|
+
const cur = path.join(dir, `${FILES[kind]}.md`);
|
|
222
|
+
const v = versions[kind] ?? 1;
|
|
223
|
+
const arc = archivePath(root, dir, kind, v);
|
|
224
|
+
if (!fs.existsSync(cur) || !fs.existsSync(arc) || fs.readFileSync(arc).equals(fs.readFileSync(cur))) continue;
|
|
225
|
+
versions[kind] = v + 1;
|
|
226
|
+
archived.push({ kind, version: v, file: relOf(root, arc) });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
206
229
|
const artifacts = fs.readdirSync(dir).map((name) => {
|
|
207
230
|
const p = parseFile(name);
|
|
208
231
|
if (!p) return null;
|
|
209
232
|
const text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
210
|
-
return { kind: p.kind, version: p.version, file: name, branch: parseBranch(text), date: headerDate(text), wiki: parseWiki(text), docnexus: parseDocnexus(text) };
|
|
233
|
+
return { kind: p.kind, version: name === `${FILES[p.kind]}.md` ? versions[p.kind] ?? 1 : p.version, file: name, branch: parseBranch(text), date: headerDate(text), wiki: parseWiki(text), docnexus: parseDocnexus(text) };
|
|
211
234
|
}).filter(Boolean).sort((a, b) => Object.keys(FILES).indexOf(a.kind) - Object.keys(FILES).indexOf(b.kind) || a.version - b.version);
|
|
212
235
|
const branches = [...new Set([...(old.branches ?? []), ...artifacts.map((a) => a.branch).filter(Boolean)])];
|
|
213
236
|
const latest = Object.fromEntries(Object.keys(FILES).map((k) => [k, artifacts.filter((a) => a.kind === k).at(-1)?.file ?? null]));
|
|
@@ -215,11 +238,11 @@ export function sync(root, dirRel) {
|
|
|
215
238
|
const changed = JSON.stringify(old.artifacts ?? null) !== JSON.stringify(artifacts) || JSON.stringify(old.branches ?? null) !== JSON.stringify(branches);
|
|
216
239
|
const meta = {
|
|
217
240
|
name: old.name ?? path.basename(dir), named_by: old.named_by ?? 'user', ...(old.project ? { project: old.project } : {}), branches, base: old.base ?? null,
|
|
218
|
-
created: old.created ?? now(), updated: changed || !old.updated ? now() : old.updated, latest, artifacts,
|
|
241
|
+
created: old.created ?? now(), updated: changed || !old.updated ? now() : old.updated, latest, ...(Object.keys(versions).length ? { versions } : {}), artifacts,
|
|
219
242
|
};
|
|
220
243
|
const out = `${JSON.stringify(meta, null, 2)}\n`;
|
|
221
244
|
if (!fs.existsSync(f) || fs.readFileSync(f, 'utf8') !== out) fs.writeFileSync(f, out);
|
|
222
|
-
return meta;
|
|
245
|
+
return bump ? { ...meta, archived } : meta;
|
|
223
246
|
}
|
|
224
247
|
|
|
225
248
|
const notReady = (L) => ({ ok: false, mode: L.mode, ...(L.need_project ? { need_project: true, suggested_project: L.suggested_project } : {}), hint: L.hint });
|
|
@@ -267,20 +290,25 @@ export function bind(root, { name = null, branch, base = null }) {
|
|
|
267
290
|
return { ok: true, mode: L.mode, created: !existedHere, joined: !!target && !existedHere && L.mode === 'repo', name: meta.name, named_by: meta.named_by, dir: rel, ...(L.mode === 'repo' ? { project: L.project } : {}), branches: sync(root, rel).branches };
|
|
268
291
|
}
|
|
269
292
|
|
|
270
|
-
//
|
|
293
|
+
// 本次落盘路径:文件名固定(review.md …),已有时先把当前版复制到 tmp/old-artifacts/<种类>_v<N>.md(previous,复审对照用),
|
|
294
|
+
// 并带上它第一行的 wiki 发布记录;新版直接写到 path(覆盖当前版),写完 sync <path> 确认版本号
|
|
271
295
|
export function nextPath(root, kind, branch) {
|
|
272
296
|
if (!FILES[kind]) return { code: 1, out: { ok: false, error: `未知产物种类「${kind}」:${Object.keys(FILES).join(' / ')}` } };
|
|
273
297
|
const L = layout(root);
|
|
274
298
|
if (!L.ok) return { code: 2, out: notReady(L) };
|
|
275
299
|
const r = find(root, branch, L);
|
|
276
300
|
if (!r) return { code: 2, out: { ok: false, ...which(root, branch) } };
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
const abs = path.join(r.abs, `${FILES[kind]}.md`);
|
|
302
|
+
let v = 1; let previous = null; let record = null;
|
|
303
|
+
if (fs.existsSync(abs)) {
|
|
304
|
+
const cur = versionsOf(r.abs)[kind] ?? 1;
|
|
305
|
+
const arc = archivePath(root, r.abs, kind, cur);
|
|
306
|
+
fs.mkdirSync(path.dirname(arc), { recursive: true });
|
|
307
|
+
fs.copyFileSync(abs, arc);
|
|
308
|
+
v = cur + 1; previous = relOf(root, arc);
|
|
309
|
+
record = /^<!-- md-to-wiki: [^\n]*-->/.exec(fs.readFileSync(abs, 'utf8'))?.[0] ?? null;
|
|
310
|
+
}
|
|
311
|
+
return { code: 0, out: { ok: true, mode: L.mode, name: r.name, dir: r.dir, path: relOf(root, abs), cite: L.ref(abs), version: v, previous, wiki_record: record } };
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
export function list(root) {
|
|
@@ -390,8 +418,12 @@ function main(argv) {
|
|
|
390
418
|
target = requirements(root, L).find((r) => abs === r.abs || abs.startsWith(r.abs + path.sep)) ?? null;
|
|
391
419
|
} else target = find(root, branch, L);
|
|
392
420
|
if (!target) { console.error('不在当前项目的需求目录下,或当前分支没有绑定需求'); return 1; }
|
|
393
|
-
const m = sync(root, target.dir);
|
|
394
|
-
const out = { dir: target.dir, name: m.name, artifacts: m.artifacts.length, latest: m.latest };
|
|
421
|
+
const m = sync(root, target.dir, { bump: true });
|
|
422
|
+
const out = { dir: target.dir, name: m.name, artifacts: m.artifacts.length, latest: m.latest, versions: m.versions ?? {} };
|
|
423
|
+
if (m.archived.length) {
|
|
424
|
+
const to = path.posix.dirname(m.archived[0].file);
|
|
425
|
+
out.archived = { files: m.archived, dir: to, hint: `上一版已归档到 ${to}/(不提交),目录里只留最新版。告诉用户归档了哪些文件,请用户确认不再需要后自己删除;不要替用户删` };
|
|
426
|
+
}
|
|
395
427
|
// 文档仓库模式:写完产物后提示提交(由用户确认;artifacts.commit_prompt 关闭时不提示)
|
|
396
428
|
if (L.mode === 'repo') {
|
|
397
429
|
const pd = pending(root, branch, target.dir);
|
|
@@ -36,7 +36,7 @@ export function viewData(root) {
|
|
|
36
36
|
if (a.branch) g.branches.add(a.branch);
|
|
37
37
|
if (a.project) g.projects.add(a.project);
|
|
38
38
|
const text = fs.readFileSync(path.join(root, a.path), 'utf8');
|
|
39
|
-
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date: (a.date || '').slice(0, 10) || null, branch: a.branch, wiki: a.wiki, text };
|
|
39
|
+
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date: (a.date || '').slice(0, 10) || null, version: a.version ?? 1, branch: a.branch, wiki: a.wiki, text };
|
|
40
40
|
if (a.kind === 'review') item.review = parseReview(text);
|
|
41
41
|
if (a.kind === 'delivery') item.delivery = parseDelivery(text);
|
|
42
42
|
if (a.kind === 'review' || a.kind === 'delivery') item.docnexus = captureState(text);
|
|
@@ -214,8 +214,8 @@ function renderList() {
|
|
|
214
214
|
function reviewBlock(r) {
|
|
215
215
|
const rs = r.artifacts.filter((a) => a.kind === 'review');
|
|
216
216
|
if (!rs.length) return '';
|
|
217
|
-
const row = (a
|
|
218
|
-
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (
|
|
217
|
+
const row = (a) => { const v = a.review; const f = v.follow_up;
|
|
218
|
+
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (v.is_rereview ? '复审' : '首次') + '</td>'
|
|
219
219
|
+ '<td><span class="lv c">' + v.levels.CRITICAL + '</span> <span class="lv h">' + v.levels.HIGH + '</span> ' + v.levels.MEDIUM + ' / ' + v.levels.LOW + '</td>'
|
|
220
220
|
+ '<td>✅ ' + v.verified + ' · ⚠ ' + v.pending + '</td><td>' + (v.is_rereview ? '<span class="lv f">已修复 ' + f.fixed + '</span> 仍存在 ' + f.open + ' · 不再适用 ' + f.obsolete : '—') + '</td><td>' + esc(a.docnexus === 'captured' ? '已沉淀' : a.docnexus === 'not_captured' ? '未沉淀' : a.docnexus === 'not_connected' ? '未接入' : '—') + '</td></tr>'; };
|
|
221
221
|
return '<section class="block"><h3>审查记录</h3><table><thead><tr><th>报告</th><th>日期</th><th>轮次</th><th>CRITICAL · HIGH · MEDIUM / LOW</th><th>复核</th><th>与上次对比</th><th>DocNexus</th></tr></thead><tbody>' + rs.map(row).join('') + '</tbody></table></section>';
|
|
@@ -253,7 +253,7 @@ function select(id, docPath) {
|
|
|
253
253
|
try { history.replaceState(null, '', '#' + encodeURIComponent(id)); } catch {}
|
|
254
254
|
const latest = (k) => r.artifacts.filter((a) => a.kind === k).at(-1);
|
|
255
255
|
const pipeline = '<div class="pipeline">' + D.kinds.map((k) => { const a = latest(k.kind); const n = r.stages[k.kind];
|
|
256
|
-
return '<div class="stage' + (n ? ' y' : '') + '"><div class="t">' + k.label + '</div><div class="c">' + (n ? (
|
|
256
|
+
return '<div class="stage' + (n ? ' y' : '') + '"><div class="t">' + k.label + '</div><div class="c">' + (n ? (a && a.version > 1 ? 'v' + a.version : '✓') + (a && a.date ? ' · ' + a.date : '') : '—') + '</div>' + (a && a.wiki ? '<div class="t">wiki ' + (a.wiki.url ? '<a href="' + esc(a.wiki.url) + '" target="_blank" rel="noopener">pageId ' + esc(a.wiki.pageId) + '</a>' : 'pageId ' + esc(a.wiki.pageId)) + '</div>' : '') + '</div>'; }).join('') + '</div>';
|
|
257
257
|
const order = D.kinds.map((k) => k.kind);
|
|
258
258
|
const files = '<div class="files">' + r.artifacts.slice().sort((x, y) => order.indexOf(x.kind) - order.indexOf(y.kind)).map((a) => '<button data-doc="' + esc(a.path) + '">' + esc(a.label) + ' · ' + esc(a.file) + '</button>').join('') + '</div>';
|
|
259
259
|
$('#main').innerHTML = overview() + '<h2>' + esc(r.name) + '</h2><div class="sub">' + esc(r.dir ?? '') + (r.projects.length ? ' · 项目 ' + esc(r.projects.join('、')) : '') + ' · 分支 ' + esc(r.branches.join('、') || '无') + (r.named_by === 'branch' ? ' · <span class="tag">需求名由分支名生成,可在 meta.json 改</span>' : '') + '</div>'
|