paseo-req-kit 1.13.0 → 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 (106) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/contracts/cli-json.md +4 -4
  3. package/contracts/usage-record.md +5 -1
  4. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
  5. 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 +5 -0
  6. package/engine/src/api.ts +106 -5
  7. package/engine/src/cli-args.ts +3 -0
  8. package/engine/src/features.ts +21 -1
  9. package/engine/src/launch.ts +25 -4
  10. package/engine/src/paseo-runtime.ts +5 -0
  11. package/engine/src/preflight.ts +2 -0
  12. package/engine/src/records.ts +24 -2
  13. package/engine/src/runtime.ts +2 -0
  14. package/engine/src/worktree.ts +180 -25
  15. package/package.json +1 -1
  16. package/paseo-plugin/client/format.ts +6 -2
  17. package/paseo-plugin/client/inbox.tsx +6 -1
  18. package/paseo-plugin/package.json +1 -1
  19. package/paseo-plugin/shared/contracts.ts +4 -2
  20. package/site-dist/404.html +2 -2
  21. package/site-dist/assets/{app.C6QQarr3.js → app.ByJ4x-zl.js} +1 -1
  22. package/site-dist/assets/changelog.md.BUTM13q-.js +1 -0
  23. package/site-dist/assets/chunks/@localSearchIndexroot.BJ01YY7h.js +1 -0
  24. package/site-dist/assets/chunks/{VPLocalSearchBox.NZKA1cjk.js → VPLocalSearchBox.LeFVc-_M.js} +1 -1
  25. package/site-dist/assets/chunks/{theme.BP9XiatK.js → theme.BMoBTK1B.js} +2 -2
  26. package/site-dist/assets/{guide_getting-started.md.CA-aS0r_.js → guide_getting-started.md.DDZak2vT.js} +1 -1
  27. package/site-dist/assets/guide_project.md.Csi5wiyt.js +77 -0
  28. package/site-dist/assets/guide_project.md.Csi5wiyt.lean.js +77 -0
  29. package/site-dist/assets/{guide_user.md.Cenm9vlN.js → guide_user.md.2v7b_fzd.js} +1 -1
  30. package/site-dist/assets/{guide_user.md.Cenm9vlN.lean.js → guide_user.md.2v7b_fzd.lean.js} +1 -1
  31. package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.js +1 -0
  32. package/site-dist/assets/reference_contracts_cli-json.md.BR6gwq-e.lean.js +1 -0
  33. package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.js +10 -0
  34. package/site-dist/assets/reference_contracts_usage-record.md.D5krNLW7.lean.js +10 -0
  35. package/site-dist/changelog.html +5 -5
  36. package/site-dist/guide/getting-started.html +5 -5
  37. package/site-dist/guide/how-it-works.html +4 -4
  38. package/site-dist/guide/index.html +4 -4
  39. package/site-dist/guide/install.html +4 -4
  40. package/site-dist/guide/maintainer.html +4 -4
  41. package/site-dist/guide/phases.html +4 -4
  42. package/site-dist/guide/project.html +6 -6
  43. package/site-dist/guide/user.html +5 -5
  44. package/site-dist/hashmap.json +1 -1
  45. package/site-dist/index.html +4 -4
  46. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  47. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  48. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  49. package/site-dist/reference/adr/0004-result-files.html +4 -4
  50. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  51. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  52. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  53. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  54. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  55. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  56. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  57. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  58. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  59. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  60. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  61. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  62. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  63. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  64. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  65. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  66. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  67. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  68. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  69. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  70. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  71. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  72. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  73. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  74. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  75. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  76. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  77. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  78. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  79. package/site-dist/reference/adr/index.html +4 -4
  80. package/site-dist/reference/commands.html +4 -4
  81. package/site-dist/reference/config.html +4 -4
  82. package/site-dist/reference/contracts/cli-json.html +5 -5
  83. package/site-dist/reference/contracts/feature-files.html +4 -4
  84. package/site-dist/reference/contracts/index.html +4 -4
  85. package/site-dist/reference/contracts/notifier-event.html +4 -4
  86. package/site-dist/reference/contracts/result-file.html +4 -4
  87. package/site-dist/reference/contracts/spec.html +4 -4
  88. package/site-dist/reference/contracts/state.html +4 -4
  89. package/site-dist/reference/contracts/usage-record.html +6 -6
  90. package/site-dist/reference/contracts/verify.html +4 -4
  91. package/site-dist/reference/scope.html +4 -4
  92. package/skills/req/SKILL.md +1 -0
  93. package/skills/req/playbooks/incident.md +3 -3
  94. package/skills/req/playbooks/new.md +1 -1
  95. package/skills/req/playbooks/small.md +1 -1
  96. package/skills/spec-writing/SKILL.md +3 -1
  97. package/site-dist/assets/changelog.md.BzAEYWi7.js +0 -1
  98. package/site-dist/assets/chunks/@localSearchIndexroot.BjRGqGQa.js +0 -1
  99. package/site-dist/assets/guide_project.md.qQLH2fWy.js +0 -77
  100. package/site-dist/assets/guide_project.md.qQLH2fWy.lean.js +0 -77
  101. package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.js +0 -1
  102. package/site-dist/assets/reference_contracts_cli-json.md.B7o8deSY.lean.js +0 -1
  103. package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.js +0 -10
  104. package/site-dist/assets/reference_contracts_usage-record.md.DBMNY4OP.lean.js +0 -10
  105. /package/site-dist/assets/{changelog.md.BzAEYWi7.lean.js → changelog.md.BUTM13q-.lean.js} +0 -0
  106. /package/site-dist/assets/{guide_getting-started.md.CA-aS0r_.lean.js → guide_getting-started.md.DDZak2vT.lean.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
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
+
5
20
  ## 1.13.0
6
21
 
7
22
  稳定版:内网能装能用、在 Paseo 网页端审查原型、不靠看图检查界面、看得见 token 花在哪。内容包括 1.13.0-beta.1 至 beta.5(详见下面各条)和下面新增的三项,设计决定见 ADR 0032、0033。
@@ -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 |
@@ -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 里打开(手机上会在浏览器中打开;嫌窗口小时点右上角"在新窗口打开"):
@@ -285,6 +285,11 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
285
285
 
286
286
  在项目主目录、主分支上发起的需求,可以用 `--worktree` 启动(`/req` 中的"启动(自动新建工作区)"):流水线从当前分支切出 `feature/<功能目录名>`,在 Paseo 中新建 worktree 工作区,把需求说明移过去并提交,之后的步骤都在那个工作区中运行,仍然挂在发起的对话下。对话的工作目录不变,查看进度、暂停、继续都在原对话中进行;读写需求文件用通知中给出的完整路径。只能在第一次启动时选择。见 ADR 0018。
287
287
 
288
+ 1.14 起:
289
+ - **启动没成功也能接着做。** 工作区一建好就记下来(`~/.req-kit/worktrees/`)。之后不管哪一步失败(建工作区慢、启动前的检查没通过),再次执行启动命令都会在这个工作区里接着开始,不会报"分支已存在",也不会留下只有需求说明的分支。新建工作区最多等 15 分钟(以前是 2 分钟,大仓库在 Windows 上常常不够)。以前版本留下的"分支和工作区都在、开发没开始"的,也会由 Paseo 接手那个目录接着做。
290
+ - **访谈改过的 `GLOSSARY.md`、`docs/adr/` 一起带过去**,和需求说明在需求自己的分支上一起提交;主目录里的这些未提交改动保留不动(可能和别的改动在一起),确认后可以撤销。
291
+ - **如实提示起点的问题**:需求说明已经提交在主分支上、本地主分支比远端落后、主目录里还有别的未提交改动时,启动结果里会写明。流水线不改动主分支。
292
+
288
293
  ## 调度:预约、提醒、自动继续、排队
289
294
 
290
295
  需要"以后再做"的事,流水线用 Paseo 的定时任务把一条写明步骤的消息发回发起的对话,由对话 agent 执行(机器或 daemon 重启后仍然有效,在 Paseo 的 Schedules 中可以看到)。暂停命令会取消这些预约。
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,
@@ -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;
@@ -16,6 +16,7 @@ import { loadConfig, PHASE_MODE_LABELS, type PipelineConfig, PROFILE_LABELS } fr
16
16
  import { cardFieldsText } from "./integrations.js";
17
17
  import { defaultConfigPath, ENGINE_DIR } from "./locations.js";
18
18
  import { describeLiveActivity, describeStatus, stopPipeline } from "./control.js";
19
+ import { trackRequirement } from "./records.js";
19
20
  import { describeFeatures, featuresWithRuns, findWorktreeRun, listFeatures, resolveFeatureArg } from "./features.js";
20
21
  import { checkChatCwd, normalizeFeatureDir, preflight, type PreflightResult, specProfile, verifyPhaseProblem } from "./preflight.js";
21
22
  import { loadRuntimeFactory } from "./runtime-loader.js";
@@ -92,6 +93,22 @@ if (args.cardFields) {
92
93
  }
93
94
  }
