@rowansenne/workflow-run 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.21.0] - 2026-10-18
8
+
9
+ ### 变更(破坏性)
10
+ - **默认模板只读,定制模板只放 DocNexus**:skills 自带的模板不能改;修改或新建模板一律写进 DocNexus 知识库,便于团队共享(每次变更有 timeline)。`workflow-run/templates/` 下的项目模板不再生效,`resolve.mjs` 输出 `stale_project_file`,`doctor` 提示导入知识库后删除;接入 DocNexus 后缺模板时仍把它作为 `import_from` 提议导入。
11
+ - 未接入 DocNexus 时:只用默认模板;`template.mjs draft` / `save` 退出码 2、输出 `need_docnexus: true`,提示先 `--with-docnexus` 接入。技术方案、上线方案没有默认模板,`resolve.mjs template` 同样返回 `need_docnexus`,要先接入 DocNexus 才能创建模板和生成文档;`resolve.mjs status` 新增 `templates_need_docnexus`。
12
+ - `template.mjs save` 不再写项目文件、不再备份到 `workflow-run/tmp/template-backup/`,只给出写入知识库的 capture 指引;撤销按 DocNexus 的 timeline 恢复。
13
+
14
+ ## [0.20.0] - 2026-10-18
15
+
16
+ ### 变更
17
+ - **project-review 独立复核**:能派子 agent 的环境(Claude Code、Codex、Qwen Code 等)里,CRITICAL / HIGH 每条交给一个只拿到结论、位置、触发条件的子 agent 重新读代码判定「成立 / 可能成立 / 不成立」,不成立的删掉并在「未覆盖与待确认」记复核排除的条数;派不了时自己复核,先找不成立的理由。复核方式写进报告「审查方式」。
18
+ - **project-review 分角度并行查找**(深审大改动,用户确认后):深审、改动超过 30 个文件或 1500 行、环境能派子 agent 时,先问用户要不要分 5 个角度(正确性与字段完整性 / 并发事务与幂等 / 安全与异常 / 数据库与配置 / 性能测试与语言专项)各派一个子 agent 同时查;同意后主 agent 合并去重、做影响面,再交独立复核。不同意照旧分批精读。
19
+ - **project-review 审查深度与降噪**:分快审 / 标准(默认)/ 深审,写进「审查方式」。快审只查正确性、空值、安全、事务与幂等;标准不查风格;深审全部维度并复核 MEDIUM。新增「只报会出问题的」:风格、linter 能拦的、个人偏好、写不出后果的建议不报,LOW 只写有实际收益的优化。
20
+ - **需求目录里每种产物只留最新版,文件名固定**(`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` 显示版本号。
21
+ - `/workflow view` 的审查记录按报告内容判断首次 / 复审(原来按排序位置,只剩一份报告时会把复审标成首次)。
22
+
7
23
  ## [0.19.0] - 2026-10-18
8
24
 
9
25
  ### 变更(破坏性)
@@ -251,7 +267,9 @@
251
267
  - 安装器改为单一 Node 实现,`init-skills.sh` / `init-skills.ps1` 只是薄封装。
252
268
  - `code-review` 改名为 `project-review`,升级时自动清理旧名。
253
269
 
254
- [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.19.0...HEAD
270
+ [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.21.0...HEAD
271
+ [0.21.0]: https://github.com/guilianerde/skills-init/compare/v0.20.0...v0.21.0
272
+ [0.20.0]: https://github.com/guilianerde/skills-init/compare/v0.19.0...v0.20.0
255
273
  [0.19.0]: https://github.com/guilianerde/skills-init/compare/v0.18.0...v0.19.0
256
274
  [0.18.0]: https://github.com/guilianerde/skills-init/compare/v0.17.0...v0.18.0
257
275
  [0.17.0]: https://github.com/guilianerde/skills-init/compare/v0.16.0...v0.17.0
package/README.md CHANGED
@@ -315,7 +315,7 @@ mv .agents/skills/.skills-init.json .agents/skills/.workflow-run.json
315
315
  | 清单里有、新版里已没有 | 没改过直接删除;改过的备份后删除 |
316
316
 
317
317
  * 这样 `SKILL.md`、`skill.json`、脚本始终是同一个版本,不会出现「新 SKILL.md 配旧 skill.json」的混搭。
318
- * 项目自己的定制不要改 skills 目录里的文件,放到不会被覆盖的地方:项目事实和约定写 `workflow/profile.json`(约定写 `notes`),文档结构用 `workflow-run/templates/<skill>/` 或 DocNexus 里的模板,通用规则写 `CLAUDE.md` / `AGENTS.md`。
318
+ * 项目自己的定制不要改 skills 目录里的文件,放到不会被覆盖的地方:项目事实和约定写 `workflow/profile.json`(约定写 `notes`),文档模板只能在 DocNexus 里定制(skills 自带的默认模板只读),通用规则写 `CLAUDE.md` / `AGENTS.md`。
319
319
  * 需要找回手改内容时到备份目录里取。
320
320
 
321
321
  ### 卸载
@@ -394,7 +394,7 @@ npx @rowansenne/workflow-run uninstall
394
394
  | `workflow/scripts/artifacts.mjs [--branch b \| --all]` | 按分支汇总 `workflow-run/docs/requirements/` 下的开发方案、交付说明、审查报告、技术方案、上线方案及其 wiki 发布记录,给出下一步建议;分支取自文档元信息行,不另存记录。上线方案没传 `--tech` 时据此自动引用同分支的技术方案;project-review 据此对照本分支方案、识别复审 |
395
395
  | `workflow/scripts/report.mjs [--since d] [--all-branches] [--json]` | 本地效果报告(`/workflow report`、`workflow-run report`):审查发现与修复率、复核率、需求澄清、沉淀率、知识引用、各分支链路 |
396
396
  | `workflow/scripts/tables.mjs [--diff b...h \| --base b \| --files a,b]` | 找出改动涉及哪些表:实体注解(`@TableName` / `@Table`,没写表名时按类名推断)、Mapper XML 与代码里的 SQL、DDL 脚本;标出 DDL、是否在 `schema.md` 里,并给出拉表结构的命令。dev 调研 / 自检与 project-review 审 SQL / DDL 时使用 |
397
- | `workflow/scripts/template.mjs list / draft / check / save <模板>` | 修改 / 创建模板(`/workflow template`):把当前模板复制成草稿(或 `--from <样例>` 抽骨架),`check` 校验格式、列出章节变化、拿最近几份已生成文档试跑 lint 看影响,`save` 备份旧模板后写入项目模板并确认生效;已接入 DocNexus 时 `save` 给出 capture 指引,不直接写知识库 |
397
+ | `workflow/scripts/template.mjs list / draft / check / save <模板>` | 修改 / 创建模板(`/workflow template`):把当前模板复制成草稿(或 `--from <样例>` 抽骨架),`check` 校验格式、列出章节变化、拿最近几份已生成文档试跑 lint 看影响,`save` 给出写入 DocNexus 知识库的 capture 指引,不直接写知识库;未接入 DocNexus 时 `draft` / `save` 提示先接入(默认模板只读) |
398
398
  | `workflow/scripts/pr-desc.mjs [--branch b] [--base m] [--out f]` | 按本分支产物生成 PR / MR 描述;没有交付说明时退回用 git 记录 |
399
399
  | `database-query/scripts/query.mjs` | 执行 SQL:选连接、取密码(只通过环境变量传给客户端)、会话层只读、拦截写语句和多语句、自动补 LIMIT、超时、脱敏敏感列;支持 mysql / psql 客户端,其他 driver 退回 JDBC |
400
400
  | `database-query/scripts/query.mjs --schema [--tables a,b] [--write]` | 按只读会话导出表结构为 Markdown;`--write` 写入 `references/schema.md` 的 `schema:begin` / `schema:end` 之间,标记外的补充说明保留;指定的表在库里不存在时标出 |
@@ -477,13 +477,13 @@ workflow-run/docs/requirements/<需求名>/
477
477
  ├── meta.json # 需求名、绑定的分支、各产物(版本、日期、wiki 发布记录、DocNexus 沉淀),给以后的可视化用
478
478
  ├── plan.md # 开发方案(dev,L 规模)
479
479
  ├── delivery.md # 交付说明(dev,每次完整交付都写)
480
- ├── review.md # 审查报告(project-review);复审 review_v2.md、review_v3.md
480
+ ├── review.md # 审查报告(project-review);复审也写这个文件,上一版归档到 workflow-run/tmp/old-artifacts/
481
481
  ├── tech-doc.md # 技术方案
482
482
  └── release-doc.md # 上线方案
483
483
  ```
484
484
 
485
485
  * **需求名由你定**:dev 确定是 M / L 规模(要落盘)时,或从 `/workflow` 完整链路开始时,如果当前分支还没有绑定需求,会问你这个需求叫什么(默认值按分支名生成;也可以选已有的需求,例如修 bug 的新分支挂到原需求下)。你不起名就按分支名生成。绑定后,同一分支上的各 skill 都写到这个目录,不再问。一个需求可以绑定多个分支。
486
- * 路径由 `workflow/scripts/requirement.mjs path <种类>` 给出,同种产物已存在时加 `_v2`、`_v3`,不覆盖旧文件;上一版发布过 wiki 时,新版沿用同一个页面。
486
+ * 路径由 `workflow/scripts/requirement.mjs path <种类>` 给出,每种产物文件名固定,目录里只留最新版,引用不会失效;版本号记在 `meta.json` 的 `versions`。已有旧版时,上一版先复制到 `workflow-run/tmp/old-artifacts/<需求>/<种类>_v<N>.md`(不提交),新版直接覆盖写入;skill 会列出归档的文件,请你确认后自己删除。上一版发布过 wiki 时,新版沿用同一个页面。
487
487
  * 需求目录写在项目根的 `.ignore` 里,ripgrep 和 agent 的代码搜索会跳过它,产物多了不会污染搜索结果(找历史决策用 DocNexus)。git 不受影响,建议随代码提交。
488
488
 
489
489
  **独立文档仓库(可选)**:不想让产物进代码仓库、或一个需求横跨多个服务时,装好 skills 后在项目根运行一条命令:
@@ -508,11 +508,10 @@ npx @rowansenne/workflow-run docs-repo <团队文档仓库地址> [--project <
508
508
  | --- | --- | --- |
509
509
  | database-query | 查询结果 | 默认对话输出;导出时 `workflow-run/tmp/database-query/result-<时间戳>.csv` |
510
510
  | md-to-wiki | 转换临时 XHTML | `workflow-run/tmp/md-to-wiki/_wiki_upload_<时间戳>.html`(上传后可删) |
511
- | 各文档类 skill | 项目模板 | `workflow-run/templates/<skill>/`(见下文「模板」) |
512
511
 
513
512
  规则:
514
513
 
515
- * 本工具的产出都在 `workflow-run/`:`docs/requirements/`(需求产物)、`docnexus/`(DocNexus 工作区)、`templates/`(项目模板)是**持久产物**,随代码提交或在独立文档仓库里提交,团队共享。
514
+ * 本工具的产出都在 `workflow-run/`:`docs/requirements/`(需求产物)、`docnexus/`(DocNexus 工作区,含定制的文档模板)是**持久产物**,随代码提交或在独立文档仓库里提交,团队共享。
516
515
  * `workflow-run/tmp/` 是**临时产物**目录,安装时自动把 `/workflow-run/tmp/` 加入 `.gitignore`,不入库。
517
516
 
518
517
  ## 自定义模板
@@ -593,8 +592,9 @@ workflow-run(安装器仓库)/
593
592
 
594
593
  文档类 skill 的模板来源取决于项目是否接入了 DocNexus,生成的文档开头会注明「模板来源」:
595
594
 
596
- * **已接入 DocNexus:只用知识库里的模板** `workflow-run/docnexus/library/templates/<skill>/<文件名>`(分类 `doc-template`)。不再使用 skill 自带的默认模板,也不使用 `workflow-run/templates/` 下的文件。库里缺模板时 skill 会停下来,提示你导入后再继续,不会悄悄回退到默认模板。
597
- * **未接入 DocNexus**:依次取项目文件 `workflow-run/templates/<skill>/<文件名>`、skill 自带默认模板。
595
+ * **已接入 DocNexus:只用知识库里的模板** `workflow-run/docnexus/library/templates/<skill>/<文件名>`(分类 `doc-template`)。不再使用 skill 自带的默认模板。库里缺模板时 skill 会停下来,提示你导入后再继续,不会悄悄回退到默认模板。
596
+ * **未接入 DocNexus**:只用 skill 自带的默认模板。
597
+ * **默认模板只读,定制只走 DocNexus**:要改模板或新建模板,都写进 DocNexus 知识库,团队共享同一份、每次变更有 timeline。不要直接改 skills 目录里的模板(升级会覆盖);`workflow-run/templates/` 下的项目模板已不再生效,`doctor` 会提示你导入知识库后删除。
598
598
 
599
599
  | skill | 文件名 | 用途 | 默认模板 |
600
600
  | --- | --- | --- | --- |
@@ -605,18 +605,17 @@ workflow-run(安装器仓库)/
605
605
  | project-review | `checklist.md` | 审查清单 | 有 |
606
606
  | project-review | `report.md` | 审查报告模板 | 有 |
607
607
 
608
- **技术方案、上线方案的模板按团队自己的格式创建**:各团队发到 wiki 的文档格式不同,skills 不再自带固定格式。第一次生成时 skill 会停下来,请你给一份团队现有的同类文档(本地 Markdown,或 wiki 的 pageId / URL)作样例,用 `workflow/scripts/make-template.mjs` 抽出章节和表格结构,把正文换成填写说明,你确认后保存:未接入 DocNexus 时存到 `workflow-run/templates/<skill>/template.md`(建议提交,团队共用),已接入时经你授权写入 DocNexus。之后想换格式,直接说「重新生成技术方案模板」。
608
+ **技术方案、上线方案的模板按团队自己的格式创建**:各团队发到 wiki 的文档格式不同,skills 不再自带固定格式。第一次生成时 skill 会停下来,请你给一份团队现有的同类文档(本地 Markdown,或 wiki 的 pageId / URL)作样例,用 `workflow/scripts/make-template.mjs` 抽出章节和表格结构,把正文换成填写说明,你确认后经你授权写入 DocNexus。因此这两类文档**需要先接入 DocNexus**(`--with-docnexus`),未接入时 skill 会停下来提示。之后想换格式,直接说「重新生成技术方案模板」。
609
609
 
610
610
  **修改模板**:说「改技术方案模板,加一节回滚方案」或 `/workflow template release 去掉灰度那节`,agent 会一路走完:
611
611
 
612
612
  1. 把当前生效的模板复制成草稿 `workflow-run/tmp/template-draft/<skill>/<文件名>`,只改草稿;
613
613
  2. `template.mjs check` 校验格式(必填 ` *`、可选「(如有)」、占位符),列出新增 / 删除的章节、必填变化,并拿最近几份已生成的文档试跑,告诉你新模板下它们会多出哪些错误;
614
- 3. 你确认后 `template.mjs save`:旧模板备份到 `workflow-run/tmp/template-backup/<时间>/`,写入 `workflow-run/templates/<skill>/`,再确认生效;已接入 DocNexus 时经你授权按 capture 流程写入知识库。
614
+ 3. 你确认后经你授权按 capture 流程写入 DocNexus 知识库,再确认生效;要撤销按 DocNexus 的 timeline 恢复旧版本。
615
615
 
616
- 模板别名:`tech`、`release`、`plan`、`delivery`、`review`、`checklist`。改带默认版本的模板(开发方案、交付说明、审查报告、审查清单)也走这条路,保存后项目文件优先,升级不会覆盖。
616
+ 模板别名:`tech`、`release`、`plan`、`delivery`、`review`、`checklist`。改带默认版本的模板(开发方案、交付说明、审查报告、审查清单)也走这条路,知识库里的版本取代默认版本,升级不会覆盖。未接入 DocNexus 时不能修改模板,agent 会提示你先接入。
617
617
 
618
- * 老项目升级时,如果还在用原来自带的默认模板(项目和 DocNexus 里都没有自己的版本),安装器会先把它保存到 `workflow-run/templates/<skill>/template.md`,生成效果不变。
619
- * `doctor` 和 `/workflow status` 会列出还没创建的模板。
618
+ * `doctor` 和 `/workflow status` 会列出还没创建的模板,以及要先接入 DocNexus 才能创建的模板。
620
619
 
621
620
  **模板导入 DocNexus**:
622
621
 
@@ -630,7 +629,7 @@ npx @rowansenne/workflow-run seed-templates --diff # 团队改过
630
629
  * 只导入 DocNexus 里还没有的、有默认版本的模板(技术方案、上线方案不在内),已有的不覆盖;项目已接入但缺模板时,安装结束会列出缺哪几份。
631
630
  * 之后模板随知识库更新:直接编辑 `workflow-run/docnexus/library/templates/...` 后在 `workflow-run/` 下执行 `./docnexus/node_modules/.bin/docnexus document sync --id <id>` 采纳;或者在生成文档后,skill 发现你做了结构性调整时会提议同步回模板(你确认后才写入)。每次变更都记录在 DocNexus 的 timeline 里,可追溯。
632
631
  * 模板里的 `{{...}}` 是给 agent 的填写提示,DocNexus 原样保存;不要写成单独一行的 `{{fact:...}}`(那是 DocNexus 的事实引用语法)。
633
- * 没接入 DocNexus 时,可用 `workflow-run/templates/` 下的同名文件覆盖默认模板;之后接入 DocNexus,skill 会提示你把这些文件导入知识库(导入后它们不再生效)。
632
+ * 以前放在 `workflow-run/templates/` 下的项目模板:接入 DocNexus 后,缺模板时 skill 会把它作为 `import_from` 提议导入知识库;导入后删除这些文件。
634
633
 
635
634
  ## 触发准确度评估
636
635
 
@@ -50,7 +50,7 @@ export function doctor(root, offline) {
50
50
  const file = path.join(dir, f);
51
51
  const label = toPosix(path.join(rel, skill, f));
52
52
  if (!fs.existsSync(file)) { missing++; meh(`缺少 ${label}:运行 workflow-run upgrade 恢复`); }
53
- else if (fileHash(file) !== h) { modified++; meh(`${label} 被手动修改过(升级时会被新版覆盖,原文件备份到 workflow-run/tmp/upgrade-backup/;项目定制请放到 profile.json、workflow-run/templates/ 或 DocNexus)`); }
53
+ else if (fileHash(file) !== h) { modified++; meh(`${label} 被手动修改过(升级时会被新版覆盖,原文件备份到 workflow-run/tmp/upgrade-backup/;项目定制请放到 profile.json 或 DocNexus,模板只能在 DocNexus 里定制)`); }
54
54
  }
55
55
  }
56
56
  if (!modified && !missing) ok(`${toPosix(rel)}:${Object.keys(man.skills).length} 个 skill 的模板文件与安装时一致`);
@@ -101,12 +101,14 @@ export function doctor(root, offline) {
101
101
  else ok(`产出目录 ${HOME}/:${mode}`);
102
102
  if (st.docnexus.ok) {
103
103
  if (!reportTemplateStatus(root, meh)) ok('DocNexus 已接入,文档模板齐全且与默认模板同步(或为团队定制版)');
104
- for (const f of st.docnexus.stale_project_templates) meh(`${f} 在接入 DocNexus 后已不再生效,建议删除`);
105
104
  if (docnexusSupportsCategory(root) === false) meh(`已装的 DocNexus 不支持 draft seal --category:各 skill 的分类只能经 metadata.category 写入,不记为调用方指定。升级:${docnexusUpgradeCmd()}`);
106
105
  else ok('DocNexus 支持调用方显式分类(draft seal --category)');
107
106
  if (docnexusSupportsRecallCategory(root) === false) meh(`已装的 DocNexus 不支持 recall --category:knowledge.mjs --query 只列文档,skill 改用不带分类的 recall。升级:${docnexusUpgradeCmd()}`);
108
107
  else ok('DocNexus 支持按分类检索(recall --category)');
109
- } else console.log(' -- DocNexus 未接入(可选)');
108
+ } else console.log(' -- DocNexus 未接入(可选;未接入时只能用 skills 自带的默认模板,不能修改)');
109
+ // workflow-run/templates/ 已不再生效:模板定制只放 DocNexus
110
+ for (const f of st.docnexus.stale_project_templates ?? []) meh(st.docnexus.ok ? `${f} 已不再生效:确认已导入 DocNexus 知识库后删除` : `${f} 已不再生效(模板定制只放 DocNexus):重跑 workflow-run 并加 --with-docnexus 接入后导入知识库,再删除`);
111
+ for (const t of st.templates_need_docnexus ?? []) console.log(` -- ${t.skill} 的模板 ${t.id} 不随 skills 提供,要先接入 DocNexus 才能创建(重跑 workflow-run 并加 --with-docnexus)`);
110
112
  for (const t of st.templates_to_create ?? []) console.log(` -- ${t.skill} 的模板 ${t.id} 还没有创建(不随 skills 提供):第一次生成文档时会请你给一份样例文档来创建,保存到 ${t.expected}`);
111
113
  if (st.codegraph?.ok) ok(`CodeGraph 已接入(codegraph ${codegraphVersion(root)},索引在 .codegraph/)`);
112
114
  else if (st.codegraph?.indexed) meh('有 .codegraph/ 索引但找不到 codegraph 命令:npm i -g @colbymchenry/codegraph,或重跑 workflow-run 并加 --with-codegraph');
@@ -12,7 +12,7 @@ import { detectEnv } from '../../workflow/scripts/resolve.mjs';
12
12
  export const report = { overwritten: [], removed: [], unlisted: [], unlistedDirs: [], backupDir: null };
13
13
 
14
14
  // skills 目录里的文件一律以安装包为准:手改过的先备份到 <项目根>/workflow-run/tmp/upgrade-backup/<时间>/(同一次运行共用一个目录),
15
- // 再覆盖或删除。项目自己的定制应放在不会被升级覆盖的地方:workflow/profile.json、workflow-run/templates/、DocNexus、CLAUDE.md / AGENTS.md
15
+ // 再覆盖或删除。项目自己的定制应放在不会被升级覆盖的地方:workflow/profile.json、DocNexus(文档模板只能在这里定制)、CLAUDE.md / AGENTS.md
16
16
  let stamp = null;
17
17
  function backup(root, file) {
18
18
  stamp ??= timestamp();
@@ -265,7 +265,7 @@ async function main() {
265
265
  if (docnexusConnected(o.root)) reportTemplateStatus(o.root);
266
266
  if (docnexus === 'failed') warn('注意:DocNexus 接入失败(见上方输出),其他 skills 不受影响;修复后可重跑(加 --with-docnexus)。');
267
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)';
268
+ const keep = '(skills 目录里的文件以安装包为准;项目定制请放到 workflow/profile.json、DocNexus(文档模板只能在这里定制)或 CLAUDE.md / AGENTS.md)';
269
269
  if (report.overwritten.length || report.removed.length) {
270
270
  warn(`以下文件被手动修改过,已按新版${report.removed.length ? '覆盖或删除' : '覆盖'},原文件备份在 ${report.backupDir}/${keep}:`);
271
271
  for (const f of report.overwritten) warn(` ${f}`);
@@ -39,8 +39,9 @@ description: "Generate a Markdown release/deployment plan (上线方案) from th
39
39
 
40
40
  - 模板:在项目根运行 `node <skills>/workflow/scripts/resolve.mjs template create-release-md-doc template.md`(`<skills>` 是本 skill 所在的 skills 目录,如 `.agents/skills`),读输出的 `path`,并把 `source` 写进元数据行。上线方案模板**不随 skills 提供**,由各团队按自己 wiki 上的格式创建:
41
41
  - 退出码 2 且 `on_demand: true` → 还没有创建模板:**停下这一步**,按 `<skills>/workflow/references/templates.md`「创建模板」请用户给一份团队现有的上线方案作样例,生成模板、用户确认后保存到输出的 `expected`,再继续。不要自己编一份通用模板凑数。
42
- - 退出码 2 且没有 `on_demand` → 已接入 DocNexus 但知识库缺模板,按输出的 `hint` 处理。
43
- - 用户要修改、重新生成或换一份上线方案模板时,按 `<skills>/workflow/references/templates.md`「修改模板」用 `template.mjs` 处理(模板别名 `release`;按样例重做用 `draft release --from <样例>`),保存时会备份旧模板。
42
+ - 退出码 2 且 `need_docnexus: true` → 未接入 DocNexus:上线方案模板只保存在 DocNexus 里,**停下这一步**告诉用户先重跑 workflow-run 并加 `--with-docnexus` 接入,再按「创建模板」创建。不要另存到别处。
43
+ - 退出码 2 的其他情况 → 按输出的 `hint` 处理。
44
+ - 用户要修改、重新生成或换一份上线方案模板时,按 `<skills>/workflow/references/templates.md`「修改模板」用 `template.mjs` 处理(模板别名 `release`;按样例重做用 `draft release --from <样例>`),保存写入 DocNexus 知识库。
44
45
  - 填充:模板的章节和编号因团队而异,**按章节含义**放事实,不按编号。下表是事实的来源,放进模板里含义对应的章节;模板里没有对应章节的事实不擅自加章节,在交付摘要里列出「模板中没有位置的事实」;模板有、但 diff 和 PRD 都给不出内容的章节写「待补充」。
45
46
 
46
47
  | 事实 | 来源 |
@@ -69,10 +70,10 @@ description: "Generate a Markdown release/deployment plan (上线方案) from th
69
70
 
70
71
  ### Step 4 — 写文件
71
72
 
72
- - 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path release --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/release-doc.md`,已有旧版时给出 `_v2`、`_v3`,不覆盖)。规则见 `<skills>/workflow/references/requirements.md`。
73
+ - 路径:运行 `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
74
  - 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
74
75
  - `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
75
- - 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
76
+ - 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
76
77
  - 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-release-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
77
78
  - 再把路径和摘要告诉用户。
78
79
 
@@ -38,8 +38,9 @@ description: "Generate a Markdown technical design doc (技术方案) from the d
38
38
 
39
39
  - 模板:在项目根运行 `node <skills>/workflow/scripts/resolve.mjs template create-tech-md-doc template.md`(`<skills>` 是本 skill 所在的 skills 目录,如 `.agents/skills`),读输出的 `path`,并把 `source` 写进元数据行。技术方案模板**不随 skills 提供**,由各团队按自己 wiki 上的格式创建:
40
40
  - 退出码 2 且 `on_demand: true` → 还没有创建模板:**停下这一步**,按 `<skills>/workflow/references/templates.md`「创建模板」请用户给一份团队现有的技术方案作样例,生成模板、用户确认后保存到输出的 `expected`,再继续。不要自己编一份通用模板凑数。
41
- - 退出码 2 且没有 `on_demand` → 已接入 DocNexus 但知识库缺模板,按输出的 `hint` 处理。
42
- - 用户要修改、重新生成或换一份技术方案模板时,按 `<skills>/workflow/references/templates.md`「修改模板」用 `template.mjs` 处理(模板别名 `tech`;按样例重做用 `draft tech --from <样例>`),保存时会备份旧模板。
41
+ - 退出码 2 且 `need_docnexus: true` → 未接入 DocNexus:技术方案模板只保存在 DocNexus 里,**停下这一步**告诉用户先重跑 workflow-run 并加 `--with-docnexus` 接入,再按「创建模板」创建。不要另存到别处。
42
+ - 退出码 2 的其他情况 → 按输出的 `hint` 处理。
43
+ - 用户要修改、重新生成或换一份技术方案模板时,按 `<skills>/workflow/references/templates.md`「修改模板」用 `template.mjs` 处理(模板别名 `tech`;按样例重做用 `draft tech --from <样例>`),保存写入 DocNexus 知识库。
43
44
  - 填充:模板的章节和编号因团队而异,**按章节含义**放事实,不按编号。下表是事实的来源,放进模板里含义对应的章节;模板里没有对应章节的事实不擅自加章节,在交付摘要里列出「模板中没有位置的事实」;模板有、但 diff 和 PRD 都给不出内容的章节写「待补充」。
44
45
 
45
46
  | 事实 | 来源 |
@@ -67,10 +68,10 @@ description: "Generate a Markdown technical design doc (技术方案) from the d
67
68
 
68
69
  ### Step 4 — 写文件
69
70
 
70
- - 路径:运行 `node <skills>/workflow/scripts/requirement.mjs path tech --branch <dev分支>`,写到输出的 `path`(`workflow-run/docs/requirements/<需求名>/tech-doc.md`,已有旧版时给出 `_v2`、`_v3`,不覆盖)。规则见 `<skills>/workflow/references/requirements.md`。
71
+ - 路径:运行 `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
72
  - 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" --branch <dev分支>`;用户不给名字时 `bind` 按分支名生成。
72
73
  - `wiki_record` 非空:上一版已发布到 wiki,新版第一行原样写这条记录,重新发布时更新同一页面。
73
- - 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
74
+ - 写完运行 `requirement.mjs sync <path>` 更新 `meta.json`;输出里 `archived` 非空(上一版已归档到 `workflow-run/tmp/old-artifacts/`)时告诉用户归档了哪些文件,请用户确认不再需要后自己删除;输出里 `commit` 非空(独立文档仓库模式)时按 `<skills>/workflow/references/requirements.md`「文档仓库的提交」处理。文档里引用其他产物(「参考产物」等)写 `requirement.mjs path` 输出的 `cite`。
74
75
  - 写完运行 `node <skills>/workflow/scripts/lint-doc.mjs <文档路径> --skill create-tech-md-doc --facts` 自检(`--facts` 按元信息行的对比分支核对正文里的类名、文件、接口、表名):有「错误」先修正再交付(缺必填章节、残留 `{{...}}` 占位符、缺元信息行);「警告」按实际情况处理,其中「在 diff 和代码里都找不到」的名字逐个核实:确属编造就改成 diff 里的真实名字或写「待补充」,是外部系统 / 配置中心的名字就保留;「提示」里的待填写位置在交付摘要中告诉用户。
75
76
  - 再把路径和摘要告诉用户。
76
77
 
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`;已有旧版时自动给出 `_v2`、`_v3`,不覆盖。
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowansenne/workflow-run",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "项目级 Agent Skills 一键初始化:workflow / dev / project-review / code-graph / database-query / md-to-wiki / 技术与上线方案生成",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -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, security and style issues. 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."
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. **确定审查范围**:单文件 / 单方法、分支对比(如 `branch` vs `main`),还是未提交改动(`git diff HEAD`,见 `references/diff-strategy.md`「未提交改动」)。
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(MEDIUM 至少做第 1 条)。审查结论会被用来决定能不能合并,一条误报会让人白改代码,也会让后面的真问题不被当回事:
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`;已有报告时自动给出 `review_v2.md`、`review_v3.md`,不覆盖(复审对照上次报告靠它)。
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
  ## 终端命令
@@ -45,6 +45,7 @@
45
45
  - [ ] 重复计算可提缓存或预处理
46
46
 
47
47
  ## 8. 一致性 / 风格
48
+ > 只在深审或用户要求时检查;快审和标准审查跳过本节(见 SKILL.md「只报会出问题的」)。
48
49
  - [ ] 命名、包结构、日志规范是否与工程一致
49
50
  - [ ] 注释与实现是否一致、是否残留 TODO/调试代码
50
51
  - [ ] 魔法数字是否用常量/枚举
@@ -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 的文件、复核后降级或待确认的发现、复核排除的条数、改动前就存在的问题、按审查深度没查的维度;都没有写「无」}}
@@ -33,7 +33,7 @@ description: "Entry point for this project's dev workflow skills: routes a reque
33
33
  | 写 PR / MR 描述、提 PR | `workflow/scripts/pr-desc.mjs` | 汇总本分支交付说明、审查报告、技术 / 上线方案;创建 PR 需用户同意 |
