paseo-req-kit 1.13.0-beta.5 → 1.14.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/contracts/cli-json.md +4 -4
  3. package/contracts/state.md +1 -1
  4. package/contracts/usage-record.md +5 -1
  5. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
  6. package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
  7. 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 +17 -2
  8. package/engine/prompts/00-shared-context.md +2 -0
  9. package/engine/src/activity.ts +23 -0
  10. package/engine/src/api.ts +106 -5
  11. package/engine/src/cli-args.ts +3 -0
  12. package/engine/src/config-view.ts +5 -0
  13. package/engine/src/config.ts +12 -0
  14. package/engine/src/control.ts +8 -1
  15. package/engine/src/doctor.ts +18 -0
  16. package/engine/src/features.ts +21 -1
  17. package/engine/src/launch.ts +25 -4
  18. package/engine/src/paseo-runtime.ts +14 -1
  19. package/engine/src/pipeline.ts +45 -1
  20. package/engine/src/preflight.ts +2 -0
  21. package/engine/src/records.ts +24 -2
  22. package/engine/src/review.ts +3 -1
  23. package/engine/src/runtime.ts +4 -0
  24. package/engine/src/scan-guard.ts +132 -0
  25. package/engine/src/state.ts +3 -1
  26. package/engine/src/stats.ts +1 -0
  27. package/engine/src/worktree.ts +180 -25
  28. package/package.json +1 -1
  29. package/paseo-plugin/client/format.ts +6 -2
  30. package/paseo-plugin/client/inbox.tsx +6 -1
  31. package/paseo-plugin/client/review.tsx +11 -1
  32. package/paseo-plugin/client/web.ts +17 -0
  33. package/paseo-plugin/package.json +1 -1
  34. package/paseo-plugin/shared/contracts.ts +4 -2
  35. package/site-dist/404.html +2 -2
  36. package/site-dist/assets/{app.BhERz0tz.js → app.ByJ4x-zl.js} +1 -1
  37. package/site-dist/assets/changelog.md.BUTM13q-.js +1 -0
  38. package/site-dist/assets/{changelog.md.Cu4SyOh_.lean.js → changelog.md.BUTM13q-.lean.js} +1 -1
  39. package/site-dist/assets/chunks/@localSearchIndexroot.BJ01YY7h.js +1 -0
  40. package/site-dist/assets/chunks/{VPLocalSearchBox.BZONO_Xu.js → VPLocalSearchBox.LeFVc-_M.js} +1 -1
  41. package/site-dist/assets/chunks/{theme.Cdyvhz0n.js → theme.BMoBTK1B.js} +2 -2
  42. package/site-dist/assets/{guide_getting-started.md.CspCz67n.js → guide_getting-started.md.DDZak2vT.js} +1 -1
  43. package/site-dist/assets/{guide_getting-started.md.CspCz67n.lean.js → guide_getting-started.md.DDZak2vT.lean.js} +1 -1
  44. package/site-dist/assets/guide_maintainer.md.C1v7EpSY.js +1 -0
  45. package/site-dist/assets/guide_project.md.Csi5wiyt.js +77 -0
  46. package/site-dist/assets/guide_project.md.Csi5wiyt.lean.js +77 -0
  47. package/site-dist/assets/{guide_user.md.Cenm9vlN.js → guide_user.md.2v7b_fzd.js} +1 -1
  48. package/site-dist/assets/{guide_user.md.Cenm9vlN.lean.js → guide_user.md.2v7b_fzd.lean.js} +1 -1
  49. package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.js +1 -0
  50. package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.lean.js +1 -0
  51. package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +1 -0
  52. package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.js +10 -0
  53. package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.lean.js +10 -0
  54. package/site-dist/changelog.html +5 -5
  55. package/site-dist/guide/getting-started.html +5 -5
  56. package/site-dist/guide/how-it-works.html +4 -4
  57. package/site-dist/guide/index.html +4 -4
  58. package/site-dist/guide/install.html +4 -4
  59. package/site-dist/guide/maintainer.html +5 -5
  60. package/site-dist/guide/phases.html +4 -4
  61. package/site-dist/guide/project.html +7 -7
  62. package/site-dist/guide/user.html +5 -5
  63. package/site-dist/hashmap.json +1 -1
  64. package/site-dist/index.html +4 -4
  65. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  66. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  67. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  68. package/site-dist/reference/adr/0004-result-files.html +4 -4
  69. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  70. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  71. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  72. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  73. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  74. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  75. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  76. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  77. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  78. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  79. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  80. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  81. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  82. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  83. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  84. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  85. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  86. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  87. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  88. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  89. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  90. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  91. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  92. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  93. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  94. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  95. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  96. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  97. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  98. package/site-dist/reference/adr/index.html +4 -4
  99. package/site-dist/reference/commands.html +4 -4
  100. package/site-dist/reference/config.html +4 -4
  101. package/site-dist/reference/contracts/cli-json.html +5 -5
  102. package/site-dist/reference/contracts/feature-files.html +4 -4
  103. package/site-dist/reference/contracts/index.html +4 -4
  104. package/site-dist/reference/contracts/notifier-event.html +4 -4
  105. package/site-dist/reference/contracts/result-file.html +4 -4
  106. package/site-dist/reference/contracts/spec.html +4 -4
  107. package/site-dist/reference/contracts/state.html +5 -5
  108. package/site-dist/reference/contracts/usage-record.html +6 -6
  109. package/site-dist/reference/contracts/verify.html +4 -4
  110. package/site-dist/reference/scope.html +4 -4
  111. package/skills/req/SKILL.md +1 -0
  112. package/skills/req/playbooks/incident.md +3 -3
  113. package/skills/req/playbooks/new.md +1 -1
  114. package/skills/req/playbooks/small.md +1 -1
  115. package/skills/spec-writing/SKILL.md +3 -1
  116. package/site-dist/assets/changelog.md.Cu4SyOh_.js +0 -1
  117. package/site-dist/assets/chunks/@localSearchIndexroot.C1vUA1BF.js +0 -1
  118. package/site-dist/assets/guide_maintainer.md.RZIzyCTV.js +0 -1
  119. package/site-dist/assets/guide_project.md.DnD2-Dsd.js +0 -77
  120. package/site-dist/assets/guide_project.md.DnD2-Dsd.lean.js +0 -77
  121. package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.js +0 -1
  122. package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.lean.js +0 -1
  123. package/site-dist/assets/reference_contracts_state.md.TRfzD5bc.js +0 -1
  124. package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.js +0 -10
  125. package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.lean.js +0 -10
  126. /package/site-dist/assets/{guide_maintainer.md.RZIzyCTV.lean.js → guide_maintainer.md.C1v7EpSY.lean.js} +0 -0
  127. /package/site-dist/assets/{reference_contracts_state.md.TRfzD5bc.lean.js → reference_contracts_state.md.EEBd_l2P.lean.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
4
4
 
5
+ ## 1.14.0-beta.1
6
+
7
+ ### 使用者会看到的变化
8
+
9
+ - **修复:自动新建工作区后只有需求说明、没有代码。** 以前新建工作区只等 2 分钟,超时(大仓库在 Windows 上常见)或建好后启动前的检查没通过,启动命令就退出了,留下一个只有需求说明提交的分支;再启动又报"分支已存在"。现在工作区一建好就记下来,再次启动会在这个工作区里接着开始;最多等 15 分钟。以前版本留下的这种工作区,也会由 Paseo 接手接着做。
10
+ - **访谈改过的术语表(`GLOSSARY.md`)和设计决定(`docs/adr/`)随需求说明一起带到新工作区**,在需求自己的分支上提交。
11
+ - **修复:需求说明可能被提交到主分支。** 写需求说明的说明和"新需求"手册对是否提交说法不一致,现在统一为:要自动新建工作区的,确认后不提交,由流水线在新工作区里提交。需求说明已经提交在主分支上、本地主分支比远端落后、主目录还有别的未提交改动时,启动结果里会写明。
12
+ - **需求从开始谈就出现在插件里**("对齐中"):建好功能目录后 `/req` 会登记它(`start-pipeline --feature <功能目录> --track`);没登记的,插件也会在本机用过的项目里找到。
13
+ - **确认了却没开始开发的需求进"等你处理"**:需求说明确认超过 30 分钟还没开始,或者上次启动没成功,插件里显示"还没开始开发",点"开始开发"即可:在项目主目录中会新建(或接着用)需求自己的工作区,进展发回谈需求的那个对话。
14
+
15
+ ### 升级须知
16
+
17
+ - `req api list --all` 也会列出对齐中的需求(没有运行状态);`attention` 新增 `not_started`,`waiting` 新增 `canStart`;`req api run` 新增 `start`;需求索引新增可选的 `chatAgentId`;新增 `~/.req-kit/worktrees/` 记录。
18
+ - 更新后请在 Paseo 中重新加载插件(`req upgrade` 会自动做)。
19
+
20
+ ## 1.13.0
21
+
22
+ 稳定版:内网能装能用、在 Paseo 网页端审查原型、不靠看图检查界面、看得见 token 花在哪。内容包括 1.13.0-beta.1 至 beta.5(详见下面各条)和下面新增的三项,设计决定见 ADR 0032、0033。
23
+
24
+ ### 使用者会看到的变化
25
+
26
+ - **内网安装和文档**:`npx -y [--registry=<镜像地址>] paseo-req-kit@latest install` 一条命令装好,只需要 npm 源;`req docs` 在本机打开文档站点。
27
+ - **在 Paseo 里审查原型**:需求页面和"等你处理"里的主按钮"审查原型并确认方案",在原型上点要改的地方、选改法、用自己的话补一句,提交后整理成清单交给需求会话。
28
+ - **界面检查 `req ui-check`**:用本机的 Edge 或 Chrome,用文字报告显示不全、遮挡、超出屏幕、文字太淡、没名字的控件、报错,并和确认过的原型对文字;功能验证自动使用,不需要模型看图。
29
+ - **token**:`req stats` 和插件的"token 花在哪";可选的每个需求预算(`budget`);开始时和 `req status` 里的"预计还要多久"。
30
+ - **后台进程意外退出时由插件直接继续**,不再每小时叫醒对话;等待批准的权限进"等你处理"。
31
+ - **新增:打断大目录搜索**(`scanGuard`,默认开):步骤 agent 搜索整个磁盘、用户目录、Maven 本地仓库(`~/.m2`)、Gradle 缓存、全局或整个 `node_modules` 时,流水线一分钟后打断这个命令,并告诉 agent 改用什么办法(`git grep`、按坐标直接打开那一个依赖、`mvn dependency:tree`);任何搜索运行超过 5 分钟也打断。构建和测试命令不受影响。各步骤的提示词也写明了只在仓库里搜索。
32
+ - **新增:预算不起作用时会说**:agent 不报告 token 用量时,设了预算的需求在第一步之后提醒一次"预算不会起作用",`req doctor` 也会检查。
33
+ - **新增:从别的电脑打开审查页时说明原因**:审查页默认只在运行 Paseo 的电脑上能打开;从别的电脑打开 Paseo 网页、点"审查原型"时,页面写明为什么看不到、怎么设置。只设 `REQ_KIT_REVIEW_HOST` 时,审查页地址用这台电脑的计算机名。
34
+
35
+ ### 升级须知
36
+
37
+ - 新配置都可选:`budget`(默认不限)、`scanGuard`(默认开:`{ "enabled": true, "minutes": 5, "paths": [] }`,关闭写 `"enabled": false`)。
38
+ - 打开 `scanGuard` 时,流水线每分钟查看一次步骤 agent 的状态(以前每 10 分钟),只是读取状态,不占模型 token。
39
+ - 原型按新约定写(`data-ac`、`data-label`、`<section data-state>`、不写脚本),旧原型照常能看,只是审查页认不出区域和状态。
40
+ - 1.12 时预约的"对话定时检查"会留到过期(最多 3 天),装了插件后新需求不再预约,不用手动删。
41
+ - 装过插件的,`req upgrade` 会一并更新插件。新的安装脚本只能安装 1.13 及以后的版本。
42
+ - 状态文件、使用记录和 `req api` 的新增字段见各 beta 的升级须知;`recoveries` 新增 `stop_scan`。
43
+ - 发布前 Linux 和 Windows 的测试都必须通过。
44
+
5
45
  ## 1.13.0-beta.5
6
46
 
7
47
  ### 使用者会看到的变化
@@ -22,12 +22,12 @@
22
22
  | --- | --- |
23
23
  | `req api version` | `{ kitVersion, kitRoot, apiSchema, paseo, featuresDir }`:`paseo` 是这个版本支持的 Paseo 范围 |
24
24
  | `req api list [--repo <目录>]` | 这个仓库所有工作区中的需求,数组,每项见下面的"需求" |
25
- | `req api list --all` | 本机记录过的所有需求([需求索引](usage-record.md)),同上;目录已不在的 `present: false` |
25
+ | `req api list --all` | 本机记录过的所有需求([需求索引](usage-record.md)),同上;目录已不在的 `present: false`。1.14 起还包括:用 `--track` 登记、还在对齐中的需求(`stage` 为"对齐中",没有 `spec.md` 时 `title` 取自 `alignment.md`),以及索引里出现过的项目中没登记的需求 |
26
26
  | `req api status <需求> [--repo <目录>]` | 一个需求的详情,见下面的"详情" |
27
27
  | `req api file <需求> <文件> [--repo <目录>]` | `{ featureDir, file, size, truncated, content }`;只能读取功能目录中的文档(见下),最多 512 KB |
28
28
  | `req api stats [--since 30d] [--month 2026-10]` | `req stats --json` 的内容,见 [使用记录](usage-record.md) |
29
29
  | `req api index [--rebuild <目录>…] [--prune]` | 不带选项:索引的全部条目;`--rebuild`:`{ scanned, registered, pruned, total }` |
30
- | `req api run <approve\|stop\|resume\|verify\|sync\|reply> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1 |
30
+ | `req api run <approve\|stop\|resume\|verify\|sync\|reply\|start> <需求> [--repo <目录>] [选项]` | `{ command, featureDir, ok, exitCode, message }`。在 `--repo`(默认当前目录)中执行与 `req <命令> <需求>` 完全相同的操作;`ok` 为 false 时退出码为 1。`start`(1.14):在项目主目录中自动加 `--worktree`(上次已建好工作区的接着用),并把进展发回登记时的对话 |
31
31
  | `req api models` | 1.12 起。`{ current, choices, recommended }`:本机配置中现在的 `agents`;daemon 上能无人值守运行的选择(`{ id, label, provider, profile }`,Paseo profile 在前);按推荐的 `agents`。连不上 daemon 时报错 |
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`,原文件备份 |
@@ -52,8 +52,8 @@
52
52
  | `status` | 状态文件的 `status`;还没运行过为 null |
53
53
  | `running` | 后台进程是否在运行 |
54
54
  | `phase` | `develop` / `verify` |
55
- | `attention` | 需要人处理的事:`approval`(确认方案)、`business`(需求方决定)、`permission`(1.13,步骤在等权限批准;`waiting.questions` 是要批准的操作,`waiting.agentId` 是那个 agent)、`interrupted`(中断,可以继续)、`not_completed`(结束但未完成);没有为 null |
56
- | `waiting` | 1.12 起。`attention` 不为 null 时:`since`(从什么时候开始等)、`questions`(需求方要回答的问题)、`reason`(停止原因)、`card`(`{ system, id, url, requester }`,从卡片开始的需求)、`canReply`(能否用 `req api run reply <需求> <回复>` 把回复转给需求的对话);否则为 null |
55
+ | `attention` | 需要人处理的事:`approval`(确认方案)、`business`(需求方决定)、`permission`(1.13,步骤在等权限批准;`waiting.questions` 是要批准的操作,`waiting.agentId` 是那个 agent)、`interrupted`(中断,可以继续)、`not_completed`(结束但未完成)、`not_started`(1.14,需求说明已确认超过 30 分钟、或已为它建好工作区,但流水线没有运行过);没有为 null |
56
+ | `waiting` | 1.12 起。`attention` 不为 null 时:`since`(从什么时候开始等)、`questions`(需求方要回答的问题)、`reason`(停止原因)、`card`(`{ system, id, url, requester }`,从卡片开始的需求)、`canReply`(能否用 `req api run reply <需求> <回复>` 把回复转给需求的对话)、`canStart`(1.14,`not_started` 时为 true:可以用 `req api run start <需求>` 开始开发);否则为 null |
57
57
  | `current` | 正在运行的步骤 `[{ key, title, agentId }]` |
58
58
  | `updatedAt` | 最近一次变化的时间 |
59
59
  | `usage` | `{ steps, agentMinutes, inputTokens, outputTokens, costUsd }`;没运行过为 null |
@@ -21,7 +21,7 @@
21
21
  | `stopKind` | string \| null | 可选,1.12 起。停下等人的原因代码:必要的决定 `approval`(确认方案)、`phase_gate`(研发完成,等待测试)、`business`(需求方决定)、`reproduce`(缺陷复现不了);中断 `timeout`、`agent_error`、`invalid_result`、`transient`、`stopped`、`process_exit`、`budget`(1.13,用完 token 预算)、`other`。运行中或完成时为 null |
22
22
  | `budget` | object | 可选,1.13 起,配置了 `budget.tokens` 时。`{ allowance, warnedAt }`:停下前允许用到的 tokens(预算,每次用完后继续再加一份);已经提醒过的那一档 |
23
23
  | `waitingPermission` | object \| null | 可选,1.13 起。步骤 agent 在等权限批准时:`{ agentId, stepTitle, requests, since }`;没有在等时为 null |
24
- | `recoveries` | array | 可选,1.12 起。流水线自己处理掉的问题(最多保留 200 条):`at`、`key`(步骤键)、`kind`(`retry` 重试 / `reattach` 接着等上一次的 agent / `reuse_result` 沿用上一次 agent 的结果 / `extend_timeout` 超时后多等一轮 / `redo` 新开 agent 重做 / `fallback` 换备用模型 / `auto_resume` 预约自动继续 / `quota_wait` 预约在额度恢复后继续 / `self_answer` 方案审核的问题由 builder 自答)、`detail` |
24
+ | `recoveries` | array | 可选,1.12 起。流水线自己处理掉的问题(最多保留 200 条):`at`、`key`(步骤键)、`kind`(`retry` 重试 / `reattach` 接着等上一次的 agent / `reuse_result` 沿用上一次 agent 的结果 / `extend_timeout` 超时后多等一轮 / `redo` 新开 agent 重做 / `fallback` 换备用模型 / `auto_resume` 预约自动继续 / `quota_wait` 预约在额度恢复后继续 / `self_answer` 方案审核的问题由 builder 自答 / `stop_scan` 打断了搜索大目录的命令,1.13 起)、`detail` |
25
25
  | `profile` | `quick` \| `standard` | 档位 |
26
26
  | `branch` | string \| null | 工作分支,启动时确定 |
27
27
  | `milestones` | string[] | 已发送的进展通知,续跑时不重复发送 |
@@ -19,7 +19,11 @@
19
19
  }
