paseo-req-kit 1.14.0-beta.1 → 1.14.0-beta.3

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/req.mjs +8 -0
  3. package/contracts/cli-json.md +1 -1
  4. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +7 -0
  5. package/engine/src/api.ts +7 -3
  6. package/engine/src/checklist-cmd.ts +37 -0
  7. package/engine/src/checklist.ts +254 -0
  8. package/engine/src/features.ts +22 -3
  9. package/engine/src/reply.ts +7 -4
  10. package/engine/src/review-assets/check.css +57 -0
  11. package/engine/src/review-assets/check.html +35 -0
  12. package/engine/src/review-assets/check.js +258 -0
  13. package/engine/src/review.ts +94 -1
  14. package/engine/src/try.ts +134 -0
  15. package/package.json +1 -1
  16. package/paseo-plugin/client/data.ts +3 -3
  17. package/paseo-plugin/client/detail.tsx +5 -2
  18. package/paseo-plugin/client/nav.ts +5 -0
  19. package/paseo-plugin/client/review.tsx +20 -6
  20. package/paseo-plugin/package.json +1 -1
  21. package/paseo-plugin/server/handlers.ts +2 -2
  22. package/paseo-plugin/shared/contracts.ts +2 -1
  23. package/site-dist/404.html +2 -2
  24. package/site-dist/assets/{app.ByJ4x-zl.js → app.BpC99f5a.js} +1 -1
  25. package/site-dist/assets/changelog.md.m4cSU5e4.js +1 -0
  26. package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +1 -0
  27. package/site-dist/assets/chunks/{VPLocalSearchBox.LeFVc-_M.js → VPLocalSearchBox.CI43rhTA.js} +1 -1
  28. package/site-dist/assets/chunks/{theme.BMoBTK1B.js → theme.S6pNEF23.js} +2 -2
  29. package/site-dist/assets/{guide_getting-started.md.DDZak2vT.js → guide_getting-started.md.a7iNP1Ky.js} +1 -1
  30. package/site-dist/assets/{guide_user.md.2v7b_fzd.js → guide_user.md.D9GpGJ7y.js} +1 -1
  31. package/site-dist/assets/{guide_user.md.2v7b_fzd.lean.js → guide_user.md.D9GpGJ7y.lean.js} +1 -1
  32. package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.js → reference_contracts_cli-json.md.BV1HtTmr.js} +1 -1
  33. package/site-dist/assets/{reference_contracts_cli-json.md.BR6gwq-e.lean.js → reference_contracts_cli-json.md.BV1HtTmr.lean.js} +1 -1
  34. package/site-dist/changelog.html +5 -5
  35. package/site-dist/guide/getting-started.html +5 -5
  36. package/site-dist/guide/how-it-works.html +4 -4
  37. package/site-dist/guide/index.html +4 -4
  38. package/site-dist/guide/install.html +4 -4
  39. package/site-dist/guide/maintainer.html +4 -4
  40. package/site-dist/guide/phases.html +4 -4
  41. package/site-dist/guide/project.html +4 -4
  42. package/site-dist/guide/user.html +5 -5
  43. package/site-dist/hashmap.json +1 -1
  44. package/site-dist/index.html +4 -4
  45. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  46. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  47. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  48. package/site-dist/reference/adr/0004-result-files.html +4 -4
  49. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  50. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  51. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  52. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  53. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  54. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  55. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  56. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  57. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  58. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  59. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  60. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  61. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  62. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  63. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  64. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  65. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  66. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  67. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  68. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  69. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  70. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  71. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  72. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  73. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  74. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  75. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  76. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  77. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  78. package/site-dist/reference/adr/index.html +4 -4
  79. package/site-dist/reference/commands.html +4 -4
  80. package/site-dist/reference/config.html +4 -4
  81. package/site-dist/reference/contracts/cli-json.html +5 -5
  82. package/site-dist/reference/contracts/feature-files.html +4 -4
  83. package/site-dist/reference/contracts/index.html +4 -4
  84. package/site-dist/reference/contracts/notifier-event.html +4 -4
  85. package/site-dist/reference/contracts/result-file.html +4 -4
  86. package/site-dist/reference/contracts/spec.html +4 -4
  87. package/site-dist/reference/contracts/state.html +4 -4
  88. package/site-dist/reference/contracts/usage-record.html +4 -4
  89. package/site-dist/reference/contracts/verify.html +4 -4
  90. package/site-dist/reference/scope.html +4 -4
  91. package/skills/req/SKILL.md +2 -0
  92. package/skills/req/playbooks/accept.md +2 -2
  93. package/site-dist/assets/changelog.md.BUTM13q-.js +0 -1
  94. package/site-dist/assets/chunks/@localSearchIndexroot.BJ01YY7h.js +0 -1
  95. /package/site-dist/assets/{changelog.md.BUTM13q-.lean.js → changelog.md.m4cSU5e4.lean.js} +0 -0
  96. /package/site-dist/assets/{guide_getting-started.md.DDZak2vT.lean.js → guide_getting-started.md.a7iNP1Ky.lean.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
4
4
 
5
+ ## 1.14.0-beta.3
6
+
7
+ ### 使用者会看到的变化
8
+
9
+ - **验收清单**(`req checklist <需求>`,插件需求页面上的"验收清单(可一键试用)"):按需求说明的验收标准生成清单,每条写着前置条件、操作、预期结果和流水线的验证结果,没验证或没通过的排在前面,报告里"需要你确认的决定"也列上。逐条点"通过 / 不对 / 先不验",进度自动保存;提交后写成 `acceptance.md` 并提交,不通过时把不对的地方转给需求的对话。
10
+ - **一键试用**(`req try <需求>`,验收清单页右上角"把它跑起来试用"):用项目的 Paseo 服务脚本在需求自己的工作区里启动,给出访问地址和提测说明里的测试账号、数据;`--stop` 或"停止试用"停掉。没有服务脚本的项目给出提测说明里的启动方法。
11
+
12
+ ### 升级须知
13
+
14
+ - `req api review-url` 新增 `--page check`;`req reply` 新增 `--acceptance`(验收页用)。验收页和原型审查页是同一个本机服务。
15
+
16
+ ## 1.14.0-beta.2
17
+
18
+ ### 使用者会看到的变化
19
+
20
+ - **修复:已经开发完(待验收、已验收)的需求在插件里显示"已确认,未开始开发"和"开始开发"按钮。** 需求分支合并到主分支后,主分支里的需求文件夹没有运行状态(运行状态不提交),beta.1 把它当成了没开始。现在看到方案、完成报告等只有流水线才会写的文件,就按已开发处理:有完成报告的显示"待验收"(或验收记录里的结论),不会进"等你处理"。
21
+
5
22
  ## 1.14.0-beta.1
6
23
 
7
24
  ### 使用者会看到的变化
package/bin/req.mjs CHANGED
@@ -31,6 +31,8 @@ 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 try <需求> [--stop] 把做好的需求跑起来试用:给出访问地址和测试数据(--stop 停止)
35
+ req checklist <需求> 验收清单:按验收标准逐条勾选,提交后写成验收记录
34
36
  req ui-check <网址> [--prototype <原型文件>] [--width 1280,390]
35
37
  不靠看图检查网页:显示不全、遮挡、超出屏幕、文字太淡、报错,和原型对文字
36
38
  req reply <需求> <回复> 把回答或方案意见转给需求的对话(插件的"等你处理"用它)
@@ -221,6 +223,12 @@ switch (command) {
221
223
  case "review":
222
224
  code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "review.ts"), ...rest]) : 1;