34
34
  | 这套工作流效果怎么样、用得怎么样 | `/workflow report` | 只读统计 `workflow-run/docs/requirements/` 下的产物 |
35
35
  | 看看项目文档、某个需求留下了哪些文档、审查记录 | `/workflow view` | 生成本地静态页面,按需求浏览 |
36
- | 改技术方案 / 上线方案 / 交付说明 / 审查报告的模板,按样例建模板 | `/workflow template` | 草稿 → 校验与影响 → 用户确认后保存 |
36
+ | 改技术方案 / 上线方案 / 交付说明 / 审查报告的模板,按样例建模板 | `/workflow template` | 草稿 → 校验与影响 → 用户确认后写入 DocNexus(默认模板只读,未接入 DocNexus 时不能改) |
37
37
  | 记住 / 查找项目决策与约定 | DocNexus(入口 skill `docnexus`) | 需已接入;写入必须用户授权 |
38
38
 
39
39
  意图明确时直接按对应 skill 执行,不必先输出这张表。目标 skill 没有安装(`resolve.mjs status` 输出的 `skills` 里没有它,安装时用了 `--skills` 子集)→ 告诉用户可以运行 `workflow-run --skills <名称>` 补装,不要假装执行。意图横跨多个阶段时按下面的「完整链路」逐段推进,每段结束告诉用户下一段是什么,由用户决定是否继续。走到哪一步不靠记忆:运行 `node <skills>/workflow/scripts/artifacts.mjs [--branch <分支>]`,它扫描 `workflow-run/docs/requirements/` 下的产物,按元信息行里的分支归组,输出每类最新的一份、wiki 发布记录和 `next`(下一步建议)。