20
20
  ```
21
21
 
22
- `key` 由对话所在的目录(没有自己的工作区时即 `repo`)和功能目录算出。`req index --rebuild` 为 1.9 之前运行过的需求补登,`req index --prune` 移除目录已不在的。
22
+ `key` 由对话所在的目录(没有自己的工作区时即 `repo`)和功能目录算出。1.14 起:开始谈需求时(`start-pipeline --feature <功能目录> --track`)就登记,此时多一个可选字段 `chatAgentId`(谈需求的对话),之后的登记保留它;新建需求自己的工作区时,条目改为指向那个工作区。目录里有 `spec.md`、`alignment.md` 或 `.pipeline/` 下的状态或登记标记时算作存在。
23
+
24
+ ## 新建工作区的记录 `worktrees/<key>.json`(1.14)
25
+
26
+ 用 `--worktree` 启动时,工作区一建好就写下:`{ schema, sourceRepo, featureDir, repo, workspaceId, branch, baseRef, createdAt, originAgentId }`。之后的启动失败了,再次启动时在这个工作区里接着做,不再新建;插件据此把"工作区已建、开发没开始"的需求列进"等你处理"。`req index --rebuild` 为 1.9 之前运行过的需求补登,`req index --prune` 移除目录已不在的。
23
27
 
24
28
  ## 使用记录 `usage/runs.jsonl`
25
29
 
@@ -62,6 +62,8 @@ req-kit 帮你把一个需求从"我想要……"做到"可以验收":你在 P
62
62
 
63
63
  装了 req-kit 插件的话(见 [安装与升级](安装与升级.md) 第 13 节),在 Paseo 侧边栏点"需求":本机所有项目的需求一页看完,"需要你处理"的排在最前;点开一个需求能看到每一步的进度和用时、读需求说明、方案、确认预览和完成报告,也可以直接确认方案、停止、继续或开始测试验证。网页端、桌面版和手机上都能用。
64
64
 
65
+ 1.14 起,需求从开始谈的时候就出现在插件里(状态"对齐中");需求说明确认了却还没开始开发(超过 30 分钟),或者上次启动没成功的,会出现在"等你处理"里,点"开始开发"就能开始,进展照常发回谈需求的那个对话。
66
+
65
67
  1.12 起,首页最上面是"等你处理":所有等你确认方案、回答问题、中断了的需求,等得最久的在前,每条写明要做什么、等了多久。方案可以直接确认,也可以写修改意见;方案审核的问题可以直接写回答,插件会转给这个需求的对话,效果和在对话里回答一样。首页的"配置"列出 req-kit 的全部设置:每一项是什么、现在的值、来自本机、团队还是项目的配置文件,可以按项目查看、搜索、只看改过的。
66
68
 
67
69
  **在原型上提意见(1.13 起)。** 界面改动的需求,在请你确认方案时会有一份界面原型。在插件首页的"等你处理"或需求页面最上面,点**"审查原型并确认方案"**按钮(终端里是 `req review <需求>`),原型审查页就在 Paseo 里打开(手机上会在浏览器中打开;嫌窗口小时点右上角"在新窗口打开"):
@@ -39,7 +39,7 @@
39
39
  3. **写 `CHANGELOG.md`**:新增一节,分"使用者会看到的变化"和"升级须知"两部分。升级须知写清:要不要重跑 `/req-setup`、提示词有哪些变化(影响用 `prompts.local/` 覆盖过的人)、文件格式有没有变化、旧行为的兼容方式。
40
40
  4. **验证**:`npm test`、`npm run typecheck`、`npm run eval:self-check`;步骤提示词或引擎有改动时跑 `npm run eval`,把 `summary.md` 复制到 `evals/baselines/<版本>.md`;入口或手册有改动时跑对话评估,记入记分表。主版本发布前,在一个真实项目里试运行一周。
41
41
  5. **冒烟测试**:在一台干净的机器(或临时的 `REQ_KIT_HOME` 和 HOME)上按 [安装与升级](安装与升级.md) 安装,跑冒烟测试第 1–4 项;至少有一台是 Windows。
42
- 6. **合并到 `main`,标签自动打上**:`main` 收到 `package.json` 版本号还没有标签的提交时,`.github/workflows/release.yml` 先在这个提交上跑测试、类型检查和评估自检,通过后才打上同名标签(不用自己打,也不需要推送标签的权限),接着在同一次运行里发布。测试没通过时不会留下标签,把修复合到 `main` 就会用同一个版本号重新发布(1.12.1 起;1.13 起任何推到 `main` 的提交都会检查,不必再改 `package.json`)。也可以照旧自己打:在 GitHub 上建 Release 并新建标签 `<版本>`,或推送 `git tag -a <版本>`,同样会触发发布。发布流程:跑测试、类型检查和评估自检,核对标签与 `package.json` 版本一致,然后发布到 npm(预发布版用 `beta` 标签,正式版用 `latest`),正式版再设置 `paseo-<小版本>` 标签,把 `npm pack` 的安装包附在 Release 上,Release 正文取自 CHANGELOG 的这一节。标签同时也是 git 方式安装的版本列表,所以改版本号的提交要在验证完成后再合到 `main`(自动打标签的时机就是它);打错了尽快删除标签,npm 上发布错的版本用 `npm deprecate` 标注(npm 不允许重新发布同一版本号)。
42
+ 6. **合并到 `main`,标签自动打上**:`main` 收到 `package.json` 版本号还没有标签的提交时,`.github/workflows/release.yml` 先在这个提交上跑测试(Linux 和 Windows 各一遍,1.13 起)、类型检查和评估自检,通过后才打上同名标签(不用自己打,也不需要推送标签的权限),接着在同一次运行里发布。测试没通过时不会留下标签,把修复合到 `main` 就会用同一个版本号重新发布(1.12.1 起;1.13 起任何推到 `main` 的提交都会检查,不必再改 `package.json`)。也可以照旧自己打:在 GitHub 上建 Release 并新建标签 `<版本>`,或推送 `git tag -a <版本>`,同样会触发发布。发布流程:跑测试、类型检查和评估自检,核对标签与 `package.json` 版本一致,然后发布到 npm(预发布版用 `beta` 标签,正式版用 `latest`),正式版再设置 `paseo-<小版本>` 标签,把 `npm pack` 的安装包附在 Release 上,Release 正文取自 CHANGELOG 的这一节。标签同时也是 git 方式安装的版本列表,所以改版本号的提交要在验证完成后再合到 `main`(自动打标签的时机就是它);打错了尽快删除标签,npm 上发布错的版本用 `npm deprecate` 标注(npm 不允许重新发布同一版本号)。
43
43
  7. **通知**:把"使用者会看到的变化"发给使用者,把"升级须知"发给各台机器的安装者;团队配置的 `requiredVersion` 需要提高时,在团队配置仓库中修改。
44
44
 
45
45
  **npm 发布的一次性设置**:可信发布(trusted publisher)要在 npm 上已有的包的设置页里配置,所以第一个版本在本机手工发布一次,不需要生成任何口令:
@@ -47,7 +47,7 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
47
47
 
48
48
  为了让审查页能用,方案设计按约定写原型:每个改动区域标 `data-ac`(对应的 AC)和 `data-label`(用需求方的话说是什么),各种状态包在 `<section data-state="状态名">` 中,按钮、标题、字段名写最终的文字。原型里的脚本不会运行。
49
49
 
50
- 审查页由一个本机服务提供:用到时在后台启动,空闲 4 小时后自己退出,只监听 `127.0.0.1`,地址中带随机令牌(插件设为只读时给只读的地址)。审查页允许被本机地址上的页面(Paseo 网页端)嵌入;要限定或放开,设 `REQ_KIT_REVIEW_FRAME_ANCESTORS`(CSP `frame-ancestors` 的值)。需求方在别的电脑上用浏览器访问 Paseo 网页端时,在运行 Paseo daemon 的机器上设置环境变量 `REQ_KIT_REVIEW_HOST`(例如 `0.0.0.0`)、`REQ_KIT_REVIEW_PUBLIC_URL`(别人访问这台机器用的地址,例如 `http://10.1.2.3:6781`),需要时用 `REQ_KIT_REVIEW_PORT` 换端口(默认 6781),然后重启 Paseo daemon 让插件拿到新的环境变量。
50
+ 审查页由一个本机服务提供:用到时在后台启动,空闲 4 小时后自己退出,只监听 `127.0.0.1`,地址中带随机令牌(插件设为只读时给只读的地址)。审查页允许被本机地址上的页面(Paseo 网页端)嵌入;要限定或放开,设 `REQ_KIT_REVIEW_FRAME_ANCESTORS`(CSP `frame-ancestors` 的值)。需求方在别的电脑上用浏览器访问 Paseo 网页端时,在运行 Paseo daemon 的机器上设置环境变量 `REQ_KIT_REVIEW_HOST`(例如 `0.0.0.0`)、`REQ_KIT_REVIEW_PUBLIC_URL`(别人访问这台机器用的地址,例如 `http://10.1.2.3:6781`),需要时用 `REQ_KIT_REVIEW_PORT` 换端口(默认 6781),然后重启 Paseo daemon 让插件拿到新的环境变量(只设 `REQ_KIT_REVIEW_HOST` 时,地址用这台电脑的计算机名)。没有设置时从别的电脑打开 Paseo 网页、点"审查原型",页面会说明为什么打不开、该怎么设置(1.13)。
51
51
 