94
95
 
96
+ if (args.track) {
97
+ // 1.14: the requirement shows in the plugin from the start of the interview, with its chat.
98
+ try {
99
+ const repo = fs.realpathSync(args.repo);
100
+ const featureDir = normalizeFeatureDir(repo, args.featureDir);
101
+ const dir = path.join(repo, featureDir);
102
+ if (!fs.existsSync(dir)) throw new Error(`功能目录不存在:${featureDir}`);
103
+ trackRequirement(repo, featureDir, args.originAgentId);
104
+ console.log(`已登记需求:${featureDir}(Paseo 的 req-kit 插件里能看到它,状态"对齐中"${args.originAgentId ? ",关联这个对话" : ""})`);
105
+ process.exit(0);
106
+ } catch (error) {
107
+ console.error(`没能登记需求:${(error as Error).message}`);
108
+ process.exit(2);
109
+ }
110
+ }
111
+
95
112
  if (args.list) {
96
113
  console.log(describeFeatures(listFeatures(args.repo), args.repo));
97
114
  process.exit(0);
@@ -344,7 +361,9 @@ if (args.worktree) {
344
361
  try {
345
362
  ownWorktree = await Promise.race([
346
363
  create(),
347
- new Promise<never>((_, reject) => setTimeout(() => reject(new Error("连接 Paseo 新建工作区超过 2 分钟没有完成")), 120_000).unref()),
364
+ // 1.14: big repositories (Windows especially) can take minutes to check out. Giving up early
365
+ // left a branch with only the spec on it; now it waits, and a later start continues there.
366
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error("连接 Paseo 新建工作区超过 15 分钟没有完成。工作区可能稍后才建好:再执行一次启动命令,会在已建好的工作区里接着开始")), 15 * 60_000).unref()),
348
367
  ]);