@@ -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);复审 review_v2.md、review_v3.md
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` 相同;文档仓库模式下是「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样。同种产物已存在时会给出下一个版本(`_v2`、`_v3`),不覆盖旧版,复审靠它对照上次的报告。
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/)
@@ -9,7 +9,7 @@
9
9
  |---|---|---|
10
10
  | 项目画像 | ✅ / ⚠️ 部分字段为空 | <空字段>;运行 /workflow profile |
11
11
  | wiki | ✅ / ❌ | 填写 md-to-wiki/wiki-config.json 或接入 wiki MCP |
12
- | DocNexus | ✅ / ❌ | 重跑 workflow-run 并加 --with-docnexus;已接入但缺模板时列出缺的模板并提示 `workflow-run seed-templates` |
12
+ | DocNexus | ✅ / ❌ | 重跑 workflow-run 并加 --with-docnexus;已接入但缺模板时列出缺的模板并提示 `workflow-run seed-templates`;未接入时说明默认模板只读、不能定制,技术方案 / 上线方案要先接入(`templates_need_docnexus`);`stale_project_templates` 非空时提示这些项目模板已不再生效 |
13
13
  | 数据库 | ✅ / ❌ | 重跑 `npx @rowansenne/workflow-run` 补配 |
14
14
  | 代码图谱 | ✅ / ⚠️ 已装未建索引 / ❌ | 重跑 workflow-run 并加 --with-codegraph;已装 codegraph 但没有 .codegraph/ 时提示用户自己运行 codegraph init |
15
15
 
@@ -6,19 +6,25 @@
6
6
  node <skills>/workflow/scripts/resolve.mjs template <skill> <文件名>
7
7
  ```