52
52
  ## 代码评审分两个方向
53
53
 
@@ -267,6 +267,8 @@ spec 头部的 `- 类型:` 决定流水线走哪种做法(1.8 起)。缺
267
267
 
268
268
  写在本机、团队或项目配置中都可以,例如 `"budget": { "tokens": 3000000 }`。还没有数据时,先不设预算跑几个需求,用 `req stats` 的"token 花在哪"看每个需求的中位数,再按它的两三倍设。
269
269
 
270
+ agent 不报告 token 用量时(有些模型或接入方式不报告),预算不会起作用:设了预算的需求在第一个步骤结束后会在对话里说明一次,流水线照常继续;`req doctor` 也会检查本机最近的需求有没有用量数据。
271
+
270
272
  用时估算不用配置:本机已完成的同档位需求有 3 个以上时,开始开发的回复里写出"agent 合计用时和 token 的中位数"作为参考;`req status` 和插件的需求页面显示"预计还要约多久"(按中位数减去已用的 agent 时间,不含等人的时间),超过中位数时如实说"已超过通常的用时"。
271
273
 
272
274
  ## 并行编排:确认前审阅、方案提案、浏览器验证
@@ -283,6 +285,11 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
283
285
 
284
286
  在项目主目录、主分支上发起的需求,可以用 `--worktree` 启动(`/req` 中的"启动(自动新建工作区)"):流水线从当前分支切出 `feature/<功能目录名>`,在 Paseo 中新建 worktree 工作区,把需求说明移过去并提交,之后的步骤都在那个工作区中运行,仍然挂在发起的对话下。对话的工作目录不变,查看进度、暂停、继续都在原对话中进行;读写需求文件用通知中给出的完整路径。只能在第一次启动时选择。见 ADR 0018。