223
225
  break;
226
+ case "try":
227
+ code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "try.ts"), ...rest]) : 1;
228
+ break;
229
+ case "checklist":
230
+ code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "checklist-cmd.ts"), ...rest]) : 1;
231
+ break;
224
232
  case "ui-check":
225
233
  code = needTsx() ? node([tsxCli, path.join(kitRoot, "engine", "src", "ui-check.ts"), ...rest]) : 1;
226
234
  break;
@@ -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 起。`{ url, readOnly }`:这个需求的原型审查页地址(需要时在后台启动审查页的服务)。`--read-only` 的地址只能看,不能提交意见或确认方案。需求不在本机的需求索引中或还没有 `prototype/` 时报 `not_found` |
35
+ | `req api review-url <需求> [--repo <目录>] [--read-only] [--page check]` | 1.13 起;`--page check`(1.14)给出同一服务上的验收清单页地址(需要 `spec.md`)。`{ url, readOnly }`:这个需求的原型审查页地址(需要时在后台启动审查页的服务)。`--read-only` 的地址只能看,不能提交意见或确认方案。需求不在本机的需求索引中或还没有 `prototype/` 时报 `not_found` |
36
36
 
37
37
  `<需求>` 的写法与 `req status` 相同:功能目录、目录名或名称中能唯一确定的一部分。需求在为它新建的工作区中开发时,`--repo` 写对话所在的目录或那个工作区都可以。