8
8
 
9
- `<skills>` 是本 skill 所在的 skills 目录(如 `.agents/skills`),在项目根执行。输出 JSON:`path` 是要读的模板,`source` 是 `docnexus` / `project` / `default`,写进产出的「模板来源」。判定规则(实现在脚本里,这里只是说明):
9
+ `<skills>` 是本 skill 所在的 skills 目录(如 `.agents/skills`),在项目根执行。输出 JSON:`path` 是要读的模板,`source` 是 `docnexus` / `default`,写进产出的「模板来源」。
10
+
11
+ **skills 自带的默认模板只读**:不要直接编辑 skills 目录里的模板文件(升级时会被覆盖),也不要在 `workflow-run/templates/` 放项目模板(已不再生效)。要改模板只有一条路:写进 DocNexus 知识库,团队通过知识库共享同一份。
12
+
13
+ 判定规则(实现在脚本里,这里只是说明):
10
14
 
11
15
  - **已接入 DocNexus**:只用 `workflow-run/docnexus/library/templates/<skill>/<文件名>`(分类 `doc-template`,普通 Markdown,手工改动立即生效)。不使用 skill 默认模板,也不使用 `workflow-run/templates/`——知识库是唯一来源,避免几份模板各自演化。
12
16
  - 缺模板时退出码 2,输出 `expected`(应在的位置)、`import_from`(可导入的来源)和 `hint`。**停下这一步**,告诉用户缺哪份模板,给两个选择:在项目根运行 `workflow-run seed-templates`(或重跑安装加 `--seed-templates`);或者由你在用户确认后按 DocNexus 的 capture 流程把 `import_from` 写入 `expected`,以显式分类封存(`draft seal ... --category doc-template`,metadata.category 也写 `doc-template`)。导入后再继续。