285
287
 
288
+ 1.14 起:
289
+ - **启动没成功也能接着做。** 工作区一建好就记下来(`~/.req-kit/worktrees/`)。之后不管哪一步失败(建工作区慢、启动前的检查没通过),再次执行启动命令都会在这个工作区里接着开始,不会报"分支已存在",也不会留下只有需求说明的分支。新建工作区最多等 15 分钟(以前是 2 分钟,大仓库在 Windows 上常常不够)。以前版本留下的"分支和工作区都在、开发没开始"的,也会由 Paseo 接手那个目录接着做。
290
+ - **访谈改过的 `GLOSSARY.md`、`docs/adr/` 一起带过去**,和需求说明在需求自己的分支上一起提交;主目录里的这些未提交改动保留不动(可能和别的改动在一起),确认后可以撤销。
291
+ - **如实提示起点的问题**:需求说明已经提交在主分支上、本地主分支比远端落后、主目录里还有别的未提交改动时,启动结果里会写明。流水线不改动主分支。
292
+
286
293
  ## 调度:预约、提醒、自动继续、排队
287
294
 
288
295
  需要"以后再做"的事,流水线用 Paseo 的定时任务把一条写明步骤的消息发回发起的对话,由对话 agent 执行(机器或 daemon 重启后仍然有效,在 Paseo 的 Schedules 中可以看到)。暂停命令会取消这些预约。