349
368
  } catch (error) {
350
369
  fail(`没能为这个需求新建工作区:${(error as Error).message}`);
@@ -356,7 +375,7 @@ if (args.worktree) {
356
375
  try {
357
376
  checked = preflight(args, hashFile);
358
377
  } catch (error) {
359
- fail(`新工作区 ${ownWorktree.repo} 已建好,但无法在那里启动:${(error as Error).message}`);
378
+ fail(`新工作区 ${ownWorktree.repo} 已建好,但无法在那里启动:${(error as Error).message}\n问题解决后再执行一次启动命令,会在这个工作区里接着开始(不会再新建)。`);
360
379
  }
361
380
  }
362
381
 
@@ -403,8 +422,10 @@ console.log(
403
422
  ? "只做研发:写好提测说明和研发报告后结束,测试由人完成。"
404
423
  : null,
405
424
  ownWorktree
406
- ? `已为这个需求新建工作区:${ownWorktree.repo}(分支 ${ownWorktree.branch},在 Paseo 侧边栏中可以看到)。开发在那里进行,步骤仍挂在这个对话下;这个对话的工作目录不变。` +
407
- `需求文件已移到 ${path.join(ownWorktree.repo, checked.featureDir)}${ownWorktree.removedSource ? "" : "(这里原有的已提交版本保留不动)"},之后读写需求文件请用这个路径。`
425
+ ? `${ownWorktree.reused ? "在上次已建好的工作区中开始" : "已为这个需求新建工作区"}:${ownWorktree.repo}(分支 ${ownWorktree.branch},在 Paseo 侧边栏中可以看到)。开发在那里进行,步骤仍挂在这个对话下;这个对话的工作目录不变。` +
426
+ `需求文件已移到 ${path.join(ownWorktree.repo, checked.featureDir)}${ownWorktree.removedSource ? "" : "(这里原有的已提交版本保留不动)"},之后读写需求文件请用这个路径。` +
427
+ (ownWorktree.carried.length ? `\n访谈中改过的 ${ownWorktree.carried.join("、")} 也一起带过去提交了;这里的未提交改动保留不动,确认无误后可以撤销。` : "") +
428
+ ownWorktree.warnings.map((w) => `\n注意:${w}`).join("")
408
429
  : args.sourceRepo
409
430
  ? `这个需求在为它新建的工作区中开发:${checked.repo}`
410
431
  : null,
@@ -307,6 +307,11 @@ export function createRuntimeWithClient(
307
307
  if (!directory) throw new Error("Paseo 没有返回新工作区的目录");
308
308
  return { workspaceId: workspace.id, directory };
309
309
  },
310
+ async openWorkspace(directory) {
311
+ if (!client.workspaces) throw new Error("这个 Paseo 版本不支持接手已有的工作区目录");
312
+ const handle = await client.workspaces.open({ cwd: directory });
313
+ return { workspaceId: handle.id };
314
+ },
310
315
  async waitForWorkspaceSetup(workspaceId, timeoutMs) {
311
316
  const api = await getScripts();
312
317
  if (!api) return { status: "none", error: null };
@@ -31,6 +31,8 @@ export interface LaunchArgs {
31
31
  list: boolean;
32
32
  /** --card-fields: what /req confirms with the spec for the external systems (card.json). */
33
33
  cardFields?: boolean;
34
+ /** 1.14 --track: register a requirement being aligned, before the pipeline runs (shown in the plugin). */
35
+ track?: boolean;
34
36
  /** --spec-review: independent review of the draft spec, before confirmation. */
35
37
  specReview: boolean;
36
38
  /** --worktree: create a Paseo worktree workspace for this requirement and develop there. */
@@ -47,6 +47,8 @@ export interface IndexEntry {
47
47
  /** Name of the project: the source checkout's directory name. */
48
48
  project: string;
49
49
  registeredAt: string;
50
+ /** 1.14: the chat the requirement is being aligned in (registered before the pipeline ran). */
51
+ chatAgentId?: string | null;
50
52
  }
51
53
 
52
54
  export function indexDir(env: NodeJS.ProcessEnv = process.env): string {
@@ -59,7 +61,7 @@ export function requirementKey(sourceOrRepo: string, featureDir: string): string
59
61
  }
60
62
 
61
63
  /** Records where this requirement lives, if it is not recorded that way already. Never throws. */
62
- export function registerRequirement(state: Pick<PipelineState, "repo" | "featureDir" | "worktree">, env: NodeJS.ProcessEnv = process.env): void {
64
+ export function registerRequirement(state: Pick<PipelineState, "repo" | "featureDir" | "worktree">, env: NodeJS.ProcessEnv = process.env, chatAgentId: string | null = null): void {
63
65
  if (recordingOff(env)) return;
64
66
  try {
65
67
  const source = state.worktree?.sourceRepo ?? null;
@@ -75,6 +77,7 @@ export function registerRequirement(state: Pick<PipelineState, "repo" | "feature
75
77
  workspaceId: state.worktree?.workspaceId ?? null,
76
78
  project: path.basename(real(source ?? state.repo)),
77
79
  registeredAt: existing?.registeredAt ?? new Date().toISOString(),
80
+ ...(chatAgentId ?? existing?.chatAgentId ? { chatAgentId: chatAgentId ?? existing?.chatAgentId ?? null } : {}),
78
81
  };
79
82
  if (existing && JSON.stringify(existing) === JSON.stringify(entry)) return;
80
83
  fs.mkdirSync(path.dirname(file), { recursive: true });
@@ -86,6 +89,24 @@ export function registerRequirement(state: Pick<PipelineState, "repo" | "feature
86
89
  }
87
90
  }
88
91
 
92
+ /**
93
+ * 1.14: a requirement whose interview has started: marked in its feature directory
94
+ * (.pipeline/tracked.json, not committed) and registered with its chat, so the plugin lists it
95
+ * before the pipeline ever runs.
96
+ */
97
+ export function trackRequirement(repo: string, featureDir: string, chatAgentId: string | null, env: NodeJS.ProcessEnv = process.env): void {
98
+ const marker = path.join(repo, featureDir, ".pipeline", "tracked.json");
99
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
100
+ let since = new Date().toISOString();
101
+ try {
102
+ since = (JSON.parse(fs.readFileSync(marker, "utf8")) as { since?: string }).since ?? since;
103
+ } catch {
104
+ // First time.
105
+ }
106
+ fs.writeFileSync(marker, `${JSON.stringify({ since, chatAgentId }, null, 2)}\n`);
107
+ registerRequirement({ repo, featureDir, worktree: null }, env, chatAgentId);
108
+ }
109
+
89
110
  function readEntry(file: string): IndexEntry | null {
90
111
  try {
91
112
  const raw = JSON.parse(fs.readFileSync(file, "utf8")) as IndexEntry;
@@ -104,7 +125,8 @@ export function readIndex(env: NodeJS.ProcessEnv = process.env): Array<IndexEntr
104
125
  .filter((n) => n.endsWith(".json"))
105
126
  .map((n) => readEntry(path.join(dir, n)))
106
127
  .filter((e): e is IndexEntry => e !== null)
107
- .map((e) => ({ ...e, present: fs.existsSync(path.join(e.repo, e.featureDir, ".pipeline", "state.json")) }));
128
+ // 1.14: a requirement still in its interview has no state yet, only its notes.
129
+ .map((e) => ({ ...e, present: ["spec.md", "alignment.md", ".pipeline/state.json", ".pipeline/tracked.json"].some((f) => fs.existsSync(path.join(e.repo, e.featureDir, f))) }));
108
130
  }
109
131
 
110
132
  /** Removes the entries whose requirement is gone. Returns how many. */
@@ -135,6 +135,8 @@ export interface AgentRuntime {
135
135
  stopServices(target: ServiceTarget, names: string[]): Promise<void>;
136
136
  /** Creates a Paseo worktree workspace (paseo.json setup runs in the background). */
137
137
  createWorktree(request: WorktreeRequest): Promise<{ workspaceId: string; directory: string }>;
138
+ /** 1.14: the Paseo workspace of an existing directory (opened as one if it is not yet). */
139
+ openWorkspace?(directory: string): Promise<{ workspaceId: string }>;
138
140
  /** Waits for the workspace's paseo.json setup; "none" when there is nothing to wait for or it cannot be read. */
139
141
  waitForWorkspaceSetup(workspaceId: string, timeoutMs: number): Promise<{ status: "completed" | "failed" | "none" | "timeout"; error: string | null }>;
140
142
  /** Subscription windows of the given providers; null when Paseo has no usage data. */