13
- - 输出里 `stale_project_file` 非空:提醒用户 `workflow-run/templates/` 下的这份已不再生效,建议导入后删除。
14
- - **未接入 DocNexus**:先取项目文件 `workflow-run/templates/<skill>/<文件名>`,没有就用 skill 默认模板。
15
- - **技术方案、上线方案没有默认模板**(`create-tech-md-doc`、`create-release-md-doc` 的 `template.md`):各团队发到 wiki 的格式不同,skills 不再自带一份固定格式。还没创建时退出码 2,输出 `on_demand: true`、`expected`(创建后保存的位置:接入 DocNexus 时是知识库,否则是项目文件)和 `hint`,按下面「创建模板」处理。老项目升级时,安装器会把原来在用的默认版本保存到项目模板目录,不受影响。
17
+ - **未接入 DocNexus**:只用 skill 默认模板,不能修改或新建模板。
18
+ - 输出里 `stale_project_file` 非空:`workflow-run/templates/` 下的这份已不再生效,提醒用户导入 DocNexus 后删除(未接入时先接入)。
19
+ - **技术方案、上线方案没有默认模板**(`create-tech-md-doc`、`create-release-md-doc` 的 `template.md`):各团队发到 wiki 的格式不同,skills 不自带固定格式,第一次使用时按样例创建到知识库。
20
+ - 已接入 DocNexus、还没创建:退出码 2,输出 `on_demand: true`、`expected`(知识库里的位置)和 `hint`,按下面「创建模板」处理。
21
+ - 未接入 DocNexus:退出码 2,输出 `need_docnexus: true` 和 `hint`。**停下这一步**,告诉用户这两类文档的模板只保存在 DocNexus 里,要先重跑 workflow-run 并加 `--with-docnexus` 接入,再创建模板。不要自己编一份模板凑数。
16
22
 
17
23
  `resolve.mjs templates` 列出所有登记模板的来源;登记表在各 skill 的 `skill.json`(`templates` 字段)。`--with-docnexus` 接入时会自动导入默认模板,之后可运行 `workflow-run seed-templates` 补导入(已存在的不覆盖)。
18
24
 
19
25
  ## 修改模板(/workflow template)
20
26
 