@@ -526,7 +533,7 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
526
533
 
527
534
  **结果文件**:每步必须写一个 JSON 结果文件,流水线只看它,不看 agent 的回复文字。文件缺失或格式不对时,会在同一个会话里提醒 agent 补写,最多 2 次,仍不对则判定该步骤失败。
528
535
 
529
- **等待与超时**:每步有总时限(`stepTimeoutMinutes`)。到时 agent 还在做时,先再给它同样长的时间(`selfHeal`,见下);仍未完成才判定失败,但不停止 agent。继续(`--resume`、自动重试)时先看这一步上一次的 agent:已经做完、结果文件有效就直接用它的结果;还在做就接着等它一轮(这一轮不再额外延长);第二次仍超时才停止它、另起一个 agent。需求改过、开始了新一次尝试时,上一次留下的 agent 直接停掉,不接手(1.12.1)。同一步不会有两个 agent 同时在做(它们会改同样的文件、在同一个工作区提交、写同一个结果文件)。连续读不到 agent 状态时同样处理。等待被拆成每段 10 分钟,连接断开时自动重试。agent 等待权限批准时,同一个请求只通知一次,然后继续等待。
536
+ **等待与超时**:每步有总时限(`stepTimeoutMinutes`)。到时 agent 还在做时,先再给它同样长的时间(`selfHeal`,见下);仍未完成才判定失败,但不停止 agent。继续(`--resume`、自动重试)时先看这一步上一次的 agent:已经做完、结果文件有效就直接用它的结果;还在做就接着等它一轮(这一轮不再额外延长);第二次仍超时才停止它、另起一个 agent。需求改过、开始了新一次尝试时,上一次留下的 agent 直接停掉,不接手(1.12.1)。同一步不会有两个 agent 同时在做(它们会改同样的文件、在同一个工作区提交、写同一个结果文件)。连续读不到 agent 状态时同样处理。等待被拆成每段 10 分钟(打开 `scanGuard` 时每段 1 分钟,以便查看正在运行的命令),连接断开时自动重试。agent 等待权限批准时,同一个请求只通知一次,然后继续等待。
530
537
 
531
538
  **临时错误自动重试**:步骤因限流(429)、服务暂时不可用(502/503/504/529、overloaded)、网络中断等临时错误失败时,等待后用新的 agent 重做这一步,并告诉它工作区里可能留有上一次没做完的改动。默认最多重试 2 次,分别等 1 分钟和 5 分钟:
532
539
 
@@ -554,6 +561,14 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
554
561
 
555
562
  自动处理的每一次都记在进度日志、状态文件的 `recoveries` 和使用记录中,`req stats` 的"人工介入"一节汇总:停下等人的原因、自动处理了多少次、到达提测的需求中没有非必要介入的比例。
556
563
 
564
+ **打断大目录搜索**(1.13,`scanGuard`,默认开):步骤 agent 有时为了找一个类或一个包,去搜索整个磁盘、用户目录、Maven 本地仓库(`~/.m2`)、Gradle 缓存、全局 `node_modules` 或整个 `node_modules`,一次就是几十分钟,这一步迟迟做不完。步骤运行时,流水线大约每分钟看一次 agent 正在运行的命令:搜索这类大目录的命令运行超过 1 分钟、或者任何搜索命令运行超过 `minutes` 分钟,就打断它,并告诉 agent 改用什么办法(在仓库里用 `git grep`;按 `pom.xml` 里的坐标直接打开那一个依赖目录、用 `unzip -l` 看那一个 jar;`mvn dependency:tree`;找不到就写进"假设"继续)。构建和测试命令(`mvn`、`gradle`、`npm test` 等)不打断;只搜具体的子目录(如 `~/.m2/repository/com/acme/foo`)或排除了 `node_modules` 的搜索也不算。同一个命令只打断一次,每轮最多打断 5 次;每次都记在进度日志和 `recoveries`(`stop_scan`)中。各步骤的提示词里也写明了只在仓库里搜索。
565
+
566
+ ```json
567
+ "scanGuard": { "enabled": true, "minutes": 5, "paths": ["D:/share"] }
568
+ ```
569
+
570
+ `paths`:另外不允许整个搜索的目录(例如共享盘);`enabled: false` 关闭。
571
+
557
572
  **同一个目录一次只运行一条流水线**,因为各步骤都在这个目录中修改和提交。需要并行时,为每个需求使用单独的 worktree 工作区。
558
573
 
559
574
  ## 注意事项
@@ -65,6 +65,8 @@ time step ref decision alternatives reason kind needs_human
65
65
 
66
66
  **遵循项目已有的做法。** 开始前阅读 `CLAUDE.md`、`AGENTS.md`、`README`、`CONTRIBUTING`(存在哪个读哪个),以及 `GLOSSARY.md`(业务与领域术语;较早的项目可能叫 `CONTEXT.md`)和 `docs/adr/`(已有决定)。命名、目录结构、测试写法、提交信息格式都跟随项目现状。
67
67
 
68
+ **只在仓库里搜索,不要扫大目录。** 不要搜索整个磁盘、用户目录、依赖缓存(Maven 的 `~/.m2`、Gradle 的 `~/.gradle`、npm 缓存、全局 `node_modules`)或整个 `node_modules`,这类搜索要几十分钟,流水线会在一分钟后打断它。在仓库里找用 `git grep -n` 或 `git ls-files`。要看某个依赖:先从 `pom.xml`、`build.gradle`、`package.json` 确定它的坐标和版本,再直接打开那一个目录(如 `~/.m2/repository/<groupId 的点换成 />/<artifactId>/<版本>/`、`node_modules/<包名>/`),用 `unzip -l`、`jar tf` 看那一个 jar。不知道类在哪个依赖里时,`mvn dependency:tree` 比搜索快。
69
+
68
70
  **只做这个需求。** 不顺手重构无关代码,不升级无关依赖,不改无关的格式。看到无关的问题可以在产物里提一句,留给人来决定。
69
71
 
70
72
  **多轮的步骤。** `<pipeline-context>` 写明"第 2 轮"或之后时,说明这一步之前已经执行过:
@@ -85,3 +85,26 @@ export function describeActivity(items: ActivityItem[], now = new Date()): strin
85
85
  return ` ${when ? `${when}:` : ""}${a.text}`;
86
86
  });
87
87
  }