38
38
 
@@ -97,6 +97,13 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
97
97
 
98
98
  然后说"我要验收"。AI 会按验收标准一条一条带你核对:告诉你怎么操作、应该看到什么;能用命令验证的给出命令,你可以自己执行。每条结果记入功能目录的 `acceptance.md`。
99
99
 
100
+ **在插件里自己验收(1.14 起)。** 需求页面上点**"验收清单(可一键试用)"**(终端里是 `req checklist <需求>`),验收清单在 Paseo 里打开:
101
+
102
+ - 每条验收标准写着前置条件、怎么操作、应该看到什么,以及流水线的验证结果;流水线没验证或没通过的排在最前面,报告里"需要你确认的决定"也列在后面请你表态。
103
+ - 右上角**"把它跑起来试用"**(终端里是 `req try <需求>`):用项目的 Paseo 服务脚本(`paseo.json`)在这个需求自己的工作区里把它启动起来,给出访问地址和提测说明里的测试账号、数据;用完点"停止试用"。项目没有服务脚本时,会给出提测说明里写的启动方法,也可以在需求的对话里说"帮我把它跑起来"。
104
+ - 每条点"通过"、"不对"(写下看到了什么)或"先不验",进度自动保存。必须验的都核对完才能提交"通过";有"不对"的就提交"不通过"。
105
+ - 提交后写成 `acceptance.md` 并提交。不通过时,不对的地方会转给这个需求的对话,它会和你一起确定怎么处理。
106
+
100
107
  - **全部通过**:请开发同事审阅代码并合并上线。流水线只做本地提交,不会自己合并或上线。
101
108
  - **有不通过的**:逐项决定怎么处理:做得不对的,补进需求说明后继续开发;发现想要的不是这样的,按改需求处理;也可以接受现状并记下理由。
102
109
 