21
- 用户要改某份模板(「技术方案模板加一节回滚」「上线方案去掉灰度那节」「审查报告的严重问题表加一列」),或要新建、按样例重做模板时,用 `template.mjs` 一路走完,不要直接编辑生效中的模板文件。`<模板>` 写别名 `tech` / `release` / `plan` / `delivery` / `review` / `checklist`,或 `<skill>/<文件名>`;没说哪份时先 `template.mjs list` 列出来问。
27
+ 用户要改某份模板(「技术方案模板加一节回滚」「上线方案去掉灰度那节」「审查报告的严重问题表加一列」),或要新建、按样例重做模板时,用 `template.mjs` 一路走完,不要直接编辑生效中的模板文件。未接入 DocNexus 时 `draft` / `save` 退出码 2、输出 `need_docnexus: true`:告诉用户默认模板不能改,要先接入 DocNexus(重跑 workflow-run 并加 `--with-docnexus`),不要另找地方保存。`<模板>` 写别名 `tech` / `release` / `plan` / `delivery` / `review` / `checklist`,或 `<skill>/<文件名>`;没说哪份时先 `template.mjs list` 列出来问。
22
28
 
23
29
  1. **起草稿**:`node <skills>/workflow/scripts/template.mjs draft <模板>`,把当前生效的模板复制到 `workflow-run/tmp/template-draft/<skill>/<文件名>`(输出的 `draft`)。草稿已存在会沿用(上次没改完);要从当前模板重来加 `--force`。
24
30
  - 退出码 2、`need_sample: true`:还没有这份模板,按下面「创建模板」第 1 步向用户要样例,再运行 `draft <模板> --from <样例文件>`(从 stdin 读用 `--from -`)。用户要「按这份文档重做模板」时同样用 `--from`。
@@ -28,14 +34,12 @@ node <skills>/workflow/scripts/resolve.mjs template <skill> <文件名>
28
34
  - `impact`:最近几份已生成的文档按新模板会多出哪些错误(例如新的必填章节它们没有)。只影响以后生成和 lint,已有文档不会被改;
29
35
  - `warnings`:例如没有任何必填章节;
30
36
  - 附上草稿全文(或改动的部分)。
31
- 4. **用户确认后保存**:`node <skills>/workflow/scripts/template.mjs save <模板>`。
32
- - 未接入 DocNexus:写到 `workflow-run/templates/<skill>/<文件名>`,原模板备份到输出的 `backup`(`workflow-run/tmp/template-backup/<时间>/`),并重新判定确认 `effective: true`,删除草稿。告诉用户保存位置与备份位置,建议随代码提交,团队共用(提交要用户同意)。改的是带默认版本的模板(plan / delivery / review / checklist)时,保存后项目文件优先于默认版本,以后升级 skills 也不会覆盖它。
33
- - 已接入 DocNexus:退出码 2,按输出的 `hint` 走 capture 流程把草稿写入知识库(写入要用户授权),完成后运行 `resolve.mjs template <skill> <文件名>` 确认 `path` 指向知识库里的那份,再删除草稿。
34
- 5. 用户想撤销:把 `backup` 复制回 `saved` 的位置(未接入 DocNexus);DocNexus 里的模板按 timeline 恢复旧版本。
37
+ 4. **用户确认后保存**:`node <skills>/workflow/scripts/template.mjs save <模板>`。它不直接写知识库,退出码 2:按输出的 `hint` 走 capture 流程把草稿写入知识库(写入要用户授权),完成后运行 `resolve.mjs template <skill> <文件名>` 确认 `path` 指向知识库里的那份,再删除草稿。改的是带默认版本的模板(plan / delivery / review / checklist)时,知识库里的版本取代默认版本,以后升级 skills 也不会覆盖它。
38
+ 5. 用户想撤销:DocNexus 里的模板按 timeline 恢复旧版本。
35
39
 
36
40
  ## 创建模板
37
41
 
38
- 技术方案、上线方案第一次生成时没有模板,由用户给一份**团队现有的同类文档**作样例来创建。用户也可以随时主动要求「创建 / 重新生成技术方案(上线方案)模板」,流程相同。
42
+ 需要已接入 DocNexus。技术方案、上线方案第一次生成时没有模板,由用户给一份**团队现有的同类文档**作样例来创建。用户也可以随时主动要求「创建 / 重新生成技术方案(上线方案)模板」,流程相同。
39
43
 
40
44
  1. **要样例**:停下来告诉用户还没有这份模板,请用户给一份团队现有的同类文档:本地 Markdown 路径,或 wiki 页面的 pageId / URL。
41
45
  - wiki 页面:运行 `node <skills>/md-to-wiki/scripts/upload.mjs get <pageId> --text`,取输出的 `text` 作样例(需要 wiki 已配置,见 `<skills>/workflow/SKILL.md`「增强能力探测」;没配置就请用户导出成 Markdown 或直接贴出内容)。
@@ -45,8 +49,8 @@ node <skills>/workflow/scripts/resolve.mjs template <skill> <文件名>
45
49
  - 把每节的 `{{...}}` 改成具体的填写说明(这一节写什么、从哪类事实来,例如「受影响的接口、模块,带文件路径」),不要把样例里的具体业务内容留在模板里;
46
50
  - 必填章节在标题末尾加 ` *`,可选章节在标题里加「(如有)」:`lint-doc.mjs` 靠这两个标记检查生成的文档;
47
51
  - 对照这份 skill 会采集的事实(`references/diff-collection.md`),diff 能提供、但样例里没有对应章节的(例如配置、SQL、定时任务、回滚),列给用户问要不要加章节,不要擅自加。
48
- 4. **确认后保存**:`template.mjs check` 通过后把完整模板给用户看,用户确认后运行 `template.mjs save <模板>`(同「修改模板」第 4 步):未接入 DocNexus 时写到 `workflow-run/templates/<skill>/template.md`,建议随代码提交,团队共用;已接入时按输出的 `hint` 走 capture 流程写入知识库(写入需要用户授权),以显式分类 `--category doc-template` 封存,metadata.category 也写 `doc-template`。
52
+ 4. **确认后保存**:`template.mjs check` 通过后把完整模板给用户看,用户确认后运行 `template.mjs save <模板>`(同「修改模板」第 4 步),按输出的 `hint` 走 capture 流程写入知识库(写入需要用户授权),以显式分类 `--category doc-template` 封存,metadata.category 也写 `doc-template`。
49
53
  5. 保存后确认生效(`save` 输出 `effective: true`,或 DocNexus 写入后重新运行 `resolve.mjs template <skill> template.md`),再继续原来的生成步骤。
50
54
 
51
- - **模板随知识库更新**:用户在生成的文档里做了结构性调整(新增 / 删除 / 重排章节、改表格列、改必填项),且项目已接入 DocNexus 时,交付后列出这些调整,**询问**是否同步回模板;同意后按 DocNexus 的 capture 流程更新同一路径(先 `document get --include document,metadata,timeline` 取 `record_version`,以 `--base-version <record_version> --category doc-template` 封存,用户确认后 `document add --draft <id> --replace`),DocNexus 会在 timeline 中记录每次变更。只改结构,不把某次需求的具体内容写进模板。用户也可以说「改模板」走上面的「修改模板」,或直接编辑库里的模板文件,再用 `<docnexus.bin> document sync --id <id>` 采纳(`<docnexus.bin>` 取 `resolve.mjs status` 的输出)。
55
+ - **模板随知识库更新**:用户在生成的文档里做了结构性调整(新增 / 删除 / 重排章节、改表格列、改必填项)时,交付后列出这些调整,**询问**是否同步回模板;同意后按 DocNexus 的 capture 流程更新同一路径(先 `document get --include document,metadata,timeline` 取 `record_version`,以 `--base-version <record_version> --category doc-template` 封存,用户确认后 `document add --draft <id> --replace`),DocNexus 会在 timeline 中记录每次变更。只改结构,不把某次需求的具体内容写进模板。用户也可以说「改模板」走上面的「修改模板」,或直接编辑库里的模板文件,再用 `<docnexus.bin> document sync --id <id>` 采纳(`<docnexus.bin>` 取 `resolve.mjs status` 的输出)。
52
56
  - 模板里的 `{{...}}` 是给 agent 看的填写提示,不是模板引擎语法;不要写成单独一行的 `{{fact:...}}`(那是 DocNexus 的事实引用语法)。
@@ -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
- // 同一天的多版(_v2、_v3)与无日期文件按修改时间排
68
+ // 同一天、无日期的文件按修改时间排
69
69
  return out.sort(byDate);