88
+
89
+ /**
90
+ * 1.13: the tool call still running at the end of the timeline (for the scan guard): its
91
+ * command or search pattern and when it started (the first entry seen for that call).
92
+ */
93
+ export function runningToolOf(entries: TimelineEntry[]): import("./scan-guard.js").RunningTool | null {
94
+ const calls = new Map<string, { first: string; last: Record<string, unknown> }>();
95
+ for (const entry of entries) {
96
+ const it = entry.item as Record<string, unknown> | null;
97
+ if (!it || typeof it !== "object" || it.type !== "tool_call" || typeof it.callId !== "string") continue;
98
+ const seen = calls.get(it.callId);
99
+ if (seen) seen.last = it;
100
+ else calls.set(it.callId, { first: entry.timestamp, last: it });
101
+ }
102
+ const running = [...calls.entries()].filter(([, c]) => c.last.status === "running");
103
+ if (running.length === 0) return null;
104
+ const [callId, call] = running[running.length - 1];
105
+ const detail = (call.last.detail ?? {}) as Record<string, unknown>;
106
+ const kind = String(detail.type ?? "unknown");
107
+ const raw = kind === "shell" ? detail.command : kind === "search" ? detail.query : (detail.command ?? "");
108
+ const text = Array.isArray(raw) ? raw.join(" ") : String(raw ?? "");
109
+ return { kind, text, startedAt: call.first, callId };
110
+ }
package/engine/src/api.ts CHANGED
@@ -26,6 +26,7 @@ import { FEATURES_DIR, findWorktreeRun, listFeatures, resolveFeatureArg, summari
26
26
  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
+ import { listWorktreeRecords } from "./worktree.js";
29
30
  import { isAlive, type PipelineState, readState, statePath, type StepRecord } from "./state.js";
30
31
  import { parsePeriod, summarizeRuns } from "./stats.js";
31
32
  import { readRuns, runsFile } from "./records.js";
@@ -68,7 +69,7 @@ export const READABLE_FILES = [
68
69
 
69
70
  const MAX_FILE_BYTES = 512 * 1024;
70
71
 
71
- export type Attention = "approval" | "business" | "permission" | "interrupted" | "not_completed" | null;
72
+ export type Attention = "approval" | "business" | "permission" | "interrupted" | "not_completed" | "not_started" | null;
72
73
 
73
74
  /** What a person needs to do about a requirement, if anything. */
74
75
  export function attentionOf(state: Pick<PipelineState, "status" | "pid" | "outcome" | "waitingPermission"> | null): Attention {
@@ -139,14 +140,57 @@ export interface ListItem {
139
140
  canReply: boolean;
140
141
  /** 1.13, attention "permission": the step agent that waits for an approval. */
141
142
  agentId?: string;
143
+ /** 1.14, attention "not_started": the plugin can start development (req api run start). */
144
+ canStart?: boolean;
142
145
  } | null;
143
146
  }
144
147
 
148
+ /** 1.14: a confirmed spec with no run gets attention after this long (the chat may still be starting it). */
149
+ export const NOT_STARTED_AFTER_MS = 30 * 60_000;
150
+
151
+ function recordFor(repo: string, featureDir: string) {
152
+ let real = repo;
153
+ try {
154
+ real = fs.realpathSync(repo);
155
+ } catch {
156
+ // As given.
157
+ }
158
+ return listWorktreeRecords().find((r) => r.repo === real && r.featureDir === featureDir) ?? null;
159
+ }
160
+
161
+ /** A confirmed requirement nobody started: its own worktree already made (a start that failed), or just confirmed. */
162
+ function notStartedOf(repo: string, featureDir: string, confirmed: boolean, updatedAt: string, now = Date.now()): NonNullable<ListItem["waiting"]> | null {
163
+ if (!confirmed) return null;
164
+ const real = (() => {
165
+ try {
166
+ return fs.realpathSync(repo);
167
+ } catch {
168
+ return repo;
169
+ }
170
+ })();
171
+ const made = recordFor(real, featureDir);
172
+ if (!made && now - new Date(updatedAt).getTime() < NOT_STARTED_AFTER_MS) return null;
173
+ return {
174
+ since: made?.createdAt ?? updatedAt,
175
+ questions: [],
176
+ reason: made ? "新工作区已经建好,但开发没有开始(上次启动没有完成)。点\"开始开发\"会在这个工作区里接着开始。" : "需求说明已经确认,但开发还没有开始。",
177
+ card: null,
178
+ canReply: false,
179
+ canStart: true,
180
+ };
181
+ }
182
+
145
183
  function item(repo: string, branch: string | null, featureDir: string, extra: { sourceRepo: string | null; workspaceId: string | null; project: string }): ListItem | null {
146
184
  const name = path.basename(featureDir);
147
185
  const summary = summarize({ path: repo, branch }, name);
148
186
  if (!summary) return null;
149
187
  const state = readStateSafe(repo, featureDir);
188
+ const notStarted = state ? null : notStartedOf(repo, summary.featureDir, summary.confirmed, summary.updatedAt);
189
+ // 1.14: a worktree made for it before the pipeline ran: it still belongs to the chat's checkout.
190
+ if (!state && !extra.sourceRepo) {
191
+ const made = recordFor(repo, summary.featureDir);
192
+ if (made) extra = { ...extra, sourceRepo: made.sourceRepo, workspaceId: extra.workspaceId ?? made.workspaceId, project: path.basename(made.sourceRepo) };
193
+ }
150
194
  return {
151
195
  key: requirementKey(extra.sourceRepo ?? repo, featureDir),
152
196
  project: extra.project,
@@ -163,12 +207,12 @@ function item(repo: string, branch: string | null, featureDir: string, extra: {
163
207
  status: state?.status ?? null,
164
208
  running: !!state && state.status === "running" && isAlive(state.pid),
165
209
  phase: state?.phase ?? null,
166
- attention: attentionOf(state),
210
+ attention: attentionOf(state) ?? (notStarted ? "not_started" : null),
167
211
  current: currentSteps(state),
168
212
  updatedAt: summary.updatedAt,
169
213
  usage: usageOf(state),
170
214
  present: true,
171
- waiting: waitingOf(repo, featureDir, state),
215
+ waiting: waitingOf(repo, featureDir, state) ?? notStarted,
172
216
  hasPrototype: fs.existsSync(path.join(repo, featureDir, "prototype")),
173
217
  };
174
218
  }
@@ -206,7 +250,8 @@ export function apiList(input: { repo: string; all: boolean }): ListItem[] {
206
250
  .filter((x): x is ListItem => x !== null);
207
251
  }
208
252
  const out: ListItem[] = [];
209
- for (const e of readIndex()) {
253
+ const index = readIndex();
254
+ for (const e of index) {
210
255
  const found = e.present ? item(e.repo, null, e.featureDir, { sourceRepo: e.sourceRepo, workspaceId: e.workspaceId, project: e.project }) : null;
211
256
  out.push(
212
257
  found ?? {
@@ -234,9 +279,38 @@ export function apiList(input: { repo: string; all: boolean }): ListItem[] {
234
279
  },
235
280
  );
236
281
  }
282
+ // 1.14: requirements in projects this machine knows that were never registered (the chat did not
283
+ // run --track, or an older version): found in the projects' feature directories.
284
+ const seen = new Set(out.map((x) => x.key));
285
+ const projects = new Set(index.filter((e) => e.present).map((e) => e.sourceRepo ?? e.repo));
286
+ for (const dir of projects) {
287
+ let found: ReturnType<typeof listFeatures> = [];
288
+ try {
289
+ found = listFeatures(dir);
290
+ } catch {
291
+ continue;
292
+ }
293
+ for (const f of found) {
294
+ const state = readStateSafe(f.worktree, f.featureDir);
295
+ const source = state?.worktree?.sourceRepo ?? (samePathSafe(f.worktree, dir) ? null : dir);
296
+ const x = item(f.worktree, f.branch, f.featureDir, { sourceRepo: source, workspaceId: state?.worktree?.workspaceId ?? null, project: path.basename(dir) });
297
+ if (x && !seen.has(x.key)) {
298
+ seen.add(x.key);
299
+ out.push(x);
300
+ }
301
+ }
302
+ }
237
303
  return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
238
304
  }
239
305
 
306
+ function samePathSafe(a: string, b: string): boolean {
307
+ try {
308
+ return fs.realpathSync(a) === fs.realpathSync(b);
309
+ } catch {
310
+ return a === b;
311
+ }
312
+ }
313
+
240
314
  /** The repository and feature directory a requirement argument names, as `req status` resolves it. */
241
315
  export function locate(repoArg: string, feature: string): { repo: string; featureDir: string; sourceRepo: string | null } {
242
316
  if (!fs.existsSync(repoArg)) throw new ApiError("not_found", `目录不存在:${repoArg}`);
@@ -346,9 +420,35 @@ export function apiIndex(input: { rebuild: string[]; prune: boolean }) {
346
420
  return { scanned: [...dirs], registered, pruned, total: readIndex().length };
347
421
  }
348
422
 
349
- const RUNNABLE = ["approve", "stop", "resume", "verify", "sync", "reply"] as const;
423
+ const RUNNABLE = ["approve", "stop", "resume", "verify", "sync", "reply", "start"] as const;
350
424
 
351
425
  /** Runs `req <command> <需求>` where people would, and reports what it said. */
426
+ /**
427
+ * 1.14, starting from the plugin: like the chat would. In a project's main checkout the
428
+ * requirement gets its own worktree (or continues in the one an earlier start made); the
429
+ * pipeline reports to the chat the requirement was aligned in.
430
+ */
431
+ export function startOptions(cwd: string, feature: string): string[] {
432
+ const out: string[] = [];
433
+ const git = (...a: string[]) => spawnSync("git", a, { cwd, encoding: "utf8", windowsHide: true });
434
+ const dirs = git("rev-parse", "--git-dir", "--git-common-dir");
435
+ if (dirs.status === 0) {
436
+ const [own, common] = dirs.stdout.trim().split(/\r?\n/).map((d) => path.resolve(cwd, d.trim()));
437
+ const branch = git("branch", "--show-current").stdout.trim();
438
+ // In the main checkout, unless the requirement's own branch is checked out here ("develop here").
439
+ if (own === common && branch !== `feature/${path.basename(feature)}`) out.push("--worktree");
440
+ }
441
+ let featureDir = feature;
442
+ try {
443
+ featureDir = resolveFeatureArg(cwd, feature);
444
+ } catch {
445
+ // The launcher reports it.
446
+ }
447
+ const chat = readIndex().find((e) => e.key === requirementKey(cwd, featureDir))?.chatAgentId;
448
+ if (chat) out.push("--origin-agent", chat);
449
+ return out;
450
+ }
451
+
352
452
  export function apiRun(command: string, repoArg: string, feature: string, extra: string[]) {
353
453
  if (!(RUNNABLE as readonly string[]).includes(command)) throw new ApiError("not_allowed", `只能执行:${RUNNABLE.join("、")}`);
354
454
  if (!fs.existsSync(repoArg)) throw new ApiError("not_found", `目录不存在:${repoArg}`);
@@ -357,6 +457,7 @@ export function apiRun(command: string, repoArg: string, feature: string, extra:
357
457
  const env = { ...process.env };
358
458
  delete env.PASEO_AGENT_ID;
359
459
  delete env.PASEO_AGENT_CWD;
460
+ if (command === "start") extra = [...extra, ...startOptions(cwd, feature)];
360
461
  const r = spawnSync(process.execPath, [path.join(KIT_ROOT, "bin", "req.mjs"), command, feature, ...extra], {
361
462
  cwd,
362
463
  env,
@@ -11,6 +11,7 @@ export const USAGE = `用法:
11
11
  start-pipeline [--feature <功能目录>] --status 查看进度(不写 --feature 时:工作区里只有一个需求运行过就看它)
12
12
  start-pipeline --feature <功能目录> --stop 停止(之后可用 --resume 继续)
13
13
  start-pipeline --list 列出这个项目所有工作区中的需求
14
+ start-pipeline --feature <功能目录> --track 开始谈需求时登记它,Paseo 插件里从"对齐中"就能看到(1.14)
14
15
  start-pipeline --card-fields 团队对接了外部系统时,确认需求说明时要一并确认的卡片字段
15
16
  start-pipeline --feature <功能目录> --spec-review 确认前请另一个模型独立审阅需求说明草稿(后台进行,结果发回对话)
16
17
  start-pipeline --feature <功能目录> --worktree 为这个需求新建 Paseo worktree 工作区并在那里开发(对话目录不变)
@@ -54,6 +55,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
54
55
  stop: { type: "boolean", default: false },
55
56
  list: { type: "boolean", default: false },
56
57
  "card-fields": { type: "boolean", default: false },
58
+ track: { type: "boolean", default: false },
57
59
  "spec-review": { type: "boolean", default: false },
58
60
  worktree: { type: "boolean", default: false },
59
61
  "source-repo": { type: "string" },
@@ -124,6 +126,7 @@ export function parseLaunchArgs(argv: string[]): LaunchArgs {
124
126
  stop: values.stop ?? false,
125
127
  list: values.list ?? false,
126
128
  cardFields: values["card-fields"] ?? false,
129
+ track: values.track ?? false,
127
130
  specReview: values["spec-review"] ?? false,
128
131
  worktree: values.worktree ?? false,
129
132
  sourceRepo: values["source-repo"] ? path.resolve(values["source-repo"]) : null,
@@ -122,6 +122,11 @@ const CATALOG: Array<{ id: string; title: string; description: string; keys: Rec
122
122
  description: "一个需求所有步骤合计(输入 + 输出)最多用多少 tokens;用到一定比例时提醒,用完后做完当前步骤就停下,继续开发会再给一份预算。",
123
123
  display: (v) => { const b = v as PipelineConfig["budget"]; return b.tokens === null ? "不限" : `${b.tokens >= 10_000 ? `${Math.round(b.tokens / 1000) / 10} 万` : b.tokens},用到 ${b.warnPercent}% 时提醒`; },
124
124
  },
125
+ scanGuard: {
126
+ label: "打断大目录搜索",
127
+ description: "步骤 agent 搜索整个磁盘、用户目录、Maven 仓库(.m2)、全局 node_modules 等很大的目录时,一分钟后打断它并告诉它该怎么查;任何搜索运行太久也打断。",
128
+ display: (v) => { const g = v as PipelineConfig["scanGuard"]; return g.enabled ? `开,搜索超过 ${g.minutes} 分钟也打断${g.paths.length ? `;另外不许整个搜索:${g.paths.join("、")}` : ""}` : "关"; },
129
+ },
125
130
  },
126
131
  },
127
132
  {
@@ -245,6 +245,12 @@ export interface PipelineConfig {
245
245
  * pipeline stops to ask; null: no limit. A notice at warnPercent; continuing gives one more budget.
246
246
  */
247
247
  budget: { tokens: number | null; warnPercent: number };
248
+ /**
249
+ * 1.13: interrupt a step agent's search of a very large directory (the whole disk, the home
250
+ * directory, ~/.m2, the global node_modules, …) after a minute, and any search after `minutes`,
251
+ * and tell it how to find what it needs instead. `paths`: more directories never to search whole.
252
+ */
253
+ scanGuard: import("./scan-guard.js").ScanGuardSettings;
248
254
  /** Automatic retries of a step after a transient error (rate limit, dropped connection). */
249
255
  retry: { attempts: number; backoffSeconds: number[] };
250
256
  /**
@@ -355,6 +361,7 @@ export const DEFAULT_CONFIG: PipelineConfig = {
355
361
  factory: { pollMinutes: 5, titleTags: ["[AI]", "【AI】"], maxNewPerPoll: 3 },
356
362
  autoConfirm: { hours: 0 },
357
363
  budget: { tokens: null, warnPercent: 80 },
364
+ scanGuard: { enabled: true, minutes: 5, paths: [] },
358
365
  retry: { attempts: 2, backoffSeconds: [60, 300] },
359
366
  paseoServices: "auto",
360
367
  serviceReadySeconds: 120,
@@ -424,6 +431,7 @@ function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConf
424
431
  merged.factory = { ...base.factory, ...(isRecord(raw.factory) ? raw.factory : {}) } as PipelineConfig["factory"];
425
432
  merged.autoConfirm = { ...base.autoConfirm, ...(isRecord(raw.autoConfirm) ? raw.autoConfirm : {}) } as PipelineConfig["autoConfirm"];
426
433
  merged.budget = { ...base.budget, ...(isRecord(raw.budget) ? raw.budget : {}) } as PipelineConfig["budget"];
434
+ merged.scanGuard = { ...base.scanGuard, ...(isRecord(raw.scanGuard) ? raw.scanGuard : {}) } as PipelineConfig["scanGuard"];
427
435
  if (isRecord(raw.cardSource)) {
428
436
  const c = raw.cardSource;
429
437
  merged.cardSource = {
@@ -645,6 +653,10 @@ function validate(config: PipelineConfig): void {
645
653
  if (typeof config.budget.warnPercent !== "number" || config.budget.warnPercent <= 0 || config.budget.warnPercent >= 100) {
646
654
  throw new ConfigError("budget.warnPercent 必须在 1 到 99 之间(用到预算的百分之几时提醒)");
647
655
  }
656
+ const sg = config.scanGuard;
657
+ if (typeof sg.enabled !== "boolean") throw new ConfigError("scanGuard.enabled 必须是 true 或 false");
658
+ if (typeof sg.minutes !== "number" || !(sg.minutes >= 1)) throw new ConfigError("scanGuard.minutes 必须是至少 1 的分钟数(搜索运行多久后打断)");
659
+ if (!Array.isArray(sg.paths) || !sg.paths.every((p) => typeof p === "string" && p.trim())) throw new ConfigError('scanGuard.paths 必须是目录路径的数组,例如 ["D:/share"]');
648
660
  const f = config.factory;
649
661
  if (typeof f.pollMinutes !== "number" || !(f.pollMinutes >= 1)) throw new ConfigError("factory.pollMinutes 必须是至少 1 的分钟数");
650
662
  if (!Number.isInteger(f.maxNewPerPoll) || f.maxNewPerPoll < 1) throw new ConfigError("factory.maxNewPerPoll 必须是正整数");
@@ -5,7 +5,7 @@ import { describeActivity } from "./activity.js";
5
5
  import { describeExternal } from "./integrations.js";
6
6
  import { PHASE_MODE_LABELS, PROFILE_LABELS, type PipelineConfig, REQUIREMENT_TYPE_LABELS } from "./config.js";
7
7
  import type { AgentRuntime, RuntimeFactory } from "./runtime.js";
8
- import { isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
8
+ import { currentLockHolder, isAlive, type PipelineState, readState, type StepRecord, writeState } from "./state.js";
9
9
  import { stepTitle } from "./playbooks.js";
10
10
  import { describeTotals, totals } from "./usage.js";
11
11
 
@@ -195,6 +195,13 @@ export async function stopPipeline(input: StopInput): Promise<{ stopped: boolean
195
195
  latest.stopReason = wasAlive ? "已被手动停止" : "后台进程已意外退出";
196
196
  latest.stopKind = wasAlive ? "stopped" : "process_exit";
197
197
  writeState(input.statePath, latest);
198
+ // On Windows a stopped process gets no chance to remove the repository lock itself; this
199
+ // removes a lock whose holder is gone (anywhere, harmless when it was already removed).
200
+ try {
201
+ currentLockHolder(latest.repo);
202
+ } catch {
203
+ // The next start checks the lock the same way.
204
+ }
198
205
 
199
206
  // The step agent keeps working unless told otherwise; stop it so it changes nothing more.
200
207
  if (interrupted.some((r) => r.agentId)) {
@@ -18,6 +18,8 @@ import { defaultConfigPath, findDependency, KIT_ROOT, kitVersion } from "./locat
18
18
  import { PROJECT_DIR } from "./prompts.js";
19
19
  import { describeAgentProfile, ProfileResolutionError, profilesFromDaemonConfig, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
20
20
  import { isAlive, STATE_VERSION } from "./state.js";
21
+ import { readRuns, runsFile } from "./records.js";
22
+ import { byRequirement, dedupe } from "./stats.js";
21
23
  import { resolveCommand } from "./integrations.js";
22
24
 
23
25
  const counts = { fail: 0, warn: 0 };
@@ -160,6 +162,7 @@ function configuration(configPath: string, repo: string | null): PipelineConfig
160
162
  if (satisfies(kitVersion(), required)) report("ok", `版本要求 ${required}:当前 ${kitVersion()} 满足`);
161
163
  else report("warn", `团队或项目要求 req-kit ${required},当前为 ${kitVersion()}`, "按 docs/guide/安装与升级.md 升级到符合要求的版本");
162
164
  }
165
+ budgetUsage(loaded.config);
163
166
  return loaded.config;
164
167
  } catch (error) {
165
168
  if (!(error instanceof ConfigError)) throw error;
@@ -168,6 +171,21 @@ function configuration(configPath: string, repo: string | null): PipelineConfig
168
171
  }
169
172
  }
170
173
 
174
+ /** 1.13: a token budget only works when the agents report usage; this machine's recent requirements say whether they do. */
175
+ function budgetUsage(config: PipelineConfig): void {
176
+ if (config.budget.tokens === null) return;
177
+ let recent: ReturnType<typeof byRequirement> = [];
178
+ try {
179
+ recent = byRequirement(dedupe(readRuns(runsFile()))).slice(-3);
180
+ } catch {
181
+ return;
182
+ }
183
+ if (recent.length === 0) return;
184
+ if (recent.every((q) => q.inputTokens === null && q.outputTokens === null)) {
185
+ report("warn", `设了 token 预算(每个需求 ${config.budget.tokens}),但本机最近 ${recent.length} 个需求的 agent 都没有报告 token 用量,预算不会起作用`, "换用会报告用量的 agent;不需要预算时把 budget.tokens 设为 null");
186
+ } else report("ok", "token 预算:agent 会报告用量");
187
+ }
188
+
171
189
  /** Daemon address from the raw file, so the daemon can be checked even when the agents are not filled in yet. */
172
190
  function daemonSettings(configPath: string): PipelineConfig {
173
191
  try {
@@ -114,7 +114,27 @@ function mtime(file: string): number {
114
114
  export function summarize(worktree: Worktree, name: string): FeatureSummary | null {
115
115
  const dir = path.join(worktree.path, FEATURES_DIR, name);
116
116
  const specFile = path.join(dir, "spec.md");
117
- if (!fs.existsSync(specFile)) return null;
117
+ const alignmentFile = path.join(dir, "alignment.md");
118
+ if (!fs.existsSync(specFile)) {
119
+ // 1.14: still in the interview (registered with --track, or the notes have begun).
120
+ if (!fs.existsSync(alignmentFile) && !fs.existsSync(path.join(dir, ".pipeline", "tracked.json"))) return null;
121
+ const notes = fs.existsSync(alignmentFile) ? fs.readFileSync(alignmentFile, "utf8") : "";
122
+ const updated = Math.max(mtime(alignmentFile), mtime(path.join(dir, ".pipeline", "tracked.json")), mtime(dir));
123
+ return {
124
+ name,
125
+ featureDir: `${FEATURES_DIR.split(path.sep).join("/")}/${name}`,
126
+ worktree: worktree.path,
127
+ branch: worktree.branch,
128
+ title: (/^#\s+(.+)$/m.exec(notes)?.[1] ?? name).replace(/^(访谈记录|需求对齐|对齐记录)[::\s]*/, "").trim() || name,
129
+ type: null,
130
+ specVersion: null,
131
+ confirmed: false,
132
+ profile: null,
133
+ stage: "对齐中",
134
+ updatedAt: new Date(updated || Date.now()).toISOString(),
135
+ usage: null,
136
+ };
137
+ }
118
138
  const spec = fs.readFileSync(specFile, "utf8");
119
139
  const stateFile = path.join(dir, ".pipeline", "state.json");
120
140
  let state: RawState | null = null;