paseo-req-kit 1.7.0-beta.3 → 1.8.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 +94 -0
- package/README.md +15 -10
- package/bin/req.mjs +4 -0
- package/config.example.json +32 -1
- package/contracts/feature-files.md +30 -0
- package/contracts/notifier-event.md +3 -2
- package/contracts/result-file.md +8 -4
- package/contracts/spec.md +3 -1
- package/contracts/state.md +7 -2
- package/contracts/verify.md +2 -0
- package/docs/adr/0023-types-gate-slices.md +54 -0
- package/docs/adr/0024-confirm-early-review-axes.md +27 -0
- package/docs/adr/0025-joint-requirements.md +27 -0
- package/docs/adr/README.md +3 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +18 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +16 -7
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +105 -0
- package/engine/prompts/00-shared-context.md +4 -0
- package/engine/prompts/02-spec-review.md +1 -1
- package/engine/prompts/03-design.md +31 -8
- package/engine/prompts/04-design-review.md +3 -2
- package/engine/prompts/04b-design-revise.md +8 -1
- package/engine/prompts/05-implement.md +11 -5
- package/engine/prompts/06-code-review.md +13 -9
- package/engine/prompts/07-review-fix.md +1 -1
- package/engine/prompts/09-verify.md +1 -1
- package/engine/prompts/10-defect-fix.md +1 -1
- package/engine/prompts/11-report.md +20 -1
- package/engine/prompts/incremental.md +1 -0
- package/engine/prompts/joint.md +55 -0
- package/engine/prompts/related-repos.md +7 -0
- package/engine/prompts/review-axis-spec.md +10 -0
- package/engine/prompts/review-axis-standards.md +11 -0
- package/engine/prompts/types/assessment.md +41 -0
- package/engine/prompts/types/defect.md +55 -0
- package/engine/prompts/types/refactor.md +39 -0
- package/engine/src/cli-args.ts +14 -1
- package/engine/src/config.ts +140 -0
- package/engine/src/control.ts +27 -11
- package/engine/src/digest.ts +12 -3
- package/engine/src/doctor.ts +28 -1
- package/engine/src/eval.ts +1 -1
- package/engine/src/features.ts +2 -0
- package/engine/src/gate.ts +157 -0
- package/engine/src/joint.ts +173 -0
- package/engine/src/launch.ts +1 -1
- package/engine/src/lessons.ts +72 -0
- package/engine/src/main.ts +38 -5
- package/engine/src/messages.ts +87 -1
- package/engine/src/notifiers.ts +1 -1
- package/engine/src/notify-test.ts +4 -0
- package/engine/src/pipeline.ts +777 -63
- package/engine/src/playbooks.ts +46 -0
- package/engine/src/preflight.ts +41 -2
- package/engine/src/state.ts +58 -3
- package/engine/src/steps.ts +29 -5
- package/notifiers/im-template.mjs +3 -1
- package/package.json +2 -2
- package/skills/acceptance-criteria/SKILL.md +10 -1
- package/skills/req/SKILL.md +5 -3
- package/skills/req/playbooks/new.md +16 -5
- package/skills/req/playbooks/small.md +1 -1
- package/skills/req/playbooks/status.md +2 -0
- package/skills/req-retro/SKILL.md +2 -0
- package/skills/req-setup/SKILL.md +22 -5
- package/skills/requirement-interview/SKILL.md +3 -0
- package/skills/spec-writing/SKILL.md +18 -4
- package/skills/spec-writing/spec-template.md +4 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 你的评审方向:规格(做的是不是要做的事)
|
|
2
|
+
|
|
3
|
+
这一轮代码评审分两个方向,由两位评审者同时进行:你负责**规格**,另一位负责**规范**。在按上文"评审标准"完整评审的基础上,你重点回答:
|
|
4
|
+
|
|
5
|
+
- **每条 AC**:是否真的实现了(读代码,运行测试,能操作的实际操作一遍),是否有测试覆盖;测试的期望值是否与 spec 中这条 AC 的例子一致。
|
|
6
|
+
- **范围**:有没有做 spec 没要求的事(多出来的功能、改变了不该变的行为);`<pipeline-context>` 中"范围检查"列出的方案外文件是否必要。
|
|
7
|
+
- **与确认过的预览一致**:有 `preview.md`、`prototype/` 时,做出来的界面和行为是否与它们一致。
|
|
8
|
+
- **异常和边界**:spec 中写到的异常情况、业务规则是否都处理了,处理得是否与 spec 一致。
|
|
9
|
+
|
|
10
|
+
项目惯例、代码风格、测试写法不是你的重点,另一位会看;顺带发现的可以简要提一句。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 你的评审方向:规范(做得是否符合项目的标准)
|
|
2
|
+
|
|
3
|
+
这一轮代码评审分两个方向,由两位评审者同时进行:你负责**规范**,另一位负责**规格**。在按上文"评审标准"完整评审的基础上,你重点检查:
|
|
4
|
+
|
|
5
|
+
- **项目惯例**:命名、目录结构、错误处理、日志、配置的写法是否跟随项目现状,以及方案中写明的参照文件;`GLOSSARY.md` 中业务词对应的代码名称是否一致使用。
|
|
6
|
+
- **测试质量**:测试检查的是行为还是实现细节;有没有恒为真的断言;有没有删除、跳过、放宽已有测试;新测试的位置和写法是否符合项目约定。
|
|
7
|
+
- **安全与数据**:输入校验、权限、注入、敏感信息、数据变更能否回退。
|
|
8
|
+
- **简单性**:不必要的抽象、重复的代码、遗留的调试代码。
|
|
9
|
+
- **可以变成规则的问题**:一类能用 lint 规则、类型约束或测试挡住的写法问题,在意见里写明建议的规则;完成报告会把它记入 `lessons.tsv`,同类问题在几个需求里反复出现时,会建议把它变成项目的规则。
|
|
10
|
+
|
|
11
|
+
每条 AC 是否实现不是你的重点,另一位会看。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 本需求类型的做法:评估
|
|
2
|
+
|
|
3
|
+
这个需求是一次调研评估:spec 列出了要回答的问题(能不能做、怎么做、影响多大、大概要多少工作量)。**只读,不写代码**:不修改产品代码、测试和配置,功能目录之外不提交任何文件。流程是调研评估 → 评估审阅 ⇄ 评估修订 → 评估报告,没有代码实现、代码评审和验证。
|
|
4
|
+
|
|
5
|
+
## 所有步骤
|
|
6
|
+
|
|
7
|
+
- 结论要有依据:引用代码位置(文件和行号)、实际运行的命令和输出、文档链接。推测要标明是推测。
|
|
8
|
+
- 可以运行只读的命令(运行测试、构建、查看依赖、在本地启动服务看看),不做会留下改动的操作,启动的进程用完关掉。需要写一次性的试验代码来回答问题时,写在功能目录的 `spike/` 下,不改项目代码。
|
|
9
|
+
|
|
10
|
+
## 调研评估(design)
|
|
11
|
+
|
|
12
|
+
本步骤写 `assessment.md`,不写 `design.md`。结构:
|
|
13
|
+
|
|
14
|
+
1. **结论摘要**:逐条回答 spec 中的问题,各一两句话
|
|
15
|
+
2. **现状**:相关的模块、数据、接口,带代码位置
|
|
16
|
+
3. **可选方案**:两到三种(只有一种合理做法时就写一种)。每种写做法、优点、风险、影响到的模块和功能、大致工作量(按"小 / 中 / 大"给,并列出主要的工作项,不编造精确的人天)
|
|
17
|
+
4. **推荐**:推荐哪一种、为什么、前提条件
|
|
18
|
+
5. **风险与未知**:哪些还不确定,要怎样才能确定
|
|
19
|
+
6. **如果要做**:把推荐方案写成需求草稿(标题、要做的事、建议的验收标准),可以直接作为下一个需求的起点
|
|
20
|
+
|
|
21
|
+
结果文件中 `has_runnable_service` 写 `false`,不需要 `slices` 和 `files`。
|
|
22
|
+
|
|
23
|
+
## 评估审阅(design_review)
|
|
24
|
+
|
|
25
|
+
你审阅的是 `assessment.md`,不是技术方案:逐条核实其中关于代码的说法(读对应的代码);结论是否回答了 spec 中的每个问题;方案比较是否公平、风险有没有遗漏;工作量的判断有没有依据。意见按"必须修改 / 建议 / 备注"写进 `design-review.md`,结论和结果文件的规则不变。
|
|
26
|
+
|
|
27
|
+
## 评估修订(design_revise)
|
|
28
|
+
|
|
29
|
+
按审阅意见修订 `assessment.md`(不是 `design.md`),在它的末尾追加修订记录。
|
|
30
|
+
|
|
31
|
+
## 评估报告(report)
|
|
32
|
+
|
|
33
|
+
`report.md` 是给需求方的评估结论,不按"验收对照"的结构写:
|
|
34
|
+
|
|
35
|
+
1. **结论**:逐条回答 spec 中的问题
|
|
36
|
+
2. **推荐的做法**和理由;其他做法为什么没选
|
|
37
|
+
3. **风险**和还不确定的事
|
|
38
|
+
4. **如果决定要做**:下一步怎么走(需求草稿在 `assessment.md` 最后一节;在对话里说"按评估结果提一个新需求"即可开始)
|
|
39
|
+
5. **附录**:调研中实际运行的命令、引用的代码位置、审阅经过了几轮、用时与用量
|
|
40
|
+
|
|
41
|
+
结果文件的 `outcome`:问题都有了有依据的回答时写 `completed`;部分问题没能回答时写 `completed_with_issues`;评估没有完成时写 `not_completed`。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 本需求类型的做法:缺陷修复
|
|
2
|
+
|
|
3
|
+
这个需求是修复一个缺陷:spec 描述了现象、复现步骤和修复后应有的行为。缺陷修复的关键是**先证明问题存在、找到原因,再修**。没有复现就动手修,等于在猜。
|
|
4
|
+
|
|
5
|
+
## 所有步骤
|
|
6
|
+
|
|
7
|
+
- "复现"指一个能稳定重现问题的自动化测试,或者一个可以重复执行的复现脚本(命令、接口调用),修复前失败、修复后通过。它是这个需求最重要的证据,各步骤都围绕它来做和查。
|
|
8
|
+
- 只修这个缺陷和它的根因。看到别处有不相关的问题,记下来交给人决定;同一个根因在别处造成的问题,修在同一处的一并修好并写明。
|
|
9
|
+
|
|
10
|
+
## 复现与定位(design)
|
|
11
|
+
|
|
12
|
+
本步骤代替通常的方案设计,产出 `diagnosis.md` 和一份简短的 `design.md`。
|
|
13
|
+
|
|
14
|
+
1. 按 spec 中的复现步骤,在本地实际重现问题。先写成自动化测试(项目有测试框架时首选);写不了测试的,写成复现脚本,放在测试目录或功能目录的 `repro/` 下。运行它,确认它**失败,并且失败的方式与 spec 描述的现象一致**(而不是因为测试本身写错)。
|
|
15
|
+
2. 缩小范围:找到最小的触发条件,排除其他可能的原因(数据、配置、环境)。
|
|
16
|
+
3. 定位根因:读代码、加临时日志、查提交历史(`git log -p`、`git bisect`),直到能用代码位置说清楚"为什么会这样"。临时的日志和调试代码用完删掉。
|
|
17
|
+
4. 判断影响面:同样的原因还会在哪里造成问题;已有的错误数据是否需要修正(spec 没有提到的,写进阻塞问题或假设)。
|
|
18
|
+
5. 提交复现测试或脚本。这时它是失败的,提交信息注明"复现,修复前失败"。
|
|
19
|
+
|
|
20
|
+
`diagnosis.md` 的内容:现象与复现(复现测试或脚本的位置、运行命令、失败输出的摘要)、根因(带代码位置)、排除了哪些其他原因、影响面、修复思路。
|
|
21
|
+
|
|
22
|
+
`design.md` 按通常的结构写,但可以简短:修复方案、改动的文件、实施计划(通常一片)、测试策略(复现测试,加上相邻的、不应受影响的行为)。
|
|
23
|
+
|
|
24
|
+
**复现不了时**:按 spec 的步骤和合理的变化尝试过,仍然重现不了,不要猜着改代码。在 `diagnosis.md` 中写明你尝试了什么、看到了什么,然后在结果文件中写 `"reproduced": false`,在 `questions` 中写需要需求方补充的信息,具体到能直接回答的程度(例如"出错时用的是哪种账号?大约几点操作的?")。流水线会暂停,把这些问题转给需求方。
|
|
25
|
+
|
|
26
|
+
结果文件在通常的字段之外加上 `reproduced`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "has_runnable_service": true, "reproduced": true, "files": ["src/order/price.ts", "test/order/price.test.ts"] }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 方案审核(design_review)
|
|
33
|
+
|
|
34
|
+
重点审核 `diagnosis.md`:复现是否真的对应 spec 描述的现象;根因有没有代码上的依据,还是只是猜测;修复针对的是根因,还是只让这个现象消失;影响面是否查过。复现或根因站不住的,提为"必须修改"。
|
|
35
|
+
|
|
36
|
+
## 代码实现(implement)
|
|
37
|
+
|
|
38
|
+
- 复现测试已经由上一步提交。先运行它,确认仍然失败,再修复,直到它通过。
|
|
39
|
+
- 不要修改复现测试的断言来让它通过。确实写错了的,在 `implementation.md` 中说明改了什么、为什么。
|
|
40
|
+
- 为影响面中提到的相关情况补测试。
|
|
41
|
+
|
|
42
|
+
## 代码评审(code_review)
|
|
43
|
+
|
|
44
|
+
额外核对:
|
|
45
|
+
- 复现测试在修复前确实失败、修复后通过:检出修复前的提交(或用 `git stash`)实际运行一次,不要只看记录。
|
|
46
|
+
- 修复针对的是 `diagnosis.md` 中的根因;同一个根因造成的其他问题是否处理或记录。
|
|
47
|
+
- 复现测试的断言在修复过程中有没有被放宽。
|
|
48
|
+
|
|
49
|
+
## 功能验证(verify)
|
|
50
|
+
|
|
51
|
+
按 spec 的复现步骤从用户入口重做一遍,确认现象消失;再验证相邻的、不应受影响的行为。在 `verification.md` 中写明复现步骤修复前后的对比。
|
|
52
|
+
|
|
53
|
+
## 完成报告(report)
|
|
54
|
+
|
|
55
|
+
用需求方能懂的话说明:问题是什么原因造成的、修了什么、怎么确认修好了(复现步骤修复前后的结果)、有没有同类问题或需要修正的数据。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 本需求类型的做法:重构
|
|
2
|
+
|
|
3
|
+
这个需求是改进代码本身(整理结构、拆分模块、升级依赖、消除重复等),对使用者来说**行为不变**。重构的关键是先把现有行为锁住,再小步改动,每一步都保持全部检查通过。
|
|
4
|
+
|
|
5
|
+
## 所有步骤
|
|
6
|
+
|
|
7
|
+
- "行为不变"是这个需求最重要的验收标准,spec 中的改进目标(例如"订单模块不再依赖旧的支付库")是第二个。
|
|
8
|
+
- 不借重构修改行为。发现现有行为有缺陷时,记下来交给人决定,不要顺手修:修了就不再是"行为不变",也会让评审和验证无从比较。
|
|
9
|
+
|
|
10
|
+
## 方案设计与行为锁定(design)
|
|
11
|
+
|
|
12
|
+
1. 列出要改动的代码对外的行为:接口的输入输出、页面上看到的结果、命令的输出、写入的数据、发出的事件和日志中被别处依赖的部分。
|
|
13
|
+
2. 检查这些行为现有的测试覆盖。没有覆盖的,在方案中安排**特征测试**(按代码现在的实际行为写断言,哪怕行为看起来不合理)或快照,在改动代码之前补上。
|
|
14
|
+
3. 实施计划:第一个任务(或第一片)是补特征测试并提交,这时它们应当全部通过;之后每个任务都是一小步,每步结束时全部检查通过、可以单独提交和回退。
|
|
15
|
+
4. 写明怎么证明"行为不变":用哪些测试和快照,改动前后怎样各运行一次来对比。
|
|
16
|
+
|
|
17
|
+
## 方案审核(design_review)
|
|
18
|
+
|
|
19
|
+
重点审核:行为清单是否完整(尤其是边界情况和错误处理);特征测试是否安排在改动之前;每一步是否小到可以单独回退;有没有夹带行为变化。
|
|
20
|
+
|
|
21
|
+
## 代码实现(implement)
|
|
22
|
+
|
|
23
|
+
- 第一个任务是补特征测试:在改动前的代码上运行,全部通过后单独提交,提交信息注明"特征测试"。之后不再修改这些测试的断言;确实需要改的(例如测的是内部细节而不是行为),在 `implementation.md` 中逐个说明理由。
|
|
24
|
+
- 之后每一小步:改动 → 运行全部测试和检查 → 通过后提交。不通过就修到通过或撤回这一步,不要攒一大批改动再一起修。
|
|
25
|
+
|
|
26
|
+
## 代码评审(code_review)
|
|
27
|
+
|
|
28
|
+
首要问题是"行为有没有变":
|
|
29
|
+
- 找到特征测试的提交,检查之后的提交有没有修改这些测试的断言(`git diff <特征测试的提交>..HEAD -- <测试文件>`)。有修改且理由不充分的,提为"阻塞"。
|
|
30
|
+
- 对外接口、数据格式、错误信息、被别处依赖的日志有没有变化。
|
|
31
|
+
- 改进目标是否真的达成(例如依赖是否真的去掉了)。
|
|
32
|
+
|
|
33
|
+
## 功能验证(verify)
|
|
34
|
+
|
|
35
|
+
重点验证行为没有变:运行全部测试和快照对比;从用户入口把主要流程走一遍,与改动前的表现对比(需要时检出改动前的提交运行一次作为对照)。
|
|
36
|
+
|
|
37
|
+
## 完成报告(report)
|
|
38
|
+
|
|
39
|
+
用需求方能懂的话说明:改进了什么,怎么确认使用者看到的行为没有变化(哪些测试、对比的结果),以及发现但没有修的现有问题。
|
package/engine/src/cli-args.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const USAGE = `用法:
|
|
|
16
16
|
start-pipeline --feature <功能目录> --at 22:00 预约在指定时间开始(也可以写"明天 8:30""2026-10-14 08:30""+2h")
|
|
17
17
|
start-pipeline --feature <功能目录> --phases gated 分阶段:研发完成后停下等待测试验证(continuous | gated | develop-only)
|
|
18
18
|
start-pipeline --feature <功能目录> --phase verify 开始测试验证阶段(研发阶段完成后;可以加 --at 预约)
|
|
19
|
+
start-pipeline --feature <功能目录> --approve 需求方确认了方案(方案确认点),继续开发
|
|
19
20
|
|
|
20
21
|
其他选项:[--repo <仓库目录>] [--origin-agent <agent ID>] [--config <配置文件>]
|
|
21
22
|
|
|
@@ -34,6 +35,7 @@ export const USAGE = `用法:
|
|
|
34
35
|
--verify-at 分阶段时,研发完成后在这个时间自动开始测试验证(写法同 --at)
|
|
35
36
|
--verify-env 测试验证的环境:local(本地启动服务)或 deployed(已部署的测试环境,需要 test-env.md)
|
|
36
37
|
--defects 测试验证发现缺陷时:fix(当场修复并复验)、handback(退回研发阶段修复)、report(只记录)
|
|
38
|
+
--approve 方案确认点:需求方看过 preview.md 并确认,继续开发(等同于 --resume 加上确认)
|
|
37
39
|
--config 配置文件,默认为 ~/.req-kit/config.json(可用环境变量 REQ_KIT_HOME 改变目录)`;
|
|
38
40
|
|
|
39
41
|
export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
@@ -60,6 +62,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
60
62
|
"verify-at": { type: "string" },
|
|
61
63
|
"verify-env": { type: "string" },
|
|
62
64
|
defects: { type: "string" },
|
|
65
|
+
approve: { type: "boolean", default: false },
|
|
63
66
|
help: { type: "boolean", default: false },
|
|
64
67
|
},
|
|
65
68
|
strict: true,
|
|
@@ -101,6 +104,9 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
101
104
|
if (defects !== undefined && !["fix", "handback", "report"].includes(defects)) {
|
|
102
105
|
throw new Error(`--defects 只能是 fix、handback 或 report,收到 "${values.defects}"`);
|
|
103
106
|
}
|
|
107
|
+
if (values.approve && (values.status || values.stop || values["spec-review"] || values.restart || values.worktree || phase || values.at)) {
|
|
108
|
+
throw new Error("--approve 只用于确认等待中的方案并继续开发");
|
|
109
|
+
}
|
|
104
110
|
// In a Paseo chat, the chat itself is the origin unless told otherwise.
|
|
105
111
|
const origin = (values["origin-agent"] ?? process.env.PASEO_AGENT_ID)?.trim();
|
|
106
112
|
return {
|
|
@@ -108,7 +114,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
108
114
|
repo: path.resolve(values.repo ?? process.env.PASEO_AGENT_CWD ?? process.cwd()),
|
|
109
115
|
featureDir: values.feature ?? "",
|
|
110
116
|
originAgentId: origin ? origin : null,
|
|
111
|
-
resume: (values.resume ?? false) || phase === "verify",
|
|
117
|
+
resume: (values.resume ?? false) || phase === "verify" || (values.approve ?? false),
|
|
112
118
|
restart: values.restart ?? false,
|
|
113
119
|
configPath: values.config ? path.resolve(values.config) : null,
|
|
114
120
|
profile: (profile as ProfileName | undefined) ?? null,
|
|
@@ -125,6 +131,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
125
131
|
verifyAt: values["verify-at"] ?? null,
|
|
126
132
|
verifyEnv: (verifyEnv as VerifyEnvironment | undefined) ?? null,
|
|
127
133
|
defects: (defects as DefectHandling | undefined) ?? null,
|
|
134
|
+
approve: values.approve ?? false,
|
|
128
135
|
};
|
|
129
136
|
}
|
|
130
137
|
|
|
@@ -162,6 +169,11 @@ export function startCommand(args: {
|
|
|
162
169
|
return parts.map(quote).join(" ");
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
/** The command that confirms a design waiting at the confirmation point. */
|
|
173
|
+
export function approveCommand(args: { repo: string; featureDir: string; configPath: string | null }): string {
|
|
174
|
+
return resumeCommand(args).replace(/ --resume$/, " --approve");
|
|
175
|
+
}
|
|
176
|
+
|
|
165
177
|
/** The command that starts the verify phase once development is done. */
|
|
166
178
|
export function verifyCommand(args: {
|
|
167
179
|
repo: string;
|
|
@@ -200,5 +212,6 @@ export function launchArgv(args: LaunchArgs): string[] {
|
|
|
200
212
|
if (args.verifyAt) out.push("--verify-at", args.verifyAt);
|
|
201
213
|
if (args.verifyEnv) out.push("--verify-env", args.verifyEnv);
|
|
202
214
|
if (args.defects) out.push("--defects", args.defects);
|
|
215
|
+
if (args.approve) out.push("--approve");
|
|
203
216
|
return out;
|
|
204
217
|
}
|
package/engine/src/config.ts
CHANGED
|
@@ -100,6 +100,52 @@ export interface PhaseSettings {
|
|
|
100
100
|
verify: VerifyPhaseSettings;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Requirement types with their own playbook (see playbooks.ts and ADR 0023). The spec's
|
|
105
|
+
* "- 类型:" line picks one; anything else (新需求, 小改动) runs the normal flow.
|
|
106
|
+
*/
|
|
107
|
+
export type RequirementType = "defect" | "refactor" | "assessment";
|
|
108
|
+
|
|
109
|
+
export const REQUIREMENT_TYPES: readonly RequirementType[] = ["defect", "refactor", "assessment"];
|
|
110
|
+
|
|
111
|
+
export const REQUIREMENT_TYPE_LABELS: Record<RequirementType, string> = { defect: "缺陷修复", refactor: "重构", assessment: "评估" };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One project check the engine runs itself (the quality gate): formatting, lint, type
|
|
115
|
+
* check, tests, build. Commands should read the same on every system (see /req-setup).
|
|
116
|
+
*/
|
|
117
|
+
export interface CheckDef {
|
|
118
|
+
/** Short name, e.g. "lint", "web 测试". */
|
|
119
|
+
name: string;
|
|
120
|
+
command: string;
|
|
121
|
+
/** Directory to run in, relative to the repository root; default the root. */
|
|
122
|
+
cwd?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Run only when the change touches these paths (prefixes like "web/", or globs with *);
|
|
125
|
+
* default: `cwd` when given, otherwise always. Lets a monorepo check only changed modules.
|
|
126
|
+
*/
|
|
127
|
+
paths?: string[];
|
|
128
|
+
/** Minutes before the check is stopped; default qualityGate.timeoutMinutes. */
|
|
129
|
+
timeoutMinutes?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface QualityGateSettings {
|
|
133
|
+
/** How many times the step agent is asked to fix newly failing checks before the run moves on. */
|
|
134
|
+
fixAttempts: number;
|
|
135
|
+
/** Default time limit of one check. */
|
|
136
|
+
timeoutMinutes: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A repository the step agents may read but never change (e.g. the backend of a frontend project). */
|
|
140
|
+
export interface RelatedRepo {
|
|
141
|
+
/** Name used in prompts and reports, e.g. "后端". */
|
|
142
|
+
name: string;
|
|
143
|
+
/** Path on this machine: absolute, "~/…", or relative to the project's checkout. */
|
|
144
|
+
path: string;
|
|
145
|
+
/** What it is for, e.g. "提供登录和订单接口". */
|
|
146
|
+
role?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
103
149
|
export interface PipelineConfig {
|
|
104
150
|
daemonUrl: string;
|
|
105
151
|
/** Name of the environment variable holding the daemon password, if the daemon has one. */
|
|
@@ -165,6 +211,31 @@ export interface PipelineConfig {
|
|
|
165
211
|
watchdogMinutes: number;
|
|
166
212
|
/** Develop and verify phases: continuous (default), gated, or develop-only, plus verify-phase options. */
|
|
167
213
|
phases: PhaseSettings;
|
|
214
|
+
/** Requirement types whose playbook is used; a type left out runs the normal flow. */
|
|
215
|
+
requirementTypes: RequirementType[];
|
|
216
|
+
/** Project checks for the quality gate; empty: no gate. Usually in the project config, written by /req-setup. */
|
|
217
|
+
checks: CheckDef[];
|
|
218
|
+
qualityGate: QualityGateSettings;
|
|
219
|
+
/**
|
|
220
|
+
* Slice-by-slice implementation: when the design splits the work into vertical slices,
|
|
221
|
+
* each slice is implemented by a fresh agent, committed and checked on its own.
|
|
222
|
+
*/
|
|
223
|
+
slices: { profiles: ProfileName[]; max: number };
|
|
224
|
+
/** Repositories the steps may read for reference, never change. */
|
|
225
|
+
relatedRepos: RelatedRepo[];
|
|
226
|
+
/**
|
|
227
|
+
* Design confirmation point (1.8): after the design review, show the requester what they
|
|
228
|
+
* will get (preview.md, a prototype for UI changes) and wait for "yes" before writing code.
|
|
229
|
+
* mode "ui": only when the design says the change is visible in the UI; "always"; "off".
|
|
230
|
+
* autoContinueHours > 0: carry on with the design after that long without an answer.
|
|
231
|
+
*/
|
|
232
|
+
designApproval: { mode: "off" | "ui" | "always"; profiles: ProfileName[]; autoContinueHours: number };
|
|
233
|
+
/**
|
|
234
|
+
* Code review in two axes (1.8): the first round runs a spec reviewer (is every AC done, is
|
|
235
|
+
* anything outside the scope) and a standards reviewer (project conventions, tests, security)
|
|
236
|
+
* in parallel; the reviewer consolidates. A configured review panel takes precedence.
|
|
237
|
+
*/
|
|
238
|
+
reviewAxes: { enabled: boolean; profiles: ProfileName[] };
|
|
168
239
|
}
|
|
169
240
|
|
|
170
241
|
export const DEFAULT_CONFIG: PipelineConfig = {
|
|
@@ -220,6 +291,13 @@ export const DEFAULT_CONFIG: PipelineConfig = {
|
|
|
220
291
|
autoResumeMax: 2,
|
|
221
292
|
watchdogMinutes: 0,
|
|
222
293
|
phases: { mode: "continuous", verify: { environment: "local", agents: {}, defects: "fix", serviceStart: null } },
|
|
294
|
+
requirementTypes: ["defect", "refactor", "assessment"],
|
|
295
|
+
checks: [],
|
|
296
|
+
qualityGate: { fixAttempts: 2, timeoutMinutes: 20 },
|
|
297
|
+
slices: { profiles: ["standard"], max: 6 },
|
|
298
|
+
relatedRepos: [],
|
|
299
|
+
designApproval: { mode: "ui", profiles: ["standard"], autoContinueHours: 0 },
|
|
300
|
+
reviewAxes: { enabled: true, profiles: ["standard"] },
|
|
223
301
|
};
|
|
224
302
|
|
|
225
303
|
/** Project file, at the repository root, that overrides the machine-wide configuration. */
|
|
@@ -293,6 +371,13 @@ function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConf
|
|
|
293
371
|
agents: { ...base.phases.verify.agents, ...(isRecord(rawVerify.agents) ? rawVerify.agents : {}) },
|
|
294
372
|
},
|
|
295
373
|
};
|
|
374
|
+
merged.qualityGate = { ...base.qualityGate, ...(isRecord(raw.qualityGate) ? (raw.qualityGate as Partial<QualityGateSettings>) : {}) };
|
|
375
|
+
merged.slices = { ...base.slices, ...(isRecord(raw.slices) ? (raw.slices as Partial<PipelineConfig["slices"]>) : {}) };
|
|
376
|
+
merged.designApproval = {
|
|
377
|
+
...base.designApproval,
|
|
378
|
+
...(isRecord(raw.designApproval) ? (raw.designApproval as Partial<PipelineConfig["designApproval"]>) : {}),
|
|
379
|
+
};
|
|
380
|
+
merged.reviewAxes = { ...base.reviewAxes, ...(isRecord(raw.reviewAxes) ? (raw.reviewAxes as Partial<PipelineConfig["reviewAxes"]>) : {}) };
|
|
296
381
|
delete (merged as unknown as Record<string, unknown>).rounds;
|
|
297
382
|
return merged;
|
|
298
383
|
}
|
|
@@ -344,6 +429,60 @@ function validatePhases(phases: PhaseSettings): void {
|
|
|
344
429
|
}
|
|
345
430
|
}
|
|
346
431
|
|
|
432
|
+
function validateChecks(config: PipelineConfig): void {
|
|
433
|
+
if (!Array.isArray(config.requirementTypes) || !config.requirementTypes.every((t) => REQUIREMENT_TYPES.includes(t))) {
|
|
434
|
+
throw new ConfigError(`requirementTypes 只能包含 ${REQUIREMENT_TYPES.map((t) => `"${t}"`).join("、")}`);
|
|
435
|
+
}
|
|
436
|
+
if (!Array.isArray(config.checks)) throw new ConfigError('checks 必须是数组,例如 [{ "name": "test", "command": "npm test" }]');
|
|
437
|
+
const names = new Set<string>();
|
|
438
|
+
config.checks.forEach((c, i) => {
|
|
439
|
+
if (!isRecord(c) || typeof c.name !== "string" || c.name.trim() === "") throw new ConfigError(`checks[${i}].name 必须是非空字符串`);
|
|
440
|
+
if (names.has(c.name)) throw new ConfigError(`checks 中有两个检查都叫 "${c.name}",名称不能重复`);
|
|
441
|
+
names.add(c.name);
|
|
442
|
+
if (typeof c.command !== "string" || c.command.trim() === "") throw new ConfigError(`checks[${i}].command 必须是要运行的命令`);
|
|
443
|
+
if (c.cwd !== undefined && (typeof c.cwd !== "string" || path.isAbsolute(c.cwd) || c.cwd.split(/[\\/]/).includes(".."))) {
|
|
444
|
+
throw new ConfigError(`checks[${i}].cwd 必须是仓库之内的相对路径`);
|
|
445
|
+
}
|
|
446
|
+
if (c.paths !== undefined && (!Array.isArray(c.paths) || !c.paths.every((p) => typeof p === "string" && p.trim() !== ""))) {
|
|
447
|
+
throw new ConfigError(`checks[${i}].paths 必须是路径数组,例如 ["web/"]`);
|
|
448
|
+
}
|
|
449
|
+
if (c.timeoutMinutes !== undefined && (typeof c.timeoutMinutes !== "number" || c.timeoutMinutes <= 0)) {
|
|
450
|
+
throw new ConfigError(`checks[${i}].timeoutMinutes 必须是正数`);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
const gate = config.qualityGate;
|
|
454
|
+
if (!Number.isInteger(gate.fixAttempts) || gate.fixAttempts < 0) throw new ConfigError("qualityGate.fixAttempts 必须是不小于 0 的整数");
|
|
455
|
+
if (typeof gate.timeoutMinutes !== "number" || gate.timeoutMinutes <= 0) throw new ConfigError("qualityGate.timeoutMinutes 必须是正数");
|
|
456
|
+
if (!Array.isArray(config.slices.profiles) || !config.slices.profiles.every((p) => PROFILE_NAMES.includes(p))) {
|
|
457
|
+
throw new ConfigError('slices.profiles 只能包含 "quick" 和 "standard"(空数组表示不按切片实现)');
|
|
458
|
+
}
|
|
459
|
+
if (!Number.isInteger(config.slices.max) || config.slices.max < 1) throw new ConfigError("slices.max 必须是不小于 1 的整数");
|
|
460
|
+
const approval = config.designApproval;
|
|
461
|
+
if (!["off", "ui", "always"].includes(approval.mode)) throw new ConfigError('designApproval.mode 只能是 "off"、"ui" 或 "always"');
|
|
462
|
+
if (!Array.isArray(approval.profiles) || !approval.profiles.every((p) => PROFILE_NAMES.includes(p))) {
|
|
463
|
+
throw new ConfigError('designApproval.profiles 只能包含 "quick" 和 "standard"');
|
|
464
|
+
}
|
|
465
|
+
if (typeof approval.autoContinueHours !== "number" || approval.autoContinueHours < 0) {
|
|
466
|
+
throw new ConfigError("designApproval.autoContinueHours 必须是不小于 0 的数(0 表示一直等待确认)");
|
|
467
|
+
}
|
|
468
|
+
if (typeof config.reviewAxes.enabled !== "boolean") throw new ConfigError("reviewAxes.enabled 必须是 true 或 false");
|
|
469
|
+
if (!Array.isArray(config.reviewAxes.profiles) || !config.reviewAxes.profiles.every((p) => PROFILE_NAMES.includes(p))) {
|
|
470
|
+
throw new ConfigError('reviewAxes.profiles 只能包含 "quick" 和 "standard"');
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(config.relatedRepos)) throw new ConfigError('relatedRepos 必须是数组,例如 [{ "name": "后端", "path": "../backend" }]');
|
|
473
|
+
config.relatedRepos.forEach((r, i) => {
|
|
474
|
+
if (!isRecord(r) || typeof r.name !== "string" || r.name.trim() === "") throw new ConfigError(`relatedRepos[${i}].name 必须是非空字符串`);
|
|
475
|
+
if (typeof r.path !== "string" || r.path.trim() === "") throw new ConfigError(`relatedRepos[${i}].path 必须是这台机器上的目录`);
|
|
476
|
+
if (r.role !== undefined && typeof r.role !== "string") throw new ConfigError(`relatedRepos[${i}].role 必须是字符串`);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Where a related repository is on this machine; relative paths start from the project's checkout. */
|
|
481
|
+
export function resolveRelatedRepo(repo: RelatedRepo, projectDir: string): string {
|
|
482
|
+
if (repo.path.startsWith("~/") || repo.path.startsWith("~\\")) return path.join(os.homedir(), repo.path.slice(2));
|
|
483
|
+
return path.resolve(projectDir, repo.path);
|
|
484
|
+
}
|
|
485
|
+
|
|
347
486
|
function validate(config: PipelineConfig): void {
|
|
348
487
|
for (const role of ["builder", "reviewer"] as const) validateProvider(`agents.${role}`, config.agents[role]);
|
|
349
488
|
if (!PROFILE_NAMES.includes(config.defaultProfile)) {
|
|
@@ -396,6 +535,7 @@ function validate(config: PipelineConfig): void {
|
|
|
396
535
|
if (typeof config[key] !== "number" || config[key] < 0) throw new ConfigError(`${key} 必须是不小于 0 的数(0 表示关闭)`);
|
|
397
536
|
}
|
|
398
537
|
validatePhases(config.phases);
|
|
538
|
+
validateChecks(config);
|
|
399
539
|
for (const key of ["waitChunkSeconds", "permissionPollSeconds", "serviceReadySeconds", "queuePollSeconds"] as const) {
|
|
400
540
|
if (typeof config[key] !== "number" || config[key] <= 0) throw new ConfigError(`${key} 必须是正数`);
|
|
401
541
|
}
|
package/engine/src/control.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// is it?" or "stop for now". Output is plain language for relaying to the user.
|
|
3
3
|
|
|
4
4
|
import { describeActivity } from "./activity.js";
|
|
5
|
-
import { PHASE_MODE_LABELS, PROFILE_LABELS, type PipelineConfig } from "./config.js";
|
|
5
|
+
import { PHASE_MODE_LABELS, PROFILE_LABELS, type PipelineConfig, REQUIREMENT_TYPE_LABELS } from "./config.js";
|
|
6
6
|
import type { AgentRuntime, RuntimeFactory } from "./runtime.js";
|
|
7
7
|
import { isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
|
|
8
|
-
import {
|
|
8
|
+
import { stepTitle } from "./playbooks.js";
|
|
9
9
|
import { describeTotals, totals } from "./usage.js";
|
|
10
10
|
|
|
11
11
|
export function duration(fromIso: string, to: Date = new Date()): string {
|
|
@@ -42,8 +42,12 @@ function resultText(record: StepRecord): string {
|
|
|
42
42
|
return "完成";
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function stepName(record: StepRecord): string {
|
|
46
|
-
const
|
|
45
|
+
function stepName(record: StepRecord, state: Pick<PipelineState, "type" | "joint"> | null = null): string {
|
|
46
|
+
const [base, slug] = record.key.split("@");
|
|
47
|
+
const repo = slug ? (state?.joint ?? []).find((j) => j.slug === slug)?.name ?? slug : null;
|
|
48
|
+
const title = `${stepTitle(state?.type, record.stepId)}${repo ? `·${repo}` : ""}`;
|
|
49
|
+
const slice = /^implement-s(\d+)$/.exec(base);
|
|
50
|
+
if (slice) return `${title}(第 ${slice[1]} 片)`;
|
|
47
51
|
return record.round !== null ? `${title}(第 ${record.round} 轮)` : title;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -56,6 +60,8 @@ export function describeStatus(state: PipelineState | null, featureDir: string,
|
|
|
56
60
|
? "已中断(后台进程意外退出,可以继续)"
|
|
57
61
|
: state.status === "waiting_for_business"
|
|
58
62
|
? "已暂停,等待需求方决定"
|
|
63
|
+
: state.status === "waiting_for_approval"
|
|
64
|
+
? "已暂停,等待需求方确认方案(做完后会看到什么,见功能目录中的 preview.md)"
|
|
59
65
|
: state.status === "phase_done"
|
|
60
66
|
? "研发完成,待测试验证(提测说明见功能目录中的 handoff.md)"
|
|
61
67
|
: state.status === "failed"
|
|
@@ -65,7 +71,7 @@ export function describeStatus(state: PipelineState | null, featureDir: string,
|
|
|
65
71
|
: `已完成(${RESULT_TEXT[state.outcome ?? ""] ?? state.outcome ?? ""})`;
|
|
66
72
|
|
|
67
73
|
const lines = [
|
|
68
|
-
`需求:${featureDir}(档位:${PROFILE_LABELS[state.profile ?? "standard"]})`,
|
|
74
|
+
`需求:${featureDir}(档位:${PROFILE_LABELS[state.profile ?? "standard"]}${state.type ? `,类型:${REQUIREMENT_TYPE_LABELS[state.type]}` : ""})`,
|
|
69
75
|
`状态:${running && state.queuedSince ? `排队中(本机同时运行的流水线已达上限,自 ${new Date(state.queuedSince).toLocaleString("zh-CN", { hour12: false })} 起等待,轮到时自动开始)` : statusText}`,
|
|
70
76
|
...(state.phases && state.phases.mode !== "continuous"
|
|
71
77
|
? [`执行方式:${PHASE_MODE_LABELS[state.phases.mode]}(当前阶段:${state.phase === "verify" ? "测试验证" : "研发"}${state.handbacks ? `,测试已退回 ${state.handbacks} 次` : ""})`]
|
|
@@ -73,7 +79,8 @@ export function describeStatus(state: PipelineState | null, featureDir: string,
|
|
|
73
79
|
`开始时间:${new Date(state.startedAt).toLocaleString("zh-CN", { hour12: false })},距今 ${duration(state.startedAt, now)}`,
|
|
74
80
|
];
|
|
75
81
|
if (state.stopReason && !running) lines.push(`原因:${state.stopReason}`);
|
|
76
|
-
const
|
|
82
|
+
for (const j of state.joint ?? []) lines.push(`联合开发:${j.name} 在工作区 ${j.repo}(分支 ${j.branch})`);
|
|
83
|
+
const KIND_TEXT = { reminder: "提醒", "auto-resume": "自动继续", watchdog: "定时检查", "verify-start": "开始测试验证", "approval-continue": "未确认时按方案继续" } as const;
|
|
77
84
|
for (const s of state.schedules ?? []) {
|
|
78
85
|
if (s.kind === "watchdog" && !running) continue;
|
|
79
86
|
const when = s.at ? `${new Date(s.at).toLocaleString("zh-CN", { hour12: false, month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" })} ` : "";
|
|
@@ -83,20 +90,29 @@ export function describeStatus(state: PipelineState | null, featureDir: string,
|
|
|
83
90
|
const records = state.history.filter((h) => h.attempt === state.attempt);
|
|
84
91
|
const current = records.find((h) => h.outcome === "running");
|
|
85
92
|
if (current && running) {
|
|
86
|
-
lines.push(`当前步骤:${stepName(current)},已进行 ${duration(current.startedAt, now)}`);
|
|
93
|
+
lines.push(`当前步骤:${stepName(current, state)},已进行 ${duration(current.startedAt, now)}`);
|
|
87
94
|
}
|
|
88
95
|
const done = records.filter((h) => h.outcome === "completed");
|
|
89
96
|
if (done.length > 0) {
|
|
90
97
|
lines.push("已完成的步骤:");
|
|
91
98
|
done.forEach((h, i) => {
|
|
92
99
|
const took = h.endedAt ? duration(h.startedAt, new Date(h.endedAt)) : "";
|
|
93
|
-
lines.push(` ${i + 1}. ${stepName(h)}:${resultText(h)}${took ? `(用时 ${took})` : ""}`);
|
|
100
|
+
lines.push(` ${i + 1}. ${stepName(h, state)}:${resultText(h)}${took ? `(用时 ${took})` : ""}`);
|
|
94
101
|
});
|
|
95
102
|
} else if (!current) {
|
|
96
103
|
lines.push("还没有完成的步骤。");
|
|
97
104
|
}
|
|
98
105
|
const failed = records.filter((h) => h.outcome === "failed").at(-1);
|
|
99
|
-
if (failed && !running) lines.push(`中断在:${stepName(failed)}(${failed.error ?? "原因未知"})`);
|
|
106
|
+
if (failed && !running) lines.push(`中断在:${stepName(failed, state)}(${failed.error ?? "原因未知"})`);
|
|
107
|
+
const gateRuns = (state.gate?.runs ?? []).filter((r) => r.attempt === state.attempt);
|
|
108
|
+
const lastGate = gateRuns.at(-1);
|
|
109
|
+
if (lastGate) {
|
|
110
|
+
lines.push(
|
|
111
|
+
lastGate.passed
|
|
112
|
+
? `质量门(流水线自己运行的检查):最近一次通过(${lastGate.key} 之后)`
|
|
113
|
+
: `质量门(流水线自己运行的检查):${lastGate.newFailures.join("、")} 在 ${lastGate.key} 之后仍失败,已交给代码评审和报告`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
100
116
|
if (records.length > 0) lines.push(`用时与用量:${describeTotals(totals(records, now))}`);
|
|
101
117
|
if (state.attempt > 1) lines.push(`这个需求的全部 ${state.attempt} 次尝试合计:${describeTotals(totals(state.history, now))}`);
|
|
102
118
|
if (state.phases && state.phases.mode !== "continuous") {
|
|
@@ -196,7 +212,7 @@ export async function stopPipeline(input: StopInput): Promise<{ stopped: boolean
|
|
|
196
212
|
}
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
const where = interrupted.length > 0 ? `,停在「${stepName(interrupted[0])}」` : "";
|
|
215
|
+
const where = interrupted.length > 0 ? `,停在「${stepName(interrupted[0], latest)}」` : "";
|
|
200
216
|
return {
|
|
201
217
|
stopped: true,
|
|
202
218
|
message: wasAlive
|
|
@@ -224,7 +240,7 @@ export async function describeLiveActivity(
|
|
|
224
240
|
new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs).unref()),
|
|
225
241
|
]);
|
|
226
242
|
if (!items || items.length === 0) continue;
|
|
227
|
-
lines.push(`「${stepName(record)}」的 agent 最近在做:`, ...describeActivity(items, now));
|
|
243
|
+
lines.push(`「${stepName(record, state)}」的 agent 最近在做:`, ...describeActivity(items, now));
|
|
228
244
|
}
|
|
229
245
|
return lines;
|
|
230
246
|
}
|
package/engine/src/digest.ts
CHANGED
|
@@ -10,6 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
12
12
|
import { type FeatureSummary, listFeatures } from "./features.js";
|
|
13
|
+
import { handledCategories, type RecurringLesson, recurringLessons } from "./lessons.js";
|
|
13
14
|
import { defaultConfigPath, KIT_ROOT, kitVersion } from "./locations.js";
|
|
14
15
|
import { dispatchNotifiers } from "./notifiers.js";
|
|
15
16
|
import { resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
@@ -18,7 +19,7 @@ import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
|
18
19
|
const GROUPS: Array<{ title: string; match: (stage: string) => boolean }> = [
|
|
19
20
|
{
|
|
20
21
|
title: "需要处理",
|
|
21
|
-
match: (s) => s.startsWith("等待需求方决定") || s.startsWith("已中断") || s.startsWith("验收未通过") || s.startsWith("研发完成,待测试验证"),
|
|
22
|
+
match: (s) => s.startsWith("等待需求方决定") || s.startsWith("等待确认方案") || s.startsWith("已中断") || s.startsWith("验收未通过") || s.startsWith("研发完成,待测试验证"),
|
|
22
23
|
},
|
|
23
24
|
{ title: "待验收", match: (s) => s.startsWith("待验收") || s.startsWith("开发结束") },
|
|
24
25
|
{ title: "开发中", match: (s) => s === "开发中" },
|
|
@@ -26,7 +27,7 @@ const GROUPS: Array<{ title: string; match: (stage: string) => boolean }> = [
|
|
|
26
27
|
{ title: "已验收", match: (s) => s === "已验收" },
|
|
27
28
|
];
|
|
28
29
|
|
|
29
|
-
export function digestText(project: string, features: FeatureSummary[], now = new Date(), days = 7): string {
|
|
30
|
+
export function digestText(project: string, features: FeatureSummary[], now = new Date(), days = 7, lessons: RecurringLesson[] = []): string {
|
|
30
31
|
const since = now.getTime() - days * 24 * 3600_000;
|
|
31
32
|
const recent = features.filter((f) => new Date(f.updatedAt).getTime() >= since);
|
|
32
33
|
const lines = [`【req-kit 需求总览】${project}(${now.toLocaleDateString("zh-CN")})`];
|
|
@@ -41,6 +42,12 @@ export function digestText(project: string, features: FeatureSummary[], now = ne
|
|
|
41
42
|
lines.push(`- ${f.title}(${f.name}):${f.stage}${f.branch ? `,分支 ${f.branch}` : ""}${f.usage ? `;${f.usage}` : ""}`);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
if (lessons.length > 0) {
|
|
46
|
+
lines.push("", "反复出现的问题(建议变成项目的规则,可以运行 /req-retro):");
|
|
47
|
+
for (const l of lessons.slice(0, 5)) {
|
|
48
|
+
lines.push(`- ${l.category}:${l.features.length} 个需求(${l.features.join("、")})${l.rules.length > 0 ? `;建议:${l.rules[0]}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
44
51
|
return lines.join("\n");
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -89,7 +96,8 @@ async function main(): Promise<number> {
|
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
const features = listFeatures(repo);
|
|
92
|
-
const
|
|
99
|
+
const lessons = recurringLessons(features, handledCategories(repo));
|
|
100
|
+
const text = digestText(path.basename(repo), features, new Date(), Number(values.days) || 7, lessons);
|
|
93
101
|
console.log(text);
|
|
94
102
|
if (!values.notify) return 0;
|
|
95
103
|
if (config.notifiers.length === 0) {
|
|
@@ -112,6 +120,7 @@ async function main(): Promise<number> {
|
|
|
112
120
|
text,
|
|
113
121
|
details: {
|
|
114
122
|
features: features.map((f) => ({ name: f.name, title: f.title, stage: f.stage, branch: f.branch, updatedAt: f.updatedAt, usage: f.usage })),
|
|
123
|
+
recurringLessons: lessons,
|
|
115
124
|
},
|
|
116
125
|
},
|
|
117
126
|
(message) => {
|
package/engine/src/doctor.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type PaseoJson, paseoJsonIssues } from "./paseo-json.js";
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { parseArgs } from "node:util";
|
|
13
|
-
import { ConfigError, DEFAULT_CONFIG, loadConfig, type PipelineConfig, PROJECT_CONFIG_FILE } from "./config.js";
|
|
13
|
+
import { ConfigError, DEFAULT_CONFIG, loadConfig, type PipelineConfig, PROJECT_CONFIG_FILE, resolveRelatedRepo } from "./config.js";
|
|
14
14
|
import { checkProfile, connect, type Report, satisfies } from "./daemon-check.js";
|
|
15
15
|
import { daemonVersion } from "./daemon-version.js";
|
|
16
16
|
import { gitInfo } from "./git.js";
|
|
@@ -121,6 +121,26 @@ function configuration(configPath: string, repo: string | null): PipelineConfig
|
|
|
121
121
|
if (proposals.enabled) {
|
|
122
122
|
report("ok", `方案提案:${proposals.members.length || "builder 和 reviewer 各"} ${proposals.members.length ? "位成员" : "一份"},用于 ${proposals.profiles.join("、")}档`);
|
|
123
123
|
}
|
|
124
|
+
if (repo) {
|
|
125
|
+
const { checks, relatedRepos } = loaded.config;
|
|
126
|
+
if (checks.length > 0) {
|
|
127
|
+
report("ok", `质量门:${checks.length} 项检查(${checks.map((c) => c.name).join("、")}),流水线在写代码和修复的步骤之后自己运行`);
|
|
128
|
+
for (const c of checks) {
|
|
129
|
+
if (c.cwd && !fs.existsSync(path.join(repo, c.cwd))) report("warn", `检查「${c.name}」的目录 ${c.cwd} 不存在`, `修改 ${PROJECT_CONFIG_FILE} 中的 checks`);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
report(
|
|
133
|
+
"warn",
|
|
134
|
+
"没有配置质量门的检查(checks):流水线不会自己运行 lint、测试等检查,只能依靠各步骤自己的记录",
|
|
135
|
+
"在项目根目录的对话中运行 /req-setup,它会把实际跑通的检查命令写进项目配置",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
for (const r of relatedRepos) {
|
|
139
|
+
const dir = resolveRelatedRepo(r, repo);
|
|
140
|
+
if (fs.existsSync(dir)) report("ok", `关联仓库(只读参考):${r.name} → ${dir}`);
|
|
141
|
+
else report("warn", `关联仓库「${r.name}」不在这台机器上:${dir}`, "把它克隆到这个位置;或修改 relatedRepos 中的路径(相对项目目录,团队约定把相关仓库放在同一个上级目录最方便)");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
124
144
|
const required = loaded.config.requiredVersion;
|
|
125
145
|
if (required) {
|
|
126
146
|
if (satisfies(kitVersion(), required)) report("ok", `版本要求 ${required}:当前 ${kitVersion()} 满足`);
|
|
@@ -349,6 +369,10 @@ function project(repo: string): void {
|
|
|
349
369
|
paseoJson(repo, git.isRepo);
|
|
350
370
|
if (!fs.existsSync(path.join(repo, "GLOSSARY.md")) && fs.existsSync(path.join(repo, "CONTEXT.md"))) {
|
|
351
371
|
report("ok", "术语表为旧名 CONTEXT.md:会继续读取,新术语写入 GLOSSARY.md");
|
|
372
|
+
} else if (fs.existsSync(path.join(repo, "GLOSSARY.md"))) {
|
|
373
|
+
report("ok", "术语表:GLOSSARY.md");
|
|
374
|
+
} else {
|
|
375
|
+
report("warn", "没有术语表 GLOSSARY.md:需求方的说法和代码里的叫法对不上时容易做偏", "在项目根目录的对话中运行 /req-setup,它会整理主要的业务词和代码中的名字");
|
|
352
376
|
}
|
|
353
377
|
|
|
354
378
|
const featuresDir = path.join(repo, "docs", "features");
|
|
@@ -380,6 +404,9 @@ function project(repo: string): void {
|
|
|
380
404
|
if (raw.pid && isAlive(raw.pid)) report("ok", `${label}:运行中(进程 ${raw.pid})${migrate}`);
|
|
381
405
|
else report("warn", `${label}:状态为运行中,但进程已不在${migrate}`, `在该需求的对话中对 /req 说"继续开发",或执行 start-pipeline --feature ${label} --resume`);
|
|
382
406
|
break;
|
|
407
|
+
case "waiting_for_approval":
|
|
408
|
+
report("warn", `${label}:方案等待需求方确认${migrate}`, "在该需求的对话中看 preview.md 并确认(或修改需求)");
|
|
409
|
+
break;
|
|
383
410
|
case "waiting_for_business":
|
|
384
411
|
report("warn", `${label}:等待需求方决定${migrate}`, "在该需求的对话中回答问题、更新 spec 后继续");
|
|
385
412
|
break;
|