70
70
  }
71
71
 
@@ -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] # 本次落盘的文件路径(不覆盖:已有则 _v2、_v3)
16
- // requirement.mjs sync [<需求目录或其中的文件> | --all] # 按目录里的文件重建 meta.json;文档仓库模式下带上提交提示
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,7 +36,7 @@ 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
- // 产物种类 → 需求目录里的文件名(版本:<base>.md、<base>_v2.md …)
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 目录(安装器从安装包调用本脚本时,到项目里找画像)
@@ -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
- export function sync(root, dirRel) {
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
- // 本次落盘路径:同种产物已存在就用下一个版本号(不覆盖),并带上上一版第一行的 wiki 发布记录
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 versions = fs.readdirSync(r.abs).map(parseFile).filter((p) => p?.kind === kind).map((p) => p.version);
278
- const v = versions.length ? Math.max(...versions) + 1 : 1;
279
- const file = (n) => `${FILES[kind]}${n > 1 ? `_v${n}` : ''}.md`;
280
- const prevAbs = versions.length ? path.join(r.abs, file(v - 1)) : null;
281
- const record = prevAbs ? /^<!-- md-to-wiki: [^\n]*-->/.exec(fs.readFileSync(prevAbs, 'utf8'))?.[0] ?? null : null;
282
- const abs = path.join(r.abs, file(v));
283
- 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: prevAbs ? relOf(root, prevAbs) : null, wiki_record: record } };
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);
@@ -10,7 +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
- // 输出 JSON。退出码:0 正常;2 已接入 DocNexus 但知识库缺该模板(按输出的 hint 处理);1 参数错误 / 该 skill 没有声明分类
13
+ // 输出 JSON。退出码:0 正常;2 缺该模板(知识库里没有,或未接入 DocNexus 而该模板没有默认版本;按输出的 hint 处理);1 参数错误 / 该 skill 没有声明分类
14
14
 
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
@@ -75,8 +75,10 @@ export function resolveTemplate(root, skill, id) {
75
75
  // 没有 src 的模板(技术方案、上线方案)不随 skills 提供,缺失时按「创建模板」由用户给样例生成
76
76
  const builtin = t.src ? path.join(SKILLS_DIR, skill, t.src.replace(/\.tpl$/, '')) : null;
77
77
  const CREATE = '按 <skills>/workflow/references/templates.md「创建模板」处理:请用户给一份团队现有的同类文档作样例(本地 md 或 wiki pageId / URL),用 workflow/scripts/make-template.mjs 抽骨架,和用户确认后保存到 expected';
78
+ // workflow-run/templates/ 下的项目模板已不再生效(定制模板只放 DocNexus,便于共享),留着的只用来提示导入
79
+ const stale = fs.existsSync(project) ? rel(root, project) : null;
78
80
  if (docnexusConnected(root)) {
79
- const res = { skill, id, docnexus: true, stale_project_file: fs.existsSync(project) ? rel(root, project) : null };
81
+ const res = { skill, id, docnexus: true, stale_project_file: stale };
80
82
  if (fs.existsSync(lib)) return { ok: true, ...res, source: 'docnexus', path: rel(root, lib) };
81
83
  if (!builtin) {
82
84
  return {
@@ -91,14 +93,15 @@ export function resolveTemplate(root, skill, id) {
91
93
  hint: '已接入 DocNexus,只使用知识库中的模板。停下这一步告诉用户缺这份模板:可在项目根运行 workflow-run seed-templates(或重跑安装加 --seed-templates),或经用户确认后把 import_from 按 capture 流程写入 expected(以显式分类 --category doc-template 封存)。',
92
94
  };
93
95
  }
94
- if (fs.existsSync(project)) return { ok: true, skill, id, docnexus: false, source: 'project', path: rel(root, project) };
96
+ // 未接入 DocNexus:只用 skill 自带的默认模板(只读,不能改);没有默认版本的(技术方案、上线方案)要先接入 DocNexus 才能创建
97
+ const res = { skill, id, docnexus: false, stale_project_file: stale };
95
98
  if (!builtin) {
96
99
  return {
97
- ok: false, skill, id, docnexus: false, error: 'missing', on_demand: true, expected: rel(root, project),
98
- hint: `这份模板不随 skills 提供,项目里还没有创建。停下这一步告诉用户,${CREATE}。`,
100
+ ok: false, ...res, error: 'missing', need_docnexus: true, expected: rel(root, lib),
101
+ hint: `这份模板不随 skills 提供,自定义模板只保存在 DocNexus 知识库里(便于团队共享),本项目还没有接入 DocNexus。停下这一步告诉用户:先重跑 workflow-run 并加 --with-docnexus 接入${stale ? `(项目里的 ${stale} 已不再生效,接入后可导入知识库)` : ''},再${CREATE}。`,
99
102
  };
100
103
  }
101
- return { ok: true, skill, id, docnexus: false, source: 'default', path: rel(root, builtin) };
104
+ return { ok: true, ...res, source: 'default', path: rel(root, builtin) };
102
105
  }
103
106
 
104
107
  // DocNexus 对显式分类名的约束:1–80 字符,无首尾空白与控制字符(与 bin/skills.mjs 一致)
@@ -186,7 +189,8 @@ export function status(root) {
186
189
  // 运行 DocNexus 命令的写法(在项目根执行,任何终端都一样):后面接原命令的参数;node 脚本前加 --node
187
190
  run: `node ${(rel(root, path.join(SKILLS_DIR, 'workflow', 'scripts', 'docnexus.mjs')))}`,
188
191
  missing_templates: dn ? templates.filter((t) => !t.ok && !t.on_demand).map((t) => t.expected) : [],
189
- stale_project_templates: dn ? templates.map((t) => t.stale_project_file).filter(Boolean) : [],
192
+ // workflow-run/templates/ 下已不生效的项目模板:接入 DocNexus 后导入知识库再删除
193
+ stale_project_templates: templates.map((t) => t.stale_project_file).filter(Boolean),
190
194
  // 各 skill 写入时使用的显式分类(没声明的 skill 不列出)
191
195
  categories: Object.fromEntries(skillMetas().map((m) => [m.name, resolveCategory(root, m.name)]).filter(([, c]) => c.ok).map(([n, c]) => [n, c.category])),
192
196
  how: dn ? '缺模板时运行 workflow-run seed-templates' : '重跑 workflow-run 并加 --with-docnexus',
@@ -201,9 +205,11 @@ export function status(root) {
201
205
  connections: Object.fromEntries(Object.entries(db?.connections ?? {}).map(([k, c]) => [k, { driver: c.driver ?? 'mysql', readonly: c.readonly !== false }])),
202
206
  how: '重跑 workflow-run 补配数据库',
203
207
  },
204
- templates: templates.map(({ skill, id, source, path: p2, ok, on_demand, expected }) => ({ skill, id, ok, source: source ?? 'missing', path: p2 ?? null, ...(on_demand ? { on_demand: true, expected } : {}) })),
208
+ templates: templates.map(({ skill, id, source, path: p2, ok, on_demand, need_docnexus, expected }) => ({ skill, id, ok, source: source ?? 'missing', path: p2 ?? null, ...(on_demand ? { on_demand: true, expected } : {}), ...(need_docnexus ? { need_docnexus: true } : {}) })),
205
209
  // 不随 skills 提供、还没创建的模板(技术方案、上线方案):第一次生成文档时按「创建模板」引导用户创建
206
210
  templates_to_create: templates.filter((t) => t.on_demand).map((t) => ({ skill: t.skill, id: t.id, expected: t.expected })),
211
+ // 没有默认版本、要先接入 DocNexus 才能创建的模板(未接入时)
212
+ templates_need_docnexus: templates.filter((t) => t.need_docnexus).map((t) => ({ skill: t.skill, id: t.id })),
207
213
  };
208
214
  }
209
215
 
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // 修改 / 创建文档模板(/workflow template)的确定性步骤:起草稿 → 校验并与当前模板、已生成文档比对 → 确认后保存。
3
- // 来源判定沿用 resolve.mjs template;改动只落在草稿 workflow-run/tmp/template-draft/<skill>/<模板>,save 时才写到生效位置。
4
- // 已接入 DocNexus 时 save 不直接写知识库(写入需按 capture 流程、经用户授权),退出码 2 并给出指引。
3
+ // 来源判定沿用 resolve.mjs template;改动只落在草稿 workflow-run/tmp/template-draft/<skill>/<模板>。
4
+ // skill 自带的默认模板只读:定制模板只保存在 DocNexus 知识库里(便于团队共享),未接入 DocNexus 时 draft / save 退出码 2 提示先接入;
5
+ // save 也不直接写知识库(写入需按 capture 流程、经用户授权),退出码 2 并给出指引。
5
6
  // 流程说明见 workflow/references/templates.md「修改模板」。
6
7
  //
7
8
  // 用法(在项目根执行;--root 可指定项目根):
@@ -12,8 +13,8 @@
12
13
  // <模板>:tech | release | plan | delivery | review | checklist,或 <skill>[/<模板文件名>](如 create-tech-md-doc/template.md)
13
14
  // draft:把当前生效的模板复制成草稿;还没有模板(或 --from)时按样例抽骨架(make-template.mjs)。草稿已存在时不覆盖,--force 重来
14
15
  // check:草稿本身的格式问题、与当前模板相比的章节变化、拿最近几份已生成文档按新旧模板各跑一次 lint-doc 看影响
15
- // save:check 无错误才写入;原模板备份到 workflow-run/tmp/template-backup/<时间>/,写入后重新 resolve 确认生效,删除草稿
16
- // 输出 JSON。退出码:0 正常;1 参数错误 / 草稿有错误;2 需要按输出的 hint 处理(缺样例、已接入 DocNexus 需 capture)
16
+ // save:check 无错误后给出写入知识库的 capture 指引(写入后重新运行 resolve.mjs template 确认生效)
17
+ // 输出 JSON。退出码:0 正常;1 参数错误 / 草稿有错误;2 需要按输出的 hint 处理(缺样例、未接入 DocNexus、需 capture 写入)
17
18
 
18
19
  import fs from 'node:fs';
19
20
  import path from 'node:path';
@@ -37,7 +38,6 @@ const SAMPLE_DOCS = 5;
37
38
 
38
39
  const rel = (root, p) => path.relative(root, p).split(path.sep).join('/');
39
40
  const read = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } };
