paseo-req-kit 1.14.0-beta.2 → 1.14.0-beta.4
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 +25 -0
- package/bin/req.mjs +16 -0
- package/contracts/cli-json.md +1 -1
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +15 -0
- package/engine/prompts/06-code-review.md +12 -1
- package/engine/prompts/incremental.md +2 -0
- package/engine/src/api.ts +18 -2
- package/engine/src/checklist-cmd.ts +37 -0
- package/engine/src/checklist.ts +254 -0
- package/engine/src/cli-args.ts +4 -1
- package/engine/src/code-review.ts +419 -0
- package/engine/src/codeowners.ts +87 -0
- package/engine/src/main.ts +13 -3
- package/engine/src/preflight.ts +10 -1
- package/engine/src/reply.ts +7 -4
- package/engine/src/review-assets/check.css +57 -0
- package/engine/src/review-assets/check.html +35 -0
- package/engine/src/review-assets/check.js +258 -0
- package/engine/src/review-assets/code.css +93 -0
- package/engine/src/review-assets/code.html +43 -0
- package/engine/src/review-assets/code.js +402 -0
- package/engine/src/review.ts +177 -2
- package/engine/src/try.ts +134 -0
- package/package.json +1 -1
- package/paseo-plugin/client/data.ts +3 -3
- package/paseo-plugin/client/detail.tsx +10 -2
- package/paseo-plugin/client/nav.ts +10 -0
- package/paseo-plugin/client/review.tsx +30 -6
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +2 -2
- package/paseo-plugin/shared/contracts.ts +4 -1
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.gI1JF182.js → app.Cp7WwPWE.js} +1 -1
- package/site-dist/assets/changelog.md.Zi-_3Rjh.js +1 -0
- package/site-dist/assets/chunks/@localSearchIndexroot.COxp5Rrc.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.DNmhkkfS.js → VPLocalSearchBox.CMTXQK-Q.js} +1 -1
- package/site-dist/assets/chunks/{theme.CZzkCOAC.js → theme.DvVUfKr3.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.zmtXv15G.js → guide_getting-started.md.D8XlIQOg.js} +1 -1
- package/site-dist/assets/{guide_user.md.2v7b_fzd.js → guide_user.md.B4N9ywmr.js} +1 -1
- package/site-dist/assets/{guide_user.md.2v7b_fzd.lean.js → guide_user.md.B4N9ywmr.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.js → reference_contracts_cli-json.md.CCvKLasM.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.lean.js → reference_contracts_cli-json.md.CCvKLasM.lean.js} +1 -1
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +4 -4
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +5 -5
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +4 -4
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +4 -4
- package/site-dist/reference/contracts/state.html +4 -4
- package/site-dist/reference/contracts/usage-record.html +4 -4
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/skills/req/SKILL.md +3 -0
- package/skills/req/playbooks/accept.md +2 -2
- package/site-dist/assets/changelog.md.2xf690VW.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.CiFeI6qC.js +0 -1
- /package/site-dist/assets/{changelog.md.2xf690VW.lean.js → changelog.md.Zi-_3Rjh.lean.js} +0 -0
- /package/site-dist/assets/{guide_getting-started.md.zmtXv15G.lean.js → guide_getting-started.md.D8XlIQOg.lean.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.14.0-beta.4
|
|
6
|
+
|
|
7
|
+
### 使用者会看到的变化
|
|
8
|
+
|
|
9
|
+
- **代码审查**(插件需求页面上的"代码审查",`req code-review <需求>`):列出需求分支相对起点的全部改动(需求文档放最后、折叠),AI 代码评审的意见放在对应的代码行旁边,带级别和修复步骤的处理结果;评审人可以在任意一行写意见、把 AI 意见标为"要改 / 不用改"、写总体意见。
|
|
10
|
+
- **审核人默认取自 `CODEOWNERS`**(支持 GitLab / GitHub 格式和 GitLab 的分节),按改动的文件给出,提交前可以修改,以后建合并请求时使用。
|
|
11
|
+
- **退回修改**:意见写进 `code-review.md` 的"人工评审"一节(HR-1 起)和 `changes.md` 并提交,流水线开始新的一次尝试(`req review-fix`),需求说明不变、方案确认不用重来,从方案设计开始只处理这些意见,之后照常经过 AI 代码评审和验证。**评审通过**:记下评审人、审核人和评审的提交;之后又有新提交时提示需要重新评审。
|
|
12
|
+
- AI 代码评审的结果文件新增 `findings`(每条意见的级别、文件、行号、问题、修复方向);没有它的旧需求从 `code-review.md` 的 CR 条目中读取。
|
|
13
|
+
|
|
14
|
+
### 升级须知
|
|
15
|
+
|
|
16
|
+
- 合并请求还没有接入:等 shenbing 提供类似 `gh pr create / gh pr merge` 的命令后,用评审通过时记下的审核人建合并请求。
|
|
17
|
+
- `req api status` 新增 `humanReview`;`req api review-url` 的 `--page` 新增 `code`;启动参数新增 `--review-fix`(`req review-fix <需求>`)。
|
|
18
|
+
|
|
19
|
+
## 1.14.0-beta.3
|
|
20
|
+
|
|
21
|
+
### 使用者会看到的变化
|
|
22
|
+
|
|
23
|
+
- **验收清单**(`req checklist <需求>`,插件需求页面上的"验收清单(可一键试用)"):按需求说明的验收标准生成清单,每条写着前置条件、操作、预期结果和流水线的验证结果,没验证或没通过的排在前面,报告里"需要你确认的决定"也列上。逐条点"通过 / 不对 / 先不验",进度自动保存;提交后写成 `acceptance.md` 并提交,不通过时把不对的地方转给需求的对话。
|
|
24
|
+
- **一键试用**(`req try <需求>`,验收清单页右上角"把它跑起来试用"):用项目的 Paseo 服务脚本在需求自己的工作区里启动,给出访问地址和提测说明里的测试账号、数据;`--stop` 或"停止试用"停掉。没有服务脚本的项目给出提测说明里的启动方法。
|
|
25
|
+
|
|
26
|
+
### 升级须知
|
|
27
|
+
|
|
28
|
+
- `req api review-url` 新增 `--page check`;`req reply` 新增 `--acceptance`(验收页用)。验收页和原型审查页是同一个本机服务。
|
|
29
|
+
|
|
5
30
|
## 1.14.0-beta.2
|
|
6
31
|
|
|
7
32
|
### 使用者会看到的变化
|
package/bin/req.mjs
CHANGED
|
@@ -31,6 +31,10 @@ const HELP = `req-kit ${version}(${kitRoot})
|
|
|
31
31
|
让 Paseo 按时间表发送需求总览(例子为每周一 9:00);取消:req digest --cancel <ID>
|
|
32
32
|
req sync <需求> 把需求同步到对接的外部系统(补发失败的调用、补建卡片、更新状态)
|
|
33
33
|
req review <需求> 在浏览器打开原型审查页:点选要改的地方、直接改文字,提交意见或确认方案
|
|
34
|
+
req code-review <需求> 代码审查页:看这次的改动和 AI 评审意见,在代码行上写意见,退回修改或评审通过
|
|
35
|
+
req review-fix <需求> 按代码审查页上退回的意见开始修改(插件里提交"退回修改"时自动执行)
|
|
36
|
+
req try <需求> [--stop] 把做好的需求跑起来试用:给出访问地址和测试数据(--stop 停止)
|
|
37
|
+
req checklist <需求> 验收清单:按验收标准逐条勾选,提交后写成验收记录
|
|
34
38
|
req ui-check <网址> [--prototype <原型文件>] [--width 1280,390]
|
|
35
39
|
不靠看图检查网页:显示不全、遮挡、超出屏幕、文字太淡、报错,和原型对文字
|
|
36
40
|
req reply <需求> <回复> 把回答或方案意见转给需求的对话(插件的"等你处理"用它)
|
|
@@ -166,6 +170,12 @@ switch (command) {
|
|
|
166
170
|
case "approve":
|
|
167
171
|
code = node([launcher, "--approve", ...feature(rest, true)]);
|
|
168
172
|
break;
|
|
173
|
+
case "review-fix":
|
|
174
|
+
code = node([launcher, "--review-fix", ...feature(rest, true)]);
|
|
175
|
+
break;
|
|
176
|
+
case "code-review":
|
|
177
|
+
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "review.ts"), "--page", "code", ...rest]) : 1;
|
|
178
|
+
break;
|
|
169
179
|
case "start":
|
|
170
180
|
// From a card in the requirement management system (1.10): no chat needed to begin.
|
|
171
181
|
if (rest.some((a) => a === "--from" || a.startsWith("--from="))) code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "card-cmd.ts"), "start", ...rest]) : 1;
|
|
@@ -221,6 +231,12 @@ switch (command) {
|
|
|
221
231
|
case "review":
|
|
222
232
|
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "review.ts"), ...rest]) : 1;
|
|
223
233
|
break;
|
|
234
|
+
case "try":
|
|
235
|
+
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "try.ts"), ...rest]) : 1;
|
|
236
|
+
break;
|
|
237
|
+
case "checklist":
|
|
238
|
+
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "checklist-cmd.ts"), ...rest]) : 1;
|
|
239
|
+
break;
|
|
224
240
|
case "ui-check":
|
|
225
241
|
code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "ui-check.ts"), ...rest]) : 1;
|
|
226
242
|
break;
|
package/contracts/cli-json.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
| `req api config [--repo <目录>] [--machine]` | 1.12 起。`{ sources, ignored, error, repo, groups }`:`sources` 是生效的配置文件(`layer` 为 `machine` / `team` / `project`,按叠加顺序);`groups` 按主题分组,每项 `{ key, label, description, display, value, setBy, scope, changed }`:`display` 是用大白话写的值,`value` 中看起来像密钥的值和个人账号替换为 `••••`,`setBy` 是写了它的文件,`changed` 表示与默认值不同。配置有错误时 `error` 写明原因,值按各文件所写。`--machine` 不看项目配置 |
|
|
33
33
|
| `req api set-agents --json <设置>` | 1.12 起。`{ ok, message }`。`<设置>` 是 `{ builder, reviewer, fallback }`(`fallback` 可以为 null),检查后写入本机配置的 `agents`,原文件备份 |
|
|
34
34
|
| `req api watchdog [--by plugin\|factory] [--dry-run]` | 1.13 起。`{ checked, resumed, failed, skipped }`:把本机"运行中但后台进程已不在"的需求直接继续(`req resume`)。每个进程只继续一次,每个需求 24 小时内最多 3 次;`--by plugin` 同时记下插件在检查(流水线据此不再预约对话的定时检查) |
|
|
35
|
-
| `req api review-url <需求> [--repo <目录>] [--read-only]` | 1.13
|
|
35
|
+
| `req api review-url <需求> [--repo <目录>] [--read-only] [--page check]` | 1.13 起;`--page check`(1.14)给出同一服务上的验收清单页地址(需要 `spec.md`),`--page code`(1.14)给出代码审查页地址(需要已经写过代码)。`{ url, readOnly }`:这个需求的原型审查页地址(需要时在后台启动审查页的服务)。`--read-only` 的地址只能看,不能提交意见或确认方案。需求不在本机的需求索引中或还没有 `prototype/` 时报 `not_found` |
|
|
36
36
|
|
|
37
37
|
`<需求>` 的写法与 `req status` 相同:功能目录、目录名或名称中能唯一确定的一部分。需求在为它新建的工作区中开发时,`--repo` 写对话所在的目录或那个工作区都可以。
|
|
38
38
|
|
|
@@ -97,6 +97,21 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
|
|
|
97
97
|
|
|
98
98
|
然后说"我要验收"。AI 会按验收标准一条一条带你核对:告诉你怎么操作、应该看到什么;能用命令验证的给出命令,你可以自己执行。每条结果记入功能目录的 `acceptance.md`。
|
|
99
99
|
|
|
100
|
+
**代码审查(1.14 起,给开发同事和评审人)。** 需求页面上点**"代码审查"**(终端里是 `req code-review <需求>`),代码审查页在 Paseo 里打开:
|
|
101
|
+
|
|
102
|
+
- 列出这次改动的全部文件(相对需求分支的起点,需求文档排在最后、默认折叠),上面是提测说明里"这次改了什么"。
|
|
103
|
+
- AI 代码评审的每条意见放在它说的那一行旁边,标着级别(阻塞 / 重要 / 建议)和修复步骤的处理结果(已修复、判为不成立、决定不修复);可以标**要改**或**不用改**,补一句话。
|
|
104
|
+
- 点任意一行代码写意见;最下面可以写总体意见。
|
|
105
|
+
- **审核人**默认取自仓库的 `CODEOWNERS`(按改动的文件),提交前可以改;以后建合并请求时会用它。
|
|
106
|
+
- **退回修改**:意见写进 `code-review.md` 的"人工评审"一节(HR-1 起)和 `changes.md`,流水线随即开始新的一次尝试,需求说明不变,从方案设计开始只处理这些意见,改完再经过 AI 代码评审和验证,进展发回需求的对话。**评审通过**:记下评审人和审核人;之后代码又有改动时,页面会提示需要重新评审。
|
|
107
|
+
|
|
108
|
+
**在插件里自己验收(1.14 起)。** 需求页面上点**"验收清单(可一键试用)"**(终端里是 `req checklist <需求>`),验收清单在 Paseo 里打开:
|
|
109
|
+
|
|
110
|
+
- 每条验收标准写着前置条件、怎么操作、应该看到什么,以及流水线的验证结果;流水线没验证或没通过的排在最前面,报告里"需要你确认的决定"也列在后面请你表态。
|
|
111
|
+
- 右上角**"把它跑起来试用"**(终端里是 `req try <需求>`):用项目的 Paseo 服务脚本(`paseo.json`)在这个需求自己的工作区里把它启动起来,给出访问地址和提测说明里的测试账号、数据;用完点"停止试用"。项目没有服务脚本时,会给出提测说明里写的启动方法,也可以在需求的对话里说"帮我把它跑起来"。
|
|
112
|
+
- 每条点"通过"、"不对"(写下看到了什么)或"先不验",进度自动保存。必须验的都核对完才能提交"通过";有"不对"的就提交"不通过"。
|
|
113
|
+
- 提交后写成 `acceptance.md` 并提交。不通过时,不对的地方会转给这个需求的对话,它会和你一起确定怎么处理。
|
|
114
|
+
|
|
100
115
|
- **全部通过**:请开发同事审阅代码并合并上线。流水线只做本地提交,不会自己合并或上线。
|
|
101
116
|
- **有不通过的**:逐项决定怎么处理:做得不对的,补进需求说明后继续开发;发现想要的不是这样的,按改需求处理;也可以接受现状并记下理由。
|
|
102
117
|
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
6. **评审意见**:按级别排列
|
|
50
50
|
7. **处理记录**:留空,由下一步填写
|
|
51
51
|
|
|
52
|
+
## 人工评审意见
|
|
53
|
+
|
|
54
|
+
`code-review.md` 中有"人工评审"一节、列出 HR-1、HR-2… 时,那是评审人在插件的代码审查页上提出的意见,这一次尝试就是为了处理它们。逐条核对是否已经解决:没有解决的按"重要"重新提出(在 CR 意见里注明对应的 HR 编号);评审人的意见与 spec 冲突时,按 spec 判断,并写明冲突、记入 `decisions.tsv`(`needs_human` 写 `yes`)。
|
|
55
|
+
|
|
52
56
|
## 第 2 轮及以后
|
|
53
57
|
|
|
54
58
|
上一轮的意见已经由修复步骤处理,处理记录在 `code-review.md` 中。这一轮:
|
|
@@ -60,11 +64,18 @@
|
|
|
60
64
|
## 结果文件
|
|
61
65
|
|
|
62
66
|
```json
|
|
63
|
-
{
|
|
67
|
+
{
|
|
68
|
+
"verdict": "changes_required",
|
|
69
|
+
"findings": [
|
|
70
|
+
{ "id": "CR-1", "severity": "阻塞", "file": "src/order/export.ts", "line": 42, "title": "导出没有校验权限,普通用户也能导出全部订单", "suggestion": "调用 checkPermission('order:export')" }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
64
73
|
```
|
|
65
74
|
|
|
66
75
|
- `verdict`,二选一:
|
|
67
76
|
- `pass`:没有尚未解决的"阻塞"或"重要"级别意见。
|
|
68
77
|
- `changes_required`:存在尚未解决的"阻塞"或"重要"级别意见,接下来会执行评审问题修复。
|
|
69
78
|
|
|
79
|
+
- `findings`:这一轮提出的每条意见(和 `code-review.md` 中的 CR 编号一致),给 Paseo 插件的代码审查页使用:人在改动的代码旁边看到它们,决定要不要改。`severity` 写"阻塞"、"重要"、"建议"或"待核实";`file` 是相对仓库根目录的路径,`line` 是改动后文件中的行号(不确定就省略);`title` 一句话说清问题,`suggestion` 一句话说修复方向。没有意见时写空数组。
|
|
80
|
+
|
|
70
81
|
代码改动不存在,或者无法确定评审范围时,选择 `changes_required` 并在结论里说明情况。
|
|
@@ -20,3 +20,5 @@
|
|
|
20
20
|
- **完成报告**:在开头说明这是第几次尝试、因为什么变化,验收对照表中标出哪些 AC 是这次新增或修改的。
|
|
21
21
|
|
|
22
22
|
上一次没有写过代码时(`<pipeline-context>` 中"上一次是否已经写过代码:否"),实现类步骤按正常方式进行,只是方案设计要在已有的 `design.md` 基础上修订。
|
|
23
|
+
|
|
24
|
+
**人工代码评审退回的**(`changes.md` 最后一节的来源是"人工代码评审"):这次要处理的是 `code-review.md` 中最后一个"人工评审"小节里的 HR-1、HR-2…意见,需求说明没有变。方案只写针对这些意见的改法;实现逐条处理,并在 `implementation.md` 中逐条写明每个 HR 怎么改的(或为什么不改,记入 `decisions.tsv`,`needs_human` 写 `yes`);不要借机做意见之外的改动。
|
package/engine/src/api.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { defaultConfigPath, KIT_ROOT, kitVersion } from "./locations.js";
|
|
|
27
27
|
import { configView } from "./config-view.js";
|
|
28
28
|
import { pruneIndex, readIndex, registerRequirement, requirementKey } from "./records.js";
|
|
29
29
|
import { listWorktreeRecords } from "./worktree.js";
|
|
30
|
+
import { readHumanReview } from "./code-review.js";
|
|
30
31
|
import { isAlive, type PipelineState, readState, statePath, type StepRecord } from "./state.js";
|
|
31
32
|
import { parsePeriod, summarizeRuns } from "./stats.js";
|
|
32
33
|
import { readRuns, runsFile } from "./records.js";
|
|
@@ -376,12 +377,21 @@ export function apiStatus(repoArg: string, feature: string) {
|
|
|
376
377
|
steps: state ? state.history.map((h) => stepJson(h, state)) : [],
|
|
377
378
|
// 1.13: what to expect, from this machine's completed requirements of the same profile.
|
|
378
379
|
estimate: estimateOf(state),
|
|
380
|
+
// 1.14: the person's code review (plugin's code review page).
|
|
381
|
+
humanReview: humanReviewOf(repo, featureDir),
|
|
379
382
|
gate: state?.gate ?? null,
|
|
380
383
|
external: Object.entries(state?.external ?? {}).map(([name, r]) => ({ name, id: r.id, url: r.url, status: r.status, syncedAt: r.syncedAt, pending: r.pending.length, lastError: r.pending.at(-1)?.error ?? null })),
|
|
381
384
|
joint: (state?.joint ?? []).map((j) => ({ name: j.name, slug: j.slug, repo: j.repo, sourceRepo: j.sourceRepo, branch: j.branch, workspaceId: j.workspaceId, gate: j.gate ?? null })),
|
|
382
385
|
};
|
|
383
386
|
}
|
|
384
387
|
|
|
388
|
+
function humanReviewOf(repo: string, featureDir: string) {
|
|
389
|
+
const r = readHumanReview(path.join(repo, featureDir));
|
|
390
|
+
if (!r) return null;
|
|
391
|
+
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", windowsHide: true }).stdout?.trim() || null;
|
|
392
|
+
return { status: r.status, reviewer: r.reviewer, at: r.at, reviewers: r.reviewers, items: r.items.length, stale: r.status === "approved" && !!head && r.head !== head, consumedAttempt: r.consumedAttempt };
|
|
393
|
+
}
|
|
394
|
+
|
|
385
395
|
function estimateOf(state: PipelineState | null) {
|
|
386
396
|
if (!state || state.status === "completed") return null;
|
|
387
397
|
const t = typical(state.profile ?? "standard");
|
|
@@ -579,12 +589,18 @@ export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
|
|
|
579
589
|
// 1.13: the prototype review page of a requirement (starts the page's server when needed).
|
|
580
590
|
const feature = need("需求");
|
|
581
591
|
const readOnly = flag(argv, "--read-only");
|
|
592
|
+
// 1.14 --page check: the acceptance checklist page of the same server.
|
|
593
|
+
const pageArg = option(argv, "--page");
|
|
594
|
+
const page = pageArg === "check" ? "check" : pageArg === "code" ? "code" : "prototype";
|
|
582
595
|
const { repo: r, featureDir } = locate(repo, feature);
|
|
583
596
|
return import("./review.js").then(async ({ ensureReviewServer, keyOf, reviewUrl }) => {
|
|
584
597
|
const key = keyOf(r, featureDir);
|
|
585
598
|
if (!key) throw new ApiError("not_found", `${featureDir} 不在本机的需求列表中`);
|
|
586
|
-
if (!fs.existsSync(path.join(r, featureDir, "prototype"))) throw new ApiError("not_found", `${featureDir} 还没有界面原型`);
|
|
587
|
-
|
|
599
|
+
if (page === "prototype" && !fs.existsSync(path.join(r, featureDir, "prototype"))) throw new ApiError("not_found", `${featureDir} 还没有界面原型`);
|
|
600
|
+
if (page === "check" && !fs.existsSync(path.join(r, featureDir, "spec.md"))) throw new ApiError("not_found", `${featureDir} 中没有 spec.md`);
|
|
601
|
+
if (page === "code" && !["implementation.md", "code-review.md"].some((f) => fs.existsSync(path.join(r, featureDir, f)))) throw new ApiError("not_found", `${featureDir} 还没有写代码,没有可以审查的改动`);
|
|
602
|
+
const url = reviewUrl(await ensureReviewServer(), key, readOnly);
|
|
603
|
+
return { url: page === "check" ? `${url}check/` : page === "code" ? `${url}code/` : url, readOnly };
|
|
588
604
|
});
|
|
589
605
|
}
|
|
590
606
|
default:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// `req checklist <需求> [--no-open]` (1.14): the acceptance checklist in words, and the page where
|
|
2
|
+
// the requester ticks it (served by the review page's server).
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { buildChecklist, checklistText } from "./checklist.js";
|
|
6
|
+
|
|
7
|
+
async function main(argv: string[]): Promise<number> {
|
|
8
|
+
const feature = argv.find((a) => !a.startsWith("--"));
|
|
9
|
+
if (!feature || argv.includes("--help") || argv.includes("-h")) {
|
|
10
|
+
console.log("用法:req checklist <需求> [--no-open] [--text]\n\n按需求说明的验收标准生成验收清单(流水线没验证、没通过的排在前面),在浏览器里逐条勾选,提交后写成 acceptance.md。--text 只在终端里列出清单。");
|
|
11
|
+
return feature ? 0 : 2;
|
|
12
|
+
}
|
|
13
|
+
const { locate } = await import("./api.js");
|
|
14
|
+
const { repo, featureDir } = locate(process.cwd(), feature);
|
|
15
|
+
if (!fs.existsSync(path.join(repo, featureDir, "spec.md"))) {
|
|
16
|
+
console.error(`${featureDir} 中没有 spec.md`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const list = buildChecklist(path.join(repo, featureDir));
|
|
20
|
+
let url: string | null = null;
|
|
21
|
+
if (!argv.includes("--text")) {
|
|
22
|
+
const { ensureReviewServer, keyOf, reviewUrl } = await import("./review.js");
|
|
23
|
+
const key = keyOf(repo, featureDir);
|
|
24
|
+
if (key) url = `${reviewUrl(await ensureReviewServer(), key)}check/`;
|
|
25
|
+
}
|
|
26
|
+
console.log(checklistText(list, url));
|
|
27
|
+
if (url && !argv.includes("--no-open")) (await import("./review.js")).openBrowser(url);
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main(process.argv.slice(2)).then(
|
|
32
|
+
(code) => process.exit(code),
|
|
33
|
+
(error: Error) => {
|
|
34
|
+
console.error(error.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// The acceptance checklist (1.14): what the requester checks by hand, built from the spec's
|
|
2
|
+
// acceptance criteria, with what the pipeline found for each one. People tick each item on a
|
|
3
|
+
// page (or read it with `req checklist`); the result is written as acceptance.md, the same record
|
|
4
|
+
// the /req acceptance playbook writes. Plain parsing, no model.
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
export type PipelineResult = "pass" | "fail" | "unverified" | "unknown";
|
|
10
|
+
|
|
11
|
+
export interface ChecklistItem {
|
|
12
|
+
/** AC-2.1, or D-1 for a decision the report asks the requester to confirm. */
|
|
13
|
+
id: string;
|
|
14
|
+
kind: "criterion" | "decision";
|
|
15
|
+
title: string;
|
|
16
|
+
/** 前置条件 / 操作 / 预期结果 and any example rows, as written in the spec. */
|
|
17
|
+
details: string[];
|
|
18
|
+
/** False for criteria of a "可以以后再说" requirement: they do not decide the verdict. */
|
|
19
|
+
required: boolean;
|
|
20
|
+
pipeline: PipelineResult;
|
|
21
|
+
/** The line of verification.md / report.md the pipeline result was read from. */
|
|
22
|
+
evidence: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Checklist {
|
|
26
|
+
title: string;
|
|
27
|
+
specVersion: string | null;
|
|
28
|
+
reportVerdict: string | null;
|
|
29
|
+
items: ChecklistItem[];
|
|
30
|
+
/** handoff.md: what to watch for (已知问题) and where test data comes from, for the page. */
|
|
31
|
+
notes: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const clean = (s: string) => s.replace(/\*\*|__|`/g, "").replace(/\s+/g, " ").trim();
|
|
35
|
+
|
|
36
|
+
function read(file: string): string {
|
|
37
|
+
try {
|
|
38
|
+
return fs.readFileSync(file, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function header(text: string, name: string): string | null {
|
|
45
|
+
const m = new RegExp(`^[-*]\\s*${name}[::]\\s*(.+)$`, "m").exec(text);
|
|
46
|
+
return m ? m[1].trim() : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The lines under the first heading whose text matches `title` (until the next heading of the same or higher level). */
|
|
50
|
+
export function section(text: string, title: RegExp): string[] {
|
|
51
|
+
const lines = text.split(/\r?\n/);
|
|
52
|
+
const start = lines.findIndex((l) => /^#{1,6}\s/.test(l) && title.test(l));
|
|
53
|
+
if (start < 0) return [];
|
|
54
|
+
const level = /^(#+)/.exec(lines[start])![1].length;
|
|
55
|
+
const out: string[] = [];
|
|
56
|
+
for (const line of lines.slice(start + 1)) {
|
|
57
|
+
const h = /^(#+)\s/.exec(line);
|
|
58
|
+
if (h && h[1].length <= level) break;
|
|
59
|
+
out.push(line);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The criteria of a spec, with their details and whether their requirement is a must. */
|
|
65
|
+
export function parseCriteria(spec: string): Array<Omit<ChecklistItem, "pipeline" | "evidence">> {
|
|
66
|
+
const items: Array<Omit<ChecklistItem, "pipeline" | "evidence">> = [];
|
|
67
|
+
let required = true;
|
|
68
|
+
let current: (typeof items)[number] | null = null;
|
|
69
|
+
for (const raw of spec.split(/\r?\n/)) {
|
|
70
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(raw);
|
|
71
|
+
if (heading) {
|
|
72
|
+
current = null;
|
|
73
|
+
if (/FR-\d+/.test(heading[1])) required = !/可以以后再说|可选|以后再说/.test(heading[1]);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const ac = /^\s{0,3}[-*]\s+(.*?\bAC-(\d+(?:\.\d+)*)\b.*)$/.exec(raw);
|
|
77
|
+
if (ac) {
|
|
78
|
+
const line = ac[1];
|
|
79
|
+
if (/~~/.test(line) || /已取消/.test(line)) {
|
|
80
|
+
current = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const id = `AC-${ac[2]}`;
|
|
84
|
+
if (items.some((i) => i.id === id)) {
|
|
85
|
+
current = null;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const title = clean(line.replace(new RegExp(`.*?\\bAC-${ac[2].replace(/\./g, "\\.")}\\b[\\s*_\`))\\]】::-]*`), ""));
|
|
89
|
+
current = { id, kind: "criterion", title: title || id, details: [], required };
|
|
90
|
+
items.push(current);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!current) continue;
|
|
94
|
+
if (/^\s*$/.test(raw)) continue;
|
|
95
|
+
// Its details: indented bullets, or a table under it.
|
|
96
|
+
if (/^\s{2,}\S/.test(raw) || /^\s*\|/.test(raw)) {
|
|
97
|
+
const t = raw.trim();
|
|
98
|
+
if (/^\|?\s*-{3,}/.test(t)) continue;
|
|
99
|
+
current.details.push(clean(t.replace(/^[-*]\s+/, "")));
|
|
100
|
+
} else current = null;
|
|
101
|
+
}
|
|
102
|
+
return items;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A table row as words: "| AC-1.2 | 未验证 | 没有空数据 |" → "AC-1.2,未验证,没有空数据". */
|
|
106
|
+
const evidenceOf = (line: string) =>
|
|
107
|
+
clean(line.trim().replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim()).filter(Boolean).join(",")).slice(0, 300);
|
|
108
|
+
|
|
109
|
+
/** What the pipeline wrote about one criterion, from verification.md (preferred) and report.md. */
|
|
110
|
+
export function pipelineResult(id: string, ...documents: string[]): { result: PipelineResult; evidence: string | null } {
|
|
111
|
+
const re = new RegExp(`\\b${id.replace(/\./g, "\\.")}\\b(?![.\\d])`);
|
|
112
|
+
for (const doc of documents) {
|
|
113
|
+
const lines = doc.split(/\r?\n/).filter((l) => re.test(l));
|
|
114
|
+
// The last mention wins: a later round (复验) supersedes an earlier one.
|
|
115
|
+
for (const line of lines.reverse()) {
|
|
116
|
+
if (/未验证|没有验证|无法验证|未验/.test(line)) return { result: "unverified", evidence: evidenceOf(line) };
|
|
117
|
+
if (/未通过|不通过|失败|未达成|没有达成|✗|❌/.test(line)) return { result: "fail", evidence: evidenceOf(line) };
|
|
118
|
+
if (/通过|达成|✓|✅|pass/i.test(line)) return { result: "pass", evidence: evidenceOf(line) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { result: "unknown", evidence: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The decisions the report asks the requester to confirm ("需要你确认的决定"). */
|
|
125
|
+
function decisions(report: string): ChecklistItem[] {
|
|
126
|
+
return section(report, /需要你确认/)
|
|
127
|
+
.map((l) => /^\s*(?:[-*]|\d+[.、)])\s+(.+)$/.exec(l)?.[1])
|
|
128
|
+
.filter((x): x is string => !!x && clean(x).length > 0 && !/^(无|没有)/.test(clean(x)))
|
|
129
|
+
.map((text, i) => ({ id: `D-${i + 1}`, kind: "decision" as const, title: clean(text).slice(0, 300), details: [], required: true, pipeline: "unknown" as const, evidence: null }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const ORDER: Record<PipelineResult, number> = { fail: 0, unverified: 1, unknown: 2, pass: 3 };
|
|
133
|
+
|
|
134
|
+
export function buildChecklist(featureAbs: string): Checklist {
|
|
135
|
+
const spec = read(path.join(featureAbs, "spec.md"));
|
|
136
|
+
const report = read(path.join(featureAbs, "report.md"));
|
|
137
|
+
const verification = read(path.join(featureAbs, "verification.md"));
|
|
138
|
+
const handoff = read(path.join(featureAbs, "handoff.md"));
|
|
139
|
+
const criteria = parseCriteria(spec).map((c) => {
|
|
140
|
+
const { result, evidence } = pipelineResult(c.id, verification, report);
|
|
141
|
+
return { ...c, pipeline: result, evidence };
|
|
142
|
+
});
|
|
143
|
+
// What the pipeline did not verify, or found wrong, first: that is where a person's eyes help most.
|
|
144
|
+
criteria.sort((a, b) => ORDER[a.pipeline] - ORDER[b.pipeline]);
|
|
145
|
+
const notes = [...section(handoff, /已知问题|未完成/), ...section(handoff, /测试需要的准备|测试数据|准备/)]
|
|
146
|
+
.map((l) => clean(l.replace(/^\s*[-*]\s+/, "")))
|
|
147
|
+
.filter((l) => l && !/^(无|没有)[。.]?$/.test(l))
|
|
148
|
+
.slice(0, 20);
|
|
149
|
+
return {
|
|
150
|
+
title: (/^#\s+(.+)$/m.exec(spec)?.[1] ?? path.basename(featureAbs)).replace(/^需求说明[::\s]*/, "").trim(),
|
|
151
|
+
specVersion: header(spec, "版本"),
|
|
152
|
+
reportVerdict: header(report, "结论") ?? (/结论[::]\s*(.+)/.exec(report)?.[1]?.trim() ?? null),
|
|
153
|
+
items: [...criteria, ...decisions(report)],
|
|
154
|
+
notes,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type Mark = "pass" | "fail" | "skip";
|
|
159
|
+
|
|
160
|
+
export interface Answer {
|
|
161
|
+
id: string;
|
|
162
|
+
mark: Mark;
|
|
163
|
+
note?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Verdict {
|
|
167
|
+
verdict: "通过" | "不通过";
|
|
168
|
+
failed: ChecklistItem[];
|
|
169
|
+
/** Required items not checked yet: the verdict cannot be "通过" while there are any. */
|
|
170
|
+
missing: ChecklistItem[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function verdictOf(list: Checklist, answers: Answer[]): Verdict {
|
|
174
|
+
const by = new Map(answers.map((a) => [a.id, a]));
|
|
175
|
+
const failed = list.items.filter((i) => by.get(i.id)?.mark === "fail" && i.required);
|
|
176
|
+
const missing = list.items.filter((i) => i.required && (!by.get(i.id) || by.get(i.id)!.mark === "skip"));
|
|
177
|
+
return { verdict: failed.length > 0 ? "不通过" : "通过", failed, missing };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const MARK_TEXT: Record<Mark, string> = { pass: "通过", fail: "不通过", skip: "未验" };
|
|
181
|
+
|
|
182
|
+
/** acceptance.md in the format of contracts/feature-files.md. Keeps a "不通过的处理" section already there. */
|
|
183
|
+
export function acceptanceText(list: Checklist, answers: Answer[], input: { verdict: "通过" | "不通过"; reviewer: string; date: string; previous?: string }): string {
|
|
184
|
+
const by = new Map(answers.map((a) => [a.id, a]));
|
|
185
|
+
const cell = (s: string) => s.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
186
|
+
const rows = list.items.map((i) => {
|
|
187
|
+
const a = by.get(i.id);
|
|
188
|
+
const mark = a ? MARK_TEXT[a.mark] : "未验";
|
|
189
|
+
const how = i.kind === "decision" ? "需求方表态(同意 / 不同意)" : "在验收页上亲自核对";
|
|
190
|
+
const note = [i.kind === "decision" ? i.title : null, a?.note?.trim() || null, !i.required ? "可以以后再说,不影响结论" : null].filter(Boolean).join(";");
|
|
191
|
+
return `| ${i.id} | ${i.kind === "decision" ? (a?.mark === "pass" ? "同意" : a?.mark === "fail" ? "不同意" : "未表态") : mark} | ${how} | ${cell(note)} |`;
|
|
192
|
+
});
|
|
193
|
+
const kept = input.previous ? section(input.previous, /不通过的处理/).join("\n").trim() : "";
|
|
194
|
+
const failed = list.items.filter((i) => by.get(i.id)?.mark === "fail");
|
|
195
|
+
const handling = kept || failed.map((i) => `- ${i.id}:${cell(by.get(i.id)?.note?.trim() || i.title)} → 待和需求方确定处理方式(作为缺陷处理 / 作为需求修改 / 接受现状)`).join("\n");
|
|
196
|
+
return [
|
|
197
|
+
`# 验收记录:${list.title}`,
|
|
198
|
+
"",
|
|
199
|
+
`- 验收结论:${input.verdict}(${input.date})`,
|
|
200
|
+
`- 验收人:${input.reviewer}`,
|
|
201
|
+
`- 对照的需求版本:${list.specVersion ?? "未写明"}`,
|
|
202
|
+
`- 对照的报告:report.md${list.reportVerdict ? `(流水线结论:${list.reportVerdict})` : ""}`,
|
|
203
|
+
"",
|
|
204
|
+
"| AC | 结果 | 怎么验证的 | 备注 |",
|
|
205
|
+
"| --- | --- | --- | --- |",
|
|
206
|
+
...rows,
|
|
207
|
+
...(handling ? ["", "## 不通过的处理", "", handling] : []),
|
|
208
|
+
"",
|
|
209
|
+
].join("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Writes acceptance.md and commits it alone (when the feature directory is in a git repository). */
|
|
213
|
+
export function recordAcceptance(repo: string, featureDir: string, text: string): { committed: boolean } {
|
|
214
|
+
const file = path.join(repo, featureDir, "acceptance.md");
|
|
215
|
+
fs.writeFileSync(file, text);
|
|
216
|
+
try {
|
|
217
|
+
const rel = path.join(featureDir, "acceptance.md");
|
|
218
|
+
execFileSync("git", ["add", "--", rel], { cwd: repo, stdio: "pipe" });
|
|
219
|
+
const staged = execFileSync("git", ["diff", "--cached", "--name-only", "--", rel], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
220
|
+
if (!staged) return { committed: false };
|
|
221
|
+
execFileSync("git", ["commit", "-q", "-m", `docs(${path.basename(featureDir)}): 验收记录`, "--", rel], { cwd: repo, stdio: "pipe" });
|
|
222
|
+
return { committed: true };
|
|
223
|
+
} catch {
|
|
224
|
+
return { committed: false };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** What the requirement's chat is told when the requester found problems. */
|
|
229
|
+
export function acceptanceFailMessage(featureDir: string, list: Checklist, answers: Answer[]): string {
|
|
230
|
+
const by = new Map(answers.map((a) => [a.id, a]));
|
|
231
|
+
const failed = list.items.filter((i) => by.get(i.id)?.mark === "fail");
|
|
232
|
+
return [
|
|
233
|
+
`【需求开发流水线通知】需求方在验收页上完成了验收(${featureDir}),结论:不通过。验收记录已写入 acceptance.md 并提交。`,
|
|
234
|
+
"",
|
|
235
|
+
"不通过的项:",
|
|
236
|
+
...failed.map((i) => `- ${i.id} ${i.title}${by.get(i.id)?.note?.trim() ? `:${by.get(i.id)!.note!.trim()}` : ""}`),
|
|
237
|
+
"",
|
|
238
|
+
`请按 /req 的"验收"手册第 5 步,和需求方逐项确定处理方式(实现与需求不符 / 需求本身要改 / 接受现状),记入 acceptance.md 的"不通过的处理"一节,再按对应的手册继续。`,
|
|
239
|
+
].join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `req checklist` in a terminal: the list in words. */
|
|
243
|
+
export function checklistText(list: Checklist, url: string | null): string {
|
|
244
|
+
const mark: Record<PipelineResult, string> = { pass: "流水线:通过", fail: "流水线:没通过", unverified: "流水线:没验证", unknown: "流水线:没有记录" };
|
|
245
|
+
const lines = [`# 验收清单:${list.title}`, ""];
|
|
246
|
+
if (list.reportVerdict) lines.push(`流水线结论:${list.reportVerdict}`, "");
|
|
247
|
+
list.items.forEach((i, n) => {
|
|
248
|
+
lines.push(`${n + 1}. ${i.id} ${i.title}${i.required ? "" : "(可以以后再说)"}${i.kind === "criterion" ? ` [${mark[i.pipeline]}]` : " [请表态:同意 / 不同意]"}`);
|
|
249
|
+
for (const d of i.details) lines.push(` ${d}`);
|
|
250
|
+
});
|
|
251
|
+
if (list.notes.length) lines.push("", "注意(提测说明):", ...list.notes.map((n) => `- ${n}`));
|
|
252
|
+
if (url) lines.push("", `在验收页上逐条勾选、写下哪里不对:${url}`);
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
package/engine/src/cli-args.ts
CHANGED
|
@@ -56,6 +56,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
56
56
|
list: { type: "boolean", default: false },
|
|
57
57
|
"card-fields": { type: "boolean", default: false },
|
|
58
58
|
track: { type: "boolean", default: false },
|
|
59
|
+
"review-fix": { type: "boolean", default: false },
|
|
59
60
|
"spec-review": { type: "boolean", default: false },
|
|
60
61
|
worktree: { type: "boolean", default: false },
|
|
61
62
|
"source-repo": { type: "string" },
|
|
@@ -118,7 +119,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
118
119
|
repo: path.resolve(values.repo ?? process.env.PASEO_AGENT_CWD ?? process.cwd()),
|
|
119
120
|
featureDir: values.feature ?? "",
|
|
120
121
|
originAgentId: origin ? origin : null,
|
|
121
|
-
resume: (values.resume ?? false) || phase === "verify" || (values.approve ?? false),
|
|
122
|
+
resume: (values.resume ?? false) || phase === "verify" || (values.approve ?? false) || (values["review-fix"] ?? false),
|
|
122
123
|
restart: values.restart ?? false,
|
|
123
124
|
configPath: values.config ? path.resolve(values.config) : null,
|
|
124
125
|
profile: (profile as ProfileName | undefined) ?? null,
|
|
@@ -127,6 +128,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
|
|
|
127
128
|
list: values.list ?? false,
|
|
128
129
|
cardFields: values["card-fields"] ?? false,
|
|
129
130
|
track: values.track ?? false,
|
|
131
|
+
reviewFix: values["review-fix"] ?? false,
|
|
130
132
|
specReview: values["spec-review"] ?? false,
|
|
131
133
|
worktree: values.worktree ?? false,
|
|
132
134
|
sourceRepo: values["source-repo"] ? path.resolve(values["source-repo"]) : null,
|
|
@@ -210,6 +212,7 @@ export function launchArgv(args: LaunchArgs): string[] {
|
|
|
210
212
|
if (args.profile) out.push("--profile", args.profile);
|
|
211
213
|
if (args.resume) out.push("--resume");
|
|
212
214
|
if (args.restart) out.push("--restart");
|
|
215
|
+
if (args.reviewFix) out.push("--review-fix");
|
|
213
216
|
if (args.sourceRepo) out.push("--source-repo", args.sourceRepo);
|
|
214
217
|
if (args.workspaceId) out.push("--workspace-id", args.workspaceId);
|
|
215
218
|
if (args.specReview) out.push("--spec-review");
|