@rowansenne/workflow-run 0.21.2 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +32 -14
- package/bin/lib/docsrepo.mjs +52 -18
- package/bin/lib/doctor.mjs +4 -1
- package/bin/lib/install.mjs +22 -4
- package/bin/workflow-run.mjs +7 -2
- package/create-release-md-doc/SKILL.md.tpl +1 -1
- package/create-tech-md-doc/SKILL.md.tpl +1 -1
- package/dev/SKILL.md.tpl +20 -14
- package/dev/references/delivery-template.md +8 -3
- package/dev/skill.json +2 -2
- package/md-to-wiki/SKILL.md.tpl +1 -1
- package/package.json +1 -1
- package/project-review/SKILL.md.tpl +3 -2
- package/workflow/SKILL.md.tpl +2 -2
- package/workflow/references/feedback.md +34 -0
- package/workflow/references/profile.md +1 -1
- package/workflow/references/requirements.md +11 -9
- package/workflow/scripts/artifacts.mjs +6 -5
- package/workflow/scripts/feedback.mjs +204 -0
- package/workflow/scripts/requirement.mjs +79 -19
- package/workflow/scripts/resolve.mjs +4 -3
- package/workflow/scripts/view.mjs +5 -4
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> 参考产物:{{开发方案路径,没有写「无」}}
|
|
6
6
|
> DocNexus:{{沉淀后写 docnexus/library/<path>;未沉淀写「未沉淀」,未接入写「未接入」}}
|
|
7
7
|
|
|
8
|
-
<!-- F8 / B5 交付时按此落盘到 requirement.mjs path delivery 给出的路径(workflow-run/docs/requirements
|
|
8
|
+
<!-- F8 / B5 交付时按此落盘到 requirement.mjs path delivery 给出的路径(workflow-run/docs/requirements/<需求名>/<项目名>/delivery.md)。小改动每节写一两行即可,不适用的节写「无」;验证结果必须如实 -->
|
|
9
9
|
|
|
10
10
|
## 结果 *
|
|
11
11
|
|
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
|
|
23
23
|
## 验证 *
|
|
24
24
|
|
|
25
|
+
| 验收项 | 测试 / 操作 | 结果 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| {{F1 / F3 的验收标准,每条一行;缺陷修复写「现象不再出现」}} | {{测试类.方法,或手动操作步骤}} | {{通过 / 失败 / 未验证(原因)}} |
|
|
28
|
+
|
|
25
29
|
- 构建:{{命令}} → {{通过 / 失败 / 未运行(原因)}}
|
|
26
30
|
- 测试:{{命令}} → {{通过 n 个 / 失败 / 未运行(原因)}};新增测试:{{列表}}
|
|
27
|
-
-
|
|
31
|
+
- 开工前已有的失败:{{Step 0 基线里已经失败的测试 / 编译错误,本次未处理;没有写「无」,没跑基线写原因}}
|
|
32
|
+
- 未验证项:{{表里「未验证」的验收项及其他:需要用户在什么环境怎么验证;没有写「无」}}
|
|
28
33
|
|
|
29
34
|
## 根因(如有)
|
|
30
35
|
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
|
|
46
51
|
## 范围外发现
|
|
47
52
|
|
|
48
|
-
{{
|
|
53
|
+
{{顺带看到但本次未处理的问题;开工前工作区里与本需求无关的改动(文件列表,未提交)及自审在其中发现的问题;无则写「无」}}
|
|
49
54
|
|
|
50
55
|
## 建议下一步
|
|
51
56
|
|
package/dev/skill.json
CHANGED
package/md-to-wiki/SKILL.md.tpl
CHANGED
|
@@ -70,7 +70,7 @@ node <skillDir>/scripts/md-to-wiki.mjs <mdPath> workflow-run/tmp/md-to-wiki/_wik
|
|
|
70
70
|
- `references/upload.md`:通道 A / B 的调用细节与验证
|
|
71
71
|
- `references/troubleshooting.md`:转换脚本说明、XHTML 规则、校验命令、常见错误
|
|
72
72
|
|
|
73
|
-
典型串联:`create-tech-md-doc` / `create-release-md-doc` 生成 `workflow-run/docs/requirements
|
|
73
|
+
典型串联:`create-tech-md-doc` / `create-release-md-doc` 生成 `workflow-run/docs/requirements/<需求名>/<项目名>/` 下的 md → 本 skill 发布。
|
|
74
74
|
|
|
75
75
|
## 终端命令
|
|
76
76
|
|
package/package.json
CHANGED
|
@@ -30,10 +30,11 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
30
30
|
3. 找有没有别处已经兜住:调用方校验、`@Validated` / 全局异常处理、数据库约束、框架默认行为、配置。涉及索引、字段、唯一约束、表大小的发现,以第 2 步拉到的实际表结构为准(复核列写「已与 <连接名> 核对」)。已兜住 → 删掉或降级,并在说明里写依据。
|
|
31
31
|
4. 确认是本次改动引入的:改动前就存在的问题移到「未覆盖与待确认」,不计入合并结论。
|
|
32
32
|
结果:「成立」在报告「复核」列标「✅」;「可能成立」标「⚠」;「不成立」从问题表里删掉(独立复核否掉的,在「未覆盖与待确认」里记一句「复核排除 N 条」)。复核方式(独立 / 自行)写进报告「审查方式」元信息。
|
|
33
|
-
7.
|
|
33
|
+
7. **输出报告**(由 dev 自审调用时跳过第 7~9 步,问题表、影响面、未覆盖项交回 dev 统一交付,见 dev 的 F7):按报告模板输出(`node <skills>/workflow/scripts/resolve.mjs template project-review report.md` 取 `path`;退出码 2 时停下并按 `hint` 处理),落盘规则见 Artifact Location。
|
|
34
34
|
8. **沉淀到 DocNexus(需用户授权)**:`resolve.mjs status` 的 `docnexus.ok` 为 true 时,交付报告后**每次都问**用户「要不要启动 DocNexus 流程,把本次审查沉淀到 DocNexus?」,并从本次发现里列出建议沉淀的内容:同类问题再次出现(知识库里已有类似沉淀,或本次多处出现)、新确立的编码约定、容易踩的坑;没有特别值得记的也照问,并说明这一点。未接入时不提问,报告的「DocNexus」元信息行写「未接入」。
|
|
35
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>`(多篇用「、」分隔),并回报给用户。
|
|
36
36
|
- 用户不同意 → 元信息行保持「未沉淀」。
|
|
37
|
+
9. **记录运行反馈**(不问用户):按 `<skills>/workflow/references/feedback.md` 运行 `node <skills>/workflow/scripts/feedback.mjs add --skill project-review --model <模型名> --mode review --depth <快审|标准|深审> --rounds <提问轮数> --corrections <用户纠正审查范围的次数> --findings <C>/<H>/<M>/<L>`,有跳过的步骤、不清楚或做不到的规则、用户的抱怨时再加 `--skipped` / `--unclear` / `--feedback`。只写过程,不写代码和业务名词;报错就提一句,不影响交付。
|
|
37
38
|
|
|
38
39
|
## 只报会出问题的
|
|
39
40
|
|
|
@@ -74,7 +75,7 @@ description: "Review source code (any language; Java/Spring, Go, TypeScript, Pyt
|
|
|
74
75
|
|
|
75
76
|
## Artifact Location
|
|
76
77
|
|
|
77
|
-
审查报告按需求归档在 `workflow-run/docs/requirements
|
|
78
|
+
审查报告按需求归档在 `workflow-run/docs/requirements/<需求名>/<项目名>/`,规则见 `<skills>/workflow/references/requirements.md`:
|
|
78
79
|
|
|
79
80
|
- 路径用 `node <skills>/workflow/scripts/requirement.mjs path review`(分支对比按 dev 分支加 `--branch <dev分支>`),写到输出的 `path`(文件名固定为 `review.md`);已有报告时脚本先把它归档,输出的 `previous` 是上次报告,复审对照它。
|
|
80
81
|
- 退出码 2(这个分支还没绑定需求):问用户这个需求叫什么,默认值用输出的 `suggested_name`,再 `requirement.mjs bind --name "<需求名>" [--branch <dev分支>]`;用户不给名字时 `bind` 按分支名生成。
|
package/workflow/SKILL.md.tpl
CHANGED
|
@@ -49,7 +49,7 @@ dev(需求 → 方案 → 实现 → 测试 → 自审)
|
|
|
49
49
|
→ DocNexus 沉淀(可选,需授权)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
开始完整链路时先确定需求名:`node <skills>/workflow/scripts/requirement.mjs which`,当前分支没绑定就问用户这个需求叫什么并 `bind`(见 `references/requirements.md`),之后各段产物都落在 `workflow-run/docs/requirements
|
|
52
|
+
开始完整链路时先确定需求名:`node <skills>/workflow/scripts/requirement.mjs which`,当前分支没绑定就问用户这个需求叫什么并 `bind`(见 `references/requirements.md`),之后各段产物都落在 `workflow-run/docs/requirements/<需求名>/<项目名>/`。
|
|
53
53
|
|
|
54
54
|
提交、推送、发布 wiki、写入 DocNexus 都是对外动作,每一步都要用户明确同意。
|
|
55
55
|
|
|
@@ -63,7 +63,7 @@ dev(需求 → 方案 → 实现 → 测试 → 自审)
|
|
|
63
63
|
| `references/templates.md` | 模板来源判定(`resolve.mjs template`)、创建与修改模板(`/workflow template`)、模板随知识库更新 |
|
|
64
64
|
| `references/docnexus.md` | 写入 DocNexus 的分类(`resolve.mjs category`)、按分类参考知识库(`knowledge.mjs`) |
|
|
65
65
|
| `references/status.md` | `/workflow status` 的输出格式 |
|
|
66
|
-
| `references/requirements.md` | 产物按需求归档(`workflow-run/docs/requirements
|
|
66
|
+
| `references/requirements.md` | 产物按需求归档(`workflow-run/docs/requirements/<需求名>/<项目名>/`):什么时候问需求名、`requirement.mjs` 绑定分支与给出落盘路径、`meta.json` |
|
|
67
67
|
| `references/tables.md` | 确定改动涉及哪些表(`tables.mjs`)、只读拉取实际表结构 / 索引 / 行数核对(`query.mjs --schema --indexes`),没有库连接时怎么退回 |
|
|
68
68
|
| `references/codegraph.md` | 用 CodeGraph 代码图谱读代码、查调用链与影响面(MCP 工具与 CLI 对照、图谱看不到的部分) |
|
|
69
69
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 运行反馈
|
|
2
|
+
|
|
3
|
+
每次 skill 跑完,用 `workflow/scripts/feedback.mjs add` 追加一行过程记录,写到 `workflow-run/feedback/<git 用户名>.jsonl`(每人一个文件,随 `workflow-run/` 提交)。团队攒两三周后用 `npx @rowansenne/workflow-run feedback` 汇总,作为精简 SKILL.md 的依据:哪些步骤常被跳过、哪些规则读不懂或做不到、提问是不是太多。装了新版本(`init` / `upgrade`)时会清掉本项目旧版本的记录,从新版本重新收集。
|
|
4
|
+
|
|
5
|
+
## 规则
|
|
6
|
+
|
|
7
|
+
- **不问用户、不打断**:交付时静默记录,不为了记录多问一句,也不请用户打分。
|
|
8
|
+
- **只写过程,不写内容**:不写代码、类名、方法名、表名、接口路径、业务名词、客户信息。这个文件会被发给维护 skill 的人。
|
|
9
|
+
- **如实**:跳过了就写跳过,问了几轮就写几轮;不美化。拿不准的数字不写那个参数。
|
|
10
|
+
- **失败不影响交付**:命令报错就在对话里提一句,不重试、不排查。
|
|
11
|
+
- **一次运行记一行**:dev 内部调用 project-review 自审时只由 dev 记,project-review 不另记。
|
|
12
|
+
|
|
13
|
+
## 参数
|
|
14
|
+
|
|
15
|
+
所有参数都是 `--名称 值`,值里有空格时加双引号;`--skipped`、`--unclear`、`--feedback` 可重复。
|
|
16
|
+
|
|
17
|
+
| 参数 | 值 | 说明 |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `--skill` | `dev` / `project-review` | 必填 |
|
|
20
|
+
| `--model` | 你的模型名,如 `claude-opus-5-5`、`qwen3-coder` | 不知道就写所在的工具名 |
|
|
21
|
+
| `--mode` | `feature` / `bug` / `review` | dev 的功能开发 / 缺陷修复;审查写 `review` |
|
|
22
|
+
| `--size` | `S` / `M` / `L` | dev 的规模 |
|
|
23
|
+
| `--fast` | `yes` / `no` | dev 是否走了快速通道 |
|
|
24
|
+
| `--depth` | `快审` / `标准` / `深审` | project-review 的深度 |
|
|
25
|
+
| `--rounds` | 整数 | 向用户提问的轮数(澄清、确认方案都算;只问提交那一下不算) |
|
|
26
|
+
| `--corrections` | 整数 | 用户纠正你对需求 / 审查范围理解的次数 |
|
|
27
|
+
| `--acceptance` | `已验证/总数`,如 `3/4` | dev 交付说明「验证」表里的验收项 |
|
|
28
|
+
| `--baseline-failures` | 整数 | dev Step 0 基线里已有的失败数 |
|
|
29
|
+
| `--findings` | `C/H/M/L`,如 `0/1/2/0` | project-review 各级别发现条数(复核后) |
|
|
30
|
+
| `--skipped` | `<步骤>: <原因>`,如 `F6: 缺 Redis,集成测试没跑` | 本该做却没做的步骤,每步一条 |
|
|
31
|
+
| `--unclear` | `<章节>: <哪条规则、怎么不清楚>`,如 `F4: S 和快速通道的条件重复,不确定要不要都满足` | **最有用的一项**:读不懂、互相矛盾、在这个项目做不到、或你觉得多余的规则;没有就不写 |
|
|
32
|
+
| `--feedback` | 一句话 | 用户主动说的抱怨或建议的大意(如「问得太多」「交付说明太长」);用户没说就不写 |
|
|
33
|
+
|
|
34
|
+
`<步骤>` / `<章节>` 用 SKILL.md 里的编号(`Step 0`、`F3`、`B2`,project-review 写 `第 3 步`),汇总时按它归类。
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| `job_annotation` | 定时任务注解(如 `@XxlJob`、`@Scheduled`) | 技术方案「定时任务变更」 |
|
|
14
14
|
| `data_review` | 上线前是否需要数据团队评审库表(`true` / `false`) | 技术方案模块 7、上线方案 2.3 |
|
|
15
15
|
| `notes` | 其他项目约定,一条一句(如「启动需要本地 Redis」) | 全部 |
|
|
16
|
-
| `artifacts` | 可选:`{ "project": "order-service", "commit_prompt": true }`。`project`
|
|
16
|
+
| `artifacts` | 可选:`{ "project": "order-service", "commit_prompt": true }`。`project` 是这个项目的产物目录名(产物在 `workflow-run/docs/requirements/<需求名>/<项目名>/`,随代码提交和独立文档仓库两种用法都一样):随代码提交时不配置就取 git 远端仓库名,第一次落盘时自动写进来;独立文档仓库时要先配置;`commit_prompt: false` 关闭写完后的提交提示。个人设置可写在 `workflow/profile.local.json`(不提交)覆盖 | 见 `references/requirements.md`,用 `requirement.mjs config` 修改 |
|
|
17
17
|
| `docnexus_categories` | 可选,覆盖各 skill 写入 DocNexus 时的分类,如 `{"create-tech-md-doc": "技术方案"}`;不填用 skill 默认 | 见 `references/docnexus.md` |
|
|
18
18
|
|
|
19
19
|
其他 skill 读画像统一用 `node <skills>/workflow/scripts/resolve.mjs profile`:输出 `profile`(文件内容)、`empty`(空字段)、`complete`(`main_branch` / `build` / `test` 都已填写)、`env`(本机系统与终端,见下)。
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# 产物按需求归档(dev / project-review / 技术方案 / 上线方案共用)
|
|
2
2
|
|
|
3
|
-
一个需求的全部产物放在同一个目录,整条链路一眼看全,`/workflow view` 也直接读这里。本工具的产出都在项目根的 `workflow-run/` 里(需求产物、DocNexus 工作区、项目模板、临时文件),不往项目的其他地方写。默认 `workflow-run/`
|
|
3
|
+
一个需求的全部产物放在同一个目录,整条链路一眼看全,`/workflow view` 也直接读这里。本工具的产出都在项目根的 `workflow-run/` 里(需求产物、DocNexus 工作区、项目模板、临时文件),不往项目的其他地方写。默认 `workflow-run/` 随代码提交;团队也可以让 `workflow-run/` 成为独立的文档仓库,见最后一节。两种用法的目录结构完全一样(下图),切换时文件不用挪。
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
workflow-run/docs/requirements
|
|
6
|
+
workflow-run/docs/requirements/<需求名>/<项目名>/
|
|
7
7
|
├── meta.json # 需求名、绑定的分支、各产物(种类、版本、日期、wiki 发布记录、DocNexus 沉淀)
|
|
8
8
|
├── plan.md # 开发方案(dev,L 规模或用户要求时)
|
|
9
9
|
├── delivery.md # 交付说明(dev)
|
|
@@ -12,7 +12,9 @@ workflow-run/docs/requirements/<需求名>/
|
|
|
12
12
|
└── release-doc.md # 上线方案(create-release-md-doc)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
一个需求横跨多个服务时,各服务在同一个需求目录下各占一个 `<项目名>/`。项目名是 `workflow/profile.json` 的 `artifacts.project`:随代码提交时不用配置,脚本取 git 远端仓库名,第一次 `bind` 时写进 `profile.json` 固定下来;独立文档仓库时要先配置(见最后一节)。
|
|
16
|
+
|
|
17
|
+
这个目录写在项目根的 `.ignore` 里,不参与代码搜索(找历史决策用 DocNexus)。读写一律按下面脚本给出的路径,不要靠搜索找这些文件,也不要自己拼路径。两种用法下各 skill 的做法完全一样,区别都在脚本里。
|
|
16
18
|
|
|
17
19
|
## 需求名:流程开始或第一次落盘时问用户
|
|
18
20
|
|
|
@@ -41,14 +43,14 @@ node <skills>/workflow/scripts/requirement.mjs which
|
|
|
41
43
|
node <skills>/workflow/scripts/requirement.mjs path <plan|delivery|review|tech|release>
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path` 相同;文档仓库模式下是「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样。每种产物的文件名固定(`review.md`、`tech-doc.md` …),引用它的地方不会因为出了新版而失效。已有时,脚本先把当前版复制到 `workflow-run/tmp/old-artifacts
|
|
46
|
+
- 输出的 `path` 就是要写的文件(相对项目根)。文档里引用产物(「参考产物」、上线方案引用技术方案等)写输出的 `cite`:默认模式下和 `path` 相同;文档仓库模式下是「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样。每种产物的文件名固定(`review.md`、`tech-doc.md` …),引用它的地方不会因为出了新版而失效。已有时,脚本先把当前版复制到 `workflow-run/tmp/old-artifacts/<需求名>/<项目名>/<种类>_v<N>.md`,输出的 `previous` 就是这份(复审对照上次的报告读它),`version` 是这次的版本号;然后把新版直接写到 `path`。
|
|
45
47
|
- 退出码 2:当前分支还没绑定需求,按上一节问用户需求名,`bind` 之后再运行一次。
|
|
46
48
|
- `wiki_record` 非空:上一版已经发布到 wiki。新版的**第一行原样写这条记录**,md-to-wiki 再发布时会更新同一个页面,不会新建页面。
|
|
47
49
|
- 写完运行 `requirement.mjs sync <path>`,更新 `meta.json`。md-to-wiki 发布后改了第一行的记录,或补写了 DocNexus 元信息行,也要再 sync 一次。
|
|
48
|
-
- sync 时新版内容和归档的上一版不同,才算新版本(`meta.json` 的 `versions` 加一);没写就放弃的话版本号不变。输出里 `archived` 非空时,告诉用户归档了哪些文件、在哪里(`workflow-run/tmp/old-artifacts
|
|
50
|
+
- sync 时新版内容和归档的上一版不同,才算新版本(`meta.json` 的 `versions` 加一);没写就放弃的话版本号不变。输出里 `archived` 非空时,告诉用户归档了哪些文件、在哪里(`workflow-run/tmp/old-artifacts/<需求名>/<项目名>/`,不提交),请用户确认不再需要后自己删除;**不要替用户删**。随代码或文档仓库提交过的旧版也能从 git 历史找回。
|
|
49
51
|
- sync 输出里 `commit` 非空:按下一节「文档仓库的提交」处理。
|
|
50
52
|
|
|
51
|
-
##
|
|
53
|
+
## 独立文档仓库
|
|
52
54
|
|
|
53
55
|
团队想让代码仓库保持干净、或者一个需求横跨多个服务时,用户在项目根运行(由用户执行,克隆可能要他们自己认证):
|
|
54
56
|
|
|
@@ -56,11 +58,11 @@ node <skills>/workflow/scripts/requirement.mjs path <plan|delivery|review|tech|r
|
|
|
56
58
|
npx @rowansenne/workflow-run docs-repo <团队文档仓库地址> [--project <项目名>]
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
`workflow-run/` 里还没有内容时它直接克隆;已经有产物、DocNexus 时原地 `git init` 接上远端(目录结构相同,文件不用挪;和远端同路径冲突时退回原状并列出冲突的文件)。然后代码仓库的 `.gitignore` 加 `/workflow-run/`,并配置好项目名。`workflow-run/` 自己是 git 仓库就是这个模式,不用别的配置。
|
|
60
62
|
|
|
61
|
-
-
|
|
63
|
+
- 布局和随代码提交时一样。绑定需求时 `which` 会列出 `other_projects`(其他服务已有、已拉到本地的需求),用同一个名字 `bind` 就加入了那个需求。
|
|
62
64
|
- DocNexus 也在这个仓库里(`workflow-run/docnexus/`),各服务共用一个知识库。
|
|
63
|
-
- `project`:这个项目在文档仓库里的名字,写在 `workflow/profile.json` 的 `artifacts.project
|
|
65
|
+
- `project`:这个项目在文档仓库里的名字,写在 `workflow/profile.json` 的 `artifacts.project`(随代码提交,团队共用),**只需配置一次**;随代码提交时已经自动写过的就直接沿用。没配时脚本退出码 2(`need_project`)并给出 `suggested_project`(git 远端仓库名):问用户一次,然后运行 `requirement.mjs config --project <项目名>`,之后一直沿用。
|
|
64
66
|
- 查看或修改配置:`requirement.mjs config [--project <项目名>] [--commit-prompt on|off] [--local]`。
|
|
65
67
|
- `/workflow view` 在这个模式下显示文档仓库里所有项目的需求,一个需求下按项目区分产物。
|
|
66
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 按分支汇总各 skill 的产物:开发方案、交付说明、审查报告、技术方案、上线方案,以及它们的 wiki 发布记录。
|
|
3
|
-
// 产物按需求归档在 workflow-run/docs/requirements
|
|
3
|
+
// 产物按需求归档在 workflow-run/docs/requirements/<需求>/<项目>/(两种模式结构相同,见 requirement.mjs)。
|
|
4
4
|
// 直接扫描 Markdown,从元信息行读分支(「分支:」「对比分支:」),
|
|
5
5
|
// 从第一行的 <!-- md-to-wiki: ... --> 读发布记录(md-to-wiki 上传后写入)。只读。
|
|
6
6
|
//
|
|
@@ -44,14 +44,15 @@ export function parseWiki(text) {
|
|
|
44
44
|
|
|
45
45
|
const headerDate = (text) => /(\d{4}-\d{2}-\d{2})/.exec(text.replace(/<!--[\s\S]*?-->/g, '').split(/\r?\n/).slice(0, 8).join('\n'))?.[1] ?? ''; // 跳过 md-to-wiki 发布记录里的 at=
|
|
46
46
|
|
|
47
|
-
// workflow-run/docs/requirements
|
|
47
|
+
// workflow-run/docs/requirements/<需求>/<项目>/<文件> → 产物条目;不是已知产物返回 null。
|
|
48
|
+
// 分支里还没迁移的旧结构(<需求>/<文件>,没有项目这一层)也认,project 为 null
|
|
48
49
|
function entry(rel, text, mtime) {
|
|
49
50
|
const parts = rel.split('/');
|
|
50
|
-
if (parts.length !== 5 || parts.slice(0, 3).join('/') !== REQ_DIR) return null;
|
|
51
|
-
const p = parseFile(parts
|
|
51
|
+
if ((parts.length !== 5 && parts.length !== 6) || parts.slice(0, 3).join('/') !== REQ_DIR) return null;
|
|
52
|
+
const p = parseFile(parts.at(-1));
|
|
52
53
|
const k = p && KINDS.find((x) => x.kind === p.kind);
|
|
53
54
|
// 排序键:文档头里的日期 + 版本号(需求目录里的文件名不带日期)
|
|
54
|
-
return k ? { kind: k.kind, label: k.label, path: rel, requirement: parts[3], branch: parseBranch(text), date: `${headerDate(text)}#${String(p.version).padStart(3, '0')}`, mtime, wiki: parseWiki(text) } : null;
|
|
55
|
+
return k ? { kind: k.kind, label: k.label, path: rel, requirement: parts[3], project: parts.length === 6 ? parts[4] : null, branch: parseBranch(text), date: `${headerDate(text)}#${String(p.version).padStart(3, '0')}`, mtime, wiki: parseWiki(text) } : null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const byDate = (a, b) => a.date.localeCompare(b.date) || a.mtime - b.mtime;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 运行反馈:skill 每跑完一次追加一行到 workflow-run/feedback/<git 用户名>.jsonl,攒两三周后汇总,作为精简 SKILL.md 的依据。
|
|
3
|
+
// 只记过程(提问轮数、跳过的步骤、哪条规则不清楚……),不记代码、类名、表名、业务名词;结果类数据(审查发现、未验证项)report.mjs 从产物里统计。
|
|
4
|
+
// 每人一个文件、随 workflow-run/ 提交,多人不冲突;装了新版本(init / upgrade)时清掉本项目旧版本的记录(git 历史里仍可找回)。
|
|
5
|
+
// 字段含义与记录规则见 workflow/references/feedback.md。
|
|
6
|
+
//
|
|
7
|
+
// 用法(在项目根执行;--root 可指定项目根):
|
|
8
|
+
// node .agents/skills/workflow/scripts/feedback.mjs add --skill dev --model <模型> [--mode feature] [--size M] [--fast no] [--rounds 2] …
|
|
9
|
+
// node .agents/skills/workflow/scripts/feedback.mjs summary [--json]
|
|
10
|
+
// --skipped / --unclear / --feedback 可重复。add 输出写到的文件(JSON)。退出码:0 正常;1 参数错误
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { homeDir, homeIsRepo, layout, suggestProject, dirName, HOME } from './requirement.mjs';
|
|
17
|
+
|
|
18
|
+
const SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
19
|
+
export const FEEDBACK_DIR = 'feedback';
|
|
20
|
+
|
|
21
|
+
// 字段 → 类型;list 可重复
|
|
22
|
+
const FIELDS = {
|
|
23
|
+
skill: 'string', model: 'string', env: 'string',
|
|
24
|
+
mode: ['feature', 'bug', 'review'], size: ['S', 'M', 'L'], depth: ['快审', '标准', '深审'], fast: ['yes', 'no'],
|
|
25
|
+
rounds: 'int', corrections: 'int', 'baseline-failures': 'int',
|
|
26
|
+
acceptance: /^\d+\/\d+$/, findings: /^\d+\/\d+\/\d+\/\d+$/,
|
|
27
|
+
skipped: 'list', unclear: 'list', feedback: 'list',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
31
|
+
const gitOut = (root, args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? '';
|
|
32
|
+
|
|
33
|
+
export const feedbackDir = (root) => path.join(homeDir(root), FEEDBACK_DIR);
|
|
34
|
+
export const projectName = (root) => layout(root).project ?? suggestProject(root);
|
|
35
|
+
|
|
36
|
+
// 安装清单里的版本(skills 装在 .agents/skills/,清单在同一目录);从源码仓库直接运行时用 package.json
|
|
37
|
+
export function installedVersion() {
|
|
38
|
+
return readJson(path.join(SKILLS_DIR, '.workflow-run.json'))?.version ?? readJson(path.join(SKILLS_DIR, 'package.json'))?.version ?? 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function userFile(root) {
|
|
42
|
+
const who = gitOut(root, ['config', 'user.name']) || gitOut(root, ['config', 'user.email']).split('@')[0] || 'unknown';
|
|
43
|
+
return path.join(feedbackDir(root), `${dirName(who)}.jsonl`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseAdd(argv) {
|
|
47
|
+
const rec = {};
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const m = /^--([\w-]+)$/.exec(argv[i]);
|
|
50
|
+
const type = m && FIELDS[m[1]];
|
|
51
|
+
if (!type) throw new Error(`未知参数:${argv[i]}`);
|
|
52
|
+
const v = argv[++i];
|
|
53
|
+
if (v === undefined || v.startsWith('--')) throw new Error(`--${m[1]} 缺少值`);
|
|
54
|
+
const key = m[1];
|
|
55
|
+
if (type === 'list') { (rec[key] ??= []).push(v.trim()); continue; }
|
|
56
|
+
if (type === 'int') { if (!/^\d+$/.test(v)) throw new Error(`--${key} 需要非负整数`); rec[key] = Number(v); continue; }
|
|
57
|
+
if (Array.isArray(type) && !type.includes(v)) throw new Error(`--${key} 只能是 ${type.join(' / ')}`);
|
|
58
|
+
if (type instanceof RegExp && !type.test(v)) throw new Error(`--${key} 格式不对(${type.source})`);
|
|
59
|
+
rec[key] = v;
|
|
60
|
+
}
|
|
61
|
+
if (!rec.skill) throw new Error('缺少 --skill');
|
|
62
|
+
return rec;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function add(root, rec, { now = new Date() } = {}) {
|
|
66
|
+
const file = userFile(root);
|
|
67
|
+
const line = { ts: now.toISOString(), version: installedVersion(), project: projectName(root), ...rec };
|
|
68
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
69
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`);
|
|
70
|
+
return { ok: true, file: path.relative(root, file).split(path.sep).join('/'), record: line };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readAll(root) {
|
|
74
|
+
const dir = feedbackDir(root);
|
|
75
|
+
if (!fs.existsSync(dir)) return [];
|
|
76
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl')).sort().flatMap((f) => fs.readFileSync(path.join(dir, f), 'utf8')
|
|
77
|
+
.split('\n').filter((l) => l.trim()).map((l) => { try { return { user: f.replace(/\.jsonl$/, ''), ...JSON.parse(l) }; } catch { return null; } }).filter(Boolean));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 独立文档仓库模式下各人、各服务都往文档仓库的 feedback/ 追加,同一文件在两个分支上各追加一行时合并会在末尾冲突:
|
|
81
|
+
// 在文档仓库自己的 .gitattributes 里用 git 内置的 union 合并保留两边的行。随代码提交时不加(不改代码仓库的 .gitattributes),
|
|
82
|
+
// 并去掉 0.22.0 加在代码仓库里的这一行(文件里只剩这一行时删掉文件)。
|
|
83
|
+
// 返回 { action: 'added' | 'removed', file, rule },没有改动时 null
|
|
84
|
+
const LEGACY_RULE = `${HOME}/${FEEDBACK_DIR}/*.jsonl merge=union`;
|
|
85
|
+
export function ensureUnionMerge(root) {
|
|
86
|
+
const lines = (f) => (fs.existsSync(f) ? fs.readFileSync(f, 'utf8').split(/\r?\n/) : []);
|
|
87
|
+
const relOf = (f) => path.relative(root, f).split(path.sep).join('/');
|
|
88
|
+
const codeAttr = path.join(root, '.gitattributes');
|
|
89
|
+
if (!homeIsRepo(root)) {
|
|
90
|
+
const cur = lines(codeAttr);
|
|
91
|
+
if (!cur.some((l) => l.trim() === LEGACY_RULE)) return null;
|
|
92
|
+
const keep = cur.filter((l) => l.trim() !== LEGACY_RULE);
|
|
93
|
+
if (keep.every((l) => !l.trim())) fs.rmSync(codeAttr);
|
|
94
|
+
else fs.writeFileSync(codeAttr, keep.join('\n'));
|
|
95
|
+
return { action: 'removed', file: relOf(codeAttr), rule: LEGACY_RULE };
|
|
96
|
+
}
|
|
97
|
+
const file = path.join(homeDir(root), '.gitattributes');
|
|
98
|
+
const rule = `${FEEDBACK_DIR}/*.jsonl merge=union`;
|
|
99
|
+
const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
100
|
+
if (lines(file).some((l) => l.trim() === rule)) return null;
|
|
101
|
+
fs.writeFileSync(file, `${text}${text && !text.endsWith('\n') ? '\n' : ''}${rule}\n`);
|
|
102
|
+
return { action: 'added', file: relOf(file), rule };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 新版本装好后:删掉本项目旧版本的记录,其他项目(独立文档仓库里各服务共用 feedback/)的不动。返回删掉的条数
|
|
106
|
+
export function rotate(root, version, project = projectName(root)) {
|
|
107
|
+
const dir = feedbackDir(root);
|
|
108
|
+
if (!fs.existsSync(dir)) return 0;
|
|
109
|
+
let removed = 0;
|
|
110
|
+
for (const f of fs.readdirSync(dir).filter((x) => x.endsWith('.jsonl'))) {
|
|
111
|
+
const p = path.join(dir, f);
|
|
112
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n').filter((l) => l.trim());
|
|
113
|
+
const keep = lines.filter((l) => { let r; try { r = JSON.parse(l); } catch { return true; } return r.version === version || (r.project && r.project !== project); });
|
|
114
|
+
if (keep.length === lines.length) continue;
|
|
115
|
+
removed += lines.length - keep.length;
|
|
116
|
+
fs.writeFileSync(p, keep.length ? `${keep.join('\n')}\n` : '');
|
|
117
|
+
}
|
|
118
|
+
return removed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 「F6: 缺中间件」→ F6;没有章节前缀的归到「其他」
|
|
122
|
+
const sectionOf = (s) => /^([A-Z]?\d+|Step ?\d+|第\s*\d+\s*步)\s*[::]/i.exec(s)?.[1].replace(/\s+/g, '') ?? '其他';
|
|
123
|
+
const groupBy = (xs, f) => xs.reduce((m, x) => { (m[f(x)] ??= []).push(x); return m; }, {});
|
|
124
|
+
const countBy = (xs, f) => xs.reduce((m, x) => { const k = f(x); m[k] = (m[k] ?? 0) + 1; return m; }, {});
|
|
125
|
+
const sorted = (m) => Object.entries(m).sort((a, b) => b[1] - a[1]);
|
|
126
|
+
const avg = (xs) => (xs.length ? Math.round((xs.reduce((a, b) => a + b, 0) / xs.length) * 10) / 10 : null);
|
|
127
|
+
|
|
128
|
+
export function summarize(records) {
|
|
129
|
+
const bySkill = {};
|
|
130
|
+
for (const [skill, rs] of Object.entries(groupBy(records, (r) => r.skill))) {
|
|
131
|
+
const notes = (k) => rs.flatMap((r) => r[k] ?? []);
|
|
132
|
+
const acc = rs.filter((r) => r.acceptance).map((r) => r.acceptance.split('/').map(Number));
|
|
133
|
+
const findings = rs.filter((r) => r.findings).map((r) => r.findings.split('/').map(Number));
|
|
134
|
+
bySkill[skill] = {
|
|
135
|
+
runs: rs.length,
|
|
136
|
+
mode: countBy(rs.filter((r) => r.mode), (r) => r.mode),
|
|
137
|
+
size: countBy(rs.filter((r) => r.size), (r) => r.size),
|
|
138
|
+
depth: countBy(rs.filter((r) => r.depth), (r) => r.depth),
|
|
139
|
+
fast_path: rs.filter((r) => r.fast === 'yes').length,
|
|
140
|
+
avg_rounds: avg(rs.filter((r) => r.rounds !== undefined).map((r) => r.rounds)),
|
|
141
|
+
runs_with_corrections: rs.filter((r) => r.corrections > 0).length,
|
|
142
|
+
acceptance: acc.length ? { verified: acc.reduce((a, [v]) => a + v, 0), total: acc.reduce((a, [, t]) => a + t, 0) } : null,
|
|
143
|
+
runs_with_baseline_failures: rs.filter((r) => r['baseline-failures'] > 0).length,
|
|
144
|
+
findings: findings.length ? ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].reduce((o, k, i) => ({ ...o, [k]: findings.reduce((a, f) => a + f[i], 0) }), {}) : null,
|
|
145
|
+
skipped: groupBy(notes('skipped'), sectionOf),
|
|
146
|
+
unclear: groupBy(notes('unclear'), sectionOf),
|
|
147
|
+
feedback: notes('feedback'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
records: records.length,
|
|
152
|
+
from: records.map((r) => r.ts).sort()[0] ?? null,
|
|
153
|
+
to: records.map((r) => r.ts).sort().at(-1) ?? null,
|
|
154
|
+
versions: countBy(records, (r) => r.version), projects: countBy(records, (r) => r.project),
|
|
155
|
+
users: Object.keys(countBy(records, (r) => r.user)).length, models: countBy(records, (r) => r.model ?? '未写'),
|
|
156
|
+
skills: bySkill,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toMarkdown(s) {
|
|
161
|
+
const kv = (m) => sorted(m).map(([k, v]) => `${k} ${v}`).join(',') || '无';
|
|
162
|
+
const L = ['# 运行反馈汇总', '', `- 记录:${s.records} 条(${s.from?.slice(0, 10) ?? '—'} ~ ${s.to?.slice(0, 10) ?? '—'}),${s.users} 人`, `- 版本:${kv(s.versions)}`, `- 项目:${kv(s.projects)}`, `- 模型:${kv(s.models)}`, ''];
|
|
163
|
+
for (const [skill, k] of Object.entries(s.skills)) {
|
|
164
|
+
L.push(`## ${skill}(${k.runs} 次)`, '');
|
|
165
|
+
if (Object.keys(k.mode).length) L.push(`- 模式:${kv(k.mode)}`);
|
|
166
|
+
if (Object.keys(k.size).length) L.push(`- 规模:${kv(k.size)};快速通道 ${k.fast_path} 次`);
|
|
167
|
+
if (Object.keys(k.depth).length) L.push(`- 深度:${kv(k.depth)}`);
|
|
168
|
+
if (k.avg_rounds !== null) L.push(`- 平均提问轮数:${k.avg_rounds};用户纠正过需求理解:${k.runs_with_corrections} 次`);
|
|
169
|
+
if (k.acceptance) L.push(`- 验收项:已验证 ${k.acceptance.verified} / ${k.acceptance.total}`);
|
|
170
|
+
if (k.runs_with_baseline_failures) L.push(`- 开工前已有失败:${k.runs_with_baseline_failures} 次`);
|
|
171
|
+
if (k.findings) L.push(`- 审查发现:${Object.entries(k.findings).map(([a, b]) => `${a} ${b}`).join(',')}`);
|
|
172
|
+
for (const [title, groups] of [['跳过的步骤', k.skipped], ['不清楚 / 矛盾 / 做不到的规则', k.unclear]]) {
|
|
173
|
+
const g = sorted(Object.fromEntries(Object.entries(groups).map(([sec, xs]) => [sec, xs.length])));
|
|
174
|
+
if (!g.length) continue;
|
|
175
|
+
L.push('', `### ${title}`, '');
|
|
176
|
+
for (const [sec, n] of g) { L.push(`- **${sec}**(${n})`); for (const x of groups[sec]) L.push(` - ${x}`); }
|
|
177
|
+
}
|
|
178
|
+
if (k.feedback.length) { L.push('', '### 用户反馈', ''); for (const x of k.feedback) L.push(`- ${x}`); }
|
|
179
|
+
L.push('');
|
|
180
|
+
}
|
|
181
|
+
if (!s.records) L.push('还没有记录。');
|
|
182
|
+
return L.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function main(argv) {
|
|
186
|
+
let root = process.cwd();
|
|
187
|
+
const rest = [];
|
|
188
|
+
for (let i = 0; i < argv.length; i++) { if (argv[i] === '--root') root = path.resolve(argv[++i] ?? '.'); else rest.push(argv[i]); }
|
|
189
|
+
const [cmd, ...args] = rest;
|
|
190
|
+
if (cmd === 'add') {
|
|
191
|
+
try { console.log(JSON.stringify(add(root, parseAdd(args)), null, 2)); return 0; } catch (e) { console.error(e.message); return 1; }
|
|
192
|
+
}
|
|
193
|
+
if (cmd === 'summary' && args.every((a) => a === '--json')) {
|
|
194
|
+
const s = summarize(readAll(root));
|
|
195
|
+
console.log(args.includes('--json') ? JSON.stringify(s, null, 2) : toMarkdown(s));
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
console.error('用法: feedback.mjs [--root <项目根>] add --skill <skill> [--model …] … | summary [--json]');
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { main };
|
|
203
|
+
const invoked = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
204
|
+
if (invoked) process.exit(main(process.argv.slice(2)));
|