40
- const stamp = () => new Date().toISOString().replace(/[-:]/g, '').replace(/\..*$/, '').replace('T', '-');
41
41
 
42
42
  // <模板> → { skill, id };未知返回 null
43
43
  export function target(name) {
@@ -50,6 +50,10 @@ export function target(name) {
50
50
  return t ? { skill, id: t.id } : null;
51
51
  }
52
52
 
53
+ // 未接入 DocNexus:默认模板只读,不能改也不能新建
54
+ const NEED_DOCNEXUS = '默认模板随 skills 提供、只读不能改;自定义模板只保存在 DocNexus 知识库里,便于团队共享。告诉用户先重跑 workflow-run 并加 --with-docnexus 接入,再修改或创建模板。';
55
+ const needDocNexus = (t, r) => ({ code: 2, out: { ok: false, skill: t.skill, id: t.id, docnexus: false, need_docnexus: true, current: r.ok ? r.path : null, hint: NEED_DOCNEXUS } });
56
+
53
57
  const draftPath = (root, { skill, id }) => path.join(root, 'workflow-run', 'tmp', 'template-draft', skill, id);
54
58
 
55
59
  // 草稿本身的格式检查:错误挡住保存,警告给用户看
@@ -118,8 +122,9 @@ function current(root, t) {
118
122
  export function draft(root, t, { from = null, force = false, stdin = () => fs.readFileSync(0, 'utf8') } = {}) {
119
123
  const file = draftPath(root, t);
120
124
  const { r, text } = current(root, t);
121
- const base = { skill: t.skill, id: t.id, draft: rel(root, file), source: r.ok ? r.source : null, current: r.ok ? r.path : null, docnexus: !!r.docnexus, expected: r.ok ? (r.source === 'default' ? rel(root, path.join(root, 'workflow-run', 'templates', t.skill, t.id)) : r.path) : r.expected };
122
- if (r.error === 'unknown') return { code: 1, out: { ok: false, ...base, message: r.message } };
125
+ if (r.error === 'unknown') return { code: 1, out: { ok: false, skill: t.skill, id: t.id, message: r.message } };
126
+ if (!r.docnexus) return needDocNexus(t, r);
127
+ const base = { skill: t.skill, id: t.id, draft: rel(root, file), source: r.ok ? r.source : null, current: r.ok ? r.path : null, docnexus: true, expected: r.ok ? r.path : r.expected };
123
128
  if (fs.existsSync(file) && !force && !from) return { code: 0, out: { ok: true, ...base, created: false, note: '草稿已存在,沿用上次的草稿;要从当前模板重来加 --force' } };
124
129
  let content;
125
130
  let origin;
@@ -164,28 +169,14 @@ export function save(root, t, file = null) {
164
169
  if (c.code) return c;
165
170
  if (c.out.unchanged) return { code: 0, out: { ok: true, skill: t.skill, id: t.id, saved: null, note: '草稿与当前模板相同,没有写入' } };
166
171
  const r = resolveTemplate(root, t.skill, t.id);
167
- if (r.docnexus) {
168
- return {
169
- code: 2, out: {
170
- ok: false, skill: t.skill, id: t.id, docnexus: true, draft: c.out.draft, expected: r.ok ? r.path : r.expected,
171
- hint: '已接入 DocNexus,模板只认知识库里的那份,写入要按 capture 流程并经用户授权:已有模板时先 document get --include document,metadata,timeline 取 record_version,以草稿内容 --base-version <record_version> --category doc-template 封存,用户确认后 document add --draft <id> --replace;还没有时以 --category doc-template 新建。完成后重新运行 resolve.mjs template 确认。',
172
- },
173
- };
174
- }
175
- const dst = path.join(root, 'workflow-run', 'templates', t.skill, t.id);
176
- let backup = null;
177
- if (fs.existsSync(dst)) {
178
- backup = path.join(root, 'workflow-run', 'tmp', 'template-backup', stamp(), t.skill, t.id);
179
- fs.mkdirSync(path.dirname(backup), { recursive: true });
180
- fs.copyFileSync(dst, backup);
181
- }
182
- const src = path.resolve(root, c.out.draft);
183
- fs.mkdirSync(path.dirname(dst), { recursive: true });
184
- fs.writeFileSync(dst, fs.readFileSync(src, 'utf8'));
185
- const after = resolveTemplate(root, t.skill, t.id);
186
- const effective = after.ok && after.path === rel(root, dst);
187
- if (effective && src === draftPath(root, t)) fs.rmSync(src);
188
- return { code: effective ? 0 : 1, out: { ok: effective, skill: t.skill, id: t.id, saved: rel(root, dst), backup: backup && rel(root, backup), effective, source: after.source ?? null, warnings: c.out.warnings, changes: c.out.changes } };
172
+ if (!r.docnexus) return needDocNexus(t, r);
173
+ return {
174
+ code: 2, out: {
175
+ ok: false, skill: t.skill, id: t.id, docnexus: true, draft: c.out.draft, expected: r.ok ? r.path : r.expected,
176
+ hint: '模板只认知识库里的那份,写入要按 capture 流程并经用户授权:已有模板时先 document get --include document,metadata,timeline 取 record_version,以草稿内容 --base-version <record_version> --category doc-template 封存,用户确认后 document add --draft <id> --replace;还没有时以 --category doc-template 新建。完成后重新运行 resolve.mjs template 确认,再删除草稿。',
177
+ warnings: c.out.warnings, changes: c.out.changes,
178
+ },
179
+ };
189
180
  }
190
181
 
191
182
  export function list(root) {
@@ -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, i) => { 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>' + (i ? '复审' : '首次') + '</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 ? (n > 1 ? n + ' 版' : '✓') + (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>';
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>'