package/engine/src/api.ts CHANGED
@@ -185,7 +185,7 @@ function item(repo: string, branch: string | null, featureDir: string, extra: {
185
185
  const summary = summarize({ path: repo, branch }, name);
186
186
  if (!summary) return null;
187
187
  const state = readStateSafe(repo, featureDir);
188
- const notStarted = state ? null : notStartedOf(repo, summary.featureDir, summary.confirmed, summary.updatedAt);
188
+ const notStarted = state || summary.developed ? null : notStartedOf(repo, summary.featureDir, summary.confirmed, summary.updatedAt);
189
189
  // 1.14: a worktree made for it before the pipeline ran: it still belongs to the chat's checkout.
190
190
  if (!state && !extra.sourceRepo) {
191
191
  const made = recordFor(repo, summary.featureDir);
@@ -579,12 +579,16 @@ export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
579
579
  // 1.13: the prototype review page of a requirement (starts the page's server when needed).
580
580
  const feature = need("需求");
581
581
  const readOnly = flag(argv, "--read-only");
582
+ // 1.14 --page check: the acceptance checklist page of the same server.
583
+ const page = option(argv, "--page") === "check" ? "check" : "prototype";
582
584
  const { repo: r, featureDir } = locate(repo, feature);
583
585
  return import("./review.js").then(async ({ ensureReviewServer, keyOf, reviewUrl }) => {
584
586
  const key = keyOf(r, featureDir);
585
587
  if (!key) throw new ApiError("not_found", `${featureDir} 不在本机的需求列表中`);
586
- if (!fs.existsSync(path.join(r, featureDir, "prototype"))) throw new ApiError("not_found", `${featureDir} 还没有界面原型`);
587
- return { url: reviewUrl(await ensureReviewServer(), key, readOnly), readOnly };
588
+ if (page === "prototype" && !fs.existsSync(path.join(r, featureDir, "prototype"))) throw new ApiError("not_found", `${featureDir} 还没有界面原型`);
589
+ if (page === "check" && !fs.existsSync(path.join(r, featureDir, "spec.md"))) throw new ApiError("not_found", `${featureDir} 中没有 spec.md`);
590
+ const url = reviewUrl(await ensureReviewServer(), key, readOnly);
591
+ return { url: page === "check" ? `${url}check/` : url, readOnly };
588
592
  });
589
593
  }
590
594
  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
+ }
@@ -29,6 +29,8 @@ export interface FeatureSummary {
29
29
  profile: ProfileName | null;
30
30
  /** Plain-language stage, e.g. "对齐中", "开发中", "待验收", "已验收". */
31
31
  stage: string;
32
+ /** 1.14.0-beta.2: the pipeline ran for it (its state, or the documents only it writes, are here). */
33
+ developed: boolean;
32
34
  /** Last change to the feature directory's state or spec, ISO time. */
33
35
  updatedAt: string;
34
36
  /** Time and usage of all pipeline attempts, in words; null before the first run. */
@@ -77,13 +79,27 @@ interface RawState {
77
79
  history?: Array<{ attempt: number; startedAt: string; endedAt: string | null; usage?: Record<string, number> | null }>;
78
80
  }
79
81
 
80
- function stageOf(confirmed: boolean, state: RawState | null, acceptance: string | null): string {
82
+ /**
83
+ * Files only the pipeline writes: their presence means it ran, even where its state is not (a
84
+ * copy merged into the main branch: the state file under .pipeline/ is never committed).
85
+ */
86
+ const PIPELINE_OUTPUTS = ["design.md", "assessment.md", "diagnosis.md", "implementation.md", "handoff.md", "verification.md", "report.md"];
87
+
88
+ export function developedIn(dir: string): boolean {
89
+ return PIPELINE_OUTPUTS.some((f) => fs.existsSync(path.join(dir, f)));
90
+ }
91
+
92
+ function stageOf(confirmed: boolean, state: RawState | null, acceptance: string | null, developed = false, reported = false): string {
81
93
  if (acceptance) {
82
94
  const verdict = headerValue(acceptance, "验收结论");
83
95
  if (verdict?.startsWith("通过")) return "已验收";
84
96
  if (verdict?.startsWith("不通过")) return "验收未通过";
85
97
  }
86
- if (!state) return confirmed ? "已确认,未开始开发" : "对齐中";
98
+ if (!state) {
99
+ // 1.14.0-beta.2: developed elsewhere (e.g. merged here from its own branch) is not "not started".
100
+ if (developed) return reported ? "待验收" : "已开发(这个目录里没有运行状态)";
101
+ return confirmed ? "已确认,未开始开发" : "对齐中";
102
+ }
87
103
  switch (state.status) {
88
104
  case "running":
89
105
  return state.pid && isAlive(state.pid) ? "开发中" : "已中断(可继续)";
@@ -131,6 +147,7 @@ export function summarize(worktree: Worktree, name: string): FeatureSummary | nu
131
147
  confirmed: false,
132
148
  profile: null,
133
149
  stage: "对齐中",
150
+ developed: false,
134
151
  updatedAt: new Date(updated || Date.now()).toISOString(),
135
152
  usage: null,
136
153
  };
@@ -146,6 +163,7 @@ export function summarize(worktree: Worktree, name: string): FeatureSummary | nu
146
163
  const acceptanceFile = path.join(dir, "acceptance.md");
147
164
  const acceptance = fs.existsSync(acceptanceFile) ? fs.readFileSync(acceptanceFile, "utf8") : null;
148
165
  const confirmed = isSpecConfirmed(spec);
166
+ const developed = state !== null || developedIn(dir);
149
167
  const version = Number(headerValue(spec, "版本"));
150
168
  const updated = Math.max(mtime(specFile), mtime(stateFile), mtime(acceptanceFile), mtime(path.join(dir, "changes.md")));
151
169
  return {
@@ -158,7 +176,8 @@ export function summarize(worktree: Worktree, name: string): FeatureSummary | nu
158
176
  specVersion: Number.isInteger(version) && version > 0 ? version : null,
159
177
  confirmed,
160
178
  profile: specProfile(spec),
161
- stage: stageOf(confirmed, state, acceptance),
179
+ stage: stageOf(confirmed, state, acceptance, developed, fs.existsSync(path.join(dir, "report.md"))),
180
+ developed,
162
181
  updatedAt: new Date(updated || Date.now()).toISOString(),
163
182
  usage: state?.history && state.history.length > 0 ? describeTotals(totals(state.history)) : null,
164
183
  };
@@ -8,7 +8,10 @@ import { pluginReplyMessage } from "./messages.js";
8
8
  import { loadRuntimeFactory } from "./runtime-loader.js";
9
9
  import { readState, statePath } from "./state.js";
10
10
 
11
- async function main(argv: string[]): Promise<number> {
11
+ async function main(argvIn: string[]): Promise<number> {
12
+ // 1.14 --acceptance: the checklist page found problems; the text is the whole notice.
13
+ const acceptance = argvIn.includes("--acceptance");
14
+ const argv = argvIn.filter((a) => a !== "--acceptance");
12
15
  const [feature, ...words] = argv;
13
16
  const text = words.join(" ").trim();
14
17
  if (!feature || !text) {
@@ -17,7 +20,7 @@ async function main(argv: string[]): Promise<number> {
17
20
  }
18
21
  const { repo, featureDir } = locate(process.cwd(), feature);
19
22
  const state = readState(statePath(repo, featureDir));
20
- if (!state || (state.status !== "waiting_for_business" && state.status !== "waiting_for_approval")) {
23
+ if (!state || (!acceptance && state.status !== "waiting_for_business" && state.status !== "waiting_for_approval")) {
21
24
  console.error(`${featureDir} 现在没有在等待回答或确认,不用回复。`);
22
25
  return 1;
23
26
  }
@@ -36,7 +39,7 @@ async function main(argv: string[]): Promise<number> {
36
39
  try {
37
40
  await runtime.notifyAgent(
38
41
  state.originAgentId,
39
- pluginReplyMessage({
42
+ acceptance ? text.slice(0, 8000) : pluginReplyMessage({
40
43
  featureDir,
41
44
  waitingFor: state.status === "waiting_for_approval" ? "approval" : "business",
42
45
  text: text.slice(0, 8000),
@@ -46,7 +49,7 @@ async function main(argv: string[]): Promise<number> {
46
49
  } finally {
47
50
  await runtime.close().catch(() => undefined);
48
51
  }
49
- console.log("已转给这个需求的对话,它会按回复更新需求说明并继续开发。");
52
+ console.log(acceptance ? "已转给这个需求的对话。" : "已转给这个需求的对话,它会按回复更新需求说明并继续开发。");
50
53
  return 0;
51
54
  }
52
55
 
@@ -0,0 +1,57 @@
1
+ :root {
2
+ --bg: #f6f7f9; --surface: #ffffff; --surface-2: #f1f3f6; --text: #1c2430; --muted: #5d6b7c; --border: #dfe3e9;
3
+ --accent: #2457d6; --accent-text: #ffffff; --accent-soft: #e8eefc; --ok: #15803d; --ok-soft: #e7f6ec; --danger: #b91c1c; --danger-soft: #fdecec; --warn: #b45309; --warn-soft: #fdf3e2;
4
+ color-scheme: light;
5
+ }
6
+ @media (prefers-color-scheme: dark) {
7
+ :root {
8
+ --bg: #12161c; --surface: #1a2029; --surface-2: #222a35; --text: #e6eaf0; --muted: #9aa7b8; --border: #2e3846;
9
+ --accent: #6f9bff; --accent-text: #0d1420; --accent-soft: #1f2c47; --ok: #4ade80; --ok-soft: #173322; --danger: #f87171; --danger-soft: #3a1c1c; --warn: #fbbf24; --warn-soft: #3a2c12;
10
+ color-scheme: dark;
11
+ }
12
+ }
13
+ * { box-sizing: border-box; }
14
+ [hidden] { display: none !important; }
15
+ body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; padding-bottom: 96px; }
16
+ .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; padding: 20px 24px 8px; flex-wrap: wrap; }
17
+ .eyebrow { font-size: 13px; color: var(--muted); }
18
+ h1 { margin: 2px 0 4px; font-size: 22px; }
19
+ .muted { color: var(--muted); }
20
+ main { padding: 0 24px; max-width: 980px; }
21
+ .intro { margin: 8px 0 16px; }
22
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; margin: 12px 0; }
23
+ .notes h2, .card h2 { font-size: 15px; margin: 0 0 6px; }
24
+ .notes ul { margin: 0; padding-left: 20px; }
25
+ .items { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 12px; }
26
+ .item { background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--border); border-radius: 12px; padding: 14px 16px; }
27
+ .item.pass { border-left-color: var(--ok); }
28
+ .item.fail { border-left-color: var(--danger); }
29
+ .item.skip { border-left-color: var(--warn); }
30
+ .item-head { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
31
+ .id { font-weight: 700; font-variant-numeric: tabular-nums; }
32
+ .item-title { font-weight: 600; }
33
+ .badge { font-size: 12px; padding: 1px 8px; border-radius: 999px; background: var(--surface-2); color: var(--muted); white-space: nowrap; }
34
+ .badge.fail { background: var(--danger-soft); color: var(--danger); }
35
+ .badge.unverified { background: var(--warn-soft); color: var(--warn); }
36
+ .badge.pass { background: var(--ok-soft); color: var(--ok); }
37
+ .details { margin: 8px 0 0; padding-left: 18px; }
38
+ .details li { margin: 2px 0; }
39
+ .evidence { font-size: 13px; color: var(--muted); margin-top: 6px; }
40
+ .marks { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
41
+ .btn { font: inherit; border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 8px; padding: 6px 14px; cursor: pointer; min-height: 36px; }
42
+ .btn:hover { border-color: var(--accent); }
43
+ .btn:disabled { opacity: 0.5; cursor: default; }
44
+ .btn.primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); font-weight: 600; }
45
+ .btn.on-pass { background: var(--ok); color: #fff; border-color: var(--ok); }
46
+ .btn.on-fail { background: var(--danger); color: #fff; border-color: var(--danger); }
47
+ .btn.on-skip { background: var(--warn); color: #fff; border-color: var(--warn); }
48
+ textarea { width: 100%; margin-top: 8px; font: inherit; color: var(--text); background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 8px; min-height: 64px; }
49
+ .bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 16px; padding: 12px 24px; background: var(--surface); border-top: 1px solid var(--border); flex-wrap: wrap; }
50
+ .summary { flex: 1; min-width: 200px; }
51
+ .reviewer input { font: inherit; color: var(--text); background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; width: 160px; }
52
+ .toast { position: fixed; right: 24px; bottom: 84px; background: var(--text); color: var(--bg); padding: 10px 14px; border-radius: 10px; max-width: 520px; white-space: pre-line; }
53
+ .services a { color: var(--accent); }
54
+ pre.howto { white-space: pre-wrap; font: inherit; margin: 6px 0 0; }
55
+ .done { background: var(--ok-soft); border-color: var(--ok); }
56
+
57
+ @media (max-width: 640px) { body { padding-bottom: 200px; } }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>验收清单</title>
7
+ <link rel="stylesheet" href="check.css">
8
+ </head>
9
+ <body>
10
+ <header class="top">
11
+ <div>
12
+ <div class="eyebrow" id="project"></div>
13
+ <h1 id="title">验收清单</h1>
14
+ <div class="muted" id="sub"></div>
15
+ </div>
16
+ <div class="top-actions">
17
+ <button id="try" class="btn">把它跑起来试用</button>
18
+ <button id="try-stop" class="btn" hidden>停止试用</button>
19
+ </div>
20
+ </header>
21
+ <main>
22
+ <section id="try-box" class="card" hidden></section>
23
+ <section id="notes" class="card notes" hidden></section>
24
+ <p class="muted intro">一条一条看:照着"前置条件"准备,按"操作"去做,看到的和"预期结果"一样就点<b>通过</b>;不一样点<b>不对</b>,写下看到了什么。流水线没验证或没通过的排在最前面。进度会自动保存。</p>
25
+ <ol id="items" class="items"></ol>
26
+ </main>
27
+ <footer class="bar">
28
+ <div class="summary" id="summary"></div>
29
+ <label class="reviewer">验收人 <input id="reviewer" placeholder="你的名字(可不填)" maxlength="60"></label>
30
+ <button id="submit" class="btn primary" disabled>提交验收结果</button>
31
+ </footer>
32
+ <div id="toast" class="toast" hidden></div>
33
+ <script src="check.js"></script>
34
+ </body>
35
+ </html>